|
Download README.md from chirper/gguf-array-dos-poc: direct link, hf CLI and curl.
- Browser
- Download file 2.61 kB
-
https://huggingface.co/chirper/gguf-array-dos-poc/resolve/main/README.md
- Command line
-
hf download hf://chirper/gguf-array-dos-poc/README.md
-
curl -L -H "Authorization: Bearer $HF_TOKEN" -o README.md https://huggingface.co/chirper/gguf-array-dos-poc/resolve/main/README.md
2.61 kB
| license: mit | |
| tags: | |
| - security | |
| - proof-of-concept | |
| - gguf | |
| - denial-of-service | |
| # PoC: Denial of Service in `gguf` (gguf-py) `GGUFReader` via unchecked ARRAY length | |
| This repository contains a **49-byte** proof-of-concept GGUF file that triggers a | |
| non-terminating loop with unbounded memory growth in the official `gguf` Python | |
| package (`gguf.GGUFReader`). Loading the file for metadata inspection causes the | |
| process to hang and consume memory until it is OOM-killed. | |
| This artifact is for **defensive security research / responsible disclosure** only. | |
| ## Affected | |
| - `gguf` (PyPI) **0.18.0** | |
| - `gguf` (PyPI) **0.19.0** | |
| - Upstream `ggml-org/llama.cpp` `gguf-py` at | |
| `b5f52280fb781cf63e7c3fb79f8bb8de215293e3` (verified after the `#19856` | |
| integer-overflow patch). | |
| ## Root cause | |
| In `gguf/gguf_reader.py`, `_get_field_parts()` parses a metadata ARRAY by reading a | |
| 64-bit element count straight from the file and looping that many times: | |
| ```python | |
| alen = self._get(offs, np.uint64) # attacker-controlled element count | |
| for idx in range(alen[0]): # no bound vs. remaining file bytes | |
| curr_size, curr_parts, ... = self._get_field_parts(offs, raw_itype[0]) | |
| aparts += curr_parts # grows every iteration | |
| offs += curr_size # curr_size == 0 once past EOF | |
| ``` | |
| For a 1-byte element type (`UINT8`/`BOOL`), once `offs` reaches EOF the element read | |
| returns an empty array (`curr_size == 0`), so `offs` never advances and the loop runs | |
| up to `alen = 2**64-1` times, appending to `aparts` each iteration. | |
| ## Reproduce | |
| ```bash | |
| pip install gguf==0.19.0 | |
| python -c "from gguf import GGUFReader; GGUFReader('poc_array_dos.gguf')" | |
| # never returns; resident memory climbs ~230 MiB/s | |
| ``` | |
| Safe, time-boxed demonstration (sampling RSS, aborts after 8s): | |
| ```bash | |
| python verify_dos.py poc_array_dos.gguf | |
| ``` | |
| Expected output: the parser is still running after 8s with RSS having grown >1 GiB | |
| from a 49-byte input. A finite-length control (`make_poc.py out.gguf 0x2`) returns | |
| in ~0.01s. | |
| ## Rebuild the PoC | |
| ```bash | |
| python make_poc.py poc_array_dos.gguf 0xFFFFFFFFFFFFFFFF | |
| ``` | |
| ## Suggested fix | |
| Reject array lengths that exceed the remaining file size (each element is at least | |
| 1 byte), and add a progress guard for truncated elements, mirroring the bounds | |
| checks already present in the C++ reader (`ggml/src/gguf.cpp`): | |
| ```python | |
| remaining = self.data.size - offs | |
| if alen[0] > remaining: | |
| raise ValueError(f'array length {int(alen[0])} exceeds remaining file size {remaining}') | |
| ... | |
| if curr_size <= 0: | |
| raise ValueError('truncated GGUF ARRAY element') | |
| ``` | |