Skip to content

quantized: validate the raw GGML byte slice before reinterpreting it as blocks - #3963

Open
apollo-2006 wants to merge 1 commit into
huggingface:mainfrom
apollo-2006:fix-ggml-raw-slice-validation
Open

apollo-2006 wants to merge 1 commit into
huggingface:mainfrom
apollo-2006:fix-ggml-raw-slice-validation

Conversation

@apollo-2006

Copy link
Copy Markdown

Fixes #3815.

from_raw_data in candle-core/src/quantized/ggml_file.rs turned a &[u8] into a &[T] with no checks:

let raw_data_ptr = raw_data.as_ptr();
let n_blocks = size_in_bytes / std::mem::size_of::<T>();
let data = unsafe { std::slice::from_raw_parts(raw_data_ptr as *const T, n_blocks) };

Two separate preconditions were missing. qtensor_from_ggml is public and safe, so both are reachable from safe code.

1. Length is never checked (not in the issue)

size_in_bytes is computed from dims and never compared against raw_data.len(). A buffer shorter than the dims imply produces a slice running past the end of the allocation. Miri:

error: Undefined Behavior: constructing invalid value of type &[BlockQ4_0]:
encountered a dangling reference (going beyond the bounds of its allocation)
 --> candle-core/src/quantized/ggml_file.rs:128:24
    candle_core::quantized::ggml_file::from_raw_data::<BlockQ4_0>
    candle_core::quantized::ggml_file::qtensor_from_ggml

This one is a plain out-of-bounds read, so it is now rejected with an error.

2. Alignment, as reported

A &[u8] carries 1-byte alignment while &[T] requires align_of::<T>(). Worth adding to the issue: this does not just fail under Miri. A debug build aborts on the standard library's own precondition check, so an unaligned caller crashes outright:

unsafe precondition(s) violated: slice::from_raw_parts requires the pointer to be
aligned and non-null, and the total size of the slice not to exceed `isize::MAX`
signal: 6, SIGABRT

A zero-element tensor hits the same abort today, because an empty Vec<u8> has a dangling pointer of 0x1, which is not aligned for the block type.

Why this does not assert the alignment

The issue suggests mirroring the assertions in quantized/mod.rs::as_t_slice. I started there, and it is not safe to do: the alignment a Vec<u8> ends up with is decided by the allocator, not the caller, so rejecting an unaligned buffer rejects valid data.

Miri makes this concrete, since it gives a Vec<u8> only the 1-byte alignment it actually guarantees. With an assertion in place, an ordinary well-formed tensor is rejected:

called `Result::unwrap()` on an `Err` value: ggml tensor data is not aligned to 4 bytes

and in the same run a deliberately offset buffer came out aligned and was accepted. The alignment of a byte buffer simply is not something to branch correctness on.

So the slice is borrowed when the buffer happens to be aligned and copied into an aligned Vec<T> when it is not. That is always correct, costs nothing on the usual path where the allocator over-aligns, and removes the zero-element abort as a side effect.

Copying is sound for these types for the same reason GgmlType::zeros is: the ggml block types are plain data.

Worth flagging separately: as_t_slice has the same latent fragility, since its assert_eq! on alignment would panic on valid data under such an allocator. I left it alone to keep this focused, happy to follow up if you want it changed.

Tests

candle-core/tests/ggml_file_tests.rs, asserting on loaded contents rather than on which branch is taken, so they hold under either allocator. Against the unfixed code:

test before after
unaligned_data_loads_correctly SIGABRT pass
accepts_empty_tensor SIGABRT pass
rejects_truncated_data silently accepted pass
aligned_data_loads_correctly (control) pass pass

Verified: cargo test -p candle-core 232 passed 0 failed, cargo clippy -p candle-core --all-targets -- -D warnings clean, cargo fmt --check clean, and cargo +nightly miri test -p candle-core --test ggml_file_tests reports no undefined behaviour.

…as blocks

`from_raw_data` turned a `&[u8]` into a `&[T]` with `slice::from_raw_parts` and
no checks at all:

    let raw_data_ptr = raw_data.as_ptr();
    let n_blocks = size_in_bytes / std::mem::size_of::<T>();
    let data = unsafe { std::slice::from_raw_parts(raw_data_ptr as *const T, n_blocks) };

Two separate preconditions were missing, and `qtensor_from_ggml` is public and
safe, so both are reachable from safe code.

Length. `size_in_bytes` is derived from `dims`, never compared against
`raw_data.len()`. A caller passing a buffer shorter than the dims imply gets a
slice that runs past the end of the allocation:

    error: Undefined Behavior: constructing invalid value of type
    &[BlockQ4_0]: encountered a dangling reference (going beyond the bounds
    of its allocation)

Alignment. A `&[u8]` carries 1-byte alignment while `&[T]` requires
`align_of::<T>()`. In a debug build this already aborts on the standard library
precondition check, so an unaligned caller crashes rather than silently
misbehaving. A zero-element tensor hits the same abort, because an empty
`Vec<u8>` has a dangling pointer of 0x1.

The length case is a genuine error, so it now bails. The alignment case is not:
what alignment a `Vec<u8>` ends up with is decided by the allocator, not the
caller, so rejecting an unaligned buffer would reject valid data. Under Miri,
which gives a `Vec<u8>` only the 1-byte alignment it actually guarantees, an
assertion rejects an ordinary well-formed tensor. So the slice is borrowed when
the buffer happens to be aligned and copied into an aligned `Vec<T>` when it is
not, which also removes the zero-element abort.

Copying is sound for these types for the same reason `GgmlType::zeros` is: the
ggml block types are plain data.

Adds regression tests. Without the fix, the unaligned and empty cases abort and
the truncated case is silently accepted; the aligned case is unaffected.
Copilot AI lite review requested due to automatic review settings September 7, 2026 18:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@apollo-2006

Copy link
Copy Markdown
Author

@ivarflakstad flagging one thing up front, since it is easy to read this PR as not doing what #3815 asked for.

The issue proposes mirroring the assertions in quantized/mod.rs::as_t_slice, and treats the inconsistency between the two raw-slice constructors as the defect. This PR deliberately does not do that. Under Miri, where a Vec<u8> gets only the 1-byte alignment it actually guarantees, that assertion rejects an ordinary well-formed tensor: "ggml tensor data is not aligned to 4 bytes". The alignment a byte buffer ends up with is the allocator's choice, not the caller's, so asserting on it rejects valid data. Hence borrow when aligned, copy when not.

Which leaves the part I cannot decide on my own. as_t_slice still carries that assertion, and it sits on the Metal load path for every GgmlDType at mod.rs:99-113, so it has the same latent panic on an allocator that does not over-align. I left it alone to keep this reviewable, and I see tomsanbear already fixed a UAF in that same function in #3493.

Do you want as_t_slice softened in this PR while the reasoning is in front of you, or kept as a follow-up so this one stays scoped to ggml_file.rs?

This branch has not been deployed

No deployments
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.

quantized/ggml_file.rs: from_raw_parts on a 1-byte-aligned pointer without an alignment check

2 participants