Keep gguflib input-validation asserts active in release builds#3436
Open
qflen wants to merge 2 commits intoml-explore:mainfrom
Open
Keep gguflib input-validation asserts active in release builds#3436qflen wants to merge 2 commits intoml-explore:mainfrom
qflen wants to merge 2 commits intoml-explore:mainfrom
Conversation
gguflib.c relies on assert() to validate untrusted GGUF input (e.g. tensor ndim <= GGUF_TENSOR_MAX_DIM). In release builds those asserts are compiled out by -DNDEBUG, leaving MLX to parse and dereference the corrupted tensor struct — get_shape() then reads past the fixed 8-element dim[] array, and downstream code uses clobbered offset/weights_data fields. - Force -UNDEBUG on the gguflib target so the asserts stay live in every build configuration. - Add a defensive bounds check in get_shape() so the MLX side also refuses oversized ndim if the assert is ever disabled by a packager or replaced by a system gguflib. Closes part of ml-explore#3358.
9a6cd9f to
1e6553f
Compare
4 tasks
zcbenz
reviewed
Apr 23, 2026
Collaborator
zcbenz
left a comment
There was a problem hiding this comment.
Thanks for continuing the work! I think you are doing the right fix in this PR.
qflen
added a commit
to qflen/mlx
that referenced
this pull request
Apr 24, 2026
zcbenz pointed out on ml-explore#3436 that the defense-in-depth ndim bounds check in get_shape() is unnecessary: with -UNDEBUG keeping gguflib's own assert live, a tensor with ndim > GGUF_TENSOR_MAX_DIM aborts inside gguflib before get_shape() is ever called. Remove the check and its unit test (which was the only reason tests/ needed to pull in the gguflib include path).
cd8b6d3 to
0de7fb2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the GGUF out-of-bounds read reported in #3358, using the CMake-based approach suggested in the #3359 review thread (force-enable all gguflib asserts) rather than disabling them.
Why
gguflibrelies onassert()to validate untrusted tensor headers: notablyassert(tensor->ndim <= GGUF_TENSOR_MAX_DIM)ingguflib.c. Release builds defineNDEBUG, so that assert compiles to a no-op and a crafted GGUF withndim > 8lets gguflib write past the fixed 8-elementdim[]array intooffset/bsize/weights_data/ the caller's stack frame, after which MLX'sget_shape()reads the same range and downstream code dereferences the attacker-controlledweights_datapointer.The earlier PR #3359 attempted to address this with
target_compile_definitions(gguflib PRIVATE NDEBUG), which definesNDEBUGinstead of undefining it — so the asserts remained disabled. This PR uses-UNDEBUGto keep them live.Changes
mlx/io/CMakeLists.txt:target_compile_options(gguflib PRIVATE -UNDEBUG)so gguflib's existing input-validation asserts stay in every build configuration.mlx/io/gguf.cpp: defensivendim > GGUF_TENSOR_MAX_DIMcheck inget_shape()as defense-in-depth for packagers that link a systemgguflibor otherwise strip asserts.tests/load_tests.cpp+tests/CMakeLists.txt: newTEST_CASEexercising the defensiveget_shape()check; guarded byMLX_BUILD_GGUFand pulls the gguflib include path in viaFetchContent_GetProperties.Notes
gguf_tensorwithndim = GGUF_TENSOR_MAX_DIM + 1and expectsstd::runtime_error). The primary fix — gguflib's ownassert()firing on malformed input — can't be covered by a doctestCHECK_*because it triggersabort(). I can add a death-style or subprocess test on request.Checklist
pre-commit run --all-filesagainst the four changed files; clang-format and cmake-format passed.get_shape()bounds check intests/load_tests.cpp. The primary gguflibabort()path is not covered — see Notes.