fix(zip): guard extra-field header read before bounds are known - #1044
Open
shoemoney wants to merge 1 commit into
Open
fix(zip): guard extra-field header read before bounds are known#1044shoemoney wants to merge 1 commit into
shoemoney wants to merge 1 commit into
Conversation
get_zip_cfile_uncompressed_size(), get_zip_cfile_compressed_size(), and
get_zip_cfile_offset() all walk the zip64 extra-field region with:
for (; p + ZIP_EXTRA_SIZE(p) <= pe; p += ZIP_EXTRA_SIZE(p))
ZIP_EXTRA_SIZE(p) expands to ZIP_EXTRA_CONTENTSIZE(p) + kZipExtraHdrSize,
and ZIP_EXTRA_CONTENTSIZE(p) is ZIP_READ16(p+2) — so the loop condition
itself reads p[2..3] before anything has confirmed 4 bytes exist at p.
If the declared extra-field size is 1-3 bytes, that read runs straight
past the end of the region on the very first iteration.
llamafile_open_zip() calls get_zip_cfile_offset() and
get_zip_cfile_compressed_size() on a malloc'd central-directory buffer
sized from an attacker-controlled cdirsize, and only validates that the
declared record extent fits in that buffer, not that the sub-loop stays
inside the declared extra-field bytes before its first read. A crafted
central directory entry with compressedsize (or offset) set to the
zip64 sentinel 0xFFFFFFFF and extrasize 1-3 reaches this on every
zip-format llamafile load.
Fix: check p + kZipExtraHdrSize <= pe before evaluating ZIP_EXTRA_SIZE(p)
at all, in all three functions since they share the identical loop.
kZipExtraHdrSize (4) is already defined in zip.h.
Credit: professor-moody, issue mozilla-ai#1038 — this closes the "2 byte one in
zip.c:38" they flagged as not covered by mozilla-ai#1039 (which only touches
llamafile/llamafile.c).
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.
Description
Fixes a heap-buffer-overflow READ in
llamafile/zip.cthatprofessor-moodyflagged in #1038 but that #1039 does not touch (#1039 only changesllamafile/llamafile.c, adding a bound check on the result of these functions — it does not fix the out-of-bounds read that happens inside them while walking the extra field). This PR closes that gap.#1038 says, about this exact spot:
This PR is the fix for the
zip.c:38one, plus the two other call sites that share the identical loop and are vulnerable the same way.The bug
get_zip_cfile_uncompressed_size()(zip.c:26),get_zip_cfile_compressed_size()(zip.c:38), andget_zip_cfile_offset()(zip.c:54) all walk the zip64 extra-field region with the same loop:ZIP_EXTRA_SIZE(P)expands toZIP_EXTRA_CONTENTSIZE(P) + kZipExtraHdrSize, andZIP_EXTRA_CONTENTSIZE(P)isZIP_READ16((P)+2)(zip.h:256,258). So the loop condition readsp[2]/p[3]before anything has confirmed that 4 bytes exist atp. If the extra-field region (pe - p, i.e.ZIP_CFILE_EXTRASIZE(z)) is 1-3 bytes on the first iteration, that read runs pastpebefore the check that's supposed to stop it ever executes.pe = ZIP_CFILE_EXTRA(z) + ZIP_CFILE_EXTRASIZE(z), andZIP_CFILE_EXTRASIZEis a raw 16-bit field out of the central directory record — fully attacker controlled.Reachability:
llamafile.c:177-178callsget_zip_cfile_offset()andget_zip_cfile_compressed_size()directly oncdirdata + entry_offset, wherecdirdataismalloc(cdirsize)andcdirsizeis itself attacker controlled. Entry enumeration checks that the record's declared extent fits insidecdirdata, but that says nothing about whether the sub-loop inside these functions stays inside the declared extra-field bytes before its first read. A crafted central directory entry withcompressedsize(oroffset) set to the zip64 sentinel0xFFFFFFFFandextrasize= 1, 2, or 3 hits this.llamafile_open_zip()runs on every zip-format llamafile load.The fix
Check that a full 4-byte extra-field header actually fits before evaluating
ZIP_EXTRA_SIZE(p), in all three functions (leaving two of three known-identical instances unfixed didn't seem defensible):kZipExtraHdrSizeis already defined as 4 in zip.h. No other logic changes.Testing
This is C, so the proof is AddressSanitizer, not a unit test. I compiled the actual unmodified
zip.c/zip.hfrom this repo withclang -fsanitize=addressand drove all three functions with a 47-byte heap buffer: a minimal 46-byte cfile header + 1-byte extra field, withcompressedsize/uncompressedsize/offsetall set to the zip64 sentinel so each function walks into the extra field.RED — before the fix,
get_zip_cfile_compressed_size():The same input drove the other two functions before the fix and both aborted identically:
GREEN — after the fix, same crafted input, all three functions in one run:
All three now correctly report "not found" (-1) instead of reading out of bounds, exactly the behavior the code already has for a well-formed-but-absent zip64 record.
Regression — well-formed archive still parses identically. I built a second harness with a genuine 20-byte zip64 extra field (id 0x0001, size 24, holding real uncompressed/compressed/offset 64-bit values) and called all three functions before and after the patch:
Identical output before and after the fix. The added check only rejects a header the loop can't safely read; it never changes behavior for a record that actually has one.
Build:
make setup && .cosmocc/4.0.2/bin/make -j8completed cleanly against this branch with no new warnings from the changed file.Recipe (if you want to reproduce this yourself)
Checklist
llama.cpp/,whisper.cpp/, orstable-diffusion.cpp/, I also updated the matching*.patches/files. (Not applicable — this change is entirely withinllamafile/zip.c, not a submodule.)AI Usage Information
When answering reviewer questions, please respond yourself rather than pasting reviewer comments into an AI system and posting the reply back unchanged.