Misc. bug: GGUF loader accepts a tensor size that wraps to 0 after padding
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Start in ggml/src/gguf.cpp at gguf_init_from_reader and review the TENSORS_NBYTES_PAD_WRAP handcrafted case in test-gguf. Build llama-gguf with the command in the issue and run it against poc/pad_wrap.gguf. Done means the oversized padded tensor is rejected before tensor data is built, and the test changes from non-null to null.
Written by the indexing model from the issue text.
Description
Name and Version
$ bin/llama-gguf --version
(inspection tool built from this repo; same ggml/gguf code path as llama-cli)
Tested commit: master 9558fa44 (also confirmed on branch gguf-pad-wrap-guard
with the fix). Debug build, AppleClang, Darwin arm64. The vulnerable code is
in ggml/src/gguf.cpp (gguf_init_from_reader) and is not tied to this commit;
it has been present for a long time.
Operating systems
Mac, Other? (Please let us know in description)
Which llama.cpp modules do you know to be affected?
libllama (core library), Other (Please specify in the next section)
Command line
# build the example tool
cmake -S . -B build -DLLAMA_BUILD_EXAMPLES=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target llama-gguf
# trigger: read a crafted GGUF with tensor-data checking enabled
bin/llama-gguf poc/pad_wrap.gguf r
# PoC generator (poc/gen_pad_wrap.py) builds poc/pad_wrap.gguf (384 bytes):
# tensor a: F32, 64 elts, 256 bytes (valid)
# tensor b: F32, ne = [4, 1073741823, 1073741825, 1]
# ggml_nbytes = 4 * 4 * (2^30 - 1) * (2^30 + 1) = 2^64 - 16
Problem description & steps to reproduce
gguf_init_from_reader accumulates each tensor's padded size into ctx->size:
size_t padded_size = GGML_PAD(ggml_nbytes(&ti.t), ctx->alignment);
if (SIZE_MAX - ctx->size < padded_size) { /* reject */ }
ctx->size += padded_size;
GGML_PAD rounds up: GGML_PAD(x, n) = ((x) + (n) - 1) & ~((n) - 1).
When ggml_nbytes is within (alignment - 1) of SIZE_MAX, the (x) + (n - 1)
addition wraps to a small value and the mask clears it to 0. With the default
alignment (32) and nbytes = 2^64 - 16:
GGML_PAD(2^64 - 16, 32)
= ((2^64 - 16) + 31) & ~31
= (15) & ~31 // 64-bit overflow on the add
= 0
So padded_size becomes 0, the SIZE_MAX guard (which checks padded_size, not
nbytes) always passes, and ctx->size only records tensor a's 256 bytes.
Tensor b still gets a ggml_tensor with ne = [4, 1073741823, 1073741825, 1]
and a data pointer at the end of the 256-byte backing blob, but the context
no longer reflects its real size.
Downstream code that trusts the context size, or iterates a tensor by its
own dimensions, then reads out of bounds. examples/gguf hits this directly
(see logs): it accepts size = 18446744073709551600, reads b's data past the
backing blob, and aborts.
The wrap lands on exactly 0 for every power-of-two alignment (checked over
all 32 possible alignment values), so the window is reliably hit, not a
near-miss.
Suggested fix: reject before padding when nbytes + (alignment - 1) would
overflow. ctx->alignment is already validated as a non-zero power of two
earlier in the same function, so (alignment - 1) cannot underflow.
const size_t nbytes = ggml_nbytes(&ti.t);
if (nbytes > SIZE_MAX - (ctx->alignment - 1)) {
GGML_LOG_ERROR("%s: tensor '%s' size %zu overflows after padding (alignment %zu)\n",
__func__, ti.t.name, nbytes, ctx->alignment);
gguf_free(ctx);
return nullptr;
}
const size_t padded_size = GGML_PAD(nbytes, ctx->alignment);
if (SIZE_MAX - ctx->size < padded_size) { /* reject */ }
ctx->size += padded_size;
A test-gguf handcrafted case (TENSORS_NBYTES_PAD_WRAP, single F32 tensor with
the same ne) flips with this fix: master accepts (returns non-null), patched
rejects (returns null).
First Bad Commit
Not a regression. The GGML_PAD-based accumulation in gguf_init_from_reader
has been there for a long time; no single commit introduced it.
Relevant log output
Logs
master 9558fa44 (bin/llama-gguf poc/pad_wrap.gguf r) -- accepts the wrapped
size, reads tensor b out of bounds, aborts:
gguf_ex_read_1: tensor[1]: name = b, size = 18446744073709551600, offset = 256, type = f32, n_elts = 4611686018427387900
gguf_ex_read_1: reading tensor 1 data
gguf_ex_read_1: tensor[1]: n_dims = 3, ne = (4, 1073741823, 1073741825, 1), name = b, data = 0x104c90270
b data[:10] : 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000
examples/gguf/gguf.cpp:269: GGML_ASSERT(gguf_ex_read_1(fname, check_data) && "failed to read gguf file") failed
b's data pointer (0x...270) is 256 bytes past a's (0x...170), i.e. one byte
after the 256-byte backing blob. data[0] reads out of bounds and returns
0.0; the check_data loop then aborts on the value mismatch. Exit 134.
Note: ASAN does not flag this over-read. ggml sub-allocates each tensor from
a larger arena, so the few bytes read past b stay inside one malloc region.
The assert message is the proof the read left b's own data. A harness that
walks b's full declared extent SEGVs about 15.7 KB past the arena.
With the fix (branch gguf-pad-wrap-guard), the same command rejects at the
parser before any tensor data is built:
gguf_init_from_reader: tensor 'b' size 18446744073709551600 overflows after padding (alignment 32)
gguf_ex_read_0: failed to load 'poc/pad_wrap.gguf'
examples/gguf-hash on the same PoC: --xxh64 returns a bogus hash silently
(the 2^64 - 16 length wraps the internal input+len pointer, main loop is
skipped); --all (sha1/sha256) hangs walking the declared extent.
- Dominant language
- C++
- Stars
- 129k
- Forks
- 23.5k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 411
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from ggml-org/llama.cpp
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
-
bug-unconfirmed
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
-
bug-unconfirmed
Difficulty 1/5 Under an hour Newbie friendliness 90/100
-
/v1/responses: reasoning item with "summary": null rejected as "Cannot determine type of 'item'" Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
All issues in ggml-org/llama.cpp
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 90/100
AXERA-TECH/ax-llm#77 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
NVIDIA/cuda-samples#453 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
infiniflow/infinity#3502 ·
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100