09 Jun 2026

feedPlanet Mozilla

Niko Matsakis: Only Bounds

only bounds are going to be the most impactful change to Rust that you've never heard of. They are currently being designed and developed by the Arm team (David Wood, Rémy Rakic, et al.) as part of the Sized Hierarchy and Scalable Vector Extension project goal. This post explores the feature and aims to answer a particular question about the design (the scope of bounds, I'll explain). But before I dive in, I want to give a bit of context.

Rust generics have a Sized bound by default today

In today's Rust, every type parameter (except for Self) has a default bound called Sized:

// So this function...
fn identity<T>(t: T) -> T {
    t
}

// ...is actually short for
fn identity<T>(t: T) -> T
where
    T: Sized, // <-- Added by default!
{
    t
}

A type T implements Sized if the compiler can compute the size of a T value at compilation time. This is true for almost every type, with a few notable exceptions. Consider [u32], which refers to "some number of u32 instances". We know that a single u32 is 4 bytes, but without knowing how many u32 there are, you can't know the size of [u32]. This means you can't have a value of type [u32] on the stack (how big should the stack frame be?).

You opt out with ?Sized

However, if you have a function like by_ref, that just takes the value by reference (i.e., by pointer), you shouldn't need to know how big the [u32] value is, because you're not manipulating it directly. You can have a type parameter U that doesn't require Sized, but you have to explicitly "opt out" from the default bound:

fn by_ref<U>(t: &U)
where
    U: ?Sized, // <-- Opt out from the default
{ }

As a fun bit of historical trivia, this system was introduced way back in 2014 to accommodate Dynamically Sized Types. Before that, &[u32] was actually a built-in, indivisible type; we even wrote it like [u32]/& for a time.1

But Sized vs ?Sized isn't enough for everything we need

The Sized vs ?Sized design has served us reasonably well but it is also showing its limits. It turns out that "value has a statically computable size" vs "each value has a distinct size computable at runtime" doesn't cover all the things you might want. For example, extern types are types whose values have no known size, even at runtime. And then Arm's Scalable Vector Extension want to describe SIMD types where every value of the type has the same size (unlike str and [T], where each value can have a different length) but where that size is not known until runtime.

A richer Sized hierarchy

Rather than just Sized or ?Sized, what we really want is to have a richer hierarchy. The current plans look something like this:

flowchart TD
  subgraph S["Sizedness traits"]
      Sized[["Sized (default)"]] -- extends --> MetadataSized
      MetadataSized -- extends --> MaybeSized
  end
  

where

Two caveats:

  1. I'm excluding the way that Arm's Scalable Vector Extension fit into this, because it's orthogonal.
  2. The trait names aren't settled. I'm using the names I understand the libs-api team to prefer; they're not my favorites, but that's ultimately the team who owns stdlib bikesheds, so I defer to them.2

Problem: ?Sized notation doesn't scale to this hierarchy

But now we have a kind of problem. The ?Sized notation was predicated3 on the idea that users should specify the default bound they are opting out of - i.e., the ? is meant to say "I don't know if this is Sized or not" (unlike the default, where you know it is Sized). But "opting out" from a bound doesn't work so well with a multi-level hierarchy. When you write ?Sized, does that correspond to T: MetadataSized (but not T: Sized)? And what if we want to insert another level in between T: MetadataSized and T: Sized later? Then we either have to change what T: ?Sized means (to refer to the new bound) or we have to have T: ?Sized drop two levels down the hierarchy. Even more annoying, what do we do while that middle rung is unstable? Surely T: ?Sized shouldn't refer to an unstable trait… what if we decide to remove it

Solution: only bounds

The new proposal is to write T: only MetadataSized or T: only UnknownSized instead of T: ?Sized. An only bound combines two things:

  1. Like any bound, it includes a "minimum requirement" - i.e., T: only MetadataSized means that T must implement at least MetadataSized.
  2. It additionally disables some default bounds - i.e., we will not add the default T: Sized bound.

The name only comes from the fact that T: Sized implies T: MetadataSized. So the default of T: Sized already means that T: MetadataSized for free; but when you write only MetadataSized, you are saying "I don't need the full hierarchy, just MetadataSized will do".

only bounds work like normal bounds: ask for what you need

A nice feature of only bounds is that they work more like a regular bound. Whereas a ? bound is saying "I don't need this", an only bound is saying what you do need. So e.g. if you are writing a function that just has references to values of type T does not care what their size is, you can write

fn by_ref<U>(u: &U)
where
    U: only MaybeSized,
{}

If you are writing a function that does need to compute the size of values of type V, you can ask for that capability:

fn checks_size<V>(v: &V)
where
    V: only MetadataSized,
{
    std::mem::size_of_val(v)
}

only bounds allow for new levels to be added later

A nice feature of only bounds is that, later on, we can add new levels to the hierarchy, and they work normally. For example, suppose we wish to add something like Aligned where the size is not known at compilation time but the alignment is. We could change the hierarchy to

trait Sized: Aligned
trait Aligned: MetadataSized // <-- new!
trait MetadataSized: MaybeSized
trait MaybeSized

and functions with U: only MaybeSized (like by_ref) and with V: only MetadataSized (with checks_size) would continue to have the same requirements. But new functions could be written with T: only Aligned that would use the new bound. And there is no conflict with stabilization; code that writes T: only Aligned can be considered unstable until that middle hierarchy is finalized.

only bounds compose normally

Like any other bound, only bounds are combined with other bounds to form the overall requirements. So it is possible to write e.g. T: only MetadataSized + Sized. This is equivalent to T: Sized and therefore equivalent to the default and therefore kind of pointless, but you can write it. Similarly, given that trait Clone: Sized, if you write T: only MetadataSized + Clone, that is kind of pointless too: you might as well write T: Clone, which would be equivalent. We plan to have a warn-by-default lint for that.

Scaling only to other "default bound families" (speculative)

The final strength of only bounds is that they allow us to introduce whole new families of default bounds. One example is the idea of introducing a Move bound. Note that this is a distinct feature and is not covered under the current RFC.

All types in Rust today are "movable" and "forgettable", meaning that you can memcpy the value from place to place so long as you stop using the previous location and you can recycle the memory where it is stored without running the value's destructor. There is one notable exception - when you pin a value, you it can no longer be moved, and you must run its destructor before its memory is reused - but otherwise this is a hard-and-fast rule. And that's annoying!

The problem is that not being able to guarantee that a destructor runs blocks a lot of unsafe code patterns. For example, scoped tasks a la rayon depend on a destructor for safety. In sync code, this works because we've decided it's UB to unwind a stack frame without running the destructors of values stored there, and so if you put a local variable on the stack, you can be sure its destructor will run. But that doesn't work in async code! And there are times when unwinding without running destructors would be nice.

The solution is to introduce a second family of default traits. Unlike the Sized family we saw before, this family defines fine-grained capabilities about how values of that type can be used:

flowchart TD
  subgraph A["Accessability traits"]
      Forget[["Forget (default)"]] -- extends --> Leak
      Leak -- extends --> Destruct
      Destruct -- extends --> Access
      Move[["Move (default)"]] -- extends --> Access
  end
  Copy -- extends --> Move
  

The meaning of the traits are as follows:

This introduces new checks into the compiler:

Some implications:

How only bounds could work in the presence of multiple families

The spur for writing this blog post was a question in a lang team meeting on how only bounds ought to work given the existence of multiple "families" of default traits, as I described above. Although the current RFC is looking only at the Sized traits, we expect to look at the "access family" in a future RFC, so we want to be sure we are not making any decisions that won't scale to cover both.

The way I imagine it working is like this. Each default traits is associated with one or more "families". When you have an only bound, it "opts out" from all default traits in each family that the trait is associated with:

You may also want to "opt back in" to some defaults. For example, T: only Move + Destruct is a sensible thing to do. It means values that can be moved and destructed but not leaked or forgotten.

Examples

Option::map requires only Move

map is an example of a function that only needs Move. You need to be able to destructure self (which moves the optional value out into a local variable v and then invoke the closure op, which again moves the wrapped value v:

impl<T: only Move> Option<T> {
    fn map<U: only Move>(
        self,
        op: impl FnOnce(T) -> U,
    ) -> Option<U> {
        match self {
            Some(v) => Some(op(v)),
            None => None,
        }
    }
}

One interesting thing is the result type U. Using only the stuff I wrote in this blog post, it needs to be only Move, because the result will be moved into the Some value and so forth. But in-place-init would allow for this definition to omit the U: only Move bound because we could statically guarantee that the Option will be constructed in place and never moved after that.

Option::or requires only Move + Destruct

The a.or(b) method on Option returns a if it is Some and otherwise returns b. This is an interesting one because the value b may not be used and therefore requires only Move + Destruct bounds.

impl<T: only Move> Option<T> {
    fn or(
        self,
        alternate: Option<T>,
    ) -> Option<T>
    where
        T: Destruct, // <-- because it may be dropped
    {
        match self {
            Some(v) => Some(v), // drops `alternate`
            None => alternate, // moves `alternate`
        }
    }
}

Rc requires MaybeSized + Leak

The Rc type is an example where we would want to relax bounds from both families:

struct Rc<T: only MaybeSized + only Leak> {}

I believe the proper minimum bounds for Rc are:

Frequently asked questions

What is actually under RFC today?

The post may be a bit confusing here. The current RFC is looking only at the proposed "Sized" traits. The Access family is a speculative future extension that we are exploring but at a much earlier stage.

Can I use only with any trait?

In the beginning, the plan would be that only can only be used for well-known, default traits (e.g., Move, Sized, etc). In the future though there are some thoughts to generalizing it.

Why not opt out from all defaults at once?

An alternative that was proposed is to have the opt-out be per-type-parameter. So you might write something like

fn foo<T: MetadataSized + ?default>

which would "opt out" from all defaulted bounds. Obviously we'd have to bikeshed the syntax, but ignore that for now. The question is whether opting out of all defaults is better than opting out of a single family. I prefer the per-family option for two reasons:

Why do you think it's likely that people want to opt out of being Sized xor Forget but not both?

Because the Forget, Move, and similar traits mostly apply to owned values. The examples we saw with Option<T> were quite typical. And when you are moving values of type T around, you need that T to be Sized.

But we saw that Rc wanted to opt out of both families with only Leak + only MetadataSized, right?

Yes, that's true, and I think that particular combo will be common. I don't think that's an argument for the ?default approach on its own, though, particularly since that case would not be much cleaner or shorter…

impl<T: ?default + Leak + MetadataSized> Rc<T> {}

…what I think that argues for is actually trait aliases and shorthands.

Wait, trait aliases and shorthands? Can you elaborate?

Yes! I think that a future RFC could extend only bounds to allow you to define trait aliases with "only bounds" as supertraits:

trait RefCountable = only Leak + only MetadataSized;

// Equivalent to:
// trait RefCountable: only Leak + only MetadataSized {}
// impl<T> RefCountable for T where T: only Leak + only MetadataSized {}

You could then use an only RefCountable bound to define Rc<T>:

impl<T: only Refcountable> Rc<T>

Without the only, T: Refcountable would just be a regular trait bound and would not opt-out from any defaults.

Can we use a "root" trait to opt out of all defaults?

Yes, we could! You could define an alias like Value:

trait Value = only Access + only MaybeSized;

Since Access and MaybeSized are both implemented for all types, this effectively becomes part of both families:

flowchart TD
  subgraph All["All default families"]
  subgraph A["Access family"]
    Forget[["Forget (default)"]] -- extends --> Leak
    Leak -- extends --> Destruct
    Destruct -- extends --> Access
    Move[["Move (default)"]] -- extends --> Access
  end

  subgraph S["MaybeSized family"]
    Sized[["Sized (default)"]] -- extends --> MetadataSized
    MetadataSized -- extends --> MaybeSized
  end

  Access -- extends --> Value
  MaybeSized -- extends --> Value
  end
  

Then you can do T: only Value and opt out from both families at once.

If we did that, what would happen if we wanted to add a new family in the future?

Ay, there's the rub. If we wish to add a new family in the future, let's say for values that don't live in the same memory space (T: only Distributed…?), then Value would be "out of date" because code written against Value would still be assuming uni-memory-space values. But we could make Value into an edition-dependent alias or something like that, as has been discussed.

Can we decide whether we want Value later?

Yes! We can introduce a root trait at any time. So we can add the Sized-ness family first, then the Access family, and then see how we feel. Maybe we find people are very commonly opting out of both- in which case, some aliases are useful, or perhaps a Value variant.

The only way we might "regret" it is if, in practice, people usually just opted out of both and then opted back in to what they want specifically. But we already know that T: only Move will be common and clearly T: only Value + Move + Sized is more awkward in that case, so I don't consider that very likely.

Why the name Destruct and not Drop?

That name comes from the const trait RFC. There are a few reasons to move away from Drop. The first is that it is possible to have a destructor even if you don't implement Drop: Drop really refers to user-provided logic in the destructor, but the compiler adds its own logic ("drop glue", it's sometimes called) to drop all the fields in the value. The second reason is that the Drop trait itself needs some revision, so moving away from that name lets us have other ways to specify custom logic (e.g., pinned self, or by-value, etc etc).

How does this interact with const traits anyway?

Quite beautifully! In fact, the proposal from Arm for SVE is to introduce the idea of T: const Sized being "a type whose size can be computed at compilation time", which I find quite elegant. Similarly T: const Destruct was proposed by the const RFC as a way to say that a value has a constant destructor.

It's annoying to write T: only Move + Destruct. Couldn't we have Destruct imply Move so that I can just write T: only Destruct?

My original proposal for introducing linear types had Destruct extending Move. This would mean that the Option::or proposal could simply do U: only Destruct and not U: only Move + Destruct. However, Alice Ryhl and others pointed out that there are immovable types that must nonetheless be destructed, so it doesn't make sense to combine those.

Where can I learn more?

The Project Goal has a lot of details. The latest updates are available on the tracking issue. If you like watching videos, I recommend David Wood's Rust Nation talk.

Conclusion

I want to close with a meta-observation and a big shout-out to the Arm team. I think they are showing how awesome open-source can be. The Arm team's primary motivation is adding support for Scalable Vector Extension. This helps Rust make full use of Arm processors. This is, in and of itself, a laudable goal, and valuable to Rust: One of Rust's assets, in my view, is that it gives you access to all the power your processor has to provide, and that should include unique extensions.

But rather than add the feature as a kind of special-case extension to Rust, the Arm team is going further and driving a general purpose improvement, one that will unlock a bunch of other features (extern types and, to some extent, guaranteed destructors; guaranteed destructores themselves unlock scoped async threads and better Wasm integration). I love that.


  1. In fact, I recall that in one of my blog posts I proposed writing "" as the way to spell &str. I kinda wish we had done that just for the sheer wackiness of it (fn foo(name: "")). ↩︎

  2. I prefer names that refer to the operations that can be performed on the values, so e.g. instead of MetadataSized I would prefer SizeOfVal, since it means that you can invoke the std::mem::size_of_val function on it. ↩︎

  3. Little logic pun there for you. ↩︎

09 Jun 2026 9:04am GMT

08 Jun 2026

feedPlanet Mozilla

Firefox Tooling Announcements: MozPhab 2.15.2 Released

Bugs resolved in Moz-Phab 2.15.2:

Discuss these changes in #engineering-workflow on Slack or #Conduit Matrix.

1 post - 1 participant

Read full topic

08 Jun 2026 6:41pm GMT

The Mozilla Blog: Make Firefox your World Cup sidekick this summer

Firefox fox mascot balances soccer ball beside live World Cup match score widget.

Your browser tabs say a lot about your life: work projects, vacation plans, shopping carts and all the rabbit holes in between.

Add the world's biggest soccer tournament to the mix, and your browser is suddenly juggling scores to check, streams to watch, lineups to scan and group chats to keep up with. And since many matches kick off during the workday, there will be lots of temptation to just sneak a peek at the action between meetings.

Firefox is built to be your ultimate second screen. When the tournament is on, keep Firefox open to follow the action, keep up with the conversation, and stay on top of everything else happening online - whether you're watching from the couch or checking in on your mobile device on the go.

You'll find a World Cup widget, custom wallpapers, and game-day multitasking tools. Plus, Firefox is teaming up with Trevor Noah, a soccer superfan, as he hosts live watch parties for the tournament moments everyone will be talking about.

Your second screen for every match

Firefox mobile World Cup widget lets fans follow teams and countdown to kickoff.

When the action is happening fast, keeping up should be as easy as opening a new tab.

Firefox's World Cup widget gives you the latest tournament updates every time you open a new tab (and you can turn it off anytime). With key match information always within easy reach, it's easy to stay on top of the action without bouncing between apps or having to browse around.

You can follow your favorite teams and even customize Firefox with wallpapers that bring big fan vibes to every new tab.

Game-day pro moves

Pin the picture

Firefox picture-in-picture window shows fox chasing soccer ball over soccer stats page.

With picture-in-picture in Firefox, you can detach a video from its tab and pin it anywhere on your screen so you can keep watching while working on other stuff.

Split the view

Firefox Split View shows World Cup widgets beside a sports article in one browser window.

Open two tabs side by side in one window with split view. That way, you can keep live updates on one half and stats, searches or chats on the other.

Calm the chaos

Firefox tab groups organize World Cup fixtures, player stats and soccer resources.

Remember what we said about your tabs representing your life? While 99 tabs of fandom can make it feel more chaotic, your browser doesn't have to.

With tab groups in Firefox, you can create separate groups for:

Hang out with Trevor Noah - World Cup and Firefox superfan

Match days are better with good company. This summer, Firefox is teaming up with Trevor Noah to be his second screen sidekick for his World Cup watch party on YouTube.

Hosted live throughout the tournament, the series will feature Trevor alongside some of his best friends plus celebrity guests as they react to matches, highlights and the internet moments coming out of each day's games.

Trevor is a longtime Firefox user whose comedy and commentary have explored how technology shapes everyday life. That makes this collaboration feel especially fitting for Mozilla, a company built around the idea that the internet should work better for everyone.

"Events like this are some of the biggest shared experiences on the internet," said John Solomon, Chief Marketing Officer at Mozilla. "While many people stop their lives for the World Cup, those that can't follow them while working, traveling, connecting with friends and family, and doing everything else they need to do online. Firefox is built for moments like this, and Trevor is a fitting partner. He's a longtime Firefox user who believes, like we do, that technology should work for people, helping them stay connected to the moments, information and communities they care about most."

Make Firefox your World Cup sidekick this summer. Follow the tournament with the World Cup widget, multitask like a pro with picture-in-picture, split view and tab groups, and get into the spirit with custom wallpapers, all in the browser that helps you get more out of every match.

Firefox logo with a soccer ball at the center on a dark purple background

Are you game-day ready?

Download Firefox now

The post Make Firefox your World Cup sidekick this summer appeared first on The Mozilla Blog.

08 Jun 2026 3:59pm GMT

05 Jun 2026

feedPlanet Mozilla

Will Kahn-Greene: Bleach 6.4.0 releases -- final release

What is it?

Bleach is a Python library for sanitizing and linkifying text from untrusted sources for safe usage in HTML.

Bleach v6.4.0 released!

Bleach 6.4.0 includes two security fixes, a fix to tinycss2 dependency requirements, and some other things.

See the changes here:

https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026

Bleach v6.4.0 is the final release

I haven't used Bleach on a project in years, but I still had some time to maintain it. That changed about a year ago when I got re-orged into a new role and I haven't had time to do any Bleach work since then.

To recap, Bleach sits on top of html5lib which hasn't been actively maintained in years. It is dangerous to maintain Bleach in that context.

We vendored html5lib so we could make adjustments to the library to keep Bleach going. This is not a sustainable approach, but it was ok for the short term.

Over the years, we've talked about other options:

  1. find another library to switch to

  2. take over html5lib development

  3. fork html5lib and vendor and maintain our fork

  4. write a new HTML parser

  5. etc

None of those are feasible for me.

Bleach has been a solo-maintained project for a while now. The world is crazy and it's much harder to build a team of trusted maintainers now than it was (or at least, it sure feels that way). I don't see any possibility of increasing the maintenance team or passing it to someone else responsibly.

Switching contexts from my regular work to Bleach is really hard. Bleach is complicated, the problem domain is complicated, and there's a lot of nuanced context. I can't just switch gears, spend 15 minutes on Bleach to do something, and then switch back to the rest of my day. I periodically get nag messages about this which are entirely valid, but there's nothing I can do about it. It doesn't feel great.

Then in 2025, Emil, a long-time Bleach contributor, built justhtml which gives us an easy migration path off of Bleach. He even took the time to write a migration guide.

Thoughts and statistics

In 2019, when I stepped down the first time, I wrote a post on stepping down.

In 2023, when I deprecated the project, I wrote a post on Bleach 6.0.0 and deprecation.

  • From the first commit on 2010-02-18 to today's final commit on 2026-06-05, the Bleach project lasted 16 years, 3 months - 5,951 days, or about 16.29 years.

  • There were 64 releases.

  • There were roughly 960 commits.

    • From 80 roughly contributors

    • Top 3:

      • Will Kahn-Greene: 462

      • James Socol: 182

      • Greg Guthe: 133

  • Roughly 5,040 lines of Python code excluding the vendored html5lib.

  • I was maintainer from October 2015 to now--that's a little under 11 years.

It feels weird to end a project that's outlived many of the Mozilla sites and Python web frameworks it was designed to protect.

What happens now?

This is the end of the project.

/images/bleach_deprecation.thumbnail.jpg

Bleach. Last release.

If you're still using Bleach, I think you have three options:

  1. End your project. Maybe you don't need to be maintaining your thing anymore? Use Bleach as your reason to exit and do something different with your time on Earth.

  2. Switch to the sanitizer API. Rework your project to use the sanitizer API.

  3. Swap Bleach out for justhtml. Emil provided a migration guide for switching from Bleach to justhtml.

Good luck with whatever option you choose!

Thanks!

Many thanks to James who created Bleach and gave it a set of first principles that guided our choices for 16 years.

Many thanks to Greg who I worked with on Bleach for a long while and maintained Bleach for several years. Working with Greg was always easy and his reviews were thoughtful and spot-on.

Many thanks to Emil who was a contributor to Bleach for a long while and created justhtml providing Bleach users a migration path.

Many thanks to Jonathan who, over the years, provided a lot of insight into how best to solve some of Bleach's more squirrely problems.

Many thanks to Sam who was an indispensible resource on HTML parsing and sanitizing text in the context of HTML.

Many thanks to all the users and contributors of Bleach!

Where to go for more

For more specifics on this release, see here: https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026

Documentation and quickstart here: https://bleach.readthedocs.io/en/latest/

Source code and issue tracker here: https://github.com/mozilla/bleach/

05 Jun 2026 1:00pm GMT

03 Jun 2026

feedPlanet Mozilla

Firefox Nightly: More Kit, More Control – These Weeks in Firefox: Issue 203

Highlights

Internet connection error page with an adorable Kit illustration

Two radio button controls for the Smart Window Memories feature, including "Chats in Smart Window" and "Browsing across Firefox"

Friends of the Firefox team

Resolved bugs (excluding employees)

Script to find new contributors from bug list

Volunteers that fixed more than one bug

New contributors (🌟 = first patch)

Project Updates

Add-ons / Web Extensions

Addon Manager & about:addons
WebExtensions Framework
WebExtension APIs

DevTools

WebDriver

Lint, Docs and Workflow

New Tab Page

World clock widget in New Tab featuring different time zones for YTO, BER, SYD, and LAX.

Search and Urlbar

Smart Window

Tab close and undo actions in Smart Window accompanied by an expandable log of actions taken

Storybook/Reusable Components/Acorn Design System

UX Fundamentals

Settings Redesign

03 Jun 2026 6:27pm GMT

This Week In Rust: This Week in Rust 654

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

Foundation
Official
Project/Tooling Updates
Observations/Thoughts
Rust Walkthroughs
Research

Crate of the Week

This week's crate is remyx, a framework for building TUIs on top of Ratatui.

Thanks to Manuel Garcia de la Vega for the self-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.

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

500 pull requests were merged in the last week

Compiler
Library
Cargo
Rustdoc
Clippy
Rust-Analyzer
Rust Compiler Performance Triage

This week we saw nice wins across the board thanks to merging several compiler queries together (#155678), and also substantial improvements in doc performance thanks to doing less work when sorting trait impls (#157179).

Triage done by @Kobzol. Revision range: 783eb8c8..4804ad7e

Summary:

(instructions:u) mean range count
Regressions ❌
(primary)
0.3% [0.1%, 0.7%] 14
Regressions ❌
(secondary)
0.4% [0.1%, 0.9%] 39
Improvements ✅
(primary)
-0.9% [-6.8%, -0.2%] 111
Improvements ✅
(secondary)
-1.1% [-2.9%, -0.1%] 53
All ❌✅ (primary) -0.8% [-6.8%, 0.7%] 125

3 Regressions, 1 Improvement, 2 Mixed; 4 of them in rollups 35 artifact comparisons made in total

Full report here.

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

Rust

Compiler Team (MCPs only)

Unsafe Code Guidelines

No Items entered Final Comment Period this week for Rust RFCs, 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

Upcoming Events

Rusty Events between 2026-06-03 - 2026-07-01 🦀

Virtual
Africa
Europe
North America
Oceania
South America

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

If memory safety bugs were Waldo (Wally): finding them in C programs is a "Where's Waldo?" game, and Rust's unsafe simplifies it to "Is this Waldo?"

- kornel on rust-users

Thanks to Moy2010 for the suggestion!

Please submit quotes and vote for next week!

This Week in Rust is edited by:

Email list hosting is sponsored by The Rust Foundation

Discuss on r/rust

03 Jun 2026 4:00am GMT

02 Jun 2026

feedPlanet Mozilla

The Rust Programming Language Blog: Launching the Rust Foundation Maintainers Fund

If you want to financially support the development of Rust, please consider donating to the Rust Foundation Maintainers Fund.

A few months ago, the Rust Foundation announced the Rust Foundation Maintainers Fund (RFMF). Since then, the Rust Project has been closely cooperating with the Rust Foundation to determine how exactly this fund will be used to support Rust maintainers. This resulted in the acceptance of RFC #3931, which established the Funding team and the Maintainer in Residence program.

The primary goal of the Funding team is to ensure that maintainers who work on Rust and its toolchain will be properly supported. We will talk to Rust Project members to figure out their funding situation, meet Rust team leads to learn about their maintenance needs, approach companies to find opportunities for them to invest into Rust by supporting Rust maintainers, coordinate various funding efforts and ensure that the beneficial effects of funded maintenance are visibly promoted, with the help of the Content team.

Maintainer in Residence is a new program dedicated to financially supporting existing Rust Project maintainers1. Each Maintainer in Residence will be funded to maintain one or more critical parts of Rust, such as the compiler, the standard library, Cargo, Clippy or one of many other projects that the Rust Project develops and maintains. The funded work will include activities such as performing large-scale refactorings, code reviews, unblocking new features, issue triaging, mentoring other contributors and more, and will be split between priorities guided by the teams they are supporting and priorities of their own choosing within the Project. Where applicable, Maintainers in Residence are also encouraged to propose, champion, and drive forward Rust Project Goals.

The goal of this program is to provide stable and long-term funding so that maintainers can focus on important work that ensures the long-term health of Rust. The funding team will select Maintainers in Residence based on funding availability and maintenance needs within the Rust Project, and help ensure that they are successful. We expect that this will usually be a (near) full-time position, but that will depend on the nature of the work and the area of maintenance.

This program extends our existing support for Rust maintainers, such as the program management program and the compiler-ops program. An important development is that we now have a centralized mechanism for gathering donations from both individuals and companies, and a dedicated team that will help direct those funds to specific maintainers. You can find more details about the funding team and the Maintainer in Residence program in the RFC.

We expect to hire the first Maintainer in Residence in the upcoming months and announce it on this blog, so stay tuned!

How to contribute funds

If you are an individual who wants to help Rust succeed and thrive, you can donate to the RFMF through GitHub Sponsors2. Companies who would like to invest in better maintenance of Rust can also donate through GitHub Sponsors or they can contact the Rust Foundation directly.

The important thing is that all proceeds from this fund will be directly used to support Rust Project maintainers. We currently expect that to happen primarily through the Maintainer in Residence program, but it can also be done in the form of smaller-scale grants or other mechanisms, as determined by the Funding team. We will figure this out on the go, as this is also quite new for us.

We really appreciate each donation, however small, because with more money we can hire more maintainers to ensure that we can continue to develop Rust and that important improvements are not blocked on maintenance tasks. This is especially important at this time, where Rust is starting to get used more and more in the industry in various application areas, which increases the need for sustained maintenance. The importance of multiple funding sources is underscored by an unfortunate trend we currently observe, where key Rust maintainers are losing their funding for Rust work due to budget shifts. The Rust Foundation Maintainers Fund is designed to provide stable funding for Rust maintainers that is less dependent on sudden shifts in the job market and the IT industry.

As with most things, there is no one-size-fits-all solution, so there are multiple ways to support Rust financially. The RustNL Maintainers Team recently hired several Rust Project maintainers. Previously, we wrote about how you can support specific individuals working on Rust. And there are also Rust Project Goals in search of funding. We welcome all efforts that can help support Rust Project maintainers, who often do work that is near invisible and thankless, while at the same time incredibly important and necessary, on a volunteer basis.

Thank you for considering sponsoring the development and maintenance of Rust! You can find more information about funding Rust on our Funding page.

  1. This program was inspired by the Developer in Residence concept used by the Python Software Foundation (PSF), with which we led several helpful discussions. Thank you, PSF!

  2. Note that the fact that GitHub Sponsors is currently enabled on the rustfoundation GitHub organization, and not the rust-lang organization, is an implementation detail that might change in the future. All donations raised on this Sponsors page will be routed to the Rust Foundation Maintainers Fund and will be spent on directly supporting Rust Project maintainers.

02 Jun 2026 12:00am GMT

01 Jun 2026

feedPlanet Mozilla

Firefox Nightly: Backup for a Rainy Day – These Weeks in Firefox: Issue 202

Highlights

Friends of the Firefox team

Resolved bugs (excluding employees)

Script to find new contributors from bug list

Volunteers that fixed more than one bug

New contributors (🌟 = first patch)

Project Updates

Add-ons / Web Extensions

Addon Manager & about:addons
WebExtensions Framework
WebExtension APIs

DevTools

Fluent

Migration Improvements

New Tab Page

Search and Urlbar

Smart Window

Storybook/Reusable Components/Acorn Design System

UX Fundamentals

Settings Redesign

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.

  1. gl.getParameter(gl.VENDOR) and gl.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 API
  2. let ext = gl.getExtension('WEBGL_debug_renderer_info'); and then gl.getParameter(ext.UNMASKED_VENDOR_WEBGL) and gl.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:

  1. Give a constant value. (Or don't return anything at all.)
  2. 'Round' the values into buckets
  3. 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.

WebGL Renderer Value Distribution, before bucketing

Compare that to the bucketed data. 131 distinct values.

WebGL Renderer Value Distribution, after bucketing

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:

  1. Generating structured inputs from scratch with the arbitrary crate

  2. 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:

  1. Unconstrained instruction sequence generation followed by a fixup pass to ensure validity

  2. Generating valid instructions in a forwards, bottom-up manner (from operands to operators)

  3. 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:

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:

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,
                &param_types,
                &globals,
                num_memories,
            )?;

            // Apply this instruction's effects to the
            // stack.
            apply_inst(
                &inst,
                &mut stack,
                &param_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(),
                &param_types,
                &globals,
                num_memories,
            )?;

            // Pop the result type from `needed`, if
            // any, as it's been satisfied.
            let ty = inst.result_type(
                &param_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:

  1. 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.

  2. 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:

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

5 Minutes of Fuzzing

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.


  1. WebAssembly's stack-based instructions encode an expression tree - local.get $a; local.get $b; local.get $c; i32.add; i32.mul is isomorphic to a * (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.

  2. Ignoring its rule-guided bit, which is orthogonal and could be applied to bottom_up as well.

  3. 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.

A Jake diagram showing a top-level navigable and two iframes across five history steps

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
  1. Which, naturally, meant re-implementing the whole of Session History along the way.

  2. Getting nerd-sniped by Jan Jaeschke definitely contributed as well.

01 Jun 2026 12:00am GMT

31 May 2026

feedPlanet 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$ ```

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 ```

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 ```

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 ```

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.

servoshell 0.2.0 showing several new features: better wrapping for CJK scripts, ‘tab-size’, better file pickers and `<textarea>`, `<select multiple>`, ‘::details-content::before’ and ‘::details-content::after’, and ‘color-mix()’ with any number of colors

We've shipped several new web platform features:

Plus a bunch of new DOM APIs:

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

Crypto­Key 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 local­Storage and session­Storage were usable in sandboxed <iframe> and shared with every other sandboxed <iframe>, rather than throwing Security­Error (@Taym95, #44002).

We've fixed a bug where local­Storage and session­Storage 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 tex­Image3D() on Web­GL2­Rendering­Context (@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 IDB­Database, IDB­Object­Store, IDB­Cursor, IDB­Key­Range, IDB­Request, 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 Intersection­Observer API, under --pref dom­_intersection­_observer­_enabled (@stevennovaryo, @jdm, #42204).

We're continuing to implement document.exec­Command() for rich text editing (@TimvdLippe, #44529), under --pref dom­_exec­_command­_enabled. This release adds support for the 'bold', 'font­Name', 'font­Size', '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 Shared­Worker, under --pref dom­_sharedworker­_enabled (@Taym95, #44375, #44440).

We're working on the Wake­Lock 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).

servoshell 0.2.0 showing the revamped browser UI on Android. from left to right: viewing a web page, the settings view, the history view

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:

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:

You can now load a URL with custom request headers by calling Web­View::load­_request (@Narfinger, @longvatrong111, @mrobinson, #43338).

You can now retrieve cookies asynchronously by calling Site­Data­Manager::cookies­_for­_url­_async (@longvatrong111, #43794).

The synchronous version of that method, Site­Data­Manager::cookies­_for­_url, was previously not callable because Cookie­Source 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 Site­Data­Manager::clear­_session­_cookies (@longvatrong111, #44166).

When intercepting requests with Servo­Delegate:: and Web­View­Delegate::load­_web­_resource, we now include a destination and referrer­_url in the Web­Resource­Request, 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.

Window­Rendering­Context::new and Software­Rendering­Context::new now return an error if the given size is less than 1x1 (@freyacodes, @mrobinson, #44011).

We've improved our API docs for Web­View, Web­View­Builder, Web­View­Delegate, ServoDelegate, Prompt­Dialog, Web­Resource­Load, Web­Xr­Registry, Preferences, and servoshell's EXPERIMENTAL­_PREFS (@simonwuelker, @TG199, @sabbCodes, @jdm, @rovertrack, #43892, #43787, #44171, #43947).

We've also improved our API docs for Opts, Output­Options, Diagnostics­Logging, Pref­Value, 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.random­UUID (@simonwuelker, #43989).

Exception messages have improved in Location, Static­Range, and the HTML­Element 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 view­Box> (@yezhizhen, #44420), 'attr()' (@Loirooriol, #43878), ':focus' (@mrobinson, #43873), 'font' (@RichardTjokroutomo, #44061), '@keyframes' (@simonwuelker, #43461), '@property' (@Loirooriol, #43878), 'load' events (@jdm, @arabson99, #43807, #44046), fetch­Later() (@TimvdLippe, #43627), axes and buttons on Gamepad (@log101, @rovertrack, #44411, #44357), copy­Tex­Image­2D() on Web­GL­Rendering­Context (@simartin, @mrobinson, #43608), tex­Image3D() on Web­GL2­Rendering­Context (@simartin, #44367), environment­Blend­Mode on XR­Session (@msub2, #44155), mark() and measure() on Performance (@shubhamg13, @simonwuelker, #44471, #44199, #43990, #43753), and Performance­Resource­Timing (@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), remove­Child() on Document (@rovertrack, #44133), and URL.revoke­Object­URL() (@simonwuelker, @jdm, #43746, #43977, #44035).

Performance and stability

We've fixed some big inefficiencies in Servo. append­Child() with nested shadow roots is no longer O ( 2 n ) (@yezhizhen, @webbeef, #44016), and we've halved the time it takes to load the ECMAScript spec by fixing the O ( whole DOM tree ) 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 Character­Data 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 Path­Buf 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:

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.

7349 USD/month
10000

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

28 May 2026

feedPlanet 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:

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

Two Cargo advisories

Rust 1.96 contains fixes for two vulnerabilities for users of third-party registries.

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

feedPlanet 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]:

[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

Read full topic

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
Observations/Thoughts
Rust Walkthroughs
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.

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
Library
Cargo
Rustdoc
Clippy
Rust-Analyzer
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

Full report here

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

Compiler Team (MCPs only)

Rust

Rust RFCs

Unsafe Code Guidelines

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

Upcoming Events

Rusty Events between 2026-05-27 - 2026-06-24 🦀

Virtual
Asia
Europe
North America
South America

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

- Nadrieril on their blog

Thanks to Theemathas for the suggestion!

Please submit quotes and vote for next week!

This Week in Rust is edited by:

Email list hosting is sponsored by The Rust Foundation

Discuss on r/rust

27 May 2026 4:00am GMT