From 3258f6acda17f86d640cb3a220ad1f9169e9664f Mon Sep 17 00:00:00 2001 From: Luigi Colluto Date: Tue, 14 Jul 2026 16:50:14 +0200 Subject: [PATCH] Fix out-of-bounds write in gguf_q4_0/q4_1_to_float second nibble loop 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. --- gguflib.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/gguflib.c b/gguflib.c index b84a6db..720d25a 100644 --- a/gguflib.c +++ b/gguflib.c @@ -824,6 +824,12 @@ void gguf_q4_0_to_float(void *weights_data, void *dst, uint64_t count, store_flo f[i] = weight; if (++i == count) break; } + /* Stop before the upper-nibble loop if we already emitted 'count' + * weights in the lower-nibble loop: that loop's own (++i == count) + * break only exits itself, and the check below can never fire again + * once i == count, so without this the upper loop would write up to + * 16 floats past the end of 'dst'. */ + if (i == count) break; /* Last 16 weights are in the higher bits */ for (uint32_t j = 0; j < 16; j++) { uint8_t value = block[j+2]; // j+2 to skip the scale bytes. @@ -866,6 +872,10 @@ void gguf_q4_1_to_float(void *weights_data, void *dst, uint64_t count, store_flo f[i] = weight; if (++i == count) break; } + /* Stop before the upper-nibble loop if we already emitted 'count' + * weights: as in gguf_q4_0_to_float, the lower loop's break only exits + * itself, so without this the upper loop would write past 'dst'. */ + if (i == count) break; /* Last 16 weights are in the higher bits */ for (uint32_t j = 0; j < 16; j++) { uint8_t value = block[j+4]; // j+2 to skip the scale and bias bytes.