tests/fuzz: Component Model binary-parser fuzz target + four memory-safety fixes#3
Open
matthargett wants to merge 8 commits into
Conversation
…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.
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.
Follow-up to #1 / #2 (relaxed-SIMD + legacy-EH), opening here against
dev/cm_wasip2_completeper 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-watchosit runs with a 32-bitsize_t.The existing
wasm-mutator-fuzztarget only loads core modules and never reacheswasm_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):
export_nameafter ownership transferred to*outparse_component_decl_exportparse_func_type,parse_component_instance_type,parse_component_typeparse_core_name(and a leak on success)wasm_component_parse_instances_sectionsizeof— wraps on 32-bitsize_t, allocation DoS on any targetEach 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/*.wasmseeds within ~2 minutes, so they're easy to re-confirm.Two notes:
wasm_loaderbehaviour (already inwasm-mutator-fuzz's scope), not a component bug, so it's documented as-ignore_oomsrather than "fixed" here.