Skip to content

tests/fuzz: Component Model binary-parser fuzz target + four memory-safety fixes#3

Open
matthargett wants to merge 8 commits into
airbus-forks:dev/cm_wasip2_completefrom
rebeckerspecialties:feat/component-parser-fuzz
Open

tests/fuzz: Component Model binary-parser fuzz target + four memory-safety fixes#3
matthargett wants to merge 8 commits into
airbus-forks:dev/cm_wasip2_completefrom
rebeckerspecialties:feat/component-parser-fuzz

Conversation

@matthargett

Copy link
Copy Markdown

Follow-up to #1 / #2 (relaxed-SIMD + legacy-EH), opening here against dev/cm_wasip2_complete per our LinkedIn DM thread with @dimi181 (Mihai Dimoiu).

We're evaluating WAMR + this branch's Component Model / WASIp2 work as the runtime for an interpreter-only, App-Store-eligible iOS/watchOS app, so the component binary parser matters to us specifically: it ingests fully untrusted bytes with no JIT-time mitigation, and on arm64_32-apple-watchos it runs with a 32-bit size_t.

The existing wasm-mutator-fuzz target only loads core modules and never reaches wasm_component_parse_sections. This PR adds a libFuzzer target that does (LLVM-free; component-model on, WASI-p2 host layer off so only parser/validator/WAVE are exercised — details + the dead-strip/stub rationale in the diff and README).

It found four distinct memory-safety bugs in the parser's partial-parse / error-path cleanup, all fixed here (two preceding commits):

# Bug Site
1 double-free of export_name after ownership transferred to *out parse_component_decl_export
2 wild deref walking an uninitialized count-array tail after a mid-loop parse failure (recursive, any nesting depth) parse_func_type, parse_component_instance_type, parse_component_type
3 UAF of a pre-allocated name passed to self-allocating parse_core_name (and a leak on success) inline-export parse in wasm_component_parse_instances_section
4 unbounded LEB count × sizeof — wraps on 32-bit size_t, allocation DoS on any target same three count-array sites as #2

Each fix is verified by its reducing input plus a clean 300s ASan+UBSan campaign (111k execs) over the in-tree component fixtures. All four crashing inputs were reachable from the existing tests/unit/**/wasm-apps/*.wasm seeds within ~2 minutes, so they're easy to re-confirm.

Two notes:

  • The harness is the durable part — it's wired so you (and we) can keep running it. The fuzzer still flags core-loader OOMs from embedded modules with huge declared function counts; that's pre-existing wasm_loader behaviour (already in wasm-mutator-fuzz's scope), not a component bug, so it's documented as -ignore_ooms rather than "fixed" here.
  • The count-bound checks (component-model: harden canonical ABI bounds checks and trap coverage #4) are the same hardening we flagged as important for ILP32; the README shows how to retarget the harness at a 32-bit toolchain to fuzz that path directly. This is the same differential/fuzzing rigor we apply across our runtime work — context + on-device benchmarks at https://github.com/rebeckerspecialties/wasm-benchmark

…rser

Found by a new libFuzzer target for wasm_component_parse_sections
(tests/fuzz/component-fuzz). Four crashing inputs reduced to three
distinct defects in the type-section parser's partial-parse cleanup,
all in this file:

1. Double-free / heap-use-after-free in parse_component_decl_export.
   Once (*out)->export_name = export_name transfers ownership to *out,
   the extern_desc allocation/parse error paths still called
   wasm_runtime_free(export_name). *out is returned to the caller, whose
   centralized cleanup (free_component_instance_decl ->
   free_component_export_name) then freed export_name a second time.
   Fix: drop the local frees of objects already owned by *out.

2. Wild dereference walking uninitialized count-arrays. parse_func_type
   (param list), parse_component_instance_type (instance decls) and
   parse_component_type (component decls) each set the element count to
   the full LEB-declared value, then allocated the array WITHOUT zeroing
   it. A parse failure partway through the populate loop left an
   uninitialized tail; the cleanup walk reads tag / label pointers for
   every `count` entry (recursively, at arbitrary nesting depth) and
   dereferenced garbage. Fix: memset each array to zero after
   allocation.

3. Unbounded LEB-decoded element counts. The same three sites multiplied
   the count by sizeof(element) with no bound. On 32-bit size_t targets
   (arm64_32-apple-watchos, i686, ESP32) the multiply can wrap and
   under-allocate; on any target a huge count is a cheap allocation DoS.
   Fix: reject a count larger than the remaining input (each element is
   at least one byte), which bounds the allocation and prevents the
   wrap.

Verified: the four reproducers no longer crash and a 300s ASan+UBSan
campaign (111k execs) over the in-tree component fixtures is clean.
…s parse

Found by the component-parser fuzz target (tests/fuzz/component-fuzz).

The inline-export loop in wasm_component_parse_instances_section
pre-allocated a WASMComponentCoreName and passed it to parse_core_name.
parse_core_name allocates its own result and only writes *out on
success, so:

  - on success the pre-allocated struct leaked (overwritten by
    parse_core_name's own allocation);
  - on failure *out was left pointing at the still-uninitialized
    pre-allocation, and the error path called
    free_core_name(name) -> wasm_runtime_free(name->name), dereferencing
    an uninitialized pointer (ASan: SEGV / heap-use-after-free in
    free_core_name).

Fix: initialize name to NULL and let parse_core_name own the allocation
and its own cleanup on failure, matching every other parse_core_name
call site in the component model.

Verified against the reducing input; clean under the ASan+UBSan fuzz
campaign.
The existing wasm-mutator-fuzz target only loads core wasm modules and
never reaches the component-model binary parser, which decodes a fully
untrusted buffer across all 13 section types. This adds a libFuzzer
target for wasm_component_parse_sections.

It is LLVM-free: an interpreter-only build with the component model on
and the WASI Preview 2 host layer off (WAMR_BUILD_LIBC_WASI=0), so only
the parser / validator / WAVE helpers are exercised. Dead-strip removes
the unreferenced instantiation/canonical-ABI/host code; two inert stubs
(host_resolve_stubs.c) satisfy the WASI-resolution symbols that
wasm_resolve_imports_WASI references but the parser never calls.

ASan + UBSan + libFuzzer are on by default (auto-skipped under
oss-fuzz). See README.md for build/run, corpus seeding from the in-tree
component fixtures, and how to retarget the harness at a 32-bit
toolchain to also fuzz the ILP32 size_t-overflow path.

This target found the four memory-safety bugs fixed in the two
preceding commits.
The error-path fixes touched a few lines that clang-format-14 (the Coding
Guidelines CI gate) reformats. Whitespace only, no behaviour change.
… to C/CXX

Two fixes uncovered by review of the component-parser fuzz target.

1. core/iwasm/common/component-model/wasm_component_types_section.c

   In parse_component_type's decl loop, a failed parse_component_decl freed
   the transient component_decl shell with a bare wasm_runtime_free(), without
   first freeing the decl's contents. parse_component_decl can populate those
   contents (for an import decl: import_name and its string) before failing on
   a later sub-parse such as the extern-desc, so that memory leaked. The
   centralized cleanup (free_component_component_type -> free_component_decl)
   never reaches this shell because component_decls[i] is still zeroed.

   Mirror the parse_component_instance_type fail path: call
   free_component_decl(component_decl) to release the contents before
   wasm_runtime_free(component_decl). free_component_decl was only defined
   later in the file, so add a forward declaration alongside the existing
   free_component_instance_decl / free_component_types_entry prototypes.

2. tests/fuzz/component-fuzz/CMakeLists.txt

   The ASan/UBSan/libFuzzer add_compile_options() were directory-scoped and so
   also reached the runtime's hand-written .s assembly TUs (e.g.
   invokeNative_em64*.s on x86_64), where the sanitizer/fuzzer flags are
   meaningless and a non-Clang assembler can reject them.

   Scope every instrumentation compile flag to C and CXX with
   $<$<COMPILE_LANGUAGE:C,CXX>:...> so the ASM TUs see none of them, while the
   C/C++ translation units keep full ASan + UBSan + libFuzzer coverage.
   add_link_options is left unscoped (it is per-link, not per-language). Also
   require a Clang ASM compiler via CMAKE_ASM_COMPILER_ID, matching the
   existing C-compiler Clang guard, so the assembler stays consistent with the
   Clang driver flags CMake forwards.

Verified the component-model translation unit compiles in the requested
configuration (darwin product-mini, COMPONENT_MODEL=1, LIBC_WASI=1,
FAST_INTERP=1) and that the fuzz CMake configures with the sanitizer/fuzzer
flags present on C/CXX flags and absent from ASM flags.
…h, resolve wat2wasm from /opt/wabt)

Matches the fixes already on the WASIp2 base so AOT-only build_iwasm and the unit-test AoT fixtures build on this branch; no interpreter behavior change.
The runner ships GNU bison 2.3, which can't parse the component-model
wave-parser grammar (Bison 2.4+ %code blocks); put Homebrew bison on PATH
before build_samples_wasm_c_api.
module_func_invoke is declared and set unconditionally but only read inside
WASM_ENABLE_COMPONENT_MODEL blocks, so a component-model-off build (e.g. the
nuttx AOT config) fails under gcc -Werror=unused-but-set-variable. Guard its
declaration and assignment to match its sibling component_func_invoke.
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.

1 participant