Harden DeBin against untrusted length prefixes (CWE-770 unbounded allocation, plus String overflow panic) - #172
Open
li-jin-quan wants to merge 1 commit into
Conversation
…ng overflow Vec/HashSet/HashMap deserialization preallocated capacity from an 8-byte attacker-controlled length before reading any element, allowing a truncated 8-byte buffer to force up-to-gigabyte allocations (process abort at u64-scale requests). They now grow from actual data, matching LinkedList/BTreeSet/BTreeMap. String bounds check also used unchecked `*o + len`, which could overflow and panic on the subsequent slice; now uses checked_add. Counting-allocator measurements (8-byte truncated input): Vec<u8> 64 MiB -> 124 B; HashMap<u8,u8> 384 MiB -> 544 B; HashSet<u8> 256 MiB -> 544 B.
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.
Harden
DeBindeserialization against untrusted length prefixes (CWE-770 unbounded allocation + overflow panic)Summary
DeBin(the binary format deserializer) reads a length prefix from the input and, forVec<T>,HashSet<T>andHashMap<K, V>, uses it to preallocate capacity before a single element is read. A malformed 8-byte input can therefore force allocations of gigabytes — or abort the process outright — while contributing essentially no data. This PR also fixes an integer overflow inString's bounds check that turns ausize::MAXlength into a panic.Details
Lengths are deserialized via
DeBin for usize, which reads an 8-byte LEu64from the untrusted buffer. For collections:The same unchecked pattern exists in
HashSet::with_capacity(len)andHashMap::with_capacity(len). By contrast, theLinkedList,BTreeSetandBTreeMapimpls in the same file already use::new()and grow from actual data, andStringbounds-checks the declared length — so this looks like an oversight in exactly the three impls that preallocate.The overflow.
String's check*o + len > d.len()can itself overflow on 64-bit targets (e.g.*o = 1,len = usize::MAX→ the sum wraps to8), after which&d[*o..(*o + len)]panics with "slice index starts at 9 but ends at 8". In release builds without overflow checks this is a reachable panic on malformed input.Impact
Deserializing untrusted binary data is a normal use case for
DeBin(network peers, user-supplied save files / documents, IPC payloads). With a counting global allocator, an 8-byte truncated buffer:Vec<u8>(declares 64 MiB)ErrHashMap<u8, u8>(declares 64 MiB entries)ErrHashSet<u8>(declares 64 MiB entries)ErrString(declaresusize::MAXat offset 1)ErrAmplification is bounded only by
u64::MAX * size_of::<T>(). Declaring more than physical memory aborts the process inhandle_alloc_error— verified: an 8-byte input declaring 1 TiB forVec<u8>exits with0xC0000409(Windows abort);catch_unwindcannot intercept it. A service deserializing peer-supplied payloads can be killed by a single small message.Fix
No API change, no magic-number caps: the three collection impls now grow from the data actually present (
::new()+ push/insert), consistent with the existingLinkedList/BTreeSet/BTreeMapimpls, andStringuseschecked_addfor the end offset. Valid inputs produce identical results; only the preallocation is gone (a few geometric re-growths for large legitimate vectors).Testing
New
tests/untrusted_alloc.rsuses a counting#[global_allocator]to assert that peak allocation stays proportional to the actual input (< 8 KiB) and that the overflow case returnsErrinstead of panicking. All checks run in a single#[test]because the global allocator is process-global and parallel tests would pollute the counters. Verified bidirectionally: with the fix stashed, the test fails (peak 67,112,831 bytes); with the fix applied, the fullcargo test --lib --testssuite passes.Happy to adjust if you would prefer a bounded
try_reserveinstead of pure incremental growth.Reporter: Li Jinquan — github.com/li-jin-quan