Skip to content

Fix out-of-bounds write in gguf_q4_0/q4_1_to_float second nibble loop#31

Open
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix-q4-dequant-second-loop-overrun
Open

Fix out-of-bounds write in gguf_q4_0/q4_1_to_float second nibble loop#31
gigioneggiando wants to merge 1 commit into
antirez:mainfrom
gigioneggiando:fix-q4-dequant-second-loop-overrun

Conversation

@gigioneggiando

Copy link
Copy Markdown

Problem

gguf_q4_0_to_float() and gguf_q4_1_to_float() dequantize each block with two separate 16-iteration loops (lower nibbles, then upper nibbles). The break on if (++i == count) only exits whichever loop is currently running:

for (uint32_t j = 0; j < 16; j++) { ...; if (++i == count) break; }  // lower
/* nothing stops us here */
for (uint32_t j = 0; j < 16; j++) { ...; if (++i == count) break; }  // upper

If count is reached partway through the first loop, that loop breaks correctly, but the second loop still runs unconditionally and writes up to 16 more floats past the end of dst, because i is already == count and its own ++i == count check can never fire again.

Callers size the destination as count * sizeof(float) (this is exactly what gguf_tensor_to_float() does: malloc(tensor->num_weights * sizeof(float))). So whenever count lands inside a block's first half, the destination is overrun. Since count is derived from an attacker-controlled tensor dimension in a downloaded .gguf file, this is a heap out-of-bounds write reachable by dequantizing/inspecting a crafted Q4_0/Q4_1 tensor (e.g. inspect-tensor, compare, or any gguflib caller).

Reproduction

Built release (-DNDEBUG) under AddressSanitizer, driving the real gguf_q4_0_to_float with count = 1 and a malloc(4) destination:

==ERROR: AddressSanitizer: heap-buffer-overflow
WRITE of size 4 ...
    #0 gguf_q4_0_to_float gguflib.c:835
SUMMARY: AddressSanitizer: heap-buffer-overflow gguflib.c:835 in gguf_q4_0_to_float

Fix

Stop the block loop as soon as i == count, before entering the upper-nibble loop. No integer-overflow trickery is involved and no build flag is required to trigger the bug, so this is a plain logic fix. It is also strictly more correct for any legitimate count that is not a multiple of the 32-weight block (the old code would write past the caller's buffer there too).

Verified with the same ASan harness after the patch: the call returns cleanly, no out-of-bounds write.

I ran this by a small local audit of the GGUF read path against untrusted model files; happy to share more detail privately if useful. No CVE or public advisory from my side.

Both dequantizers process each quantization block with two separate
16-iteration loops (lower nibbles, then upper nibbles). The break on
(++i == count) only exits whichever loop is currently running. When
count is reached inside the FIRST loop, that loop breaks correctly, but
the SECOND loop then still runs unconditionally and writes up to 16
more floats past the end of dst, because i is already == count and its
own (++i == count) check can never fire again.

A caller that sizes dst as count * sizeof(float) (as gguf_tensor_to_float
does) is overrun whenever count lands inside a block's first half. With
an attacker-controlled tensor dimension this is a heap out-of-bounds
write reachable by dequantizing/inspecting a crafted Q4_0/Q4_1 tensor.

Stop the block loop as soon as i == count, before entering the upper
loop. This is also strictly more correct for any legitimate count that
is not a multiple of the 32-weight block.
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