Skip to content

tests/fuzz: component-model parser fuzz target + 4 memory-safety fixes (on cm_wasip2)#7

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

tests/fuzz: component-model parser fuzz target + 4 memory-safety fixes (on cm_wasip2)#7
matthargett wants to merge 8 commits into
dev/cm_wasip2_completefrom
feat/component-parser-fuzz

Conversation

@matthargett

Copy link
Copy Markdown
Member

Internal staging PR mirroring airbus-forks#3. Already based on the wasip2 head; adds the LLVM-free component-parser libFuzzer target and four verified error-path memory-safety fixes (double-free, UAF-from-pre-alloc, uninitialized count-arrays, count bounds). Part of the cm_wasip2 integration stack.

…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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4830ce9b47

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/fuzz/component-fuzz/CMakeLists.txt Outdated
Comment thread core/iwasm/common/component-model/wasm_component_types_section.c
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