GGML legacy model-controlled n_dims stack out-of-bounds in loaders and quantizers
Submission
- Data format: GGML legacy model files
- Suggested title: GGML legacy model-controlled
n_dimsstack out-of-bounds in loaders and quantizers - Severity: High
- Impact: native memory corruption and reliable denial of service when a victim loads or quantizes a malicious GGML model file
- Affected target verified: ggml
v0.11.1, commita056a26f508b6160438ddd8aabbc859d2a2e7a97 - Not claimed: arbitrary code execution
Executive summary
Several GGML legacy model readers trust the model-controlled n_dims field before copying tensor dimensions into fixed-size stack arrays. A malicious GGML artifact can set n_dims larger than the destination array and cause an out-of-bounds stack write during normal model loading or quantization.
This repository includes five minimal malformed GGML files that hit independent loader/tool paths:
- GPT-2 loader, fixed local
ne[2] - GPT-J loader, fixed local
ne[2] - SAM loader, fixed local
ne[4] - GPT-2 quantizer helper, fixed local
ne[4] - GPT-J quantizer helper, fixed local
ne[4]
The issue is artifact-carried: the only attacker-controlled input is the GGML model file passed to the normal loader/quantizer command. ASAN reports stack-buffer-overflow, and release builds crash/abort.
Why this is security-relevant
GGML artifacts are commonly exchanged as model files and are loaded by native C/C++ tools. The parser reaches the vulnerable path before model execution and before meaningful model validation. A service that accepts, previews, converts, or quantizes untrusted GGML models can be crashed by a tiny file. The ASAN traces show a write beyond a stack buffer, which is stronger than a clean parser error or expected unsupported-format rejection.
Root cause
The vulnerable code pattern is:
- Read
n_dimsfrom the file. - Allocate a fixed local dimension array such as
int32_t ne[2]orint32_t ne[4]. - Read
n_dimsdimension entries from the file directly into that fixed array. - Validate/use tensor metadata after the out-of-bounds write has already happened.
Verified crash locations:
examples/gpt-2/main-backend.cpp, localne[2]examples/gpt-j/main.cpp, localne[2]examples/sam/sam.cpp, localne[4]examples/common-ggml.cpp, localne[4], reached by GPT-2 and GPT-J quantizers
Repository contents
artifacts/ggml_gpt2_loader_ndims3.bin GPT-2 loader PoC
artifacts/ggml_gptj_loader_ndims64.bin GPT-J loader PoC
artifacts/ggml_sam_loader_ndims64.bin SAM loader PoC
artifacts/ggml_gpt2_quant_ndims1024.bin GPT-2 quantizer PoC
artifacts/ggml_gptj_quant_ndims1024.bin GPT-J quantizer PoC
logs/*.txt ASAN traces
modelscan/* ModelScan outputs from verification
scripts/* PoC generation scripts
SHA256SUMS.txt hash manifest
Reproduction
Build the affected targets with AddressSanitizer:
git clone https://github.com/ggml-org/ggml.git
cd ggml
git checkout a056a26f508b6160438ddd8aabbc859d2a2e7a97
cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
-DGGML_NATIVE=OFF
cmake --build build-asan --target gpt-2-backend gpt-j sam gpt-2-quantize gpt-j-quantize -j
Download this repository and run the loader cases:
hf download pragnyanramtha/ggml-legacy-ndims-oob-suite --local-dir ggml-ndims-poc
./build-asan/bin/gpt-2-backend -m ggml-ndims-poc/artifacts/ggml_gpt2_loader_ndims3.bin -p test -n 1
./build-asan/bin/gpt-j -m ggml-ndims-poc/artifacts/ggml_gptj_loader_ndims64.bin -p test -n 1
./build-asan/bin/sam -m ggml-ndims-poc/artifacts/ggml_sam_loader_ndims64.bin -i /tmp/nonexistent.jpg
Run the quantizer cases:
./build-asan/bin/gpt-2-quantize ggml-ndims-poc/artifacts/ggml_gpt2_quant_ndims1024.bin /tmp/gpt2-out.bin q4_0
./build-asan/bin/gpt-j-quantize ggml-ndims-poc/artifacts/ggml_gptj_quant_ndims1024.bin /tmp/gptj-out.bin q4_0
Expected ASAN result for the quantizer path:
ERROR: AddressSanitizer: stack-buffer-overflow
WRITE of size 4
ggml_common_quantize_0(...) examples/common-ggml.cpp:114
This frame has ... 'ne' (line 112) <== Memory access ... overflows this variable
Expected ASAN result for the SAM loader path:
ERROR: AddressSanitizer: stack-buffer-overflow
sam_model_load(...) examples/sam/sam.cpp:1058
'ne' (line 1054) <== Memory access ... overflows this variable
Expected ASAN result for the GPT loader paths:
ERROR: AddressSanitizer: stack-buffer-overflow
SUMMARY: AddressSanitizer: stack-buffer-overflow ... in memcpy
'ne' (...) <== Memory access ... overflows this variable
The captured traces are included in logs/.
Scanner behavior
The artifacts were also scanned with ModelScan during local verification. They are malformed GGML parser tests, not executable payloads, and the scanner did not identify the native parser memory corruption. The scanner logs are included under modelscan/.
Hashes
62892adf21342a70b3c6dee5adfa224a435f747a5e6eb7c84e5064f576e99b0c artifacts/ggml_gpt2_loader_ndims3.bin
a0f8d2a774f739c6580e51348aa30f455de29c7a774b1f25c3ca27a593fae224 artifacts/ggml_gptj_loader_ndims64.bin
7ea36057dad923ed9feca6157fbccde22c5828bb91f51cb4ead4d8ac419154d2 artifacts/ggml_sam_loader_ndims64.bin
d9a5880f374d68f1c618945e301997b454317e203d23ca35b3d7c11bd69b8ddb artifacts/ggml_gpt2_quant_ndims1024.bin
002f3224ac62b36bc9b60da7fa1e5a4ac24f1587552846c10bec8cf3dbf7e928 artifacts/ggml_gptj_quant_ndims1024.bin
Suggested fix
Reject tensors where n_dims exceeds the destination capacity before reading dimension entries. Prefer reading into a dynamically sized container only after bounding n_dims to a format-level maximum. Add regression tests for oversized n_dims in each legacy loader and shared quantizer helper.
Limitations
This is a denial-of-service/native-memory-corruption report. I am not claiming arbitrary code execution. The PoCs are intentionally tiny malformed model files and should only be loaded in a throwaway test environment.