PoC β€” llama.cpp control-vector integer overflow β†’ heap OOB write

Proof-of-concept model file for a huntr Model File Vulnerabilities report against ggml-org/llama.cpp @ d4cff11.

This repository contains a deliberately malformed GGUF control-vector file for authorized security research (responsible disclosure via huntr). It is not a usable model.

The bug (summary)

When llama.cpp loads a control vector (--control-vector foo.gguf), each tensor named direction.<N> contributes its data at layer offset N, parsed from the tensor name with std::stoi and with no upper bound. Both the buffer-sizing expression and the destination-pointer expression multiply n_embd * layer_idx in 32-bit int, which overflows for large N:

// common/common.cpp  (common_control_vector_load_one)
:1824  layer_idx = std::stoi(name.substr(dotpos + 1));   // from tensor NAME, no upper bound
:1852  result.n_embd = ggml_nelements(tensor);           // int, set by first tensor
:1860  result.data.resize(std::max(result.data.size(), static_cast<size_t>(result.n_embd * layer_idx)), 0.0f);
//                                                        ^^^^^^^^^^^^^^^^^^^^^^^ int*int overflow
:1863  float * dst = result.data.data() + result.n_embd * (layer_idx - 1);   // int*int overflow -> negative offset
:1865  dst[j] += src[j] * load_info.strength;            // OOB WRITE of attacker floats

By choosing N so the product wraps, resize() does not grow the buffer while dst is computed to lie before the allocation β†’ attacker-controlled heap out-of-bounds (underflow) write. Both the write offset (tensor name) and the payload (tensor F32 data Γ— strength) are attacker-controlled.

Concrete overflow (in this PoC file)

Two 1-D F32 tensors: direction.1 (4 elements β†’ sets n_embd = 4, grows buffer to 4 floats) and direction.1073741824 (2^30). Then: n_embd*layer_idx = 4*2^30 = 2^32 ≑ 0 (int wrap) β†’ buffer not grown; n_embd*(layer_idx-1) = 2^32-4 ≑ -4 (int) β†’ dst = data() - 16 bytes β†’ 16-byte heap underflow write.

Files

file description
poc_ctrlvec_overflow.gguf valid GGUF control vector with direction.1 and direction.1073741824.
gen_ctrlvec_poc.py pure-python generator (no deps).

Reproduce

llama-cli -m <any-small-model.gguf> --control-vector poc_ctrlvec_overflow.gguf -p "hi"

Build llama.cpp with -fsanitize=address to observe the heap-buffer-overflow at common/common.cpp:1865. The root cause and 32-bit-wrap arithmetic are verified line-by-line against source; the offset/payload are fully attacker-controlled.

Novelty

Not in ggml/src/gguf.cpp (the file covered by the oss-sec May-2026 advisory) β€” this is consumer/glue arithmetic in common/common.cpp control-vector loading, unrelated to the GGUF parser itself.

Suggested fix

Widen to 64-bit before multiplying and bound-check layer_idx against the model's layer count:

if (layer_idx <= 0 || (size_t)layer_idx > (size_t)n_layers) return; // reject
const size_t off = (size_t)result.n_embd * (size_t)(layer_idx - 1);
Downloads last month
13
GGUF
Model size
8 params
Architecture
Hardware compatibility
Log In to add your hardware

We're not able to determine the quantization variants.

Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support