Conversation
`MemForest::deserialize` returns `io::Result`, but two inputs made it
abort or unwind instead of returning `Err`. Both are reachable by anyone
who can hand a node a serialized forest -- a snapshot file, a peer.
1. A node's type tag is a `u64` read straight from the input and matched
against 0 (branch) and 1 (leaf); everything else hit `panic!("Invalid
node type")`. 25 bytes are enough. Now an `InvalidData` error.
2. `_read_one` recurses once per branch node with no bound, so the input
chose the recursion depth. A left spine of nested branches overflows
the stack: ~1.2 MB in a debug build, ~4 MB in release. That one is
the more serious of the two, because a stack overflow aborts the
process rather than unwinding, so a caller cannot contain it with
`catch_unwind` the way it can contain a panic.
A root is a perfect tree of at most `MAX_FOREST_ROWS` rows, so its
leaves sit at most that many levels below it and anything deeper
cannot be a forest this crate serialized. Bounding the recursion
there rejects the crafted input and accepts every real one: the
existing suite, including the JSON forest cases, is unchanged, and a
spine exactly `MAX_FOREST_ROWS` deep still round-trips.
Found while fuzzing snapshot decoding in a downstream crate. Each fix
has a regression test, and each test was confirmed to fail with only its
own fix reverted.
…king `serialize` returns `io::Result<()>` and uses `?` for the two length prefixes, but then `unwrap()`ed the result of writing each root. A writer that fails partway -- a full disk, a closed pipe, a socket that went away -- panics rather than returning the `Err` the signature promises. Separate from the deserialize fixes in the previous commit and safe to take on its own: this one is about a failing writer, not untrusted input.
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.
MemForest::deserializeis not total on untrusted inputdeserializereturnsio::Result, but two inputs make it abort or unwindinstead of returning
Err. Both are reachable by anyone who can hand a node aserialized forest — a snapshot file on disk, or a peer.
Found while fuzzing snapshot decoding in a downstream crate. Two of our seven
fuzz targets reach
MemForest::deserializeand both died within seconds, sothey had to be excluded from the campaign entirely until this was fixed.
1. An out-of-range node type panics
The type tag is a
u64read straight from the input and matched against0(branch) and
1(leaf). Everything else hitpanic!("Invalid node type").25 bytes are enough. Now an
InvalidDataerror.2. Unbounded recursion overflows the stack
_read_onerecurses once per branch node with no bound, so the input choosesthe recursion depth. A left spine of nested branches overflows the stack —
~1.2 MB of input in a debug build, ~4 MB in release.
This is the more serious of the two. A stack overflow aborts the process
rather than unwinding, so a caller cannot contain it with
catch_unwindtheway it can contain a panic. We had wrapped the call in
catch_unwindspecifically to contain issue 1, and discovered while fixing it that the
containment had never covered issue 2 at all.
The bound is
MAX_FOREST_ROWS, and it is not arbitrary: a root is a perfecttree of at most that many rows, so its leaves sit at most that many levels below
it, and anything deeper is not a forest this crate could have serialized. A
spine exactly
MAX_FOREST_ROWSdeep still round-trips; 64 is rejected.3.
serializeunwraps a writer error (separate, second commit)serializereturnsio::Result<()>and?s its two length prefixes, thenunwrap()ed the result of writing each root. A writer that fails partway — fulldisk, closed pipe, socket gone — panicked instead of returning the
Errthesignature promises.
This one is about a failing writer, not untrusted input. It is a separate
commit and can be dropped if you would rather take only the deserialize fixes.
Testing
Four regression tests, all in
src/mem_forest/mod.rs:test_deserialize_rejects_invalid_node_typetest_deserialize_rejects_every_out_of_range_node_type2, 3, 255, 256, u32::MAX, u64::MAXtest_deserialize_rejects_unbounded_nestingMAX_FOREST_ROWS, rejects 64 / 100 / 10,000test_serialize_reports_writer_errorsEach fix was reverted individually and confirmed to fail exactly its own
test and nothing else — re-verified against this branch's merge base, not just
when it was written:
The five crash artifacts our fuzzer produced all replay clean against this
branch. With it applied, the two previously-excluded targets ran 10M and 300k
executions with no crashes, taking one corpus from 8 inputs to 514.
cargo test --allgreen (93 tests),cargo clippy --all-targetsclean,cargo +nightly fmt --checkclean. No public API change; no behaviour changefor any input the crate itself can produce.
Note on the base
This branch is only these two commits, on top of
main. It does not includethe
calculate_hashesfix from #152, which we also carry downstream — that isyours to land separately and I did not want to entangle the two.