01 Jun 2026
Planet Mozilla
Firefox Nightly: Backup for a Rainy Day – These Weeks in Firefox: Issue 202
Highlights
- The profile backup mechanism has been enabled by default for all desktop platforms in Nightly, as well as Beta! The current plan is to have this ride out to Firefox 151 for Windows, macOS and Linux on May 18th!
- This feature, when enabled, will create a copy of your profile data in the background and store it in a single file on your file system that you can restore from.
- You will be able to manage this feature in Settings under Sync (for now)
- You can read more about the feature here
- As followups to the recent addition to the WebExtension tabs API to support the new SplitView tabs feature, tabs.group() and tabs.ungroup() have been fixed to work correctly with split view tabs, and fixed split views being prepended instead of appended to tab groups when adopted into a new window - Bug 2029099 / Bug 2029534
- Adaptive autofill has been enabled on Nightly.
- Previously, autofill only completed domains (e.g. typing red autofilled reddit.com). Now it can also complete full URLs for pages you visit often (e.g. red → reddit.com/r/firefox), learning from what you actually click in the address bar. If a suggestion isn't helpful, you can now dismiss it so autofill learns what not to show you too.
- If you run into issues or have feedback, you can file a bug here!
- Previously, autofill only completed domains (e.g. typing red autofilled reddit.com). Now it can also complete full URLs for pages you visit often (e.g. red → reddit.com/r/firefox), learning from what you actually click in the address bar. If a suggestion isn't helpful, you can now dismiss it so autofill learns what not to show you too.
- Markus Stange [:mstange] implemented dynamic toolbar on top in RDM (#1978145), but also implemented some static skeleton UI so it's closer to what we actually have in Firefox for Android
Friends of the Firefox team
Resolved bugs (excluding employees)
Script to find new contributors from bug list
Volunteers that fixed more than one bug
- Amin Amir
- aoia7rz7l
- Chukwuka Rosemary
- DrSeed
- Frédéric Wang Nélar
- japandi
- John Iweh
- jonathancabera
- Josh Aas
- Keji Bakare
- kofoworola shonuyi
- konyhéa
- liz
- Mathew Hodson
- Okhuomon Ajayi
- Oluwatobi
- ROSHAAN
- Sam Johnson
New contributors (🌟 = first patch)
- Anthony Mclamb: Disable the legacy Edge migrator
- Amin Amir
- any1here: install_sig_alt_stack incorrectly checks mmap's return value
- 🌟 Armin Ulrich: Fix MessageHandlerRegistry.sys.mjs calling getExistingMessageHandler with an unused second argument
- japandi
- Nathan Johnson [:narjoDev]: Remove browser.display.use_system_colors pref
- DrSeed
- Keji Bakare:
- 🌟 gotyaoi: Reload toolbar button is active on about:newtab
- Itoro James: [A11y][Keyboard Navigation]Cancelling a note via Keyboard Navigation still saves it
- John Iweh: The notification dot is not displayed if the tab is in a Split View
- 🌟 John Iweh: sidebar-shown attribute remains when sidebar.revamp is false
- 🌟 jonathancabera:
- The Move tab to Split View option is also displayed for the tabs that are within the Split View
- A Note with long text (1003 characters) is saved by pressing ENTER even if the "Save" button is disabled
- Tab group guide line becomes disconnected under certain conditions related to split views in vertical tab mode
- Aloys: Remove logic that forces distribution language packs to be reinstalled when upgrading from Firefoxes older than 67
- liz:
- Mary cathline: Tab Group Label does not respect touch density in vertical tab bar
- 🌟 Brandon Lucier: Popups opened with window.open give window type normal instead of popup
- karan68: [dialog] New Shortcut dialog needs a label/accessible name
- 🌟 Vector: Button does not programmatically indicate that it opens a dialog (Recent activity section > story card > ••• disclosure > Delete from History button)
- 🌟 Osoble: Update font size and weight for synced tabs device name headers in Firefox View
- konyhéa:
- Add test for sync admin disabled to browser_syncedtabs_errors_firefoxview.js
- Check all second paramaters for TestUtils.waitForCondition in Fx View test files
- Recently Closed Tabs, Tabs from Other Devices, and History pages should have Cmd / Ctrl + Click on a link open the link in the new background tab.
- Noble Chinonso: #shouldHandleEvent in SidebarTreeView.js compares event.keyCode to string values, causing Home/End keys to never be handled
- Pranjali Srivastava: Add a test to verify that the space above tabs is consistent across PB, LWT and sizemode (where appropriate)
- Okhuomon Ajayi:
- More spacing is needed between the tab note icon and the close icon on the tab
- The tabs in vertical mode collapsed state are positioned differently in Split View
- Keep vertical split view tabs stacked vertically even when the sidebar is expanded when expand on hover is enabled
- Vertical split view tabs can be too big or small when tabs are overflowing
- 🌟 Rishan: Fix duplicated arrow function in browser_history_sidebar.js
- Chukwuka Rosemary:
- ROSHAAN:
- Sameeksha: Disclosure button expanded/collapsed state not programmatically defined (Customize button)
- kofoworola shonuyi:
- 🌟 Sayd Mateen: Page URL is displayed as tab name when page's contains about:reader?</a></p> <p>
- Oluwatobi:
- Nishchay [:nish]: Unable to add tabs to old closed tab groups (tabGroupState.splitViews is undefined)
Project Updates
Add-ons / Web Extensions
Addon Manager & about:addons
- In preparation for the Project Nova restyling of the about:addons page, we have refactored about:addons into separate per-component ES modules, splitting the monolithic aboutaddons.js and aboutaddons.html into 16 dedicated component files under components/ (with no behavior or UI changes) - Bug 2032014
- NOTE: if you have working on patches with changes to about:addons internals it is very likely you'll need to rebase and solve merge conflicts hit on top of this refactoring, the internals are still largely the same as before but don't hesitate to reach out to the Addons team if you have doubts / questions or need help to figure out how to adapt your patch of top of these changes
WebExtensions Framework
- Fixed exportFunction to preserve the constructibility of the wrapped function instead of unconditionally making all exported functions implicitly as constructors - Bug 2033173
- Thanks to Gregory Pappas for contributing this improvement to the Content Scripts' Xray Wrappers helpers!
- Fixed a Firefox 151 regression where extension content scripts accessing location.ancestorOrigins caused subsequent page script reads of the same property to fail with "Permission denied", breaking sites like Gmail - Bug 2034329
- Thanks to Simon Farre for promptly investigating and fixing this recent regression!
WebExtension APIs
- Updated sessions.getRecentlyClosed() to remove the hardcoded cap when maxResults is omitted - Bug 1392125
- Shoutout to Amine Zroual for contributing this enhancement to the sessions WebExtensions API!
DevTools
- Artem Manushenkov fixed an issue where autosuggestion popup was removing overridden indicators from properties in the Inspector (#1983408)
- Andrea Marchesini [:baku] fix DevTools cookie header serialization for long cookies, which could lead to cookies not being visible in Netmonitor (#2031299)
- Julian Descottes [:jdescottes] fixed a toolbox crash that was happening we couldn't find a localization file (e.g. when using a language pack on Nightly) (#2028930)
- Nicolas Chevobbe [:nchevobbe] improved @container tooltip so it show the value of variables used in style()(#2030239), has enough contrast in dark mode (#2033782) and contains a link to select the container (#2031688)
- Hubert Boma Manilla (:bomsy) is making good progress on migrating the Console to CodeMirror 6 (#2032758, #2026569)
Fluent
- We're now at over 72% of our strings being Fluent! Got a component still using .properties? Convert when you can!

Migration Improvements
- Thanks to dao for fixing a recent alignment issue in the migration wizard dropdown
- Thanks to volunteer contributor Anthony Mclamb for his patch that disables the legacy EdgeHTML Edge migrator! Once that finishes rolling out, presuming no surprises, we'll go ahead and remove the migrator entirely.
New Tab Page
- Nova for New Tab has ridden the trains to Beta! It will be enabled by default, globally, when Firefox 151 goes out to release on May 19th
- It's possible that we'll do a train-hop coupled with an experiment to enable HNT Nova for a few clients a bit earlier.
- Maxx Crawford enabled Nova designs for New Tab, rolling out the updated layout, widgets, and customization panel behind HNT Nova flags.
- Maxx Crawford fixed the Nova content feed to render the intended four‑column layout by correcting CSS grid breakpoints.
- Maxx Crawford resolved a first‑load failure in the Weather widget by fixing init order and fetch timing, eliminating the "Oops" error.
- Maxx Crawford synchronized the Weather toggle between about:preferences#home and the panel via the shared showWeather pref to prevent desync.
- Maxx Crawford updated Nova grid focus order to align tab flow with visual order for keyboard users.
- Maxx Crawford fixed critical UI issues in Lists and Timer widgets covering overflow, controls, and layout stability.
- Maxx Crawford guarded document.dir access in Nova render paths to avoid startup cache worker errors and improve startup stability.
- Rolf added a new normalization method for the inferred interest vector to stabilize topic relevance across sessions.
- Rolf prevented unnecessary content refreshes during Pocket New Tab experiments, reducing jank and bandwidth.
- Sameeksha defined the Customize button's expanded/collapsed state programmatically using aria-expanded for better a11y.
- liz clarified follow/unfollow/blocked button names with topic context so screen readers announce clear actions.
- Vector marked the Delete from History control as opening a dialog via aria-haspopup=dialog for assistive tech.
- Scott Downe fixed a regression that flipped the Wallpapers pref off, restoring user selections.
- Irene Ni corrected privacy link color and focus styles for contrast and keyboard visibility.
- Reem Hamoui added a wallpaper toggle reset in the Nova customization panel so users can quickly restore default wallpapers without extra steps.
- Reem Hamoui fixed the Customize pencil button to match the Nova spec, aligning placement and iconography for visual consistency.
- Dre updated the 'Fresh new' wallpapers copy to a clearer, localized message for better comprehension.
- Irene Ni fixed Nova privacy link color and focus styles to meet contrast and focus ring guidelines, improving accessibility on New Tab.
- Irene Ni adjusted Sponsored tile character limits to prevent truncation/overflow, yielding cleaner titles across grid and wide tiles.
- Scott Downe fixed a regression that flipped the Wallpapers user pref to false, restoring wallpapers for affected users and preventing unintended disablement.
- Reem Hamoui hooked the wallpaper check into the new toggle logic so the Customization Panel accurately reflects wallpaper availability and state.
- Irene Ni landed Nova UI updates for the Daily Briefing 3-pack card, improving spacing, type scale, and tap targets.
- Reem Hamoui added a wallpaper toggle reset in the Nova customization panel so users can quickly restore default wallpapers without extra steps.
- Reem Hamoui fixed the Customize pencil button to match the Nova spec, aligning placement and iconography for visual consistency.
- Dre updated the 'Fresh new' wallpapers copy to a clearer, localized message for better comprehension.
- Irene Ni fixed Nova privacy link color and focus styles to meet contrast and focus ring guidelines, improving accessibility on New Tab.
- Irene Ni adjusted Sponsored tile character limits to prevent truncation/overflow, yielding cleaner titles across grid and wide tiles.
- Scott Downe fixed a regression that flipped the Wallpapers user pref to false, restoring wallpapers for affected users and preventing unintended disablement.
- Reem Hamoui hooked the wallpaper check into the new toggle logic so the Customization Panel accurately reflects wallpaper availability and state.
- Irene Ni landed Nova UI updates for the Daily Briefing 3-pack card, improving spacing, type scale, and tap targets.
Search and Urlbar
- Marco has fixed a couple of issues with the places databases to try and improve stability. This should help with avoiding users losing bookmarks or favicons.
- Work continues on the new separate search bar to improve the functionality, e.g. allowing middle click to perform a search in a new tab, avoiding performing a search when adding a search engine.
- Work also continues on the new Nova layouts.
Smart Window
- uplifted 10 bugs to 150.0.1 dot release addressing initial user feedback from diary study and Connect
- search engine switching from smart bar 2021973
- Nova styling within smart window 2026794
Storybook/Reusable Components/Acorn Design System
- Dustin converted moz-breadcrumb-group variables into JSON design tokens Bug 2029181 - Convert moz-breadcrumb-group variables into JSON design tokens
- Dustin converted moz-box-* variables into JSON design tokens Bug 2029180 - Convert moz-box-* variables into JSON design tokens
- Dustin converted moz-promo variables to JSON design tokens Bug 2029190 - Convert moz-promo variables into JSON design tokens
- Dustin converted moz-reorderable-list variables to JSON design tokens Bug 2029191 - Convert moz-reorderable-list variables into JSON design tokens
- Dustin converted moz-visual-picker variables to JSON design tokens Bug 2029193 - Convert moz-visual-picker-item variables into JSON design tokens
- Dustin updated browser-shared.css so it passes use-design-tokens Bug 2022985 - Update browser-shared.css so it passes use-design-tokens
- Dustin updated popup.css so it passes use-design-tokens Bug 2022979 - Update popup.css so it passes use-design-tokens
- Jon added opacity tokens and added opacity to use-design-tokens stylelint rule Bug 1955325 - Create opacity tokens
- Jon converted toolbar design tokens to JSON Bug 2017970 - Convert toolbar design tokens to json
- Anna fixed moz-select with panel-list drop-down size inconsistency Bug 2032365 - Applications Action drop-down menus sometimes have a different size when opened
- Anna fixed issue with the disabled state of moz-radio component Bug 2027123 - moz-radio disabled state cannot be changed while the moz-radio-group is disabled
- Anna updated moz-button and moz-box-button components to prevent label corruption when accesskeys are present and the label changes. Bug 2022326 - moz-button with accesskey label becomes corrupted when l10nId updates dynamically
UX Fundamentals
- The error pages shown when a server sends back an invalid response header or an unsupported content encoding now display accurate, context-specific messages. The invalid response header page also gained a helpful list of next steps. - 2027209
- In progress: The error page illustrations are being replaced with new artwork, and the system now supports per-illustration size configuration, giving each image the ability to define its own appropriate dimensions. - 2031837
Settings Redesign
- Tim converted settings related to Accessibility page to config-based pane Bug 1968116 - Convert settings related to Accessibility page to config-based settings
- Benjamin converted Privacy & Security page to the config-based pane Bug 1968112 - Convert settings related to Privacy & Security page to config-based settings
- Finn integrated Firefox Labs page into setting-pane config Bug 2021047 - Integrate Firefox Labs page into setting-pane config
- Anna converted Firefox Updates section to config-based prefs Bug 1990961 - Convert Firefox Updates section to config-based prefs
- Mark Kennedy added moz-promo, that is welcoming users to the redesigned settings Bug 2015093 - Add a moz-promo to welcome users to the redesign
- Anna added possibility to search for actions in the redesigned "Applications" section Bug 2020370 - It's no longer possible to search for actions in the new "Applications" section
- Anna fixed the Settings navbar layout breakage
01 Jun 2026 6:15pm GMT
Tom Ritter: webgl renderer privacy
WebGL exposes the details of your graphics hardware (specifically, the string that describes the rendering engine) in 2 ways. There are three levels of protection that browsers have taken to protect this data.
gl.getParameter(gl.VENDOR)andgl.getParameter(gl.RENDERER)- these are the 'simple' names. At some point in the past, someone argued that it wasn't enough information, and therefore we have a second APIlet ext = gl.getExtension('WEBGL_debug_renderer_info');and thengl.getParameter(ext.UNMASKED_VENDOR_WEBGL)andgl.getParameter(ext.UNMASKED_RENDERER_WEBGL)
The unmasked values are intended to be the more detailed ones, so always make sure you're comparing apples to apples. Another axis is that WebGL can render with Hardware or Software. This isn't a guarentee which one you'll get, but you can hint towards one or the other and the browser may or may not respect it. Here are your values:
Alright, now let's talk about what browsers do about it. There's no point in talking about Vendor, Renderer, and Unmasked Vendor - they don't really show as much detailed info, it's all about Unmasked Renderer. There are three levels:
- Give a constant value. (Or don't return anything at all.)
- 'Round' the values into buckets
- Give the exact value back
Safari and Tor Browser give constant values.
Firefox 'rounds'.
Chrome (and Brave, and I assume all-ish other Chrome-based browsers) give the exact value.
Firefox actually is purusing constant values, this week. I wrote this document for our QA team to test it. (You can get a sense of the internal sausage making it takes to launch a privacy feature from it.) I don't know if you can see the dates but I made it May 20th. The problem is this - websites use this data legitimately to adjust behavior so that users get the best experience possible. I found one example where they detect a buggy graphics stack; and a couple of examples where they adjust rendering so things are more performant for users with lower end machines - a problem Apple has less to worry about because they only support certain machine models!
A common response to this seems to be ambivalence, and I would suggest that is a bit elitist. Yes, if you're caring about the details reveal by a particular Web API you probably have a computer where you don't need to worry, but making the web work well for everyone is important for equitable access to improving everyone's human condition.
We have been bucketing WebGL Renderer since 2021. While many of our (supported, on-by-default) fingerprinting protections are part of Enhanced Tracking Protection - rolling out first in PBM/ETP Strict before making it to ETP Standard/Normal Browsing Mode - the bucketing is on by default, for everyone, and is not disabled if ETP is disabled.
How much of a difference does it make? A lot! Here is the distribution of the raw values. 83,705 distinct values.

Compare that to the bucketed data. 131 distinct values.

Now this data is from Firefox, so I cant say conclusively what the distribution of data is in other browsers, but... yeah. To claim Chrome (of all browsers!) is doing this better than us is pure FUD. We're making a big impact in how fingerprintable you are today and we're trying to improve it even further.
01 Jun 2026 5:36pm GMT
Nick Fitzgerald: A Structure-Aware Fuzzing Experiment
Structure-aware fuzzing can better exercise the system under test (SUT) by crafting inputs in the format expected by the SUT, rather than throwing pseudorandom bytes against it. That is, it avoids "shallow" inputs that the SUT will reject early (for example, syntactically invalid source text when fuzzing a programming language's compiler) and only produces inputs that go "deep" into the SUT (e.g. programs that type-check and exercise the mid-end optimizer and backend code generator). The Rust fuzzing ecosystem is largely built around cargo-fuzz and the libfuzzer-sys crate, which provides two methods for structure-aware fuzzing:
-
Generating structured inputs from scratch with the
arbitrarycrate -
Mutating existing inputs from the fuzzer's corpus in a structure-aware manner, thereby producing new structured inputs, via the
fuzz_mutator!hook
While the two methods are not technically mutually exclusive, combining the two can be difficult and engineering resources are finite. So:
If we are only implementing one approach, is generation or mutation better?
To help answer this question, I implemented structure-aware generation and mutation of guaranteed-valid WebAssembly (Wasm) instruction sequences. This task is small enough to be easily understandable but large enough and real enough to (hopefully) be representative and applicable to other domains, or, at the very least, interesting.1 To evaluate their effectiveness, I used Wasmtime as the SUT, libfuzzer-sys as the fuzzing engine driving everything, and then compared code coverage over time when using mutation-based fuzzing versus generation-based fuzzing.
Additionally, there are many ways we can generate pseudorandom WebAssembly instruction sequences. In this experiment, I've evaluated three methods:
-
Unconstrained instruction sequence generation followed by a fixup pass to ensure validity
-
Generating valid instructions in a forwards, bottom-up manner (from operands to operators)
-
Generating valid instructions in a backwards, top-down manner (from operators to operands)
In contrast, while there are surely many ways to mutate a given WebAssembly instruction sequence into a new, valid instruction sequence, I've only implemented one method: perform an arbitrary instruction insertion, deletion, or replacement, producing a new but probably-invalid instruction sequence, and then run the same fixup pass mentioned previously to ensure validity. This is the direct mutation-based equivalent of the first generation-based method.
Before continuing further, I want to disclose that I am the author of wasm-smith and mutatis, and a maintainer of Wasmtime, arbitrary, libfuzzer-sys, and cargo-fuzz. That is, while I am familiar with Wasm, fuzzing, fuzzing Wasm, and both the arbitrary and mutatis crates, I may also be propagating my own biases into these implementations.
Background
Generation-Based and Mutation-Based Fuzzing
A generation-based fuzzer uses a generator to create a pseudo-random test cases from scratch, feeds these into the system under test, and reports any failures to the user:
fn generation_based_fuzzing<T>(
// A test-case generator.
generator: impl Fn() -> T,
// A function to run the system under test with a
// generated test case, returning a result that
// describes whether the run was successful or
// not.
run_system_under_test: impl Fn(&T) -> FuzzResult,
) {
loop {
// Generate an input.
let input = generator();
// Run the input through the system under test.
let result = run_system_under_test(&input);
// If the system crashed, panicked, failed an
// assertion, violated an invariant, or etc...
// then report that to the user.
if let Err(failure) = result {
report_to_user(&input, failure);
}
}
}
On the other hand, mutation-based fuzzers are given an initial corpus of inputs and create new inputs by mutating existing corpus members. They run each new input through the SUT, report failures the same as before, and if the new input was "interesting" (for example, exercised new code paths in the SUT that weren't previously covered in any other input's execution) then the new input is added into the corpus for use in future test iterations:
fn mutation_based_fuzzing<T>(
// A corpus of test cases.
corpus: &mut Corpus<T>,
// A function to pseudo-randomly mutate an existing
// input into a new input.
mutate: impl Fn(&T) -> T,
// A function to run an input in the system under
// test, returning a result that describes whether
// the run was successful or not.
run_system_under_test: impl Fn(&T) -> FuzzResult,
) {
loop {
// Choose an old test case from the corpus.
let old_input = corpus.choose_one();
// Pseudo-randomly mutate that old test case,
// creating a new one.
let input = mutate(old_input);
// Run the input through the system under test.
let result = run_system_under_test(&input);
// If the system crashed, panicked, failed an
// assertion, violated an invariant, or etc...
// then report that to the user.
if let Err(failure) = result {
report_to_user(&input, failure);
}
// If the input was interesting, for example if
// it executed previously-unknown code paths,
// then add it into the corpus for use in a
// future iteration.
if result.input_was_interesting() {
corpus.insert(input);
}
}
}
The two approaches are not mutually exclusive and hybrid generation- and mutation-based fuzzers exist.
More resources:
- Wikipedia's "Fuzzing" article's "Reuse of existing input seeds" section
- The Fuzzing Book's Mutation-Based Fuzzing chapter
- Writing a Test Case Generator for a Programming Language
Structure-Aware Fuzzing
Structure-unaware fuzzing will generate pseudorandom byte sequences and pass them directly to the SUT. If the SUT expects some sort of structured input, e.g. the source text for a programming language, it is likely that these byte sequences are invalid and will be rejected early by the SUT's frontend. For example, when fuzzing a compiler, the input is rejected as syntactically invalid by the parser or rejected as semantically invalid by the type checker. This can be useful when hardening a tokenizer, parser, or type checker, but is less useful when hunting for misoptimization in the mid-end or bad instruction encoding in the backend because the inputs are unlikely to make it that far through the compiler's pipeline.
Structure-aware fuzzing will produce inputs that match the SUT's expected input format. Returning to the compiler-fuzzing example, structure-aware fuzzing lets us generate valid programs for the compiler, so we can exercise more of the mid-end and backend, rather than just the frontend.
Structure-aware fuzzing is often generation-based: for example using grammar-based fuzzing to generate pseudorandom strings from a given language grammar or language-specific tools like csmith and wasm-smith that generate C and WebAssembly programs respectively. But structure-aware fuzzing can also be mutation-based: libFuzzer's custom mutator example implements a structure-aware mutator for zlib-compressed strings, where the raw input is decompressed, the decompressed data is mutated, and then the mutated data is recompressed to provide the new raw input. The mutator is aware of the SUT's zlib-compressed input structure.
More resources:
- Wikipedia's "Fuzzing" article's "Aware of input structure" section
google/fuzzingon structure-aware fuzzing- The
rust-fuzzbook on structure-aware fuzzing
The arbitrary Crate
The arbitrary crate helps Rust developers write custom structure-aware generators for fuzzing. It provides building blocks and abstractions for translating a raw byte sequence (usually from a fuzzing engine) into a structured type, effectively interpreting the raw bytes as a "DNA string" or set of predetermined choices for its decision tree. The library also provides a derive(Arbitrary) macro to automatically implement its functionality for a given type.
Because arbitrary is effectively implemented by combining decision trees, it is extremely easy to create imbalanced trees and unintentionally bias the distribution of generated test cases.
The mutatis Crate
The mutatis crate is, at a high-level, performing the same role for authoring structure-aware mutators that arbitrary plays for generators. That is, it provides Rust developers with abstractions and combinators for creating custom structure-aware mutators. It also provides a derive(Mutate) macro to automatically implement its functionality for a given type.
mutatis is designed to resist bias via a two-phase design: first, it enumerates all of the candidate mutations that could be applied to a test case, and only afterwards chooses a particular random mutation from the candidate set to actually apply.
WebAssembly
WebAssembly is a virtual instruction set designed to be safe, portable, and fast. It is a stack machine where an instruction's operands are popped off a stack during execution and results pushed. It has sandboxed linear memories, global variables, and local variables (the latter two effectively being two kinds of virtual registers). The following instruction sequence computes a * 3 and stores the result into memory at address p:
;; []
local.get $p
;; [p]
local.get $a
;; [p, a]
i32.const 3
;; [p, a, 3]
i32.mul
;; [p, a*3]
i32.store
;; []
Generator and Mutator Implementation
The range of all three generators and the mutator is the same universe of WebAssembly programs. They are all implemented on top of the same Module and Inst types, and, given enough time, none is capable of producing an instruction sequence that another cannot. This helps ensure that our comparison is apples-to-apples. However, due to their different implementation techniques, they do produce different distributions of WebAssembly programs within that universe, and produce test cases at different speeds from one another, which ultimately affects how efficiently they exercise the SUT.
All of the generators are built on top of the arbitrary crate. The mutator is built on top of the mutatis crate.
The Module type is our structured fuzzing input. It describes a WebAssembly module containing a variable number of linear memories, a variable number and type of globals, and one function with a variable number and type of parameters and results and a variable instruction sequence:
/// A WebAssembly module of the shape:
///
/// (module
/// (memory ...)
/// (memory ...)
/// ...
///
/// (global ...)
/// (global ...)
/// ...
///
/// (func (export "run") (param ...) (result ...)
/// ...
/// )
/// )
pub struct Module {
num_memories: u32,
globals: Vec<Global>,
param_types: Vec<ValType>,
result_types: Vec<ValType>,
instructions: Vec<Inst>,
}
The Inst type is an enum of all the WebAssembly instructions the implementations support, which is all of the integer, float, SIMD, memory, local, and global instructions. Control-flow, threading, table, and GC instructions are not supported. Here is a subset of Inst's definition:
/// A WebAssembly instruction.
pub enum Inst {
Drop,
LocalGet(u32),
GlobalGet(u32),
// ...
I32Const(i32),
I32Add,
I32Sub,
I32Mul,
// ...
I64Const(i64),
I64Add,
I64Sub,
I64Mul,
// ...
F32Const(f32),
F32Add,
F32Sub,
F32Mul,
// ...
F64Const(f64),
F64Add,
F64Sub,
F64Mul,
// ...
I32WrapI64,
I64ExtendI32S,
I64ExtendI32U,
// ...
V128Const(i128),
I8x16Add,
I8x16Sub,
// ...
I32Load(u32),
I64Load(u32),
// ...
I32Store(u32),
I64Store(u32),
// ...
MemorySize(u32),
MemoryGrow(u32),
}
There is an Inst::operand_types method that returns the types that the instruction pops from the stack, and an Inst::result_type method that returns the type of the value that the instruction pushes onto the stack, if any. Finally, the Module::to_wasm_binary method encodes the module into WebAssembly's binary format, so it can be fed into Wasmtime. These methods are used, directly or indirectly, in every generator and mutator implementation.
arb
The arb generator leverages derive(arbitrary::Arbitrary) on our structured input types to generate a pseudorandom instance of Module, unconstrained by validity. The module's instruction sequence is almost certainly not valid at this point: it likely is missing operands for instructions, producing more results than the function's signature describes, producing results of types that don't match the function signature, accessing globals and locals that don't exist, etc… Having produced an instance of Module, it next calls the Module::fixup method to mutate the Module so that it is valid.
The fixup method works by abstractly interpreting the instruction sequence to track the types of each value on the stack at every program point. Whenever an instruction's operand types don't match the types on top of the stack, it generates dummy values of the correct type. When the instructions produce more values than the function's signature proscribes, it emits drop instructions.
impl Module {
pub fn fixup(&mut self, mut make_value: impl FnMut() -> i64) {
// ...
// The fixed-up instructions.
let mut fixed = Vec::with_capacity(
self.instructions.len(),
);
// The types on the stack at any given program
// point. Similar to the Wasm spec's appendix's
// validation algorithm.
let mut stack: Vec<ValType> = Vec::new();
for inst in mem::take(&mut self.instructions) {
// Special-case `drop` because it is
// polymorphic.
if matches!(inst, Inst::Drop) {
if stack.is_empty() {
fixed.push(
ValType::I32.make_const(make_value()),
);
} else {
stack.pop();
}
fixed.push(inst);
continue;
}
// First clamp entity indices to valid
// ranges.
let Some(inst) = self.fixup_inst_immediates(
&mut make_value,
has_mutable_global,
inst,
) else {
continue
};
// Then make sure that the stack has
// operands of the correct types for this
// instruction.
self.fixup_stack(
&mut make_value,
&mut fixed,
&mut stack,
&inst,
);
// Finally, apply the effects to the stack.
let len_operands = inst.operand_types(
&self.globals,
).len();
stack.truncate(stack.len() - len_operands);
stack.extend(inst.result_type(
&self.param_types,
&self.globals,
));
fixed.push(inst);
}
// ...
self.instructions = fixed;
}
fn fixup_stack(
&mut self,
mut make_value: impl FnMut() -> i64,
fixed: &mut Vec<Inst>,
stack: &mut Vec<ValType>,
inst: &Inst,
) {
let needed = inst.operand_types(&self.globals);
let n = needed.len();
if stack.len() >= n {
if (0..n).all(|i| {
stack[stack.len() - n + i] == needed[i]
}) {
// All needed operands are on the stack.
return;
}
} else {
if stack.iter().enumerate().all(|(i, ty)| {
*ty == needed[i]
}) {
// A prefix of needed operands are on the
// stack; make constants for the tail that
// are missing.
for ty in &needed[stack.len()..] {
fixed.push(ty.make_const(make_value()));
stack.push(*ty);
}
return;
}
}
// Otherwise, just make constants for all the
// needed operands.
for ty in needed {
fixed.push(ty.make_const(make_value()));
stack.push(*ty);
}
}
// ...
}
The fixup method also makes sure that for all instructions that have an immediate referencing some entity, the referenced entity is valid. For example, for a local.get $l instruction, it ensures that local $l actually exists or else rewrites the local to one that does exist.
impl Module {
// ...
fn fixup_inst_immediates(
&mut self,
mut make_value: impl FnMut() -> i64,
has_mutable_global: bool,
mut inst: Inst,
) -> Option<Inst> {
match &mut inst {
Inst::LocalGet(l) => *l %= self.param_types.len() as u32,
// ...
Inst::I32Load(m)
| Inst::I64Load(m)
| Inst::F32Load(m)
| Inst::F64Load(m)
| Inst::V128Load(m) => {
if self.num_memories == 0 {
return None;
}
*m %= self.num_memories;
}
// ...
_ => {}
}
Some(inst)
}
}
After calling fixup, the arb generator invokes Module::to_wasm_binary to get the encoded Wasm program.
bottom_up
The bottom_up generator also uses abstract interpretation to track the types of values on the stack. It generates instructions in forwards order, from operands to operators. It begins with an empty stack, filters candidate instructions down to just those that would be valid given the types currently on the stack, randomly chooses one, updates the stack types accordingly, and repeats the process. This is the same approach that wasm-smith uses. After generating instructions this way, it then makes sure that the final types on the stack match the function signature's results, similar to the end of fixup.
impl Module {
pub fn bottom_up(u: &mut Unstructured<'_>) -> Result<Self> {
// ...
let max_insts = u.int_in_range(1..=MAX_INSTS)?;
let mut instructions = Vec::new();
let mut stack: Vec<ValType> = Vec::new();
for _ in 0..max_insts {
if stack == result_types && u.ratio(3, 4)? {
break;
}
// Choose a random instruction whose operand
// types match those currently on the stack.
let inst = choose_inst_bottom_up(
u,
&stack,
¶m_types,
&globals,
num_memories,
)?;
// Apply this instruction's effects to the
// stack.
apply_inst(
&inst,
&mut stack,
¶m_types,
&globals,
);
instructions.push(inst);
}
// ...
Ok(Module {
param_types,
result_types,
globals,
num_memories,
instructions,
})
}
}
fn choose_inst_bottom_up(
u: &mut Unstructured<'_>,
stack: &[ValType],
param_types: &[ValType],
globals: &[Global],
num_memories: u32,
) -> Result<Inst> {
// Build up all the valid candidate instructions.
let mut candidates: Vec<Inst> = Vec::new();
// Producers are always okay: [] -> [t]
candidates.push(Inst::I32Const(0));
candidates.push(Inst::I64Const(0));
candidates.push(Inst::F32Const(0.0));
candidates.push(Inst::F64Const(0.0));
candidates.push(Inst::V128Const(0));
if !param_types.is_empty() {
candidates.push(Inst::LocalGet(0));
}
// ...
let top = stack.last().copied();
let second = stack.get(stack.len() - 2).copied();
// Drop needs 1 operand of any type: [t] -> []
if top.is_some() {
candidates.push(Inst::Drop);
}
// i32 unary: [i32] -> [...]
if top == Some(I32) {
candidates.push(Inst::I32Clz);
candidates.push(Inst::I32Ctz);
candidates.push(Inst::I32Popcnt);
// ...
}
// i64 unary: [i64] -> [...]
if top == Some(I64) {
candidates.push(Inst::I64Clz);
candidates.push(Inst::I64Ctz);
candidates.push(Inst::I64Popcnt);
// ...
}
// ...
// i32 binary: [i32 i32] -> [...]
if top == Some(I32) && second == Some(I32) {
candidates.push(Inst::I32Add);
candidates.push(Inst::I32Sub);
candidates.push(Inst::I32Mul);
// ...
}
// i64 binary: [i64 i64] -> [...]
if top == Some(I64) && second == Some(I64) {
candidates.push(Inst::I64Add);
candidates.push(Inst::I64Sub);
candidates.push(Inst::I64Mul);
// ...
}
// ...
// Choose a random instruction from the
// candidates.
let mut inst = *u.choose(&candidates)?;
// If the instruction has immediates, generate
// them here, as they were hard-coded during
// candidate selection.
match &mut inst {
Inst::I32Const(v) => *v = u.arbitrary()?,
Inst::I64Const(v) => *v = u.arbitrary()?,
// ...
Inst::GlobalGet(g) => {
*g = u.int_in_range(0..=(globals.len() as u32 - 1))?;
}
// ...
Inst::I32Load(m)
| Inst::I64Load(m)
| Inst::F32Load(m)
| Inst::F64Load(m)
| Inst::V128Load(m)
| Inst::I32Store(m)
| Inst::I64Store(m)
| Inst::F32Store(m)
| Inst::F64Store(m)
| Inst::V128Store(m)
| Inst::MemorySize(m)
| Inst::MemoryGrow(m) => {
*m = u.int_in_range(0..=(num_memories - 1))?;
}
_ => {}
}
Ok(inst)
}
After constructing a Module via bottom_up, we don't need to call fixup because the module is already valid by construction, so all that's left is invoking Module::to_wasm_binary to get the encoded Wasm program.
top_down
The top_down generator is very similar to bottom_up, but instead of generating instructions forwards, from operands to operators, it generates them backwards, from operators to operands. Instead of maintaining a stack of the types of values generated thus far by the instruction sequence prefix, it maintains a stack of the types of values expected by the instruction sequence suffix. This is the approach that rgfuzz by Park, Kim, and Yun takes.2
impl Module {
pub fn top_down(
u: &mut Unstructured<'_>,
) -> Result<Self> {
// ...
let max_insts = u.int_in_range(1..=MAX_INSTS)?;
let mut instructions = Vec::new();
let mut needed = result_types.clone();
for _ in 0..max_insts {
if needed.is_empty() && u.ratio(3, 4)? {
break;
}
// Choose a random instruction in a
// top-down manner.
let inst = choose_inst_top_down(
u,
needed.last().copied(),
¶m_types,
&globals,
num_memories,
)?;
// Pop the result type from `needed`, if
// any, as it's been satisfied.
let ty = inst.result_type(
¶m_types,
&globals,
);
if ty == needed.last().copied() {
needed.pop();
}
// Add operand type demands.
match &inst {
Inst::Drop => {
// `drop` is polymorphic; choose
// a random type.
needed.push(u.arbitrary()?);
}
Inst::GlobalSet(g) => {
needed.push(globals[*g as usize].ty);
}
_ => {
needed.extend_from_slice(
inst.operand_types(&globals),
);
}
}
instructions.push(inst);
}
// Fill remaining needed types with
// constants.
for ty in needed.iter().rev() {
instructions.push(
ty.make_const(u.arbitrary()?),
);
}
// Instructions were generated backwards, so
// reverse.
instructions.reverse();
Ok(Module {
param_types,
result_types,
globals,
num_memories,
instructions: prefix,
})
}
}
fn choose_inst_top_down(
u: &mut Unstructured<'_>,
target_ty: Option<ValType>,
param_types: &[ValType],
globals: &[Global],
num_memories: u32,
) -> Result<Inst> {
let mut candidates: Vec<Inst> = Vec::new();
match target_ty {
Some(I32) => {
candidates.push(Inst::I32Const(0));
candidates.push(Inst::I32Add);
candidates.push(Inst::I32Sub);
candidates.push(Inst::I32Mul);
// ...
}
Some(I64) => {
candidates.push(Inst::I64Const(0));
candidates.push(Inst::I64Add);
candidates.push(Inst::I64Sub);
candidates.push(Inst::I64Mul);
// ...
}
Some(F32) => {
candidates.push(Inst::F32Const(0.0));
candidates.push(Inst::F32Add);
candidates.push(Inst::F32Sub);
candidates.push(Inst::F32Mul);
// ...
}
// ...
None => {
// Nothing needed. `drop`, `global.set`, and
// stores add demand.
candidates.push(Inst::Drop);
if globals.iter().any(|g| g.mutable) {
candidates.push(Inst::GlobalSet(0));
}
if num_memories > 0 {
candidates.push(Inst::I32Store(0));
// ...
}
}
}
let mut inst = *u.choose(&candidates)?;
// If the instruction has immediates, generate
// them here, as they were hard-coded during
// candidate selection. Same as `bottom_up`.
match &mut inst {
// ...
}
Ok(inst)
}
Similar to bottom_up, after we've constructed a Module via top_down, we don't need to call fixup because the module is already valid by construction. All that's left is invoking Module::to_wasm_binary to get the encoded Wasm program.
mutate
mutate is, as the name implies, a mutator rather than a generator. It is the direct equivalent of the arb generator, but for mutation: it uses derive(mutatis::Mutate) on Module and Inst to automatically generate custom mutators for these types, rather than authoring them by hand. After producing a new Module by mutating an old Module, that new Module probably represents an invalid Wasm program, in the same way that derive(arbitrary::Arbitrary) produces Modules that are probably invalid. And mutate also uses the same approach that arb does to resolve this problem: the fixup method.
But first, a mutator-specific wrinkle is that fuzz_mutator! gives us a mutable byte slice to mutate, not a Module. We address this gap by deriving the serde crate's Serialize and Deserialize traits on Module and Inst, deserializing a Module from the mutable byte slice, mutating that deserialized Module with mutatis, and then reserializing it back into the mutable byte slice. We use the postcard crate here, but could just as easily use bincode, JSON, or protobuf.
use libfuzzer_sys::{fuzz_mutator, fuzz_target, fuzzer_mutate};
fuzz_mutator!(|
data: &mut [u8],
size: usize,
max_size: usize,
seed: u32,
| {
// With probability of about 1/8, use default
// mutator.
if seed.count_ones() % 8 == 0 {
return fuzzer_mutate(data, size, max_size);
}
// Try to decode using postcard; fallback to
// default input on failure.
let mut module: Module =
postcard::from_bytes(&data[..size])
.ok()
.unwrap_or_default();
// Mutate with `mutatis`.
let mut session = mutatis::Session::new()
.seed(seed.into())
.shrink(max_size < size);
if session.mutate(&mut module).is_ok() {
if let Ok(encoded) = postcard::to_slice(
&module,
data,
) {
return encoded.len();
}
}
// Fallback to the default libfuzzer mutator if
// serialization or mutation fails because, for
// example, `data` doesn't have enough capacity.
fuzzer_mutate(data, size, max_size)
});
Finally, the fuzz target itself deserializes the Module from the raw bytes, calls fixup, encodes it to a Wasm binary via Module::to_wasm_binary, and then passes that into Wasmtime.
fuzz_target!(|data: &[u8]| {
let Ok(mut module) = postcard::from_bytes::<Module>(data) else {
return;
};
module.fixup(|| 0);
let wasm = module.to_wasm_binary();
// ...
});
Benchmarking
Methodology
We pair each of our generators and mutator with libfuzzer-sys and feed the resulting test cases into Wasmtime. All fuzzers start with an empty corpus.
The most important metric for a fuzzer is its bug-finding ability, but that can be difficult to measure directly. For example, Wasmtime is actively fuzzed 24/7 with more-complete fuzzers than those implemented here, so, as expected, I have not found any bugs via these benchmarks. Therefore, instead of reporting a found-bugs count, the benchmark harness reports two alternative metrics:
-
Coverage over time: Coverage is the cumulative code paths exercised by the fuzzer. A fuzzer cannot find bugs in code paths it does not cover. This is the most important metric reported.
-
Executions over time: An execution is one iteration of the fuzzing loop. This is basically measuring how fast the fuzzer can produce test cases. All else being equal, more executions is better, but all else is rarely equal. It is easy to generate poor test cases very quickly: just return an empty sequence of Wasm instructions every time. Unfortunately, that exclusively leads to useless executions. Therefore, this metric is really only useful when comparing two implementations of the same algorithm, and I've omitted its results in the next section.
Additionally, I report results for both 24 hours of fuzzing and 5 minutes of fuzzing. The expected behavior of long-term fuzzing, e.g. 24/7 fuzzing in OSS-Fuzz, can be extrapolated from the 24-hour results. The 5-minute results show the expected behavior of short-term fuzzing, e.g. when using mutatis::check or arbtest.
Discussion of short-term fuzzing is somewhat rare, so I feel its motivation deserves explanation. I find short-term fuzzing useful in the following scenarios, for example:
- Running a quick fuzzing session locally, to catch bugs that avoid detection in the traditional unit- and integration-test suites, before opening a pull request.
- Running some quick fuzzing in CI before allowing a pull request to merge, for similar reasons.
That is, short-term fuzzing is useful for the same reasons and in the same scenarios as property-based testing.3
As recommended in Evaluating Fuzz Testing by Klees, Ruef, Cooper, Wei, and Hicks and adopted in Fuzz Bench: An Open Fuzzer Benchmarking Platform and Service by Metzman, Szekeres, Simon, Sprabery, and Arya, the benchmark harness tests the statistical significance of its results with a Mann-Whitney U-test. The harness performs 20 trials per fuzzer, the same number of trials as Fuzz Bench.
Results
24 Hours of Fuzzing
-
arbhas 1.00 ± 0.00 times more coverage thanbottom_up(p = 0.01) -
mutatehas 1.01 ± 0.00 times more coverage thanarb(p = 0.00) -
top_downhas 1.00 ± 0.00 times more coverage thanarb(p = 0.00) -
mutatehas 1.02 ± 0.00 times more coverage thanbottom_up(p = 0.00) -
top_downhas 1.01 ± 0.00 times more coverage thanbottom_up(p = 0.00) -
mutatehas 1.01 ± 0.00 times more coverage thantop_down(p = 0.00)
5 Minutes of Fuzzing
-
bottom_uphas 1.01 ± 0.01 times more coverage thanarb(p = 0.04) -
mutatehas 1.47 ± 0.02 times more coverage thanarb(p = 0.00) -
top_downhas 1.06 ± 0.02 times more coverage thanarb(p = 0.00) -
mutatehas 1.45 ± 0.01 times more coverage thanbottom_up(p = 0.00) -
top_downhas 1.05 ± 0.02 times more coverage thanbottom_up(p = 0.00) -
mutatehas 1.38 ± 0.02 times more coverage thantop_down(p = 0.00)
Conclusion
The mutate fuzzer performs best. It vastly outperforms all the others at 5 minutes of fuzzing (36-49% more coverage), and while the rest narrow that gap after 24 hours of fuzzing, mutate maintains its lead (1-2% more coverage).
The comparison between arb and mutate is as apples-to-apples of a comparison as it gets between idiomatic test-case generation and mutation in Rust: derive(Arbitrary) and derive(Mutate). They use the same fixup method to ensure that the resulting Wasm instructions are valid. The fuzzer built with mutatis and test-case mutation provides better coverage over time than the fuzzer built with arbitrary and test-case generation. When writing structure-aware fuzzers, I used to reach for arbitrary; in the future, I will reach for mutatis instead.
The top_down fuzzer performs second-best, and is best of the generation-based fuzzers. This aligns with results from the rgfuzz paper, which found that top-down Wasm instruction generation resulted in better instruction diversity than bottom-up generation. This result is intuitive, they point out, because Wasm instructions tend to have more operands than results, which means that more candidates are filtered out from consideration when generating instructions in forward order from operands to results (bottom-up) than when generating them in backward order from results to operands (top-down).
Subjectively, none of the approaches feel significantly more-complicated nor easier to implement than the others. All approaches require a stack of types, representing the generated Wasm's operand stack, at some point in their implementation. Some require it during instruction generation (top_down and bottom_up) while others require it during fixup (mutate and arb). Adding support for new Wasm instructions is roughly the same in all of them: add a new variant to enum Inst and define its operand and result types. top_down and bottom_up additionally require adding a line for the new instruction in their choose_inst_{top_down,bottom_up} functions, but this could be avoided with some targeted macro_rules! sugar.
The fixup method fixes instructions in a forwards order; as future work, it would be interesting to implement a backwards_fixup method that fixes instructions in a backwards order and see if mutate and backwards_fixup outperforms the current mutate and forwards fixup the same way that backwards generation (top_down) outperforms forwards generation (bottom_up).
fixup makes an attempt to reuse stack operands when it can, rather than synthesize dummy constants or drop already-computed values, but the attempt is somewhat half-hearted. Dropping operands introduces dead code, which is not very interesting for exercising deep into the compiler pipeline. Dummy constants are not that interesting either. Therefore, another potential line of follow-up work would be to investigate ways to maximize operand reuse and minimize drops and dummy constants inserted while ensuring validity. That could include storing values to memory or globals instead of droping them when possible. It could even include liberating ourselves from the stack-focused paradigm we've had thus far.
WebAssembly is a stack-based language and so it is natural that our approaches have focused on producing stack-y code. But, in practice, optimizing WebAssembly compilers like Wasmtime's use a static single-assignment intermediate representation, and erase the operand stack early in their compilation pipelines. Therefore, from these compilers' point of view, the following two WebAssembly snippets are identical:
;; `x = a + (b * c)` in a "stack-y" encoding and
;; without temporary locals.
local.get $a
local.get $b
local.get $c
i32.mul
i32.add
local.set $x
;; `x = a + (b * c)` in a "non-stack-y" encoding
;; that uses temporary locals for every operation.
;;
;; Equivalent of
;;
;; temp0 = b * c
;; temp1 = a + temp0
;; x = temp1
local.get $b
local.get $c
i32.mul
local.set $temp0
local.get $a
local.get $temp0
i32.add
local.set $temp1
local.get $temp1
local.set $x
Producing code that uses many temporaries in this manner might be easier than code that doesn't, but, more importantly, it may enable better reuse of already-computed subexpressions, emit less dead code, and ultimately produce more interesting data-flow graphs that better exercise the deep innards of the compiler.
A final vein of interesting follow-up work to mine would be comparing arbitrary-based generators and mutatis-based mutators for structured inputs that are not programming languages and when the SUT we are fuzzing is not a compiler. Do we see these same results when, for example, producing PNG images to fuzz an image-transformation library?
Here is the source code for this experiment, including the three generators, one mutator, raw benchmark data, and benchmarking harness. The README includes instructions on running the benchmarks yourself.
-
WebAssembly's stack-based instructions encode an expression tree -
local.get $a; local.get $b; local.get $c; i32.add; i32.mulis isomorphic toa * (b + c)- so the experiment should be relevant and applicable to any other generator or mutator for a programming language with expressions, even if it might not appear so at first glance. ↩ -
Ignoring its rule-guided bit, which is orthogonal and could be applied to
bottom_upas well. ↩ -
Structure-aware fuzzing and property-based testing are basically the same: convergent evolution from different communities. ↩
01 Jun 2026 7:00am GMT



