Skip to content

Harden the GGUF and GGML loaders against crafted files#3712

Open
v-code01 wants to merge 3 commits into
huggingface:mainfrom
v-code01:gguf-reject-zero-alignment
Open

Harden the GGUF and GGML loaders against crafted files#3712
v-code01 wants to merge 3 commits into
huggingface:mainfrom
v-code01:gguf-reject-zero-alignment

Conversation

@v-code01

@v-code01 v-code01 commented Jul 5, 2026

Copy link
Copy Markdown

What

The quantized model loaders take length/size/alignment fields straight from the file. Several are used unchecked, so a crafted .gguf/.ggml panics or over-allocates on load. This PR fixes three (each with a regression test in gguf_tests/ggml_tests):

Loader Crafted input Was Now
GGUF general.alignment = 0 position.div_ceil(0) → divide-by-zero panic rejected (must be a non-zero power of two, per spec)
GGUF tensor shape whose product overflows usize (e.g. two dims of 2^40) elem_count()/*type_size overflow → panic (debug) / wrap-then-OOB (release) checked_mul fold → clean error
GGML overflowing shape, and n_vocab/name_len/per-token len used as alloc sizes overflow panic + multi-GiB allocs checked_mul + validate every length against remaining file bytes; n_dims capped

The GGUF metadata reader was already hardened in #3533; this brings the tensor-load path and the GGML reader up to the same bar.

Why one PR

All three are the same class (untrusted quantized-file field → panic/over-alloc) and use the same checked-arithmetic + remaining-bytes-validation approach; one coherent hardening pass is easier to review than three fragments.

Tests

rejects_zero_general_alignment, rejects_tensor_shape_product_overflow (gguf_tests), and a new ggml_tests.rs (shape overflow, oversized tensor data, oversized name). Existing gguf_tests/quantized_tests stay green; cargo fmt --all + clippy -D warnings clean.

cc @LaurentMazare @ivarflakstad @EricLBuehler

v-code01 added 3 commits July 10, 2026 13:36
`Content::read` takes `general.alignment` straight from the file's
metadata and only rejects negative signed values; U8/U16/U32 (and
non-negative I8/I16/I32) pass through unchecked. A value of 0 then
reaches

    position.div_ceil(alignment) * alignment

and panics with "attempt to divide by zero", so a crafted GGUF header
of a few bytes reliably crashes the loader (DoS on untrusted input).

The GGUF spec requires `general.alignment` to be a non-zero power of
two. Validate that and bail with a clear error before computing the
tensor-data offset. Adds a regression test that a header with
`general.alignment = 0` is rejected rather than panicking.
The legacy GGML reader trusts several length/size fields straight from
the file, unlike the GGUF reader which was hardened in huggingface#3533:

- `read_one_tensor` computes `dims.product() * type_size / block_size`
  with unchecked arithmetic, so a crafted shape (e.g. four dims of
  u32::MAX) panics with "attempt to multiply with overflow" in debug
  builds and wraps to a bogus small size in release builds.
- The tensor `size_in_bytes`, `name_len`, per-token `len` and `n_vocab`
  are used directly as allocation lengths with no check against the
  remaining file size, so a tiny crafted header can request multi-GiB
  up-front allocations (allocation-abort DoS).
- `n_dims` is an unbounded `u32` used as a `Vec` length.

Mirror the GGUF hardening: cap `n_dims` at `GGML_MAX_DIMS` (4), fold the
shape with `checked_mul`, and validate every file-derived length against
the bytes remaining in the file before allocating. Malformed files now
return a clear error instead of panicking or over-allocating.

Adds `candle-core/tests/ggml_tests.rs` covering the shape overflow,
oversized tensor data, and oversized tensor name cases.
`TensorInfo::read` computes `self.shape.elem_count()` (an unchecked
`dims.iter().product()`) and then `tensor_elems / block_size *
type_size()`, both with unchecked arithmetic. Tensor dimensions come
straight from the file (up to 4 dims, each a full u64), so a crafted GGUF
whose shape overflows `usize` — e.g. two dims of 2^40 — panics with
"attempt to multiply with overflow" in debug builds when the tensor is
loaded, and in release wraps to a bogus small `size_in_bytes` that passes
the remaining-bytes check and yields a QTensor whose huge declared shape
disagrees with its tiny storage (later out-of-bounds in matmul).

Fold the shape with `checked_mul` and guard the `type_size` multiply,
returning a clear error on overflow. This mirrors the GGML loader
hardening in huggingface#3713; the GGUF path (`TensorInfo::read`) had been missed.

Adds `rejects_tensor_shape_product_overflow` to gguf_tests.rs.
@v-code01 v-code01 force-pushed the gguf-reject-zero-alignment branch from f1defd5 to f923ce4 Compare July 10, 2026 18:37
@v-code01 v-code01 changed the title Reject invalid general.alignment in GGUF instead of panicking Harden the GGUF and GGML loaders against crafted files Jul 10, 2026
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