Skip to content

fix(module): bound section allocations during rwasm decode - #174

Open
dmitry123 wants to merge 1 commit into
develfrom
claude/instructionsetdecode-capacity-fix-b19b80
Open

fix(module): bound section allocations during rwasm decode#174
dmitry123 wants to merge 1 commit into
develfrom
claude/instructionsetdecode-capacity-fix-b19b80

Conversation

@dmitry123

Copy link
Copy Markdown
Member

Fixes FLU-1096.

Problem

Every section of the rwasm binary format starts with a u64 length read straight from untrusted input. That length went directly into Vec::with_capacity, so an 11-byte binary could request an arbitrary allocation before a single element was read:

  • u64::MAXcapacity overflow panic in raw_vec
  • a large non-overflowing value such as 2^40 → ~8 TiB request → handle_alloc_errorabort, which no catch_unwind can contain

Reachable from every decode entry point (new, new_checked, new_checked_exact, new_verified, new_verified_exact), because the allocation happens during decoding, before verification runs.

Scope correction

The issue attributed this to the hand-written Decode impl for InstructionSet bypassing bincode's claim_container_read guard, and expected the derived sibling fields to be safe. That turned out not to hold: claim_container_read is a no-op unless the config sets a decode limit, and the module is decoded with bincode::config::legacy(), which is NoLimit. bincode's own Vec decoder then reaches Vec::with_capacity(len) / vec![0u8; len] unguarded.

Verified against devel — all four sections panic identically:

section before after
code_section capacity overflow UnexpectedEnd { additional: 4 }
data_section capacity overflow UnexpectedEnd { additional: 4096 }
elem_section capacity overflow UnexpectedEnd { additional: 4 }
hint_section capacity overflow UnexpectedEnd { additional: 4096 }

So the fix covers all four rather than code_section alone.

Fix

New src/types/codec.rs holds allocation-safe primitives for length-prefixed sections, used by InstructionSet::decode and RwasmModuleInner::decode:

  • decode_section_length converts the u64 prefix through usize::try_from, returning OutsideUsizeRange instead of truncating on 32-bit targets.
  • decode_section_vec reserves at most 4096 elements up front and grows on demand per element, so peak memory tracks the input actually present. It also performs the claim_container_read / unclaim_bytes_read accounting, so the code section now honours a decode limit if one is ever configured.
  • decode_section_bytes reserves and reads byte sections in 64 KiB chunks, reading straight into the vector's tail rather than through a temporary buffer.

A truncated section now fails with UnexpectedEnd the moment the reader runs dry, with no allocation proportional to the claimed length.

Not a size limit

The issue also suggested pairing this with an explicit N_MAX_CODE_SECTION_LEN cap checked in verify_module. I deliberately left that out: picking a maximum module/section size is a consensus-affecting policy decision that would reject binaries which decode today, and it is not needed to close the DoS — bounding the allocation by the available input is sufficient and behaviour-preserving. docs/security-considerations.md:42 still lists "enforce module/section size limits" as an open mitigation, and it is worth its own issue.

Tests

  • test_decode_rejects_oversized_section_lengths — all four sections × {u64::MAX, 2^40}, asserting a clean DecodeError instead of a panic or abort.
  • test_decode_accepts_large_hint_section — a 512 KiB hint section still round-trips, guarding against the fix turning into a size cap.

Full suite green (158 tests), clippy --all-targets --all-features clean, --no-default-features builds. The remaining cargo fmt --check diffs (src/lib.rs, src/types/mod.rs:58, src/vm/memory.rs:57) pre-exist on devel and are untouched here.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@dmitry123, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 58 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c0e0593-c7cc-44e4-b349-e05a0adc1ec1

📥 Commits

Reviewing files that changed from the base of the PR and between b8f6091 and 6747f66.

📒 Files selected for processing (4)
  • src/isa/mod.rs
  • src/module/mod.rs
  • src/types/codec.rs
  • src/types/mod.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Criterion results (vs baseline)


running 77 tests
test compiler::compiled_expr::tests::compiledexpr_eval_const_returns_none_for_global_or_funcref ... ignored
test compiler::compiled_expr::tests::compiledexpr_from_const_roundtrips ... ignored
test compiler::compiled_expr::tests::compiledexpr_funcref_and_global_introspection ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_global_get_uses_context ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i32_add_mixed_const_and_global ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i32_add_mixed_global_and_funcref ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i32_add_wraps ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i32_const ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i32_sub_wraps ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i64_const ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_i64_mul_wraps ... ignored
test compiler::compiled_expr::tests::compiledexpr_new_ref_func_uses_context ... ignored
test compiler::compiled_expr::tests::compiledexpr_zero_is_zero ... ignored
test compiler::compiled_expr::tests::constop_eval_returns_value ... ignored
test compiler::compiled_expr::tests::empty_eval_context_always_none ... ignored
test compiler::compiled_expr::tests::eval_with_context_reads_globals_and_funcs ... ignored
test compiler::compiled_expr::tests::expr_op_combines_operands_and_propagates_none ... ignored
test compiler::compiled_expr::tests::funcrefop_reads_from_context ... ignored
test compiler::compiled_expr::tests::globalop_maps_value_kinds_correctly ... ignored
test compiler::compiled_expr::tests::op_clone_panics_for_expr_variant - should panic ... ignored
test compiler::compiled_expr::tests::op_clone_works_for_non_expr_variants ... ignored
test compiler::compiled_expr::tests::op_constant_encodes_f32_f64_bits ... ignored
test compiler::compiled_expr::tests::op_constant_encodes_funcref_externref_ids ... ignored
test compiler::compiled_expr::tests::op_constant_encodes_i32_i64 ... ignored
test compiler::drop_keep::tests::test_drop_keep_translation ... ignored
test compiler::func_type_registry::tests::deduplicates_matching_signatures ... ignored
test compiler::func_type_registry::tests::index_lookup_is_stable ... ignored
test compiler::func_type_registry::tests::resolves_unique_signatures_correctly ... ignored
test compiler::parser::tests::unsupported_component_model_returns_error ... ignored
test module::tests::test_decode_accepts_large_hint_section ... ignored
test module::tests::test_decode_exact_rejects_trailing_garbage ... ignored
test module::tests::test_decode_module_wo_source_pc ... ignored
test module::tests::test_decode_rejects_oversized_section_lengths ... ignored
test module::tests::test_decode_rejects_partial_source_pc ... ignored
test module::tests::test_endianness ... ignored
test module::tests::test_module_encoding ... ignored
test module::verification::tests::accepts_verified_encoded_module ... ignored
test module::verification::tests::regular_construction_does_not_verify ... ignored
test module::verification::tests::regular_decode_does_not_verify ... ignored
test module::verification::tests::rejects_branch_target_outside_code_section ... ignored
test module::verification::tests::rejects_call_target_outside_code_section ... ignored
test module::verification::tests::rejects_missing_table_index_payload ... ignored
test module::verification::tests::rejects_section_index_outside_limits ... ignored
test module::verification::tests::rejects_source_pc_outside_code_section ... ignored
test module::verification::tests::rejects_zero_local_depth ... ignored
test strategy::types::tests::checked_memory_range_end_rejects_overflow ... ignored
test types::nan_preserving_float::tests::test_neg_nan_f32 ... ignored
test types::nan_preserving_float::tests::test_neg_nan_f64 ... ignored
test types::nan_preserving_float::tests::test_ops_f32 ... ignored
test types::nan_preserving_float::tests::test_ops_f64 ... ignored
test types::opcode::tests::test_fpu_opcode_encoding_uses_offset ... ignored
test types::opcode::tests::test_opcode_code_values ... ignored
test types::opcode::tests::test_opcode_encoding ... ignored
test types::opcode::tests::test_opcode_encoding_uses_explicit_code ... ignored
test types::opcode::tests::test_opcode_size ... ignored
test types::units::tests::bytes_new16 ... ignored
test types::units::tests::bytes_new32 ... ignored
test types::units::tests::bytes_new64 ... ignored
test types::units::tests::pages_checked_add ... ignored
test types::units::tests::pages_checked_sub ... ignored
test types::units::tests::pages_max ... ignored
test types::units::tests::pages_new ... ignored
test types::units::tests::pages_to_bytes ... ignored
test types::value::copysign_regression_works ... ignored
test types::value::wasm_float_max_regression_works ... ignored
test types::value::wasm_float_min_regression_works ... ignored
test vm::store::tests::clamps_runtime_memory_limit_to_global_maximum ... ignored
test wasmtime::tests::test_call_with_charging_linear_wasmtime ... ignored
test wasmtime::tests::test_call_with_charging_param_overflow_wasmtime ... ignored
test wasmtime::tests::test_call_with_charging_quadratic_wasmtime ... ignored
test wasmtime::tests::test_wasmtime_caller_memory_read_into_vec_checks_bounds_before_allocating ... ignored
test wasmtime::tests::test_wasmtime_caller_missing_memory_returns_trap ... ignored
test wasmtime::tests::test_wasmtime_executor_memory_read_into_vec_checks_bounds_before_allocating ... ignored
test wasmtime::tests::test_wasmtime_executor_missing_entrypoint_returns_trap ... ignored
test wasmtime::tests::test_wasmtime_snapshot_missing_memory_returns_trap ... ignored
test wasmtime::types::tests::maps_unknown_wasmtime_error_to_illegal_opcode ... ignored
test wasmtime::types::tests::maps_wasmtime_traps_to_rwasm_traps ... ignored

test result: ok. 0 passed; 0 failed; 77 ignored; 0 measured; 0 filtered out; finished in 0.00s

Comparisons/bench_native
                        time:   [5.3830 ns 5.5117 ns 5.6680 ns]
Found 127 outliers among 1000 measurements (12.70%)
  27 (2.70%) high mild
  100 (10.00%) high severe
Comparisons/bench_strategy_wasmtime
                        time:   [22.595 µs 23.091 µs 23.648 µs]
Found 57 outliers among 1000 measurements (5.70%)
  40 (4.00%) high mild
  17 (1.70%) high severe
Comparisons/bench_strategy_rwasm
                        time:   [14.526 µs 14.715 µs 14.942 µs]
Found 58 outliers among 1000 measurements (5.80%)
  35 (3.50%) high mild
  23 (2.30%) high severe

Heads-up: runner perf is noisy; treat deltas as a smoke check.

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.48485% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/module/mod.rs 96.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants