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
Andreas Farre: Session History Diagrams in Firefox DevTools
I've spent a lot of time at Mozilla working on session history, the machinery that keeps track of where you've been so the back and forward buttons do something sensible. It's one of those parts of the browser that sounds simple from the outside and turns out to be anything but. Once you add iframes, nested iframes, and the subtle rules about when a navigation creates a new entry versus replacing the current one, the state you're reasoning about gets large and hard to hold in your head.
For years my main tool for understanding that state was reading code and printing things to a log. That works, but it's slow, and it never quite shows you the shape of the thing. So I built a way to see it: a new DevTools panel in Firefox Nightly called Session History Diagrams.
Enabling it
The panel is available in Firefox today, behind a pref. It's been there in some form since Firefox 150, growing more stable with each release. To turn it on, set devtools.application.sessionHistory.enabled to true in about:config, then reload DevTools. The new panel lives under the Application tab, next to Service Workers and Manifest, and it draws the browser's session history as a diagram that updates as you navigate.
Since Firefox 153 it also works over remote debugging. Connect to a device from about:debugging and you can watch the session history of a page running on Android, the same as you would on the desktop.
Jake diagrams
I didn't invent the idea of drawing this. The HTML spec already has a notation for it, called a Jake diagram after Jake Archibald, and that's where I started. It's a tabular notation where columns represent steps in session history, and rows represent navigables (the top-level browsing context plus any iframes). Background colors identify documents, a fresh color marking a new document loaded in that navigable, and the current step is shown in bold. It's a genuinely useful way to capture multi-navigable interactions that are otherwise hard to describe in prose.

These diagrams don't have to be drawn by hand. Domenic Denicola, one of the HTML spec editors, built a Jake diagram generator that turns a description of a navigation sequence into a rendered diagram. That's where I first started playing with a more dynamic approach to the visualization. The thing I missed the most was being able to build a history up step by step rather than describe a finished sequence all at once. So I wrote rejake, a small tool that draws diagrams in the same style1, but lets you construct the history one step at a time.
But rejake, like the spec's diagrams and Domenic's generator before it, was stuck with a limitation the spec itself admits to, that they only work with a single level of nesting. That was exactly my problem. Real pages nest iframes inside iframes, and the bugs I was chasing usually lived down in that deeper nesting, precisely where the diagram stops being able to help. And however I drew them, I was still typing the history out by hand. It's a short step from there to wanting the diagram to draw itself from the browser's actual session history instead2.
Firefox Session History Diagrams
So the panel extends Jake diagrams to handle arbitrary nesting. Every column is a step in the session history. Every row is a frame, listed in pre-order from the frame tree: top-level document first, then its first iframe, then that iframe's children, and so on. The current entry is highlighted in blue, and the diagram updates live as you navigate.
The recording above is an ordinary bit of browsing, a handful of pages visited one after another. The top row tracks the page you're actually looking at, and the current position is the one in blue. The interesting part is everything underneath that top row.
Some of those pages didn't just load a single document. They pulled in nested frames of their own, and the diagram stacks those below the page that owns them. None of that is visible in the address bar or anywhere in the page chrome, and the frames come and go as you move through the history. Normally you'd have no way of knowing they were ever there. Here you can read straight off the diagram which frames a given step carried, when each one entered, and when it dropped away again.
Who else might want this
I built this for myself, working on Gecko's session history internals, where being able to watch the diagram change while reproducing a bug turns opaque state into something I can point at. But it turns out I'm not the only one who hits this wall. Plenty of people working elsewhere in Gecko, anywhere near navigation, end up reasoning about the same state, and now we all share one picture of it.
If you build single-page applications, or work with the History API or Navigation API, you've probably run into the same kind of confusion from the other side. A push where you expected a replace, a missing history entry, an iframe that accumulated entries unexpectedly. These are hard to reason about without seeing the state directly, and that's exactly what the diagram gives you.
Session history isn't a Firefox-specific problem either. Every engine implements the same part of the HTML spec, and Jake diagrams come from that shared spec. The panel only ever shows Firefox's state, but the rules are the same everywhere, so if you work on another engine it can still be a useful reference for how one implementation behaves. It's often the only practical way to surface an interoperability difference, which might be a bug in any of the engines, but stays hidden until you can actually see it.
Thanks
A big thanks to Nicolas Chevobbe, whose assistance was invaluable in getting the DevTools integration right. The work, including what's still to come, is tracked in Bug 2015726. There's a fair bit still on that list, like marking whether a step was a push or a replace, surfacing back/forward cache state, tying the diagram into the Network and Inspector panels, and more, all heading toward fuller DevTools support for Navigation and Session History.
Notes
-
Which, naturally, meant re-implementing the whole of Session History along the way. ↩
-
Getting nerd-sniped by Jan Jaeschke definitely contributed as well. ↩
01 Jun 2026 12:00am GMT
31 May 2026
Planet Mozilla
Olivier Mehani: Optional Docker services and dependencies
Like many, docker and and compose have become my go-to tool to create software that can be conveniently deployed to production with a limited amount of headache. However, many tasks, and sometimes whole services, pertain only to the development side of the workflow, and need to stay there.
Moreover, some tasks, such as time-consuming provisioning tasks, are only on-demand one-offs. They shouldn't run at all most of the time, but they should slot into the dependency graph correctly when needed.
tl;dr: I realised that docker compose supports profiles, which allows services to be enabled conditionally, along with the depends_on.[].required option, to ignore them when they are disabled. Profiles are also useful to package actions and triggers to run on demand, so they are not started by default.
We can start with a simple setup where our long-running main service depends on an init service to perform preliminary steps. This can be setup with depends_on the compose.yaml.
services:
main:
image: debian:latest
command: "sh -c 'while : ; do echo main; sleep 10; done"
depends_on:
init:
condition: service_completed_successfully
init:
image: debian:latest
command: sh -c 'echo init; sleep 10'
Even when run ning the main container, we get the right dependency (and delay). So far so good (though up will show the output from all containers.
![Screenshot of a terminal. ``` [21:25:38] ~/docker-profiles$ docker compose run main 5s [+] 2/2t 2/22 ✔ Network docker-profiles_default Created 0.1s ✔ Container docker-profiles-init-1 Started 0.3s Container docker-profiles-init-1 Waiting Container docker-profiles-init-1 Exited Container docker-profiles-main-run-17209b1867a1 Creating Container docker-profiles-main-run-17209b1867a1 Created main main main ^C [21:26:50] ~/docker-profiles$ docker compose down 33s 130 ↵ [+] down 2/2 ✔ Container docker-profiles-init-1 Removed 0.0s ✔ Network docker-profiles_default Removed 0.1s [21:26:58] ~/docker-profiles$ docker compose up 1s WARN[0000] Found orphan containers ([docker-profiles-main-run-17209b1867a1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. [+] up 3/3 ✔ Network docker-profiles_default Created 0.1s ✔ Container docker-profiles-init-1 Created 0.1s ✔ Container docker-profiles-main-1 Created 0.0s Attaching to init-1, main-1 init-1 | init Container docker-profiles-init-1 Waiting init-1 exited with code 0 Container docker-profiles-init-1 Exited main-1 | main main-1 | main Gracefully Stopping... press Ctrl+C again to force Container docker-profiles-main-1 Stopping main-1 | main Container docker-profiles-main-1 Stopped Container docker-profiles-init-1 Stopping Container docker-profiles-init-1 Stopped main-1 exited with code 137 [21:28:26] ~/docker-profiles$ ```](https://blog.narf.ssji.net/wp-content/uploads/sites/3/2026/05/image.png)
But what if we have another, much more time consuming, initialisation step?
services:
[...]
opt-init:
image: debian:latest
command: sh -c 'echo opt-init; sleep 100'
Perhaps we are lucky, and while it needs to run once, we don't need it to run everytime (think: database setup).
Docker compose can use profiles to select when services are started. It will then only be started when this profile is selected. Services without explicit profile will always be started, but any service with one or more profile listed will only get started iff that profile is selected.
We can make the opt-init service part of the opt profile. We can also make the main service dependent on it, so it is started beforehand.
services:
main:
[...]
depends_on:
[...]
opt-init:
condition: service_completed_successfully
opt-init:
[...]
profiles:
- opt
This works well enough when the opt profile is specified but… Oh no! If the profile is not specified, the dependency on the opt-init isn't resolvable, and none of the stack can spin up with just docker compose up
![Screenshot of a terminal. ``` [21:36:50] ~/docker-profiles$ docker compose --profile opt up 130 ↵ [+] up 4/4 ✔ Network docker-profiles_default Created 0.1s ✔ Container docker-profiles-opt-init-1 Created 0.1s ✔ Container docker-profiles-init-1 Created 0.1s ✔ Container docker-profiles-main-1 Created 0.0s Attaching to init-1, main-1, opt-init-1 opt-init-1 | opt-init init-1 | init Container docker-profiles-opt-init-1 Waiting Container docker-profiles-init-1 Waiting Container docker-profiles-opt-init-1 Exited opt-init-1 exited with code 0 init-1 exited with code 0 Container docker-profiles-init-1 Exited main-1 | main main-1 exited with code 0 [21:36:55] ~/docker-profiles$ docker compose up 2s service "main" depends on undefined service "opt-init": invalid compose project ```](https://blog.narf.ssji.net/wp-content/uploads/sites/3/2026/05/image-1.png)
Fortunately, this is easily solved with the required attribute of the depends_on objects.
services:
main:
[...]
opt-init:
condition: service_completed_successfully
required: false
And that's really all there is to it: with the right profile, the optional dependency is started in the desired order, but its absence is otherwise transparently ignored. Both docker compose up and docker compose --profile opt work as desired.
![Screenshot of a terminal. ``` [21:40:31] ~/docker-profiles$ docker compose up 4s Attaching to init-1, main-1 init-1 | init Container docker-profiles-init-1 Waiting init-1 exited with code 0 Container docker-profiles-init-1 Exited main-1 | main main-1 exited with code 0 [21:40:35] ~/docker-profiles$ docker compose --profile opt up 3s Attaching to init-1, main-1, opt-init-1 opt-init-1 | opt-init init-1 | init Container docker-profiles-init-1 Waiting Container docker-profiles-opt-init-1 Waiting Container docker-profiles-opt-init-1 Exited opt-init-1 exited with code 0 init-1 exited with code 0 Container docker-profiles-init-1 Exited main-1 | main main-1 exited with code 0 ```](https://blog.narf.ssji.net/wp-content/uploads/sites/3/2026/05/image-2.png)
Profiles afford us another useful trick: on-demand tasks not started by default. This can be handy for maintenance tasks (data cleanup, garbage collection, …) or test scripts (running test workload, sending message, …). Those are handy during development, but would not be necessary, or take a different form, in other deployments.
services:
[...]
say-hello:
image: debian:latest
profiles:
- hello
command: echo hello
depends_on:
main:
condition: service_started
Conveniently, when explicitly running a service, it is not necessary to request a matching profile, keeping the command line lean: docker compose run say-hello.
![Screenshot of a terminal. ``` [21:47:53] ~/docker-profiles$ docker compose --profile opt down --remove-orphans 130 ↵ [+] down 5/5 ✔ Container docker-profiles-main-1 Removed 10.2s ✔ Container docker-profiles-init-1 Removed 0.0s ✔ Container docker-profiles-opt-init-1 Removed 0.0s ✔ Container docker-profiles-say-hello-run-c26e752b1edd Removed 0.0s ✔ Network docker-profiles_default Removed 0.1s [21:48:05] ~/docker-profiles$ docker compose run say-hello 11s [+] 3/3t 3/33 ✔ Network docker-profiles_default Created 0.1s ✔ Container docker-profiles-init-1 Exited 1.9s ✔ Container docker-profiles-main-1 Started 2.1s Container docker-profiles-say-hello-run-76efc04798b4 Creating Container docker-profiles-say-hello-run-76efc04798b4 Created hello ```](https://blog.narf.ssji.net/wp-content/uploads/sites/3/2026/05/image-3.png)
So here we are. Compose profiles allow us to control which services get started, and mark some as conditional. This, coupled with the ability to mark some depends_on rules as not required is a good way to seamlessly prevent heavy or otherwise time consuming services from starting when not needed, while retaining proper dependency ordering when enabled.
For completeness, the full, final, compose.yaml looks as follow.
services:
main:
image: debian:latest
command: "sh -c 'while : ; do echo main; sleep 10; done'"
depends_on:
init:
condition: service_completed_successfully
opt-init:
condition: service_completed_successfully
required: false
init:
image: debian:latest
command: sh -c 'echo init; sleep 10'
opt-init:
image: debian:latest
profiles:
- opt
command: sh -c 'echo opt-init; sleep 100'
say-hello:
image: debian:latest
profiles:
- hello
command: echo hello
depends_on:
main:
condition: service_started
The post Optional Docker services and dependencies first appeared on Narf.
31 May 2026 11:58am GMT
The Servo Blog: April in Servo: new Android UI, focus, forms, security fixes, and more!
Servo 0.2.0 contains all of the changes we landed in April, which came out to yet another record 534 commits (March: 530). For security fixes, see § Security.

We've shipped several new web platform features:
- <select multiple> (@lukewarlow, @mrobinson, #43189)
- <template shadowrootslotassignment> (@simonwuelker, #44246)
- <video> playback on OpenHarmony (@rayguo17, #43208)
- 'minimum-scale' and 'maximum-scale' values in <meta name=viewport> (@shubhamg13, #40098, #43715)
- 'color-mix()' with any number of <color> values (@Loirooriol, #43890)
- '&::before' and '&::after' in '::details-content' (@Loirooriol, #43878)
- 'revert-rule' (@Loirooriol, #43878)
- 'tab-size' (@mrobinson, @SimonSapin, #44480)
- 'text-align: match-parent' (@TG199, #44073)
- new Worker() with blob URLs (@jdm, #44004)
- getContext(
"webgl") on OffscreenCanvas (@niyabits, #44159) - the detail property on PerformanceMark and PerformanceMeasure (@shubhamg13, #44289, #44272)
Plus a bunch of new DOM APIs:
- 'selectionchange' events on <input> and <textarea> (@TimvdLippe, #44461)
- StorageManager, in experimental mode (@Taym95, #43976)
- activeElement on Document and ShadowRoot (@mrobinson, #43861)
- crypto.subtle.supports() (@kkoyung, #43703) - Servo is the first major browser engine to support this!
- cellPadding, cellSpacing, and align properties on HTMLTableElement (@mrobinson, #43903) - previously supported in HTML only
- relatedTarget on 'focus' and 'blur' events (@mrobinson, #43926)
- transferFromImageBitmap() on ImageBitmapRenderingContext (@Messi002, #43984)
Servo's support for text in Chinese, Japanese, and Korean languages has improved, with correct wrapping in the layout engine (@SharanRP, #43744), and CJK fonts now enabled in servoshell's browser UI on Windows, Linux, and FreeBSD (@yezhizhen, @CynthiaOketch, @nortti0, #44055, #44138, #44514).
Navigating to a JSON file as the top-level document now renders the JSON with an interactive pretty-printer (@webbeef, @TimvdLippe, #43702).
April was a big milestone for Servo, with some automated tests failing because they had hard-coded cookie expiry dates set to April 2016 plus ten years. Surprise! We're still here. Here's to the next 100 years of Servo (@jdm, #44341).
This is another big update, so here's an outline:
Security
CryptoKey now zeroes buffers containing key material after use (@kkoyung, #44597).
With only a few exceptions, you can only access DOM APIs in another document if that document is in the same origin. But if that document is in the same site with a different port number, Servo currently allows these accesses even though it shouldn't. We've fixed some (but not all) of these incorrect accesses, specifically those that involve binding a Window or Location method in this document with a this from the other document (@yvt, @jdm, #28583).
We've fixed a bug where localStorage and sessionStorage were usable in sandboxed <iframe> and shared with every other sandboxed <iframe>, rather than throwing SecurityError (@Taym95, #44002).
We've fixed a bug where localStorage and sessionStorage were shared between all <iframe srcdoc> documents, rather than isolated using the origin of the containing document (@niyabits, #43988, #44038).
We've fixed a bug where IndexedDB was usable in sandboxed <iframe> and data: URL web workers (@Taym95, #44088).
We've fixed a bug where pages in some IP address origins can evict cookies from other IP address origins (@officialasishkumar, #44152). Only evicting cookies was possible, not reading or writing them.
We've fixed an out-of-bounds memory read in texImage3D() on WebGL2RenderingContext (@simartin, #44270), and fixed some undefined behaviour in servoshell's signal handler (@Narfinger, #43891).
Work in progress
IndexedDB is now enabled in servoshell's experimental mode (@arihant2math, #44245). As always, embedders can enable it with Preferences::dom_indexeddb_enabled (@arihant2math, #44245, #44283).
IndexedDB now uses Servo's new "client storage" system, which is based on the Storage Standard and will allow us to have a unified on-disk format and quota management for all web platform features that persistently store data (@gterzian, #44374, #43900). We've also made key range queries more efficient (@arihant2math, #39009), landed improvements to IDBDatabase, IDBObjectStore, IDBCursor, IDBKeyRange, IDBRequest, and to the handling of transactions, keys, values, and exceptions (@Taym95, #44128, #43901, #44009, #43914, #44161, #44183, #44059, #44215, #42998, #43805).
We've made more progress on the IntersectionObserver API, under --pref dom_intersection_observer_enabled (@stevennovaryo, @jdm, #42204).
We're continuing to implement document.execCommand() for rich text editing (@TimvdLippe, #44529), under --pref dom_exec_command_enabled. This release adds support for the 'bold', 'fontName', 'fontSize', 'italic', 'strikethrough', and 'underline' commands (@TimvdLippe, @jdm, @mrobinson, #44511, #43287, #44432, #44410, #44194, #44030, #44039, #44041, #44075, #44234, #44250, #44331, #44390, #44137, #44293, #44312, #44347).
All of the features above are enabled in servoshell's experimental mode.
Servo can now build a very basic accessibility tree for web contents, under --pref accessibility_enabled (@alice, @delan, @lukewarlow, #42338, #43558, #44437, #44438). This includes text runs, plus nine other non-interactive accessibility roles (@alice, @delan, #44255). We've also fixed a crash when reloading pages with accessibility enabled (@alice, #44473), and made accessibility tree updates more efficient (@alice, #44208).
We've started implementing the Sanitizer API, under --pref dom_sanitizer_enabled (@kkoyung, #44198, #44290, #44335, #44421, #44452, #44481, #44585, #44594).
We've also started implementing SharedWorker, under --pref dom_sharedworker_enabled (@Taym95, #44375, #44440).
We're working on the WakeLock API too, under --pref dom_wakelock_enabled (@TG199, @rovertrack, #43617, #44343).
servoshell
servoshell for Android now has a revamped browser UI, including a new history view (@espy, #43795), the apk is 30% smaller (@jschwe, #44278, #44182), and we've fixed the black screen bug when closing settings or switching back from another app (@yezhizhen, #44327). You can now close tabs on OpenHarmony too (@Narfinger, #42713).

As for servoshell on desktop platforms, we've fixed some focus- and IME-related bugs (@mrobinson, #43872, #43932), and on Windows, we now install a normal shortcut without the strange behaviour of an "advertised" shortcut (@yezhizhen, #44223).
For developers
When using the Inspector tab in the Firefox DevTools, the Rules panel now includes declarations in '@layer' rules (@arabson99, #43912).
When logging expressions in the Console tab, and when hovering over symbols in the Debugger tab, you can now get more information about the contents of functions, arrays, objects, and other values (@atbrakhi, @eerii, #44172, #44173, #44022, #44233, #44196, #44181, #44064, #44023, #44164, #44369, #44262).
When using the Debugger tab, you can now use the Scopes panel to inspect local and global variables (@eerii, @atbrakhi, #43792, #43791), you can now debug web worker scripts (@atbrakhi, #43981), and we've started implementing blackboxing, aka the Ignore source button (@freyacodes, #44142).
We've also landed some initial support for the Style Editor tab (@rovertrack, #44517, #44462).
We're working towards re-enabling our automated DevTools tests in CI, which should make the feature more reliable (@freyacodes, #44577), and we've landed a small build reproducibility fix too (@jschwe, #44459).
For developers of Servo itself, please note that the Cargo 'release' profile is no longer #[cfg(debug_assertions)] (@jschwe, @mrobinson, #44177). If you've been using 'release' as a "faster 'debug' with assertions" build locally, consider switching to 'checked-release' or 'medium'.
The pull request template has been updated (@mrobinson, #44135). 'Testing' and 'Fixes' should go at the bottom of the PR description, and 'Testing' is about automated tests, not how you tested the PR locally.
We've made more progress on the new dev container, which will provide an alternative to our usual procedures for setting up a Servo build environment (@jschwe, @sagudev, #44126, #44111, #44162, #44641, #44109). Keep an eye out for that in the book!
In the meantime, did you know that you can use Lix or Nix to build Servo on Linux with a lot less hassle, even if you're not using NixOS? For now at least, head to the NixOS page in the book to learn more. We've also fixed a regression that made --debug-mozjs and MOZJS_FROM_SOURCE builds take much longer to complete on Linux when not using Nix (@jschwe, #44346).
We've fixed building Servo with the 'jitspew' feature in mozjs, allowing you to set IONFLAGS to enable JIT logging (@simonwuelker, #44010). We've also fixed build issues on Windows and FreeBSD (@zhangxichang, @mrobinson, #44264, #44591).
Embedding API
With this second monthly release of the Servo library, we have some quick notes about API stability and semver compatibility:
-
The 'servo' package follows Cargo's rules for semver compatibility. 0.1.1 is compatible with version 0.1.0, but 0.2.0 is a breaking update.
-
Until we integrate semver analysis into our release process, each monthly release will have a breaking version number, while non-breaking version numbers may be used for LTS updates.
-
In general, dependencies of 'servo', like 'servo-base' and 'servo-script', do not use semver. Any release may include breaking changes.
We've fixed a build failure affecting embedders with a new or updated Cargo.lock (@jschwe, #44093), and landed several other changes to help us with the Servo library release process (@jschwe, @mukilan, #43972, #44642, #43182, #43866, #44086, #43797).
Breaking changes:
-
WebView::animatingnow takes&selfinstead ofself, so you can call it without cloning the handle (@JavaDerg, #44253) -
Servo::site_data_managernow returns&SiteDataManagerinstead ofRef<'_, SiteDataManager>(@sabbCodes, #44116) -
WebViewDelegate::play_gamepad_haptic_effectandstop_gamepad_haptic_effecthave been removed (@mrobinson, #43895), but they have not worked since February 2026 - useGamepadDelegateinstead
You can now load a URL with custom request headers by calling WebView::load_request (@Narfinger, @longvatrong111, @mrobinson, #43338).
You can now retrieve cookies asynchronously by calling SiteDataManager::cookies_for_url_async (@longvatrong111, #43794).
The synchronous version of that method, SiteDataManager::cookies_for_url, was previously not callable because CookieSource was not exposed to the public API, but we've fixed that now (@TG199, #44124).
You can now clear session cookies without clearing permanent cookies by calling SiteDataManager::clear_session_cookies (@longvatrong111, #44166).
When intercepting requests with ServoDelegate:: and WebViewDelegate::load_web_resource, we now include a destination and referrer_url in the WebResourceRequest, which can be helpful if you're implementing ad blocking (@webbeef, #44493).
You can configure Servo to write all of its storage to a unique directory for that session by enabling Opts::temporary_storage (@janvarga, #44433). Note that these unique directories currently persist after Servo exits, so it's an isolation feature, not a privacy feature.
WindowRenderingContext::new and SoftwareRenderingContext::new now return an error if the given size is less than 1x1 (@freyacodes, @mrobinson, #44011).
We've improved our API docs for WebView, WebViewBuilder, WebViewDelegate, ServoDelegate, PromptDialog, WebResourceLoad, WebXrRegistry, Preferences, and servoshell's EXPERIMENTAL_PREFS (@simonwuelker, @TG199, @sabbCodes, @jdm, @rovertrack, #43892, #43787, #44171, #43947).
We've also improved our API docs for Opts, OutputOptions, DiagnosticsLogging, PrefValue, servo::opts, and servo_config (@mukilan, #43802).
More on the web platform
Tab navigation now works across <iframe> boundaries (@mrobinson, #44397), and Ctrl+Backspace (or ⌥⌫) now deletes a whole word in input fields (@mrobinson, #43940).
Tab characters are now rendered correctly in <pre> (and other elements with 'white-space: pre'), with proper tab stops (@mrobinson, @SimonSapin, #44480). Spaces are now rendered correctly in 2D <canvas>, instead of twice as wide as they should be (@mrobinson, #43899).
<a href> now correctly resolves the URL with the page encoding (@sabbCodes, #43822).
We've improved the default appearance of <input type=file> (@sabbCodes, #44496) and <textarea placeholder> (@mrobinson, #43770).
All keyboard events, mouse events, wheel events, and pointer events, other than 'pointerenter' and 'pointerleave', now bubble out of shadow roots (@simonwuelker, @webbeef, #43799, #44094). 'error' events on Window now report the correct filename (source in onerror) and lineno (@Gae24, #43632).
console.log() and friends now support printf-style formatting directives, although for now %c is ignored (@TG199, #43897).
file: URLs are now considered secure contexts, so they can now use features like crypto.subtle and crypto.randomUUID (@simonwuelker, #43989).
Exception messages have improved in Location, StaticRange, and the HTMLElement family of types (@arihant2math, @MuhammadMouostafa, @treetmitterglad, #44282, #43260, #43882).
We've improved the conformance of fetch algorithms (@yezhizhen, #43970, #43798), focus and tab navigation (@mrobinson, #43842, #44029, #44360, #43859, #44535), form submission (@TG199, #43700), JS modules (@elomscansio, @Gae24, #43741, #44179, #44042), page navigation (@TimvdLippe, #43857), <svg viewBox> (@yezhizhen, #44420), 'attr()' (@Loirooriol, #43878), ':focus' (@mrobinson, #43873), 'font' (@RichardTjokroutomo, #44061), '@keyframes' (@simonwuelker, #43461), '@property' (@Loirooriol, #43878), 'load' events (@jdm, @arabson99, #43807, #44046), fetchLater() (@TimvdLippe, #43627), axes and buttons on Gamepad (@log101, @rovertrack, #44411, #44357), copyTexImage2D() on WebGLRenderingContext (@simartin, @mrobinson, #43608), texImage3D() on WebGL2RenderingContext (@simartin, #44367), environmentBlendMode on XRSession (@msub2, #44155), mark() and measure() on Performance (@shubhamg13, @simonwuelker, #44471, #44199, #43990, #43753), and PerformanceResourceTiming (@shubhamg13, #44228).
We've fixed bugs related to console logging (@sabbCodes, #44243), 'animation' (@mrobinson, #44299), 'box-shadow' (@yezhizhen, #44474, #44457), 'display: contents' (@Loirooriol, @mrobinson, #44551, #44299), 'display: inline-flex' (@SimonSapin, #44281), 'display: table-cell' (@Loirooriol, #44550), 'display: table-row-group' (@Veercodeprog, #43674), 'overflow-x: clip' and 'overflow-y: clip' (@Messi002, #43620), 'position: absolute' on grid items (@nicoburns, #44324), 'word-spacing: <percentage>' (@sabbCodes, #44031), removeChild() on Document (@rovertrack, #44133), and URL.revokeObjectURL() (@simonwuelker, @jdm, #43746, #43977, #44035).
Performance and stability
We've fixed some big inefficiencies in Servo. appendChild() with nested shadow roots is no longer (@yezhizhen, @webbeef, #44016), and we've halved the time it takes to load the ECMAScript spec by fixing the processing of 'id' and 'name' attributes (@simonwuelker, #44120, #44127, #44117).
Servo makes its first TLS connection in each session 30-60 ms faster (@jschwe, #44242), and we've instrumented the Servo and servoshell startup processes to find more opportunities for optimisation (@jschwe, #44443, #44456).
Like most browser engines, Servo is a multi-threaded (and sometimes multi-process) system requiring a great deal of IPC messages to keep everything connected. Two key components of this system are the constellation thread, which manages the engine as a whole, and the script threads (or web processes), which render the web pages. Sending these messages can be expensive though, so to reduce unnecessary IPC traffic, we've landed an optimisation that allows script threads to selectively receive only the relevant messages from the constellation (@webbeef, #43124).
We've reduced the memory usage of each Attr, Text, and CharacterData node in the DOM by 16 bytes (@mrobinson, @Loirooriol, #44074), and fixed a memory leak when deleting <video controls> or <audio controls> (@Messi002, #43983).
Our about:memory page is more accurate now too, with new tracking of libc memory allocations on macOS, improved tracking of libc memory allocations on Linux (@jschwe, #44037), and more accurate tracking of PathBuf and types in tokio, http, data_url, and urlpattern (@Narfinger, #43858).
Less memory usage isn't always better in browser engines though, because there are many kinds of caches and other optimisations we can do to make browsing the web faster, at the expense of increased memory usage. For example, we can greatly speed up prototype checks for DOM objects by storing a number in each object that identifies the concrete type, at the expense of making each DOM object 64 bits larger (@webbeef, #44364).
Layout can now reuse fragments in later reflows, in many cases that involve block layout or 'position: absolute' (@mrobinson, @lukewarlow, @Loirooriol, #42904, #44231). We're also working on reusing shaping results in later reflows, and making inline layout more efficient (@mrobinson, #44370, #43974, #44436).
We've landed several changes that should reduce the binary size of Servo (@rovertrack, @mrobinson, @nicoburns, @Narfinger, #44227, #44221, #44303, #44338, #44428, #44134).
We've also reduced clones, allocations, borrow checks, GC rooting steps, and other operations in many parts of Servo (@rovertrack, @Narfinger, @Loirooriol, @yezhizhen, @simonwuelker, #44008, #44544, #44271, #44279, #43826, #44052, #44139).
Several crashes have been fixed:
- in compressedTexSubImage2D() on WebGLRenderingContext (@thebabalola, #44050)
- in console.log() (@thebabalola, #43844)
- in getData() on DataTransfer (@SimonSapin, #44607)
- in remove() on Element (@SimonSapin, #44435)
- in replaceWith() on Element (@yezhizhen, #44503)
- in
--debug-mozjsbuilds (@jdm, #44386, #44573, #44581) - in flex and grid layout (@mrobinson, @nicoburns, #44424, #44203)
- in layout queries like
offsetHeight(@mrobinson, #44560) - in the devtools Debugger tab, when stepping and when inspecting nested values (@atbrakhi, @eerii, #44024, #43995)
- when removing <colgroup> from the DOM (@Loirooriol, #43846)
- when running garbage collection (@drasticactions, #43933)
- when running servoshell with a
u64--pref(@yezhizhen, #44079) - when shadow roots are deeply nested, or when calling attachShadow() removes elements from the flat tree (@yezhizhen, @mrobinson, #43888, #43930, #44259)
- when web storage features fail to write to disk or encounter SQLite errors (@arihant2math, @sabbCodes, #43918, #43949)
We fixed a crash in servoshell when pressing keys like Ctrl+2 or ⌘2 with not enough tabs open (@mrobinson, #44070).
DOM data structures (#[dom_struct]) can refer to one another, with the help of garbage collection. But when DOM objects are being destroyed, those references can become invalid for a brief moment, depending on the order the GC finalizers run in. This can be unsound if those references are accessed, which is a very easy mistake to make if the type has an impl Drop. To help prevent that class of bug, we're reworking our DOM types so that none of them have #[dom_struct] and impl Drop at the same time (@willypuzzle, #44119, #44501, #44513).
We've improved our static analysis for GC rooting (@officialasishkumar, #44489), and we've continued our long-running effort to use the Rust type system to make certain kinds of dynamic borrow failures impossible (@sagudev, @TimvdLippe, @Narfinger, @elomscansio, @Gae24, @rovertrack, @yezhizhen, @nodelpit, #43174, #43524, #43928, #43943, #43942, #43944, #43946, #43952, #43975, #44018, #44175, #44241, #44368, #44406, #44441, #44422, #44475, #44478, #44484, #44476, #44490, #44477, #44494, #44497, #44498, #44495, #44505, #44506, #44507, #44508, #44509, #44510, #44512, #44482, #44527, #44528, #44531, #44534, #44542, #44533, #44543, #44553, #44547, #44563, #44562, #44565, #44558, #44583, #44606, #44605, #44608, #44602, #44584, #44620, #44590, #44254, #44628, #44629, #44638, #44626, #44081).
Thanks to a wide range of people, we've also landed a bunch of cleanups and refactors (@delan, @alice, @Skgland, @atbrakhi, @eerii, @sabbCodes, @jdm, @thebabalola, @CynthiaOketch, @kkoyung, @TimvdLippe, @rovertrack, @webbeef, @arabson99, @yezhizhen, @simonwuelker, @mrobinson, @nicoburns, @longvatrong111, @niyabits, @treetmitterglad, @foresterre, @mukilan, @elomscansio, @freyacodes, @StaySafe020, @TG199, #43772, #44006, #43860, #44121, #44160, #43884, #44154, #44569, #43939, #44003, #44110, #44122, #43824, #44635, #44103, #43978, #44092, #44114, #44277, #44454, #44274, #44237, #44232, #44167, #44214, #43820, #43825, #43810, #43838, #43841, #43847, #43875, #43876, #43889, #43893, #43896, #43881, #43906, #43913, #43908, #43917, #43910, #43921, #43924, #43925, #43907, #43923, #43916, #43909, #43911, #43957, #43969, #43967, #43915, #43954, #43963, #43959, #43955, #44067, #44068, #44071, #44084, #44265, #44115, #44358, #43848).
Donations
Thanks again for your generous support! We are now receiving 7349 USD/month (+2.5% from March) in recurring donations. This helps us cover the cost of our speedy CI and benchmarking servers, one of our latest Outreachy interns, and funding maintainer work that helps more people contribute to Servo.
Servo is also on thanks.dev, and already 33 GitHub users (−4 from March) that depend on Servo are sponsoring us there. If you use Servo libraries like url, html5ever, selectors, or cssparser, signing up for thanks.dev could be a good way for you (or your employer) to give back to the community.
We now have sponsorship tiers that allow you or your organisation to donate to the Servo project with public acknowlegement of your support. If you're interested in this kind of sponsorship, please contact us at join@servo.org.
Use of donations is decided transparently via the Technical Steering Committee's public funding request process, and active proposals are tracked in servo/project#187. For more details, head to our Sponsorship page.
31 May 2026 12:00am GMT
30 May 2026
Planet Mozilla
Frederik Braun: The S in interoperability
This is a blog post about standards, their proliferation and the issues that may arise. My first involvement with standards was just as a reader. To better understand complicated code or unexpected behavior in a protocol. After a while, I also got involved and helped clarify certain things to ensure implementations align on the same behavior in edge cases. Eventually, I found myself co-editing a specification - Subresource Integrity (SRI) which was published as a W3C Recommendation in 2015. The core idea behind SRI is that you include third-party JavaScript combined with a SHA2 digest of the expected file. If the browser does not find the downloaded URL to match the expected digest, the script will not execute. This allows using a fast CDN for JavaScript without giving them full control over the scripts on your page - essentially reducing the security risks.
The standard format for these digests is e.g., sha(size)-(base64 encoding of the digest). While computing the hash digest is rather straightforward, base64 comes in two encoding alphabets: First, a-zA-Z0-9/+ and secondly the url-safe variant which uses a-zA-z0-9_-. The specification examples all used the former.
Only approximately ten years after publication, in 2025, we still found a bug. As part of a compatibility report against Firefox not properly supporting a website, we found that the core issue was actually with a different browser. The other browser liberally accepted both types of encoding, which resulted in websites expecting support for base64 and base64url interchangeably. The page did not work in Firefox, because it did not accept all hashes a website wanted the browser to check, revealing a minor security issue.
The real fix would have been that the standard clarifies that the base64url variant is incorrect and the other browser engine changes their behavior.
But due to (somewhat unrelated) issues around proliferation of standards, web compatibility and the unfortunate market dominance of certain browsers, we went the other road. To support existing web content, we changed the standard to acknowledging that both types of encoding are considered valid representations.
This example shows, that it can take multiple years for subtle differences to appear. Interoperable specifications can establish a shared understanding along a "happy path", but not necessarily in adversarial settings. In addition, standards need to continuous maintenance and active stakeholders who ensure that implementations remain interoperable and secure over time.
From specification to standard
Originally, a specification is at first just a write-up, an idea how something could be better: How it should behave, how it works, what the data structures, the algorithms and the interactions of them look like. Anyone can come up with a grammar, a parser and a resulting data structure.
For a standard, this specification needs a shared agreement that is also widely and consistently implemented. This will work best with iterative co-design of the spec, the implementations and intense discussions of corner cases. Some may go further and use shared test suites.
This will lead to Interoperability (interop), but still requires constant maintenance and observation of the ecosystem beyond individual implementations. While interop is asymptotic and requires a shared agreement over time, security demands understanding - a broader reach that requires the inspection of limitations and subtle boundaries.
This deeper level of understanding is often missing when implementations consider syntax "simple enough" without reading the spec. The base64 SRI example is just one example, but there are more:
Many people have written their own parsers for text-based languages. You may have seen code that parses HTML with regular expressions. Other great examples of "easily" parsed languages are maybe XML, JSON, or YAML.
But these implementations often make different assumptions, leading to subtle incompatibilities or even security flaws.
Parser Differentials
More practical, let's look at an issue with JSON, to demonstrate the impact of handling input that is ostensibly simple. Let's examine this JSON string and the resulting data structure:
{
"test": 0,
"test": 1
}
When parsed into an object obj, what do you think will obj.test return? Most JSON parsers are so liberal that they will happily consume two dictionary keys with the same name "test". One implementation may simply assign obj.test twice: First with 0 and then overwrite it with 1. Another one might check for existing keys and reject the second "test" key silently, keeping the first one.
The lack of rigor in the original description of JSON as a "subset of JavaScript" was already acknowledged and raised as problematic in the JSON RFC (which came much later in 2017). But still to this day, many implementations allow input with duplicate dictionary keys and show divergent behavior.
While the examples with SRI and JSON are relatively harmless, real parser differential bugs were leading to code execution, authentication bypasses and more1.
What do we learn from this?
Perfect interoperability is not created through a specification, it needs constant maintenance. The ambiguity can only be removed through long-term commitment and regular feedback from implementations and users.
The same is true for security: The SRI bug persisted for ten years and nobody noticed how implementations disagreed and corner cases were overlooked. They only aligned due to a real, user-facing issue.
But these examples are not a warning sign, they are scar tissue that shows how the internet is made. Standards can only mature through vigilant maintenance.
The bug reports, the spec issues being filed, the shared test cases, sometimes even the random forum complaints. All of these help to remove ambiguity and allow internet standards to mature.
In the end, standards are not secure because they are written down. They are secure because people continue to question, understand, and maintain them.
30 May 2026 10:00pm GMT
28 May 2026
Planet Mozilla
The Rust Programming Language Blog: Announcing Rust 1.96.0
The Rust team is happy to announce a new version of Rust, 1.96.0. Rust is a programming language empowering everyone to build reliable and efficient software.
If you have a previous version of Rust installed via rustup, you can get 1.96.0 with:
$ rustup update stable
If you don't have it already, you can get rustup from the appropriate page on our website, and check out the detailed release notes for 1.96.0.
If you'd like to help us out by testing future releases, you might consider updating locally to use the beta channel (rustup default beta) or the nightly channel (rustup default nightly). Please report any bugs you might come across!
What's in 1.96.0 stable
New Range* types
Many users expect Range and related core::ops types to be Copy, but this is not the case: they implement Iterator directly, and it is a footgun to implement both Iterator and Copy on the same type so this has been avoided. RFC3550 proposed a set of replacement range types that implement IntoIterator rather than Iterator, meaning they can also be Copy. The standard library portion of that RFC is now stable, introducing:
core::range::Rangecore::range::RangeFromcore::range::RangeInclusive- Associated iterators
A Rust version in the near future will also add core::range::RangeFull and core::range::RangeTo as re-exports from core::ops (these do not implement Iterator and already implement Copy), and core::range::legacy::* as the new home for the current ranges. Range syntax like 0..1 still produces the legacy types for now, but will be updated to core::range types in a future edition.
With these stabilizations, it is now possible to store slice accessors in Copy types without splitting start and end:
use core::range::Range;
#[derive(Clone, Copy)]
pub struct Span(Range<usize>);
impl Span {
pub fn of(self, s: &str) -> &str {
&s[self.0]
}
}
The new RangeInclusive also makes its fields public, unlike the legacy version which avoided exposing the exhausted iterator state. This isn't a concern with the new type since it must be converted to begin iteration.
Library authors should consider making use of impl RangeBounds in public API, which accepts both legacy and new range types. If a concrete type is needed, prefer using new ranges as this will eventually become the default.
Assert matching patterns
The new macros assert_matches! and debug_assert_matches! check that a value matches a given pattern, panicking with a Debug representation of the value otherwise. These are essentially the same as assert!(matches!(..)) and debug_assert!(matches!(..)), but the printed value improves the possibility of diagnosing the failure.
These new macros have not been added to the standard prelude, because they would collide with popular third-party crates that provide macros with the same name. Instead, they should be manually imported from core or std before use.
use core::assert_matches;
/// [Random Number](https://xkcd.com/221/)
fn get_random_number() -> u32 {
// chosen by a fair dice roll.
// guaranteed to be random.
4
}
fn main() {
assert_matches!(get_random_number(), 1..=6);
}
Changes to WebAssembly targets
WebAssembly targets no longer pass --allow-undefined to the linker which means that undefined symbols when linking are now a linker error instead of being converted to WebAssembly imports from the "env" module. This change prevents modules from linking unless all linking-related symbols are defined to catch bugs earlier and prevent accidental issues with symbol naming or similar.
Undefined linking-related symbols are often indicative of build-time related bugs or misconfiguration. If, however, the old behavior is intended then it can be re-enabled with RUSTFLAGS=-Clink-arg=--allow-undefined or by editing the source code and using #[link(wasm_import_module = "env")] on the block defining the symbol.
This change was previously announced on this blog, and now takes effect in Rust 1.96.
Stabilized APIs
assert_matches!debug_assert_matches!From<T> for AssertUnwindSafe<T>From<T> for LazyCell<T, F>From<T> for LazyLock<T, F>core::range::RangeToInclusivecore::range::RangeFromcore::range::RangeFromItercore::range::Rangecore::range::RangeIter
Two Cargo advisories
Rust 1.96 contains fixes for two vulnerabilities for users of third-party registries.
-
CVE-2026-5223 is a medium severity vulnerability regarding extraction of crate tarballs with symlinks.
-
CVE-2026-5222 is a low severity vulnerability regarding authentication with normalized URLs.
Users of crates.io are not affected by either vulnerability.
Other changes
Check out everything that changed in Rust, Cargo, and Clippy.
Contributors to 1.96.0
Many people came together to create Rust 1.96.0. We couldn't have done it without all of you. Thanks!
28 May 2026 12:00am GMT
27 May 2026
Planet Mozilla
Firefox Tooling Announcements: New Deploy of PerfCompare (May 27th)
The latest version of PerfCompare is now live!
Check out the change-log below to see the updates:
[kala-moz]:
-
Bug 2036968: Replaced fast-kde with fftkde and used bootstrap-ci to get CI summary (#1034)
-
Bug 2037551: Reduced the size of perfcompare hero on Results Page (#1036)
[padenot]: Use SJ bandwidth for top-level results, ISJ for subtests
[shtrom]: Bug 2014041: add support for landoInstance QueryString parameter (#1038)
Thank you for the contributions!
Bugs or feature requests can be filed on Bugzilla. The team can also be found on the #perfcompare channel on Element. Come and chat!
1 post - 1 participant
27 May 2026 9:29pm GMT
This Week In Rust: This Week in Rust 653
Hello and welcome to another issue of This Week in Rust! Rust is a programming language empowering everyone to build reliable and efficient software. This is a weekly summary of its progress and community. Want something mentioned? Tag us at @thisweekinrust.bsky.social on Bluesky or @ThisWeekinRust on mastodon.social, or send us a pull request. Want to get involved? We love contributions.
This Week in Rust is openly developed on GitHub and archives can be viewed at this-week-in-rust.org. If you find any errors in this week's issue, please submit a PR.
Want TWIR in your inbox? Subscribe here.
Updates from Rust Community
Newsletters
Project/Tooling Updates
- gitoxide - May 26
- hyper User Survey 2025 Results
- Rust Update: gRPC Welcomes Tonic!
- serde-const-default v0.1: Removes boilerplate when using const values as field defaults
- BoquilaHUB 0.5: AIs for Nature. Now it includes SOTA AI bioacoustics models and embeddings models
- splog: a log viewer TUI with automatic tag categorization
- rgx v0.12.3 - Building a regex debugger for the terminal in Rust
- UI tests are the guardrails an AI needs: the story of clipboardwire
- slintcn 0.22: shadcn/ui-style copy-paste components for Slint native apps
- Releasing dtact v0.2.2 and rssn-advanced v0.1.0: the next generation async concurrent engine and scientific computing engine
Observations/Thoughts
- Noroboto: Lying Fonts and Mitigation in Rust
- Erasing Existentials
- libwce: the entropy layer of a wavelet codec, on its own
- Tech Notes: Theseus: translating win32 to wasm
- Bevy Game Engine Explained Visually
- The reflex of deriving
serdetraits - Physical AI Needs a Typed World Model, Not a Vector DB
- Keep calm and use (Rust) monorepos
- [audio] Rust for Linux Live with Alice Ryhl and Greg Kroah-Hartman
- [audio] Netstack.FM episode 38 - Building and testing network stacks with Rama
- [video] Can a QR code be made of stars?
Rust Walkthroughs
- Rust Patterns & Engineering How-Tos
- Laissez-Faire Errors
- Learn Rust HashMap and Iterators by Building a Git Object Store Reader
- Learn the Basics of Bevy by Building and Deploying Pong to Itch.io
- The Slowdown That Doesn't Show Up in Profiles
- Building an AsyncIO executor for the 3DS
- [video] Nine Ways to do Inheritance in Rust, a Language without Inheritance
Miscellaneous
Crate of the Week
This week's crate is inline_tweak, a crate to embed tweakable constants inside your Rust application without full recompilation.
Thanks to Kill The Mule for the suggestion!
Please submit your suggestions and votes for next week!
Calls for Testing
An important step for RFC implementation is for people to experiment with the implementation and give feedback, especially before stabilization.
If you are a feature implementer and would like your RFC to appear in this list, add a call-for-testing label to your RFC along with a comment providing testing instructions and/or guidance on which aspect(s) of the feature need testing.
No calls for testing were issued this week by Rust, Cargo, Rustup or Rust language RFCs.
Let us know if you would like your feature to be tracked as a part of this list.
Call for Participation; projects and speakers
CFP - Projects
Always wanted to contribute to open-source projects but did not know where to start? Every week we highlight some tasks from the Rust community for you to pick and get started!
Some of these tasks may also have mentors available, visit the task page for more information.
If you are a Rust project owner and are looking for contributors, please submit tasks here or through a PR to TWiR or by reaching out on Bluesky or Mastodon!
CFP - Events
Are you a new or experienced speaker looking for a place to share something cool? This section highlights events that are being planned and are accepting submissions to join their event as a speaker.
- No Calls for papers or presentations were submitted this week.
If you are an event organizer hoping to expand the reach of your event, please submit a link to the website through a PR to TWiR or by reaching out on Bluesky or Mastodon!
Updates from the Rust Project
352 pull requests were merged in the last week
Compiler
rustc_on_unimplemented: introduce format specifiers- account for proc macro spans in
do_not_recommenddiagnostics - implement fast path for
derive(PartialOrd)when derivingOrd - make bitset
would_modify_wordsmore vectorzer-friendly - parse
mutrestrictions - stop needing materialized places for most intrinsics
Library
Cargo
- compiler: forward verbose flag to rustc for local crates
- don't use the network for a publish dry-run test
- break out
RegistryConfigandcrate_urlfor interpretingRegistryConfig::dl - fix CVE-2026-5222 and CVE-2026-5223
- artifact: remove compat mode from artifacts
Rustdoc
Clippy
useless_format: fire on wrapped in a block-producing macroreturncan be removed from the last stmt of a block if it has an expr- add check for midpoint using multiplication by
0.5and>> 1 - avoid unnecessary
Stringallocations inMinifyingSuggarithmetic ops - extend
clippy::missing_safety_docto unsafe fields - fix
manual_range_containsNAN handling - fix error message for
useless_borrows_in_formattingfor mutable borrows - move
unnecessary_get_then_checktocomplexity - simplify
is_some() && …unwrap()tois_some_andinunit_arg
Rust-Analyzer
diagnostics: mut_refbinding feature diagnosticassists/add_reference_here: _modify_the reference type when dealing with&T->&mut Tcfg: correct separator index in CfgDiff disable loophir-ty: saturate float-to-uint cast in const evaltest-utils: draininactive_regionsbyinactive_line_region- add diagnostic for E0033
- add diagnostic for E0608
- completions imports exclude supports sub items
- filter package-scoped features
extract_modulemissing import for macro calls- add
type_matchscore forstruct_pat - allow wildcard params in foreign fn declarations
- analysis expected ty in
enumvariant - autoimport
enumvariants - do not autoref in method probe in path mode
- do not complete semicolon in match-expr place
- do not consider the path of the macro in a macro call to be inside a macro call
- emit diagnostic for rest array patterns without fixed-length arrays
- fix
SyntaxContext::roots technically overlapping valid interneds - flip
coerce_never type_mismatchtys - have a specific error for unimplemented builtin macros
- no suggest ref match when expected generic ref
- no use sad pattern on happy arm with guard
- normalize expected tuple
structpat field - refactor handling of generic params in
hir::Type - support named consts in range pattern types
- use grouped annotation for
add_label_to_loop - provide better incrementality for modules
Rust Compiler Performance Triage
This week was largely positive, with most of the improvements coming from algorithm change in visibility checking: #156228.
Triage done by @panstromek. Revision range: 281c97c3..783eb8c8
Summary:
| (instructions:u) | mean | range | count |
|---|---|---|---|
| Regressions ❌ (primary) |
0.4% | [0.1%, 0.7%] | 5 |
| Regressions ❌ (secondary) |
0.5% | [0.1%, 1.1%] | 16 |
| Improvements ✅ (primary) |
-0.9% | [-6.6%, -0.1%] | 164 |
| Improvements ✅ (secondary) |
-0.4% | [-1.3%, -0.1%] | 51 |
| All ❌✅ (primary) | -0.9% | [-6.6%, 0.7%] | 169 |
2 Regressions, 2 Improvements, 5 Mixed; 2 of them in rollups 34 artifact comparisons made in total
Approved RFCs
Changes to Rust follow the Rust RFC (request for comments) process. These are the RFCs that were approved for implementation this week:
Final Comment Period
Every week, the team announces the 'final comment period' for RFCs and key PRs which are reaching a decision. Express your opinions now.
Tracking Issues & PRs
- Promotes 5 Thumb-mode bare-metal Arm targets to Tier 2
- Add -Z dead-fn-elimination to skip codegen of BFS-unreachable functions
- Update
transmute_copyto ub_checks and?Sized - Tracking Issue for NEON dot product intrinsics
- Never break between empty parens
No Items entered Final Comment Period this week for Cargo, Language Team, Language Reference or Leadership Council. Let us know if you would like your PRs, Tracking Issues or RFCs to be tracked as a part of this list.
New and Updated RFCs
- No New or Updated RFCs were created this week.
Upcoming Events
Rusty Events between 2026-05-27 - 2026-06-24 🦀
Virtual
- 2026-05-27 | Virtual (Girona, ES) | Rust Girona
- 2026-06-02 | Virtual | libp2p Events
- 2026-06-02 | Virtual (Tel Aviv-yafo, IL) | Rust 🦀 TLV
- 2026-06-03 | Virtual (Indianapolis, IN, US) | Indy Rust
- 2026-06-04 | Virtual (Berlin, DE) | Rust Berlin
- 2026-06-04 | Virtual (Nürnberg, DE) | Rust Nuremberg
- 2026-06-04 | Virtual (Tel Aviv-yafo, IL) | Code Mavens 🦀 - 🐍 - 🐪
- 2026-06-06 | Virtual (Kampala, UG) | Rust Circle Meetup
- 2026-06-07 | Virtual (Dallas, TX, US) | Dallas Rust User Meetup
- 2026-06-09 | Virtual (Dallas, TX, US) | Dallas Rust User Meetup
- 2026-06-10 | Virtual (Girona, ES) | Rust Girona
- 2026-06-16 | Virtual (Washington, DC, US) | Rust DC
- 2026-06-17 | Hybrid (Vancouver, BC, CA) | Vancouver Rust
- 2026-06-17 | Virtual (Girona, ES) | Rust Girona
- 2026-06-18 | Hybrid (Seattle, WA, US) | Seattle Rust User Group
- 2026-06-18 | Virtual (Berlin, DE) | Rust Berlin
- 2026-06-21 | Virtual (Dallas, TX, US) | Dallas Rust User Meetup
- 2026-06-23 | Virtual (Dallas, TX, US) | Dallas Rust User Meetup
- 2026-06-23 | Virtual (London, UK) | Women in Rust
Asia
- 2026-06-02 | Beijing, CN | Voice AI and Rust Meetup (Rust for AI, lowcoderust.com)
Europe
- 2026-05-28 | Copenhagen, DK | Copenhagen Rust Community
- 2026-05-28 | London, UK | Rust London User Group
- 2026-05-29 | Berlin, DE | Rust Berlin
- 2026-05-30 | Stockholm, SE | Stockholm Rust
- 2026-06-02 | Frankfurt, DE | Rust Rhein-Main
- 2026-06-03 | Dublin, IE | Rust Dublin
- 2026-06-03 | Girona, ES | Rust Girona
- 2026-06-10 | München, DE | Rust Munich
- 2026-06-11 | Switzerland, CH | PostTenebrasLab
- 2026-06-12 - 2026-06-14 | Kraków, PL | Rustmeet
- 2026-06-16 | Leipzig, DE | Rust - Modern Systems Programming in Leipzig
- 2026-06-16 | Milano, IT | Rust Language Milan
- 2026-06-18 | Aarhus, DK | Rust Aarhus
North America
- 2026-05-27 | Austin, TX, US | Rust ATX
- 2026-05-28 | Atlanta, GA, US | Rust Atlanta
- 2026-05-28 | Los Angeles, CA, US | Rust Los Angeles
- 2026-05-28 | Mountain View, CA, US | Hacker Dojo
- 2026-05-30 | Boston, MA, US | Boston Rust Meetup
- 2026-06-04 | Saint Louis, MO, US | STL Rust
- 2026-06-06 | Boston, MA, US | Boston Rust Meetup
- 2026-06-11 | Lehi, UT, US | Utah Rust
- 2026-06-11 | Mountain View, CA, US | Hacker Dojo
- 2026-06-11 | San Diego, CA, US | San Diego Rust
- 2026-06-16 | San Francisco, CA, US | San Francisco Rust Study Group
- 2026-06-17 | Hybrid (Vancouver, BC, CA) | Vancouver Rust
- 2026-06-18 | Hybrid (Seattle, WA, US) | Seattle Rust User Group
- 2026-06-24 | Austin, TX, US | Rust ATX
- 2026-06-24 | Los Angeles, CA, US | Rust Los Angeles
South America
- 2026-06-18 | Florianópolis, BR | Rust SC
If you are running a Rust event please add it to the calendar to get it mentioned here. Please remember to add a link to the event too. Email the Rust Community Team for access.
Jobs
Please see the latest Who's Hiring thread on r/rust
Quote of the Week
This overflows the trait solver today as well as my brain
Thanks to Theemathas for the suggestion!
Please submit quotes and vote for next week!
This Week in Rust is edited by:
- nellshamrell
- llogiq
- ericseppanen
- extrawurst
- U007D
- mariannegoldin
- bdillo
- opeolluwa
- bnchi
- KannanPalani57
- tzilist
Email list hosting is sponsored by The Rust Foundation
27 May 2026 4:00am GMT
26 May 2026
Planet Mozilla
Firefox Tooling Announcements: Firefox Profiler Deployment (May 26, 2026)
The latest version of the Firefox Profiler is now live! Check out the full changelog below to see what's changed:
Highlights:
- [Markus Stange] Use
@streamparser/jsonif the input is too large to fit in a V8 string (#6016) - [Nazım Can Altınova] Include
--searchoption inpq filter push(#6026) - [fatadel] Translate URL track-index state through profile sanitization (#6000)
- [Nazım Can Altınova] Print also the status output right after cli
loadcommand (#6019)
Other Changes:
- [fatadel] Remove unused dependencies from package.json (#6010)
- [Nazım Can Altınova] Make profiler-cli work in sandboxed environments (#6003)
- [Markus Stange] Make profiler-edit run profile compacting before writing out the file (#6015)
- [Markus Stange] Migrate from prettier to oxfmt (#5986)
- [Markus Stange] Add a --symbolicate-wasm arg to profiler-edit. (#6008)
- [Markus Stange] Build and upload the cli artifact in PRs (#6020)
- [Nicolas Chevobbe] Update devtools-reps to 0.27.7 (#6030)
- [Markus Stange/fatadel] Make withSize use a wrapper element so that it can stop calling findDOMNode (#5988)
- [Markus Stange] Fix dhat importer (#6036)
- [Nazım Can Altınova] Annotate inlined frames in CLI call trees and stacks (#6041)
- [Nazım Can Altınova] Use proper types in cli tests instead of custom inline types (#6038)
- [Nazım Can Altınova] Fix text truncation for frames named after Object.prototype methods (#6044)
- [Nazım Can Altınova] Add missing key props to CodeErrorOverlay error list items (#6047)
- [depfu[bot]]
Update oxfmt to version 0.51.0 (#6054) - [Nazım Can Altınova]
Sync: l10n → main (May 26, 2026) (#6058) - [Nazım Can Altınova] Use URL-state symbol server for
profiler-cli function annotate(#6051) - [Nazım Can Altınova] Bump profiler-cli version to 0.2.0 (#6059)
Big thanks to our amazing localizers for making this release possible:
- fr: YD
- sr: Марко Костић (Marko Kostić)
- tr: Ali Demirtaş
- zh-CN: Olvcpr423
- zh-CN: wxie
Find out more about the Firefox Profiler on profiler.firefox.com! If you have any questions, join the discussion on our Matrix channel!
1 post - 1 participant
26 May 2026 3:53pm GMT
Andrew Halberstadt: Your New Job is Integrating Code
You felt it. The shift. That your role has fundamentally changed thanks to LLMs. It first entered your subconscious when you realized how easily you can now crank out PRs. You felt it more concretely (and less enthusiastically), as a reviewer when you opened your laptop one morning and noticed your review queue was double what it normally is thanks to everyone else cranking out PRs. And you feel this pervasive, general sense of friction.
It's difficult to pinpoint exactly where this friction is coming from. Depending on the repository size and CI setup, it will be slightly different for everyone. It might involve longer review times or slipping review standards. You might be noticing more merge conflicts and merge related CI failures. Perhaps there are more failures sneaking through to main or CI is taking longer to give you results. You almost certainly feel the grind. People are on edge, tired; developers are pulling in opposite directions.
Here's what LLMs shifted. The bottleneck is no longer producing code. The bottleneck is integrating it. The friction we're feeling is a result of more PRs, more ideas, more reviews, more disagreements all made possible thanks to LLMs. In short, the problem can best be summarized by Figure 1:

But we're living in a moment where many folks haven't realized this yet, and are still under the impression that their job is to produce code.
It's not. Your new job is to integrate it.
26 May 2026 1:50pm GMT
Mozilla Privacy Blog: Growing darkness: Against the rise of internet shutdowns
Disruptions to internet connectivity can occur in countless ways - from weather incidents, natural disasters and accidents to intentional interferences like cyberattacks and government-issued blackouts. Yet while some disruptions are unavoidable, deliberate shutdowns represent a fundamentally different and deeply concerning trend. They undermine the open, global nature of the internet and put the safety, security, and fundamental rights of millions at risk.
For over 25 years, Mozilla has worked to ensure that the internet remains a global public resource-open, accessible, and safe for all. This vision, grounded in the Mozilla Manifesto, holds that the internet must remain a shared, decentralized infrastructure that empowers individuals, supports civic participation, and enables economic opportunity. Internet shutdowns run counter to these principles by restricting access, concentrating control, and weakening the very foundations of the open web.
To help organizations study and document outages, Mozilla makes aggregated Firefox telemetry data available to help identify and understand connectivity disruptions. As 2026 progresses, this data continues to show significant outages affecting millions of people worldwide-many of them the result of deliberate restrictions.
As of late May, Iran's internet blackout had been in place for over 80 days, making it the longest shutdown since the Arab Spring. Following an earlier shutdown amid nationwide protests in January 2026, Iranian authorities have restricted access to the internet since 28 February. This has meant that, for almost three months, millions of Iranians have been cut off from news, communication, work, education, and basic services. It also means that almost no independent information about the situation in Iran is leaving the country, making it almost impossible for humanitarian organizations to assess the situation on the ground. The shutdown has also had a massive impact on the Iranian economy, severely disrupting financial activity and blocking international transactions. Although Iran's president has recently ordered an end to the shutdown, it is unclear how and when Iranians will be able to reconnect to the web.
When large numbers of Firefox users experience connection failures for any reason, this produces an anomaly in the recorded telemetry data. At the country or city level, this can provide a corroborative signal of whether an outage or intentional shutdown occurred. Our telemetry documents the magnitude of the latest outage in Iran. The graph below documents the effect of the outage in multiple ways, such as users' country location, language and timezone.
Across the globe, governments are increasingly interfering with and limiting access to connectivity. Both the number of states limiting connectivity and the amount of internet shutdowns has been growing steadily. In 2025 alone, 313 shutdowns across 52 countries have been documented, a sad record. This is a stark indication that shutdowns and restrictions are no longer a rare emergency measure, but established levers of control.
While the triggers for shutdowns are varied, access to the internet continues to be blocked especially often in times of conflict and political unrest. Especially in the context of hostilities, political tensions or public health emergencies, access to connectivity is a basic humanitarian need.
Beyond their immediate human impact, blackouts also affect the internet itself. Local networks depend on each other to form the global internet, and local restrictions affect the resilience and reliability of the web at large. When governments deliberately disrupt connectivity, they do not only isolate populations; they also contribute to the fragmentation of the global internet, undermining trust, interoperability, and the stability of shared infrastructure. Over time, this erosion risks replacing a single, open web with a patchwork of disconnected or controlled networks.
Governments should foster the health of the internet, not erode it. Access to the internet is widely recognized as essential for enjoying human rights. It is an integral part of modern life, facilitating education, communication, collaboration, business and entertainment. Preserving the open web requires sustained commitment: resisting shutdowns, promoting transparency, and reinforcing the technical and governance frameworks that keep the internet global, interoperable, and accessible. The internet's value-as a platform for opportunity, innovation, and human connection-depends on it remaining open to all.
The post Growing darkness: Against the rise of internet shutdowns appeared first on Open Policy & Advocacy.
26 May 2026 8:04am GMT
25 May 2026
Planet Mozilla
The Rust Programming Language Blog: Security Advisory for Cargo (CVE-2026-5223)
The Rust Security Response Team was notified that Cargo incorrectly handled symlinks inside of crate tarballs downloaded from third-party registries, allowing a malicious crate to override the source code of another crate from the same registry.
This vulnerability is tracked as CVE-2026-5223. The severity of the vulnerability is medium for users of third-party registries. Users of crates.io are not affected, as crates.io forbids uploading crates containing any symlink.
Overview
When building a crate, Cargo extracts its source code in a local cache (stored within ~/.cargo), reusing it for any future build. Cargo includes protections to prevent any file from being extracted outside of the crate's own cache directory.
It was discovered that it's possible to craft a malicious tarball able to extract files one level below the crate's own cache directory. With the way the cache is structured, that allowed the malicious crate to override the cache of other crates belonging to the same registry.
Mitigations
Rust 1.96.0, to be released on May 28th, 2026, will update Cargo to reject extracting any symlink within crate tarballs, regardless of whether they come from crates.io (which already forbids them) or third-party registries. Note that Cargo never added symlinks when running cargo package or cargo publish, so the impact of this should be minimal.
Users who are not able to upgrade to the most recent Rust version are recommended to audit the contents of their registry for the presence of any symlink, and to configure their registry to reject symlink (if such option is available).
Affected versions
All versions of Cargo shipped before Rust 1.96.0 are affected.
Acknowledgements
We'd like to thank Christos Papakonstantinou for reporting this to us according to the Rust security policy.
We also want to thank the members of the Rust project who helped us address the vulnerability: Josh Triplett for developing the fix; Arlo Siemsen for reviewing the fix; Emily Albini for writing this advisory; Emily Albini, Josh Stone and Manish Goregaokar for coordinating the disclosure; Ed Page and Eric Huss for advising during the disclosure.
25 May 2026 12:00am GMT
The Rust Programming Language Blog: Security Advisory for Cargo (CVE-2026-5222)
The Rust Security Response Team was notified that Cargo incorrectly normalized the URLs of third-party registries using the sparse index protocol. If a hosting provider allowed multiple registries to be hosted with arbitrary names within the same domain, an attacker able to publish crates in a registry could obtain the credentials of others users of the same registry.
This vulnerability is tracked as CVE-2026-5222. The severity of the vulnerability is low, due to the extremely niche requirements needed to achieve the attack.
Overview
Originally Cargo only supported storing a registry's index within git repositories. Most git hosting solutions allow accessing a git repository with or without the .git suffix, so Cargo mirrored this behavior when normalizing registry URLs. This allowed credentials for https://example.com/index to be used for https://example.com/index.git.
This normalization was unintentionally applied to the new sparse indexes too. Sparse indexes can be hosted on any HTTPS server, which treat URLs ending with .git as different URLs than those without the suffix.
If the following conditions apply:
https://example.com/indexis a sparse index.https://example.com/indexallows crates to depend on crates from any other registry.- The attacker is able to publish crates on
https://example.com/index. - The attacker is able to upload arbitrary files to
https://example.com/index.git.
...the attacker could configure https://example.com/index.git to be a Cargo sparse registry requiring authentication for downloads, and with a download URL pointing to a server recording any credentials set to it.
When the attacker then publishes a crate foo to https://example.com/index depending on a crate bar from https://example.com/index.git, and tricks the victim into downloading foo, Cargo will think the two registries share the same credential and send the victim's Cargo token to the malicious registry.
Mitigations
Rust 1.96, to be released on May 28th, 2026, will update Cargo to only strip the .git suffix from registry URLs using the git protocol. No mitigations are available for users of older versions of Cargo.
Affected versions
All versions of Cargo shipped between Rust 1.68 (the stabilization of sparse registries) and 1.96 are affected.
Acknowledgements
We'd like to thank Christos Papakonstantinou for reporting this to us according to the Rust security policy.
We also want to thank the members of the Rust project who helped us address the vulnerability: Arlo Siemens for developing the fix; Weihang Lo, Eric Huss and Emily Albini for reviewing the fix; Emily Albini for writing this advisory; Emily Albini, Josh Stone and Manish Goregaokar for coordinating the disclosure.
25 May 2026 12:00am GMT
Jonathan Almeida: Auto-resolve Jujutsu conflicts with your AI agent
With Jujutsu, I've been able to work in multiple workstreams more efficiently than before. This means that if I'm working on multiple things, there is a higher likelihood of something going stale while I wait for a review or touch multiple files. Dealing with conflicts aren't so bad these days, however if I can automate the easy ones, why not?
This is the prompt I've been using with my agent whenever I have a list of changes that have conflicts and don't need me to participate actively on it.
Using the jj version control system, fix the conflicts that are in the changesets from `<start_rev>` to `<end_rev>`. Keep trying until there are no more "(conflict)" in the changesets between those two IDs.
25 May 2026 12:00am GMT




