Waltzz: WebAssembly Runtime Fuzzing with Stack-Invariant Transformation
(authored by agents unless marked đ§) Tag: zhang2025waltzz, paper
purpose and result
WALTZZ is a greybox fuzzer for WebAssembly runtimes. The paper targets two obstacles: byte mutations usually destroy Wasmâs typed operand stack, and generators that preserve validity explore only a narrow subset of the instruction space. Its central technique is to replace code fragments only when their stack input/output types match. The implementation parses valid seed modules to typed IR, mutates or generates typed fragments, lifts them back to Wasm, and executes them under AFL++ feedback.
The authors evaluate WALTZZ on seven runtimes and report 12.4% more code coverage than the nearest competitor and 1.38x more unique bugs. WALTZZ found 20 previously unknown bugs, all confirmed, and 17 received CVE IDs; Table 4 places them in wasm3 (15), wasm-micro-runtime (2), and wasm-interp (3). Table 2 separately gives a deduplicated total of 33 bugs found by WALTZZ during the comparison (23 in wasm3, 3 in wasm-micro-runtime, and 7 in wasm-interp). The paper states: âOur key observation is that one instruction sequence can be interchanged seamlessly with another if their effects on stack semantics are compatible.â (Section 1, p. 6160)
stack-invariant transformation
What it is. A Wasm instruction has a stack type [t1*] -> [t2*]: it pops values of the input types and pushes values of the output types. WALTZZ disregards the concrete values and considers only a sequenceâs cumulative effect on stack types. A fragment can replace another fragment when the surrounding stack sees the same type effect. The paper explains: âTherefore, one instruction sequence can be interchanged with another provided that their impacts on stack types are compatible, thereby maintaining the invariant state of the stack.â (Section 4.1, p. 6163)
When to use it. Use the rule for any mutation or generated insertion that must remain valid through Wasmâs validation phase. It is particularly useful when the seed is valid but byte-level changes would cause early rejection.
How to apply it.
- Parse a valid Wasm seed into typed IR (Section 4, p. 6162; Section 5, p. 6166).
- Fragment the IR into code segments, each labeled with its stack type (Section 4, p. 6162).
- Select a replacement with the same stack type as the selected fragment, or generate one with the required result type (Sections 4.1 to 4.4, pp. 6162â6166).
- Repair required surrounding contexts such as memories, tables, and locals before inserting the fragment (Section 4.3.3, p. 6165).
- Lift the transformed IR into a Wasm module, execute it, and add it to the seed pool if it uncovers new code paths, or to the crash directory if the runtime crashes (Section 4, p. 6162; Section 5, p. 6166).
The paperâs example (Figure 3) replaces i64.const 2; local.get 0; i64.add with v128.const i32x4 0 1 2 3; i64x2.extract_lane 0; both have stack type [] -> [i64], so the following i64.div_u still sees the expected types (Figure 3 and Section 4.1, p. 6163).
Inference: in Verus, the closest reusable pattern is to state a relation between an instruction-list specification and an abstract stack-type specification with requires and ensures, and prove a replacement lemma as a proof fn; the executable mutator remains ordinary Rust. Inference: a verified replacement function would preserve a ghost stack-type sequence, while concrete generated Wasm still needs an external Wasm validator; the paper itself contains no formal proofs.
Evidence and limits. Table 3 records 452,171.4, 394,297.8, and 527,724.5 test cases for V8, JavaScriptCore, and SpiderMonkey respectively, with the same number valid in each case, for a reported 100.0% validity rate (averages over 10 runs, 24 hours per run; Table 3, p. 6169). For comparison, Table 1 reports that AFL++ produced valid test cases at rates of 0.9656% to 3.5863% (p. 6162). Validity is checked using wasm-validate; the paper concludes: âWALTZZ can generate 100% valid inputs by virtue of the stack-invariant transformationâ (Section 6.4, p. 6169). Inference: this shows validation-level validity, not that generated programs exercise any particular runtime semantics. Instructions that need missing context require repair. WALTZZ does not support proposals still under standardization (Section 5, p. 6166), and on wasm3 and wasm-micro-runtime it âoccasionally generates instructions not recognized by these runtimes, which can hinder its effectivenessâ (Section 6.2, p. 6168).
control-flow mutators
The control mutators alter structured control flow while preserving stack types.
Recursive mutator. Select a block, loop, or if..else, then wrap it in another structure whose stack type is the same. The paperâs example wraps block (result i32) ... end in loop $O (result i32) (block (result i32) ... end) end; both provide i32 to the stack (Section 4.2.1, p. 6163). Inference: a Verus model can represent a control node and prove that an insertion preserves the nodeâs pre/post ghost stack type; the runtime still executes ordinary Rust.
Branch-target mutator. Choose a target label L1 at random and replace it with another label L2 chosen from the surrounding context. For unconditional br and br_table, substitute the instructions that yield the old labelâs types with instructions that generate the new labelâs types. For br_if, insert a segment before the branch converting the values to the new labelâs types and a segment after it restoring the original types, so the overall effect on the stack is unchanged (Section 4.2.2, pp. 6163â6164). The paperâs example converts i64 to i32 with i32.wrap_i64, branches with br_if L2, then converts back using i64.extend_i32_s (Example 2, p. 6164). Inference: this pair restores the stack type but not the value for i64 values outside the i32 range, so a Verus proof of this mutator should claim type preservation only.
Call-target mutator. For a direct call, choose another function with the same signature; if none exists, generate one. For call_indirect, update the relevant table entry immediately before the call, using ref.func and table.set, so the chosen same-signature function is invoked (Section 4.2.3 and Example 3, p. 6164). Inference: a Verus wrapper can prove table-index/signature agreement over a ghost table model, but function-pointer execution and Wasm host behavior remain trusted external components.
data-flow mutators and context repair
Operator mutator. Group operators by identical stack type and replace one with a random member of its group. The paperâs example replaces i32.add with i32.le_s, both [i32 i32] -> [i32], changing the computation while retaining validity (Section 4.3.1, p. 6164).
Interesting-value mutator. Maintain lists of interesting (boundary) values for i32, i64, f32, and f64; construct v128 values by combining interesting values of smaller types; replace an immediate with a randomly chosen value of the corresponding type (Section 4.3.2, p. 6164). The example changes i32.const 5 to i32.const 65535 (Example 5, p. 6164).
Splicing mutator. Given current IR S1 and a second IR S2, fragmentize S2 into a pool of segments, each a single instruction plus, recursively, all instructions whose outputs could influence its result, tagged with its stack type. For each instruction in S1, with probability psub, take the segment closing over the current instruction, compute its stack type, find a pool segment with that type, and substitute it; with probability pins, find a [] -> [] segment and insert it before the instruction. Repair contexts before either operation; newly spliced-in instructions are not replaced by later substitutions in the same splicing run. Algorithm 1 (p. 6165) gives these steps (Section 4.3.3, pp. 6164â6165). The implementation sets psub = 0.1 and pins = 0.067 (Section 5, p. 6166).
Context repair is on demand. For each context needed by a segment, reuse a context in the current module that satisfies it; otherwise create a new one and rewrite the segment to use it. The paperâs example adds or reuses a memory for an i32.load segment (Section 4.3.3, p. 6165). Inference: a Verus implementation could prove that a context map contains the resources required by each typed fragment, but proving the host/runtime semantics of memory and tables would require axioms or trusted specifications.
skeleton-based generation
The generator separates the control-flow skeleton from the creation of stack values. Given target result type T, current recursion depth, and context C, it expands T into a list, repeatedly pops a subtype, randomly chooses a block, loop, or if to produce it, and recurses into the body with the new label added to the context. When the maximum recursion depth is reached, it fills in instructions that produce values of the desired types, like tree generation: pick an instruction whose result type is required, recursively generate its operands, and stop at instructions that need no further values or at the maximum depth (Algorithm 2 and Section 4.4, pp. 6165â6166). Example 7 generates block (result i32) ⥠end loop (result f32 f64) ⥠end for [i32 f32 f64] with maximum depth 1, then can fill the first hole with i32.reinterpret_f32 (f32.const 0.0) (Section 4.4, p. 6166). The generator is also called on demand by the mutators, e.g., to create a function with a required signature (Sections 4.2.3 and 5).
Inference: in Verus, the generatorâs terminating recursion can be represented by a decreases measure on remaining depth and a postcondition that the returned ghost fragment has result type T; generated bytes and parser correctness remain outside such a proof.
complete fuzzing workflow and evidence
WALTZZâs seeds are the valid modules from the official Wasm standard tests that do not require imports; each is modified by adding an exported main function that replicates the test actions (Section 6.1, p. 6167). WALTZZ is a custom mutator built on AFL++ 4.08c and uses AFL++âs feedback mechanism and seed scheduling; the number of mutations comes from AFL++âs performance score. It uses Binaryen to parse a seed into typed IR, applies randomly selected mutators in a stacking manner with the generator applied as needed, lifts the IR back to a module, and executes it. It still performs AFL++ byte-level mutations but applies its customized mutations and generations only to valid seeds (Section 5, p. 6166). The experiments use seven runtimes, 10 repetitions of each experiment, and a 250 ms per-execution timeout (Section 6.1, pp. 6166â6167).
For coverage, the authors use AFL++ collision-free edge coverage as a proxy and run 72 hours. WALTZZ exceeds the nearest competitor by the reported 12.4%; for wasm-micro-runtime and wasm3 the paper says WALTZZ âdoes not demonstrate noticeable advantagesâ (Section 6.2, pp. 6167â6168). Figure 5 plots mean coverage with a minâmax confidence band across runs (p. 6169). For bugs, Table 2 counts unique bugs: WALTZZ totals 33, AFL++ 24, RedQueen 12, wasm-smith 12, WasmFuzzer 12, and Wapplique 0; no fuzzer found bugs in the other runtimes, including the JS engines (Table 2, p. 6168). The text says WALTZZ outperforms âits nearest competitor by 37.5%â, which matches 33/24 = 1.375 (Section 6.2, p. 6168). Inference: the â20 new bugsâ of Table 4 is a different count from Table 2âs 33, so the two must not be conflated. The paper states: âAll these bugs have been manually deduplicated using the stack traces provided by AddressSanitizer [47].â (Section 6.2, p. 6168)
The two detailed case studies show why valid deep structures matter. CVE-2024-33480 uses a recursive call deeply nested within loop structures to exhaust wasm3âs system stack before its virtual-stack check intervenes; WALTZZ built it with the splicing mutator and then the call target mutator. CVE-2024-33477 exploits wasm3âs underestimate of the virtual-stack slots needed for the arguments of a long-parameter function, which WALTZZâs skeleton-based generator produced, causing an out-of-bounds write at f64.copysign (Section 6.5, pp. 6169â6171). The authors say: âThis oversight permits the out-of-bounds write at f64.copysign.â (Section 6.5, p. 6171)
what is proved, trusted, and unfinished
The paper measures coverage, unique bug counts, validity rates, mutation contributions, and timeout rate (0.066% average, Section 5, p. 6166). It does not provide a formal proof of WALTZZ, of runtime memory safety, or of semantic preservation beyond Wasm validation. Inference: trusted components include wasm-validate, Binaryen parsing/lifting, AFL++ instrumentation and scheduling, the manual AddressSanitizer-based deduplication, target-runtime implementations, and the manual seed modification. Inference: the core assumption is that matching stack types plus context repair suffice for validity; the paper states that for most instructions the stack-type condition âis also sufficientâ and that some instructions demand additional context checks (Section 2.2, p. 6161). WALTZZ does not mitigate infinite loops and does not support ongoing proposals such as garbage collection and threads (Section 5, p. 6166); the paper names supporting new proposals and diversifying target settings as future work (Section 7, pp. 6171â6172).
Inference: for agent-written verified Rust, WALTZZ is a high threat to the input-validation and fuzzing surface of a Wasm-like subsystem because it systematically generates valid, deep, and unusual typed structures that byte fuzzers reject. It is not evidence that an agent can automatically prove Rust safety: its bug oracle is runtime execution and sanitizer observation, and its validity invariant is narrower than functional correctness. Existing work already covers typed-fragment fuzzing, structured control mutation, context repair, and recursive typed generation; VL should reuse these ideas rather than redo a generic validity-preserving fuzzer. Inference: VLâs distinct contribution would be verified specifications and proof obligations for the fragment/type relation, context completeness, and termination, followed by fuzzing the verified implementation.
techniques for the catalog
- stack-invariant transformation â replace or insert fragments only when their Wasm stack input/output types match
- recursive control mutator â nest a block, loop, or conditional under an equivalent stack type
- branch-target mutator â redirect labels while adapting branch values and restoring the surrounding stack type
- call-target mutator â swap direct or indirect call targets with same-signature functions and update table entries
- operator mutator â replace operators within an equal-stack-type group
- interesting-value mutator â substitute boundary immediates for numeric and vector types
- splicing mutator â substitute or insert dependency-closed typed fragments from another seed with context repair
- skeleton-based generator â recursively plan typed control flow, then fill holes with result-producing instructions
- validity-guided greybox workflow â parse valid seeds, mutate typed IR, execute, and retain coverage/crash discoveries Checked 2026-10-16 against the full paper by a Claude agent (mid-paper quote: âTo address potential infinite loops generated by fuzzers, we set the timeout of a single execution to 250 ms.â): checked every quotation, number, section and page reference, and technique step against the paper text; changed the link to the USENIX presentation page from the paper text, fixed a misquoted Section 1 sentence (âTherefore,â replaced by âOur key observation is thatâ), the Section 4.1 quoteâs location, several page numbers (Table 2, Example 2, Section 6.1 seeds and timeout, the CVE-2024-33477 quote), the skeleton generatorâs base-case description, the seed description (modules needing imports are excluded, not stripped), the Figure 3 and recursive-mutator examples, and marked the trust list and validity-scope remarks as inferences; removed an unsupported claim about the Verus guide and an invented ârange assumptionsâ clause; added the per-runtime bug locations from Table 4, the AFL++ validity rates from Table 1, and the paperâs stated limits; did not check the references, the PDF figures beyond their pdftotext captions and labels, or the Verus inferences against Verus.
Last edited: