Skip to content

wasm-smith: generate well-formed components from the binary grammar - #2796

Open
jrandolf wants to merge 3 commits into
bytecodealliance:mainfrom
jrandolf:wasm-smith-component-grammar
Open

jrandolf wants to merge 3 commits into
bytecodealliance:mainfrom
jrandolf:wasm-smith-component-grammar

Conversation

@jrandolf

@jrandolf jrandolf commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Components are currently only fuzzed as raw bytes, since the wasm-smith component generator is disabled (#1000). This adds wasm_smith::WellFormedComponent, which emits a derivation of the component production of the binary grammar, one method per production, and steers its choices toward a valid component as often as the new Config::component_steering_percent says. At 0, components only follow the grammar, with indices in bounds, and about a third of them validate (mostly small ones); at 100, about 98% of them validate.

How components are generated

  • Each production of the grammar is a method of a builder that writes its bytes. The fuzzer input picks between alternatives and decides how many times a vec(...) or section* repeats. Size and count prefixes are written once the bytes they cover exist, and the grammar's side conditions (non-empty records, at most 32 flags, map key types, the text format's grammar for names) are generated directly. Where wasmparser checks a validation rule while decoding (the sorts that exports can have, unique attributes, sorted name maps), generation follows it so that decoding never fails.
  • The builder keeps a stack of scopes (component, component type, instance type and core module type), each with the length of every sort's index space, so indices are always in bounds when their index space isn't empty.
  • To steer the other choices, each scope tracks what it knows about its items as they're defined: the kind of each type; what each value type flattens to and whether it's passed in memory; the type of each function and core function, and whether each memory can be a canonical ABI memory; which types imports and exports have named; and what core modules, components, instances and their types import and export. A steered choice uses that to:
    • pick type indices of the right kind;
    • lift core functions of the type that lifting needs, with the memory, realloc, post-return and callback options it needs, and lower functions with the options that their type needs;
    • give imports and exports eq bounds and function types whose types are named;
    • give [constructor], [method], [static] and accessor names only to functions whose types fit;
    • alias exports that exist;
    • instantiate modules and components with arguments that provide each import, in any order and sometimes with extra ones.
  • Core modules come from wasm-smith's Module. When steered, they only import functions that a core instance exports, and they export a memory, a realloc and functions of the types that lifting the scope's function types needs (through a crate-internal Shape in core.rs).
  • Some sections are emitted as a group that is valid by construction, so that components export functions and core code calls them: an import lowered and lifted again, the lift of a function of any type from a module that exports it with what its options need, and the lowering of a function into a module that imports it.
  • Names come from pools of Config::name_pool_size names and are reused Config::name_reuse_percent of the time. Config::name_collision_percent of the names that collide in a scope are kept rather than replaced, and as often a new name is a variant of a pooled one that differs only in case, so that strong uniqueness is checked.
  • Every loop stops once the input is exhausted, and the first alternative of each choice doesn't recurse, so generation always terminates.

The well_formed_component fuzz target checks that every component decodes, that validating and printing it doesn't panic, and that valid ones round-trip through the text format. It picks the settings per input, steering from 80 to 100 three times in four, and about 58% of its components validate. On 20,000 random inputs, it covers 69% of the lines of the component validator and 92% of the component printer, against 27% and 46% for the disabled Component generator.

This builds on #2795, which fixes the printer bugs that this fuzzer found, and includes its commit until it merges. It also includes one more printer fix that the fuzzer found since: names that separate component-name subsections give the same index space could print as the same identifier, as with two component-name sections that each name a different core function a.

Productions that wasmparser doesn't decode as specified aren't generated:

  • externtype 0x02 is followed by a valuebound (0x00 valueidx or 0x01 valtype) since component-model@4480daf, but wasmparser reads a valtype directly.
  • thread.spawn-ref, thread.spawn-indirect and thread.available-parallelism take a sh? immediate since component-model#557, which wasmparser doesn't read.
  • Value definitions (section 12).

Start definitions (🪙) aren't generated either, since the text format can't hold the names that wasmprinter prints for their results. Recursion groups also aren't generated in component and instance types, where wast doesn't parse the (core rec ...) that wasmprinter prints until #2798.

Outer aliases now print the same identifier as the header of the
component they refer to, or its count when a scope in between has the
same identifier. They printed the target's name with its `(@name ...)`
annotation, which only parses where the component is defined; they used
a nested component's own name from its `component-name` section, while
its header uses the name its parent gives it; and they printed
identifiers that an inner scope shadows, such as the synthetic
`$"#component0 "` of both a component and its first nested component,
so the printed text didn't parse or referred to the wrong scope.

    (component (@name "") (type u8) (alias outer 0 0 (type)))

printed `(alias outer $"#component0 " (@name "") 0 (type))`.
Names that separate `component-name` (or `name`) subsections gave the
same index space were each deduplicated on their own, so two items could
print the same identifier and the printed text didn't parse, as with two
`component-name` sections that each name a different core function `a`.
A name that an earlier subsection gave another index is now taken too.
`WellFormedComponent` emits a derivation of the `component` production
of the component model's binary grammar, with one method per
production, so it always decodes. Indices are in bounds whenever their
index space isn't empty, and `Config::component_steering_percent` of
the other choices that can lead to a valid component do: at 0
components only follow the grammar, and at 100 nearly all of them
validate. For that, each scope tracks the kinds of its types, how value
types flatten, the types of functions and core functions, which types
are named, and what modules, components, instances and their types
import and export.

Names are drawn from pools that `Config::name_pool_size`,
`name_reuse_percent` and `name_collision_percent` configure, so that
references by name line up and names collide. Core modules come from
`Module`, which a crate-internal `Shape` makes import and export what a
component lifts and lowers when steered.

This sits alongside the existing `Component` generator, which generates
valid components but is disabled.

The new `well_formed_component` fuzz target checks that every generated
component decodes, that validating and printing it doesn't panic, and
that valid ones round-trip through the text format.

Productions that `wasmparser` doesn't decode as specified aren't
generated: value definitions and `valuebound`s (🪙), and the 🧵②
built-ins, whose `sh?` immediate `wasmparser` doesn't read. Neither are
start definitions (🪙), since the text format can't hold the names that
`wasmprinter` prints for their results. Recursion groups aren't
generated in component and instance types, where `wast` doesn't parse
the `(core rec ...)` that `wasmprinter` prints.
@jrandolf
jrandolf force-pushed the wasm-smith-component-grammar branch from 1d08bd1 to 423069e Compare October 24, 2026 04:01

This branch has not been deployed

No deployments
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