Skip to content

Bound file-declared length prefixes in the model loader (heap overflow)#542

Open
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/quantizer-length-oob
Open

Bound file-declared length prefixes in the model loader (heap overflow)#542
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix/quantizer-length-oob

Conversation

@gigioneggiando

Copy link
Copy Markdown

Summary

Two sites in the model-file loader read a uint64 length straight from a downloaded file and then allocate + read with no bound against the file:

  • shard_load() — the safetensors header length;
  • read_gguf_string_fp() — a GGUF string length.

Both do xmalloc((size_t)len + 1) then fread(len). A crafted length near SIZE_MAX makes (size_t)len + 1 wrap to 0, so xmalloc() hands back a 1-byte buffer (xmalloc maps 0 to malloc(1)); the following fread() of the file's real bytes then overflows it — a heap-buffer-overflow write. A large-but-non-wrapping length instead requests a multi-GB allocation from a tiny file (DoS).

These parse files people download and convert (quantized models shared online), so the input is attacker-influenced.

Fix

Add read_checked_len_fp(), which reads the length prefix and rejects it if it exceeds the bytes actually remaining in the file, and use it at both sites. Valid lengths (<= file size, so no size_t wrap) are unchanged; the file position is restored after the size check. Uses ftello/fseeko, already used elsewhere in this file — no new includes.

Verification

Built the real shard_load / read_gguf_string_fp under ASan and drove crafted files with len = SIZE_MAX:

  • before: AddressSanitizer: heap-buffer-overflow WRITE (shard_load alloc→fread, and the GGUF-string sibling).
  • after: clean rejection — error: safetensors header length (…) exceeds 64 bytes remaining in file, no sanitizer error.

Found during a coordinated security review of ds4; standalone offline PoCs available on request.

shard_load() (safetensors header) and read_gguf_string_fp() (GGUF string)
both read a uint64 length straight from a downloaded model file and then
do xmalloc((size_t)len + 1) followed by fread(len). A crafted length near
SIZE_MAX makes (size_t)len + 1 wrap to 0, so xmalloc() returns a 1-byte
buffer (xmalloc maps 0 to malloc(1)); the subsequent fread() of the file's
real bytes then overflows it -- a heap-buffer-overflow write. A large but
non-wrapping length instead requests a multi-GB allocation from a tiny file.

Add read_checked_len_fp(), which reads the length and rejects it if it
exceeds the bytes actually remaining in the file, and use it at both sites.
Valid lengths (<= file size, so no size_t wrap) are unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant