Harden format readers against untrusted length fields (CWE-770 unbounded allocation) - #203
Open
li-jin-quan wants to merge 1 commit into
Open
li-jin-quan wants to merge 1 commit into
li-jin-quan wants to merge 1 commit into
Conversation
All chunk header fields read from untrusted files previously drove up-front allocations before any payload byte was read. Readers now grow buffers from the data actually read and fail on truncation. Measured with a counting allocator: a 20-byte file with a Metadata chunk declaring 1 GiB chunk_len triggered a single 1,073,741,824-byte allocation; after the fix, 124 bytes.
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
All binary-format readers in this crate take lengths, counts and shapes from the (untrusted) embedding file and previously used them to drive up-front allocations —
vec![0; n],Vec::with_capacity(n),Array1/Array2::zeros(shape)— before reading any payload byte. A file of a few dozen bytes declaring a large length could force a single allocation of up to 4 GiB (u32 fields) or effectively unbounded (u64 fields), enabling memory-exhaustion DoS (CWE-770 / CWE-400) in any service that loads embedding files from untrusted sources.This PR makes every such reader grow its buffer from the bytes actually read, and fail with a format error if the stream is truncated.
Allocation sites fixed
Metadata::read_chunkchunk_len(u64)vec![0; chunk_len]Take::read_to_end+ truncation checkNdArray(read_ndarray_chunk)rows(u64) ×cols(u32)Array2::zeros((rows, cols))Array2::from_shape_vecNdNorms::read_chunklen(u64)Array1::zeros((len,))Array1::fromQuantizedArray::read_chunkn_embeddings(usize)Array1::zeros+Array2::zeros(u8)read_string(shared by all vocab chunks)string_len(u32)vec![0; string_len]Take::read_to_end+ truncation checkread_vocab_itemsvocab_len(u64)Vec::with_capacity(vocab_len)Header::read_chunkchunk_identifiers_len(u32)Vec::with_capacity(n)n_words×embed_len(text header)Array2::zerosbefore any readfrom_shape_vecn_words×dimsVec::with_capacity(n_words * dims)(multiplication can overflow)saturating_mul+ capped capacitysize(u32)Vec::with_capacity(size)n_buckets×embed_lenVec::with_capacity(n_buckets * embed_len)saturating_mul+ capped capacityArray reads use a shared helper (
util::read_f32_vec/read_u8_vec) that allocates at most 64 Ki elements up front and grows in ≤64 Ki-element chunks, so peak memory is proportional to the data actually present in the stream.Measured impact
With a counting global allocator, parsing the following malicious files:
Metadatachunk declaringchunk_len = 1 GiB:NdArraychunk declaringrows = 1, cols = 268,435,456:Testing
tests/untrusted_alloc.rsassert that parsing the malicious files above never exceeds a 16 MiB single allocation (they fail on the pre-fix code with ~1 GiB).Notes