Skip to content

Bound GGUF tensor data offsets against the file mapping - #4179

Open
robertomeroni wants to merge 1 commit into
ml-explore:mainfrom
robertomeroni:contrib/gguf-tensor-offset-bounds
Open

Bound GGUF tensor data offsets against the file mapping#4179
robertomeroni wants to merge 1 commit into
ml-explore:mainfrom
robertomeroni:contrib/gguf-tensor-offset-bounds

Conversation

@robertomeroni

Copy link
Copy Markdown

Proposed changes

Fixes #4136 (thanks @professor-moody for the report).

gguflib sets tensor->weights_data = ctx->data + ctx->data_off + *offset from a
file-controlled offset without bounding it against ctx->size (gguflib.c:297-298), and
mlx's memcpy in extract_tensor_data (mlx/io/gguf.cpp:71) reads through it. A crafted
offset points outside the mapping; if the addition wraps it points back inside, so the load
silently returns wrong bytes instead of faulting.

This validates offset and extent in load_arrays, the one point both the plain and the
quantized paths pass through, using only gguf_ctx fields mlx already has. mlx does the same
a few lines away at gguf.cpp:67 (CVE-2025-62609) and for safetensors at
safetensors.cpp:196.

It is still needed if gguflib is fixed upstream. The pending fix there,
antirez/gguf-tools#33, signals a rejected tensor with return 0, which in a
while (gguf_get_tensor(...)) loop is indistinguishable from "no tensors left". Patching
the pinned gguflib with #33 locally, a 2-tensor file with one bad offset loads 1 tensor,
left_tensors == 0, no error — silently missing weights rather than a raise.

Not addressed by #3436: the only assert on this path is ndim (gguflib.c:275), so
-UNDEBUG has nothing to keep alive for offset/bsize.

Scope: this bounds the tensor data region against the file. It does not make GGUF loading
safe against arbitrary crafted files -- internally consistent but false metadata is a
separate class, not addressed here.

Without the guard two of the new subcases crash (SIGSEGV, or SIGBUS depending on where the
bad pointer lands) and four read wrong bytes without raising; all pass with it. The helper
follows write_raw_safetensors above it.

Checklist

Put an x in the boxes that apply.

  • I have read the CONTRIBUTING document
  • I have run pre-commit run --all-files to format my code / installed pre-commit prior to committing changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the necessary documentation (if needed)

AI assistance was used in developing this change (Opus 5); I reviewed every changed line and
ran the commands above myself.

gguflib computes a tensor's data pointer as ctx->data + ctx->data_off +
the tensor's offset field, in unsigned arithmetic and without comparing
the result against the mapping. A crafted offset therefore produces a
pointer outside the mapped file, and mlx's own memcpy in
extract_tensor_data then reads it. If the addition wraps, the pointer
lands back inside the mapping and the wrong bytes are read silently.

Validate offset and size against the file in load_arrays, which is the
single point both the plain and the quantized paths pass through.
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.

OOB read in the GGUF loader: tensor data offset and size are not bounded against the file mapping

1 participant