TheMrEvil's picture
Upload README.md with huggingface_hub
f6009b3 verified
|
Raw
History Blame Contribute Delete
2.53 kB
# MLX GGUF quantized-tensor out-of-bounds read (proof of concept)
This repository hosts a crafted GGUF model file that triggers an out-of-bounds read in
**Apple MLX** (`mlx` Python package) when the file is loaded with the standard
`mlx.core.load()` API. The repo is gated; it is a security proof of concept, not a usable model.
## Summary
`mlx.core.load("x.gguf")` parses GGUF tensors through MLX's C++ loader. For quantized
tensor types (`Q8_0`, `Q4_0`, `Q4_1`) it calls `gguf_load_quantized`, which dequantizes the
data in `extract_q8_0_data` / `extract_q4_0_data` / `extract_q4_1_data`
(`mlx/io/gguf_quants.cpp`). Those loops iterate over the block count derived from the tensor's
**file-declared shape** and read `bytes_per_block` (34 for Q8_0) from the memory-mapped file for
each block, with no check that the declared shape's data actually fits in the file. A GGUF whose
declared quantized dimension is large while the real tensor-data section is tiny makes the loop
read far past the end of the mapping.
## Affected
* `mlx` 0.31.2 (current latest on PyPI) and current `main`.
* Verified on Linux x86_64 (`mlx[cpu]`), Python 3.13.
## Files
* `evil.gguf` - one `Q8_0` tensor declaring `dim[0] = 32,000,000` but carrying only 34 data bytes.
* `baseline.gguf` - identical structure with a well-formed single block; loads fine (control).
* `verify.py` - rebuilds both files and loads each in a child process, showing the differential.
## Reproduce
```
pip install "mlx[cpu]"
python verify.py
```
Expected:
```
load baseline.gguf -> exit 0
load evil.gguf -> SIGSEGV
```
Precise attribution (valgrind):
```
valgrind python -c "import mlx.core as mx; mx.load('evil.gguf')"
# Invalid read of size 16 / of size 2
# at mlx::core::extract_q8_0_data(...)
# by mlx::core::gguf_load_quantized(...)
# by mlx::core::load_arrays(...)
# by mlx::core::load_gguf(...)
```
## Impact
Loading an untrusted `.gguf` with `mlx.core.load()` reads out-of-bounds heap/mmap memory into the
dequantized output arrays. A large declared dimension walks off the mapping and crashes the process
(denial of service at model-load time); a smaller over-declared dimension reads adjacent heap bytes
into caller-visible arrays (information disclosure). No flags or non-default options are required.
## Fix
In `gguf_load_quantized` (or `gguf_get_tensor` in the vendored gguflib), validate that the tensor's
declared element/block count implies a byte size that fits within `offset .. file_size` before the
extractor loops run.