The rustup team is happy to announce the release of rustup version 1.29.2.
Rustup is the recommended tool to install Rust, a
programming language that empowers everyone to build reliable and efficient
software.
What's new in rustup 1.29.2
This is a small release which only updates the rustls-webpki and
webpki-root-certs dependencies to address the problematic eMudhra
intermediate certificates issue described in CVE-2026-111943.
If you have a previous version of rustup installed, getting the new one is as easy as stopping
any programs which may be using rustup (e.g. closing your IDE) and running:
$ rustup self update
Rustup will also automatically update itself at the end of a normal toolchain update:
$ rustup update
If you don't have it already, you can get rustup from the appropriate page on our website.
Rustup's documentation is also available in the rustup book.
Caveats
Rustup releases can come with problems not caused by rustup itself but just due to having a new release.
In particular, anti-malware scanners might block rustup or stop it from creating or copying
files, especially when installing rust-docs which contains many small files.
Issues like this should be automatically resolved in a few weeks when the anti-malware scanners are updated
to be aware of the new rustup release.
Thanks
Thanks again to all the contributors who made this rustup release possible!
I’ve been working at Mozilla for eleven years, my golly. I’m still on my third hardware refresh (a Dell XPS I continue to dislike), I’m up to my fourth CEO, and 76 reorgs have happened (well, I know that quite a few things that would normally have counted haven’t been counted there, so it’s probably a higher number than that).
I’m still getting Firefox Desktop to use Glean instead of Legacy Telemetry. We’re more than two-thirds of the way done with removing the legacy system, with concrete plans about what to do with the rest. I’m still not blogging as much as I’d like, but I have some honest hope that I might change that in no small part thanks to someone who mentioned in passing that they liked my posts.
Looking back at last year’s predictions, I did indeed requisition a second work monitor, which is working out well.
I was right that FOG Migration hasn’t completely removed all of Legacy Telemetry, though we’re a lot closer than I thought we’d be. I do have a little time to think about doing other things, but not all that much, so the part of the prediction where I’d do something else is a “maybe”.
My desire to push for a sensible user population measurement did not have a chance to be acted upon. We might be shifting what the business counts, but not to anything I’ve had a say in. At least one of the reorgs that happened changed the organizational distance between my team and the other parts of Data that would be involved in such a measurement, other non-work things took way too much time and resulted in slower overall work velocity, and FOG Migration kept taking priority.
I didn’t think there’d be an All Hands, and in the off-change there were, it’d be a multi-city “Summit style” affair. I was half wrong twice: there wasn’t an All Hands but there was indeed an all-Engineering Summit; it wasn’t in multiple cities but just one. I was right that Toronto finally got its chance. I enjoyed the heck out of it as it really reminded me of some of the things I really missed about the All Hands of yore. It could’ve been a day longer, it could’ve been a little more of a polished experience like Brianna-era events were… but for not having done anything like this for two years, what an accomplishment. I was initially skeptical about spending hours of the first day making small talk deliberately with people you don’t know on the subject of the work you do and how you do it, but once it was framed as an effort to scale-up and operationalize The Hallway Track I suddenly understood it.
I had wishful, hopeful thinking that the genAI bubble would have burst or a new shiny late-stage capitalist exploitation mechanism would be draining focus away from it by now. Alas neither. Though maybe things are beginning? I can dream.
I was also half-wrong about there being a new CEO but not yet sworn in. I didn’t account for the candidate coming from within the org, meaning it could be quick. We’ve had Anthony hired and active for nearly a year now and he’s… fine? So far the changes he’s made to MoCo haven’t been to any of the things I feel we need to change, so I can’t really evaluate him. (Assuming that, since he (well, his reports) are able to evaluate my performance, it’s only fair that I get to evaluate his in turn. That’s how this works, right?)
Predictions for the coming year:
We’re going to stick with Anthony as CEO, keeping my count stable
Firefox Enterprise is going to be a runaway success, finally giving MoCo that revenue diversification we’ve been aiming for
Maybe the genAI bubble will have burst? (Yes, this is a duplicate from last year)
I’ll read The Psychology of Software Teams. It will frustrate me to finally be able to name and cite sources for how badly this industry treats its workers.
FOG Migration will be “complete” (Legacy Telemetry probes will be gone, code will have been removed) and I’ll move on to working on… maybe data consent management? Developing a way to find out which metrics are unused, and removing them? I have a few ideas.
I’ll still be complaining about this Dell XPS laptop, and considering what I’ll be refreshing to the following February. Maybe I’ll finally leave Windows behind?
I will have blogged at least three times (setting my prediction deliberately and achievably low), probably mostly video game musings in my ongoing So I’ve Finished series. But I do have a draft of a work post about the history of public data tooling at Mozilla that I’ve been meaning to finish…
New print_page tool. It saves the current page as a PDF file and has optional landscape and background options. It uses print CSS, so you can use it to check print styles
Fixed
--profile-path now reuses a profile that the server created, instead of creating a new profile inside it. The server marks its profiles with a .firefox-devtools-mcp file. Paths without this marker still use the firefox_devtools_mcp_profile subfolder
list_pages, select_page, close_page, navigate_page and new_page no longer bring every tab to the front. They now read the tab list with WebDriver BiDi
select_page and close_page now act on the tab from the last tab list, even if tabs were opened or closed since then
Changed
list_pages no longer lists tabs that are not loaded yet, for example restored tabs that were never opened
If the title of a tab cannot be read within 1 second, the tab is listed with the title (unknown)
The Rust team has published a new point release of Rust, 1.99.1. Rust is a programming language that is empowering everyone to build reliable and efficient software.
If you have a previous version of Rust installed via rustup, getting Rust 1.99.1 is as easy as:
rustup update stable
If you don't have it already, you can get rustup from the appropriate page on our website.
What's in 1.99.1
Rust 1.99.1 fixes the following regressions from prior releases:
Missing symbol wcslen: Some Rust targets were getting link errors for missing wcslen, and that function is now present in Rust's compiler-builtins library as a fallback.
The LLVM loop-idiom-wcslen pass may introduce calls to wcslen as an optimization, and some Rust targets did not have that function readily available in libc or elsewhere. The UEFI targets in particular were broken for any project using std, as some of these calls were introduced in the precompiled standard library, but the issue was also reproducible for similar user code patterns.
Miscompilation of FFI bool return values on x86_64: Rust functions returning bool through FFI could leave bits 1-7 of the return register unspecified, causing callers to observe raw values other than 0 or 1. This return type is now marked for zero-extension again, as required by the ABI.
rust-lld failed to load libLLVM on macOS: This linker could fail to run if the compiler was invoked without the rustup proxy, as there was no direct setting for the appropriate library search path. Now rust-lld is built with an rpath entry for the directory containing libLLVM.dylib.
If you'd like to help us out by testing future releases, you might consider using the beta (rustup default beta) and nightly (rustup default nightly) channels locally and in your CI. Please report any bugs you might come across!
Contributors to 1.99.1
Many people came together to create Rust 1.99.1. We couldn't have done it without all of you. Thanks!
Me, calmly assessing the implications of LLMs for programming language design(ers).
As someone who has made a career of programming language design, I definitely have moments of existential dread about the rise of LLMs. I started coding in 5th grade on an x861 that ran at 5MHz, had something like a 5MB hard-disk and 512K of RAM2. Computers were really, really different back then. But honestly, programming wasn’t. For all the advances in IDEs and programming languages and so forth, the basic act of entering code into a file has remained surprisingly constant3 – right up until a few years ago, when everything kinda turned on its head. That’s been exciting and empowering at times4 but also scary and disorienting. Will my skills in programming matter? What about my skills in programming language design (such as they are)? This blog post is my attempt to answer those questions.
So as not to keep you in suspense, let me just give you the conclusion. In short, yes, I believe there will still be a role for humans in programming. And yes, I think that programming language design is still relevant. And, naturally, I also happen to think Rust is a really good fit for agentic development – but that’s not to say we can’t make it a better fit. And that’s what this post is ultimately about: what makes Rust and agentic development work well, and where we could double down on those strengths.
Stratospheric among the breakout languages is TypeScript, cropped from the chart and sitting currently at +1200% growth from the prior year. [..]
The second most rapid recent growth is in Rust. Meta engineers have written twice as much first-party Rust code so far this year than in the previous 9 years combined.
— David Tolnay (emphasis mine)
Some of that is because agents make it very easy to try out new things. And some of that is because Rust has a reputation for being performant and, if it’s just as easy to pick any language, why not pick one that will yield faster execution times?
But I think Rust is popular for another reason: because Rust happens to be a really good fit for agentic coding6. Of course, I would, wouldn’t I? The point of this post is (1) to argue for why that is and then (2) to discuss what we might change about Rust to make it work even better.
Foundational Software construction with agents
It seems to me that an effective “software factory” will have three pillars:
Design: figure out and specify clearly what the code is supposed to do.
Construction: author code that does what it is supposed to do.
Optimization: optimize the code so that it runs efficiently.
None of these pillars are solved, but some have made more progress than others.
Rust and optimization: fast by default, knobs when you need ’em
Rust is designed to give “good performance by default”, making the idiomatic thing typically also be the fastest thing. But another key part of Rust is that you have the knobs to eke out better performance when you need it. You control when something is stack- or heap-allocated and where pointer indirection occurs. If you want to, you can force the layout of your structs to be done in a particular way, or decide where to introduce boxing into your async stack frames. In the limit, you can put in inline assembly.
In the past, learning how to “tune the performance knobs” could be a bit of a black art: but agents are making that much easier.7 It turns out that agents are really good at fine-tuning the performance of existing code. For example, people who work on super high-performance cryptographic algorithms used to do so by hand-tuning assembly code. But those people tell me that they are now turning to agents for that same job. More prosaicly, a lot of people building network services are finding that, using a profiler like dial9, agents can quite effectively diagnose async performance pitfalls, like long polls or blocking.
Therefore, I think the task for Rust is clear. We keep on with our goal of “low-level enough for a kernel, high level enough for a GUI”. If anything, I think the shift around agents means we can tolerate a bit more “noise” in the form of explicit calls to clone() or share() – but not too much, since we still want the “happy path” of the code to be clear and evident8, even as we surface details that are important in specific scenarios (like tracking precisely how many clones you have of a channel endpoint). Combined with a profiler, you get an effective feedback loop to get your code running fast:
As usual though, while a reputation for good performance is what brings people to Rust, what makes them want to stay is a sense that using Rust “just seems to work better”. The basic theory is that Rust’s type system and guardrails help keep agents on track:
rust is a perfect language for agents, given that if it compiles it's ~correct
I think there’s truth in that: languages like Rust, Ocaml, or Haskell all share this “if it compiles, it works” feeling, and I think that agents will benefit from that.
Testing is not enough, type systems help
Testing can show the presence of bugs, but not their absence.
— Edsger W. Dijkstra, 1970
Agents presenting their work.
Writing tests has never been enough to show that your program is correct – but, with agents, it’s not even close. It happens so often that my first few tests seem great and then the next one goes completely bonkers. Usually bonkers in a way where I cannot even imagine how that bug could have come about. Then I dig in and I realize the agent completely missed the boat on the logic I was trying to express.9
This is where a type systems really shines – particularly an advanced one like Rust. Type systems give you some guardrails that keep the agent on track. In Rust, those guardrails extend beyond simple stuff like “do all fields have the correct names” and to stuff like never having to close a socket.
Type systems are not enough either
Type systems can show the absence of bugs, but not the presence of correctness.
— Me, failing to coin a phrase of equal eloquence10, 2026
So yeah, type systems are helpful! But they have inherent limits. A type system is a simple set of rules that, if you follow them, make sure that you avoid (certain kinds of) mistakes. But they don’t tell you that you do the right thing – only that you don’t do (certain kinds of) wrong things. If you want to go further, you need a proof system – something that will let you show that richer properties are actively true, not just that you’re avoiding bugs.
As it happens, there are a lot of interesting proof tools for Rust, like Verus, Creusot, Aeneas, and Kani. As a starting point, you can prove that your code is panic-free, which will eliminate a lot of bugs, though it is still fundamentally a “type-checker-like” property (i.e., you avoid doing things wrong). But you can also go further and add assertions that show your code is computing the results you expect.
Strong guardrails give better performance
Deplying proofs is not just good for getting code right – it’s good for getting code fast. This is somewhat counterintuitive, but it’s true. Earlier I mentioned how optimized cryptographic algorithms were now being written by agents. Part of what makes that possible is that cryptographic algorithms have a very clear mathematical definition. This means that you can basically tell the agent “do whatever you want, as long as you can prove that the code still computes the correct result”. These proofs can be done with theorem provers like Lean or Rocq. The blog post “Why Lean is faster than Rust” makes the point quite well.
For Rust, a good example of this would be around unsafe code. Right now, people are sometimes reluctant to deploy unsafe code even if it might make their code faster because it’s not worth the risk of getting it wrong. And I applaud that effort. But what if you could use a prover to show that your unsafe code is, in fact, safe (i.e., incapable of causing undefined behavior)? Then you’d be free to optimize to your heart’s content.
If we are going to prove things, do we need type systems?
You might wonder: if we’re deploying a proof system on our code, do we still need type systems? I think the answer is definitely yes. Type systems are infinitely simpler to work with, which is why they are much more popular. Using a type system lets you rule out all kinds of annoying bugs so that you can focus your “proof energy” only on the important stuff.
As a simple example of this, consider the following Java code:
Without seeing more code, it is impossible to prove whether this assertion is true or not! This is because there could be any number of other threads running around mutating this. In contrast, the following Rust code must be true (at least, so long as the Eq operator is reflexive for Bar):
We know this because the type system of Rust guarantees that &mut self implies a unique reference.
This is in fact exactly how Verus works. By trusting the Rust borrow checker to track uniqueness etc, it simplifies the job of the SMT solver, making proof checking must faster and more reliable than it would be otherwise.
Where I think Rust should go on reliability
So my take is that Rust should double down on if it compiles, it works. As expressive as Rust’s type system is, there are plenty of things that it cannot capture. This includes fairly common things, like this method only reads one particular field or internal references, but also fancy things like “no logging of personally identifiable information”. We should work to lift those limits, giving agents room to prove more complex properties.
We also need to improve the experience of integrating a proof tool by working towards an extensible compiler. As cool as the existing Rust-based proof tools are, using them is a janky experience involing annoying macros and pinned compiler versions. We can do better.
A north star: No more unsafe code
I’d like to propose a north star for Rust: no more unsafe code. By that, I don’t mean that we never use the unsafekeyword. Rather, I mean that we have the power to prove that all the unsafe code we write is correct and cannot cause undefined behavior. Don’t get me wrong: knowing exactly what code needs to be vetted is a huge step forward, as is normalizing the idea that libraries need to be responsible for protecting their users from undefined behavior via safe API boundaries11. But given agents’ ability to find vulnerabilities, I think we need to up-our-game if we want to stop the steady stream of CVEs.12
Design is an open problem
But before you go about proving things or optimizing them, you need to have a clear idea of what you are building. As far as I can tell the state of the art here is basically variations on “markdown-driven-development”, developing docs and hoping that agents pay attention.
I don’t know how much of a role for Rust there is in this space. I think there are some steps we might wind up taking that could be helpful here – for example, having ways to separate out interface from implementation so that it’s easier to review the interface in isolation or to see when the agent made changes there.13 Or perhaps just providing betters tools to review that.
I also think there’s space for dedicated “modeling languages”, something that let you iterate quickly with agents on the high-level designs and invariants without having to get into the weeds and read the code. I have more thoughts on this but it merits many blog posts all of its own. For the moment, I’ll start by pointing you all at this post by Kari Wilhelm, “Principles of Anchor Engineering”, which I think does a nice job of describing what it means to pull out the key elements of a design and drive the coding from there.
Conclusion
We covered a lot of ground here, so let me put this all together. What makes Rust a good choice for developing with agents?
It offers good default perf (why pay more?) and, in conjunction with a profiler, can be tuned as much as you want.
The type system gives stronger guardrails, and if you setup your agents right, you can strengthen those guardrails through techniques like new types.
And what could make it better, from a language design perspective? Overall, I think it’s very similar to what we were doing, except that the way we weight the tradeoffs is tweaked to account for agents doing the code authoring, and humans focused more on code comprehension:
If it compiles, it works – our type system is our big advantage, we need to make it more expressive and more functional so that agents don’t get stuck trying to work around it!
And as an adjunct, establish a North Star: Enable Unsafe code without unsafety by supporting proof, and see the most widely used Rust libraries proven sound.
Low-level enough for a kernel, high-level enough for a GUI – make sure we expose the knobs needed to tune for performance, while also making the code’s intent clear (but don’t worry about golfing the code down to an absolute minimum number of characters).
Extensible and versatile – one of Rust’s great strengths is that, with macros + our type system, we support DSLs and a wide variety of targets. We need to bring that spirit of extensibility into the compiler so we can target more styles of development (e.g., GPU, deep language interop, etc) and also integrate theorem provers and other tools that require more than just tokens.
As a final close, let me point you at this interesting article by Marc Brooker, “What’s easy now? What’s hard now?”. He posits that the more it is possible to clearly specify success criteria for a task, the easier it is, and that we may find that tasks we traditionally think of as ‘hard’ (e.g., building out an efficient implementation of a cryptographic algorithm) may soon become automated, whereas tasks that we have thoght of as ’easy’ (e.g., making a unique, compelling website) may become more valued.
Looking at it from a PL point-of-view, I think Marc’s point reinforces my sense that the biggest action is going to come from design languages, things that let you capture the architecture and intended behavior at a very high-level. The goal would be to let humans intuitively specify a design that gets translated into effective guardrails, whether that’s type system checks, lints, or theorems to be proven. Agents would then take on the task of translating the design into code written in a traditional PL like Rust, with humans looking at it only rarely. This doesn’t make the design of traditional PLs less interesting, but it definitely makes the tradeoffs very different than they were before.
Secretly I have always been jealous of people that coded using punchcards and stored things on gigantic rolls of tape or whatever. I just want to be world weary so badly! I am kinda delighted that I’m finally old enough that I can say things like “I remember when we had to edit CONFIG.SYS before we could play a computer game!” and younger programmers will gasp in horror.5 But I still feel like I must sound like a silly kid to folks who were around “back in the day”. ↩︎
I still remember when my dad told me he had bought me a 10MB disk and secretly he had bought a 40MB disk to surprise me. It seemed absurdly large. “I’ll never need a new disk again!” ↩︎
I mean my god, we’re even using vi again! My first editor! ↩︎
I built an electron app! And it was so easy to do! And reading weird papers and things is so much more accessible now that I can ask an LLM to translate all the math jargon for me. ↩︎
I remember once trying to tell Alex Crichton how I used to “order shareware from a paper catalog by putting a check in the mail” and he was like “I do not understand any of the nouns in that sentence”. Highlight of my life, right after Rust 1.0. Oh, and maybe the birth of my child and getting married. I guess. ↩︎
I’m not saying Rust is the only language that’s a good fit. I’m not even saying that the way that Rust is a good fit is the only way to be a good fit. I have heard for example that Go is a really good fit for agentic coding and I believe it: I think it hits a different sweet spot, one that helps agents iterate quickly (although I do suspect agents will let data races and subtle bugs slip through that Rust would have prevented). ↩︎
The fact that agentic development makes more room for knobs is good for Rust performance, but it is quite likely an even better fit for performance in other languages. Tuning a GC has traditionally been a job that really requires a GC expert, but agents can likely fill that role pretty well these days. ↩︎
On this pont, I’m even more convinced of the importance of ergonomic RC now that I work with agents. Agents are good at introducing the extra clones and things you need to deal with capturing around async tasks, but the resulting code is so annoying to read, it really hides what’s going on. ↩︎
“Claude, this is not what we talked about!” – me, all too often. ↩︎
So close though, right? I just .. couldn’t land the plane. ↩︎
All too often, C++ libraries consider it adequate to tell users “you’re holding it wrong” – i.e., that they are responsible for causing undefined behavior by misusing the API. Rust takes a clear position that a safe API has to protect its users, no matter how much they abuse it. ↩︎
To be fair, if Rust is encountering a stream of CVEs, other languages like C++ and Java are encountering a veritable flood, since they lack the protection of Rust’s type system.
st takes a clear position that a safe API has to protect its users, no matter how much they abuse it. ↩︎
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.
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.
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.
No Calls for participation were submitted this week.
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!
Are you a new or experienced speaker looking for a place to share something cool? This section highlights events that are being planned and are accepting submissions to join their event as a speaker.
No Calls for papers or presentations were submitted this week.
If you are an event organizer hoping to expand the reach of your event, please submit a link to the website through a PR to TWiR or by reaching out on Bluesky or Mastodon!
No significant improvements or regressions this week. Most of the PRs marked as improvements had
a similar perf. result, which was caused by codegen noise in the measurements.
There were some nice wins for memory usage though, thanks to #163915 and especially #163887.
No Items entered Final Comment Period this week for
Cargo,
Compiler Team,
Language Team or
Unsafe Code Guidelines.
Let us know if you would like your PRs, Tracking Issues or RFCs to be tracked as a part of this list.
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.
I think that there are a nonzero number of people who are experts in the field who would genuinely respond to "how does C work?" with "it doesn't, next question"
Bug 2079152 - Unable to select content-type when creating attachment
Bug 2079453 - Upgrade devbridge-autocomplete to 2.0.1 and fix autocomplete dropdown reopening after the field loses focus
Bug 2065173 - Migrate Group REST resource to native Mojo API
Bug 2078690 - Migrate User REST resource to native Mojo API: user objects (get, create, update, suggest, whoami, offer_account_by_email)
Bug 2079991 - When one update requests needinfo from several users , the webhook’s event.changes has a single flag.needinfo entry for the last requestee only; the others are dropped.
For this post, I wanted to talk about two different approaches to handling supertraits. I’m calling them modular proofs vs external proofs. The key idea of this post is that, if we want to have cyclic trait impls, we really need to use a modular proof strategy, where the impl establishes all supertraits hold. Previously we had considered an external strategy, where the piece of code using the impl has the obligation to prove the supertraits hold. Modular proofs always seemed better but I did not think they were workable in the past. But I have become convinced that external proofs are incompatible with Rust as designed, and hence modular proofs are really the only option1. This post dives into that reasoning, and also gives a bit of explanation of what I mean by proofs in the first place.
Traits and supertraits
So what do I mean by modular vs external proofs? Well, it all comes down to who is responsible for proving that supertrait obligations hold. Consider a trait like Magic:
traitMagic: Copy{}
The supertrait declaration means that, whenever X: Magic for some type X, it should be true that X: Copy. We make use of this in generic functions:
The trick is that the compiler has to make sure that this implication holds – i.e., for every type X that implements Magic, X also implements Copy. So how does it do it?
Modular proofs: the impl must show supertraits hold
The obvious answer is to make proving supertraits part of deciding whether an impl is valid. For any impl of Magic, we can require that the Copy supertrait holds. So an impl like this would be illegal:
// In a modular system, this impl is *illegal*
implMagicforString{}
This impl is illegal because it would require that String: Copy, and that does not hold. Seems good.
Modular proofs are a bit tricky
I am calling these proofs modular because the idea is that we can prove an entire program is valid by proving each part of it separately. In “programming language” theory, this is typically called a “modular” check, as it works by breaking up the entire program into modules that can be independently checked.
The idea with a modular proof is that we can trust impls to show that the supertrait relationships hold, we don’t have to go and re-prove them over and over. If the impl is wrong, the impl will be invalid, but our code is fine. So if we have impl Magic for String, that implies the rest of the program can prove that String: Magic:
fnstring_is_magic(){// Legal, because there is an impl for `String: Magic`:
is_magic::<String>();}
In fact, since we know that Magic implies Copy, the rest of the program can even rely on impl Magic for String to conclude that String: Copy:
fnstring_is_copy(){// Legal, because there is an impl for `String: Magic`,
// and `Magic` implies `Copy`:
is_copy::<String>();}
So long as impl Magic for String is invalid, none of this poses a problem to soundness, since the program overall doesn’t type-check.
Comparison with functions
An easy way to understand the idea of modular checks is to think of functions. Imagine you have a function like this one:
Clearly, this function is not legal. It takes two integers and promises to return a third integer, but in fact it returns a String. So the function is illegal. But if you have a call to that function from elsewhere, we consider that other call to be legal:
fnuse_sum(){letc: i32=compute_sum(2,20);// OK
}
Here, use_sum is relying on compute_sum to obey its contract. It’s not the job of use_sum to check that, it can just assume it is true.
The catch: how do we decide the impl is invalid
There is a bit of a catch though. How do we decide if the impl is invalid? The basic idea was that impl Magic for String would have to prove that String: Copy. But we just saw that it could, in fact, do that by using itself. In other words, if we aren’t careful, we can provide a proof that String: Copy like…
String: Copy because Magic implies Copy and
String: Magic because impl Magic for String exists
and then we would (incorrectly) conclude that the impl is valid. So clearly we need to do something to rule that out. We need a rule that says, when we are proving that an impl is valid, that proof cannot recursively rely on the impl itself.[^termination] I’ll come back in a future post to ways we might do that, but for now, I want to explore another alternative.
External proofs: the user of the impl must show supertraits hold
When we first looked at this problem, way back in 2018 or so, we thought of another approach. What if we said that an impl is not responsible for proving supertraits. Instead, the idea would be that impl Magic for String is not enough to say that String: Magic. It only says that Shallow(String: Magic) – i.e., String implements Magic in a shallow way, but not in a deep way that includes the full supertraits. To prove
that String: Magic, we have to show that Shallow(String: Magic)andShallow(String: Copy):2
This has the somewhat counterintuitive implication that impl Magic for String is actually legal in an “external proof” approach:
// In an external system, this impl is LEGAL
// (but unusable)
implMagicforString{}
The saving grace is that, while this impl is legal, you can’t actually use it. This function for example does not compile:
fnstring_is_magic(){// NOT legal in an external system:
// * We can prove that `Shallow(String: Magic)`
// * We CANNOT prove that `Shallow(String: Copy)`.
is_magic::<String>();}
Here, String: Magic doesn’t hold even though there is an impl of Magic for String, because the caller also has to check that String: Copy is implemented, and it is not. Huh, interesting.
Comparison to functions: external is awkward
the “external proof” approach for impls is clearly a bit awkward. If we make the comparison to functions, it’s as if the caller has to double check that the callee’s body matches its return type, it can’t actually trust the declared signature. But, awkward or not, it does resolve our problem: given impl Magic for String, we cannot prove String: Copy, and hence we cannot prove that String: Magic. We can only prove that Shallow(Magic: String), which doesn’t imply that the supertraits hold.
But external doesn’t work with unsafe traits
Based on the above, for a long time, I was working with the assumption that, weird as they are, we would go with the “external proof” approach. However, as Ralf Jung and lcnr pointed out to me recently, this is very challenging to reconcile with unsafe traits. Consider an unsafe trait like Nullable:
// A type that can be safely transmuted from `0_usize`.
unsafetraitNullWord{}
The way that Rust works, when we write an unsafe impl, it is the job of that impl to prove that the unsafe conditions hold. Other parts of the program get to trust the impl. So if I write a function like this one, it should be considered safe:3
Now imagine that I wrote an invalid impl like this one:
// INVALID: We are asserting that `Box` can be null,
// which is not true!
unsafeimpl<T>NullableforBox<T>{}
Given this program I could clearly call foo::<Box<u32>>(), but that would “go wrong” (cause “undefined behavior”). I think we would all agree that the fault lies in the impl. And yet, that is inconsistent: we say that the impl alone cannot be trusted to figure out if the supertraits are implemented, but it can be trusted to figure out if the unsafe impl is valid?
Conclusion
I definitely believe that we want to treat the “extra conditions indicated by unsafe” as a more general version of the other obligations that an impl has to establish to show that the trait holds– and therefore that we must have modular proofs. That’s kind of a relief, because something always felt wrong about external proofs, but it was hard to put my finger on a concrete problem. In the next post in this series (whenever that may be…), I expect to cover the approach to coinductive modular proofs that I landed on. Then I expect to talk about an alternative that was proposed to me that I find quite appealing.
I think this was obvious to Ralf Jung from the start. But it took me a bit. ↩︎
This notation is called an inference rule. The conditions above the line are the premises and the bottom line is the conclusion. It says that, if you know the premises are true, you can infer that the conclusion holds. ↩︎
In point of fact, I believe this will not compile because of special rules about unsafe, but that’s not relevant to the point I’m trying to make. ↩︎
Servo 0.6.0 contains all of the changes we landed in August, which came out to 417 commits!
The big news is that we’ve turned on support for CSS Grid by default (@nicoburns, #45621).
This has had a huge positive impact on the visual appearance of many websites!
PeriodicWave and createPeriodicWave on BaseAudioContext (@kevlu93, #47492)
This release also contains significant improvements to navigation, document loading, and session history, which are some of the gnarliest parts of the HTML specification. Who knew that loading about:blank was so complicated? The upshot to all this work is that pages that open additional windows or include iframes work much more reliably.
Panning gestures initiated by touch inputs now lock to the dominant axis (@yezhizhen, #46983).
This results in more predictable panning when interacting with pages in Servo.
Accessibility support in Servo continues to make great strides; it can be enabled with the --pref accessibility_enabled command line argument.
Servo now reports positions and sizes for items in the content accessibility tree (@philocalyst, @alice, @mrobinson, #46996), allowing macOS VoiceOver to work more reliably.
We also report many ARIA roles for web content elements to accessibility toolkits (@mukilan, #47318).
Full list of supported roles
alert
banner
blockquote
caption
code
complementary
contentinfo
definition
deletion
directory
document
emphasis
feed
figure
generic
insertion
list
log
main
math
navigation
none
note
paragraph
presentation
rowgroup
search
status
strong
term
time
timer
Servo’s support for Document.execCommand() is rapidly improving, and can be enabled with --pref dom_exec_command_enabled.
We fixed issues with the inserttext command (@Psychpsyo, #47224, #47306), editing uneditable content (@Psychpsyo, #47362), computing contained nodes (@TimvdLippe, #47429), adjusting selections in contenteditable (@mrobinson, #47393, #47581, #47581), and implemented the indent and insertLineBreak commands (@Psychpsyo, #47416, #47445).
We made a lot of progress on the LargestContentfulPaint performance metric, implemented behind the --pref largest_contentful_paint_enabled argument.
Servo now supports the element and loadTime attributes, (@shubhamg13, #46553, #47475), URLs for video element posters (@shubhamg13, #43376), accurate size information (@shubhamg13, #43378), and painting text nodes (@shubhamg13, #47212).
We’re continuing work on the ServiceWorker feature, which can be enabled with --pref dom_serviceworker_enabled.
This month we implemented the CookieStore interface behind the --pref dom_cookiestore_enabled command line argument (@Taym95, #47044) as well as the Cache interface (@gterzian, #47220).
Servo now supports a disk-backed HTTP cache that can be enabled with --pref network_http_disk_cache (@Narfinger, #43819).
This cache persists between runs, but there is not yet any way to clear it.
Embedding API
Breaking change:WebView::toggle_sampling_profiler has been removed (@lumiscosity, #47014).
To perform sampling profiling on Servo, follow the instructions in the book.
Building Servo now requires at least Clang 19 on all platforms (@jschwe, #47421).
Additionally, the cargo-ohos tool is now required for building for OpenHarmony (@jschwe, #47334).
Debug builds on Windows can now complete successfully (@jschwe, #47372).
Linux arm64 builds have been customized to work on Raspberry Pi 5 (@mukilan, #47113).
Any libservo embedders targeting the same platform will require a similar change if they are using jemalloc.
Meanwhile, the OpenHarmony port now supports external keyboards with US keyboard layouts (@jschwe, #47359).
Additionally, there are now better fallback font selections for CJK characters (@RichardTjokroutomo, #47116, #47664).
Setting the device_pixel_ratio_override preference is no longer ignored on Android and OpenHarmony (@jschwe, #47081).
On platforms that memory map font data (such as OpenHarmony and macOS), we can now save time that would otherwise be spent loading font files from disk (@jschwe, #47029).
Similarly, reading system fonts is no longer in the critical path of starting up the engine (@Narfinger, @jdm, #46476).
Canvas renderer worker threads can now be adjusted by the thread_pool_canvas_workers preference, allowing more performance granularity according to embedder needs. (@jschwe, #46691, #47018)
Historically, about:memory has reported all memory usage by Rust implementations of JS APIs as one undifferentiated js/malloc-heap number.
We now break it down by memory usage of nodes in document trees (@pylbrecht, #46931) and interfaces that are not part of the DOM (@jdm, #47531). We also report memory usage for the network stack’s cookie database (@Narfinger, #47248) and the canvas renderer backend (@janeoa, @jdm, #46958).
We’ve added an experimental threadboost mechanism (@jschwe, @mrobinson, #46843, #47027) that has been observed to improve Speedometer results by 50% and allow canvas rendering to reach 60 FPS.
This is initially enabled for OpenHarmony, but future expansion to other Linux-based platforms is expected.
More on the web platform
We upgraded to resvg 0.48, fixing many issues with SVG images with missing width/height attributes that specified a viewBox (@nicoburns, #46937).
Upgrading to Taffy 0.14 fixed a number of CSS grid-related crashes, support for grid names, better intrinsic sizing, and improved performance (@nicoburns, #47031, #47493).
We also fixed some issues in the Taffy grid integration for more accurate baselines (@nicoburns, #47518).
Session history traversals (i.e. navigating backwards or forwards in history) now wait for any outstanding traversal to complete (@mrobinson, #47293, #47485).
Meanwhile, navigations to about:blank are skipped if the initial about:blank document is present (@jdm, #47074).
about:blank documents no longer use the asynchronous HTML parser (@jdm, #47050), and no longer report a readyState of LOADING (@mrobinson, #47450).
Opening auxiliary windows will wait to dispatch the load event until the initial about:blank document has been replaced (@jdm, @mrobinson, #47442, #46975).
For non-about:blank content, we also made the document loading phases more reliable (@TimvdLippe, @mrobinson, #47426), improved conformance of javascript: URL evaluation and navigation target selection (@mrobinson, #47486, #47516), and addressed longstanding compatibility issues with cross-origin Window and Location objects (@mrobinson, #47549).
Our media playback has improved; we fixed several issues around canPlayType() on HTMLMediaElement that allow Servo to report that MP4 content is playable (@jdm, #47612, #47641, #47670).
Input elements with the autofocus property inside of a <dialog> now receive focus when that dialog is opened (@skyz1, @mrobinson, #45826).
Interested in helping build a web browser?
Take a look at our curated list of issues that are good for new contributors!
Donations
Thanks again for your generous support!
We are now receiving 8128 USD/month (+3% from July) in recurring donations.
This helps us cover the cost of our speedyCIandbenchmarkingservers, 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 36 GitHub users (+1 from July) 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.
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.
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.
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!
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!
A relatively quiet week, but a very positive one nonetheless.
Highlights are a 3.1% improvement in rustdoc speed from not using the metadata based crate_hash for rustdoc runs,
a 0.5% improvement from a new fast path in the trait solver,
and a 0.4% improvement from a cache for the parents of SpanData.
This RFC will appear in the Call for Testing section of the next issue (#) of This Week in Rust (TWiR).
You may remove the call-for-testing label. Please feel free to leave the call-for-testing label in place if you would like this RFC to appear again in another issue of TWiR.
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.
Will Lachance does a retrospective on the Glean Dictionary outreachy internship.
See also "Linh's Outreachy Internship Highlights" https://www.youtube.com/watch?v=UJdIkHDPgGQ
To learn more about Outreachy, see https://www.outreachy.org/
Firefox Telemetry Engineer and Data Steward Chris H-C (:chutten) gives a talk at Ubisoft's Data Summit 2021 about how Responsible Data Collection as practised at Mozilla makes cataloguing easy, stops instrumentation mistakes before they ship, and allows you to build self-serve analysis tooling that gets everyone invested in data quality. Oh, and it's cheaper, too.
This talk was given as part of the Data Club Lightning Talk Session on February 11th, 2022.
More on https://blog.mozilla.org/data/2022/02/25/this-week-in-glean-your-personal-glean-data-pipeline
Information about Glean: https://mozilla.github.io/glean/book/index.html
Leif Oines and Will Lachance introduce Glean Annotations: a process and technology for curating and communicating knowledge about the data we collect in Mozilla's products.
The 2021 SciPy conference (https://www.scipy2021.scipy.org/) involved the showcasing of the latest open source Python projects for advancement in scientific computing. Mozilla was a diversity sponsor and a few Mozillians attended and shared their experience of the event.
With Rust 1.100.0, the following changes to 32-bit Windows targets will happen:
i686-pc-windows-msvc Tier 1 with host tools target will be demoted to Tier 1 without host tools.
i686-pc-windows-gnu Tier 2 with host tools target will be demoted to Tier 2 without host tools.
Builds of the standard library will continue to be distributed, but host tools such as the compiler will be no longer available. i686-pc-windows-msvc as a Tier 1 target still undergoes CI testing.
To build 32-bit Windows binaries, cross-compiling from a still-supported host toolchain (such as a 64-bit Windows ones) will be required from now on.
Background
Desktop and Server 32-bit only x86 CPUs are no longer sold for over 15 years, and general 32-bit Windows support has ended in October 2025. This means that the development platforms these targets are meant for hardly exist these days, and even if they do exist they typically aren't capable enough for development.
Even on the modern x86_64 hardware, building i686 Windows toolchains has proven to be problematic. We have encountered compiler binaries crashing when built with the i686 MSVC target, and the GNU C++ toolchain failing with OOMs during LLVM build.
Considering all these things, cross-compiling these targets from a better supported one is what we have found to be the best solution forward. As part of that, we stopped producing host tools for these targets. For the time being, the prebuilt standard library is still available, and in case of i686-pc-windows-msvc still tested on CI.
What Changes?
After Rust 1.100, it will no longer be possible to install toolchains on 32-bit Windows hosts. We recommend cross-compiling from a still-supported host (such as a 64-bit Windows toolchain) instead. Other 32-bit platforms are not impacted by this change.
For more details about these demotions, see RFC 3999 for i686-pc-windows-msvc demotion, and MCP 1020 for i686-pc-windows-gnu demotion.
We try to be responsible with data. For example, we:
- store as little sensitive data as possible
- monitor changes in incoming data on which we've built models
But what happens when those two approaches conflict?
How do we monitor changes in incoming data that we don't want to store?
This talk explains the schema we use to monitor changes in what people are searching for in Firefox...even when we deliberately don't store some of what people are searching for.
The Rust team is happy to announce a new version of Rust, 1.99.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.99.0 with:
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.99.0 stable
extern "C" variadics
Rust 1.99.0 stabilizes defining C-ABI variadic functions with "C" and
"C-unwind" ABIs. Variadic functions defined this way use a variable argument
list (...) and accept an arbitrary number of arguments. Rust could already
call externally-defined variadic functions (e.g., libc::printf). With Rust
1.99, these functions can now be written in Rust itself:
/// SAFETY: must be called with (at least) 2 i32 arguments.unsafe extern "C" fn sum(mut args: ...) -> i32 { // SAFETY: guaranteed by the caller. let a = unsafe { args.next_arg::<i32>() }; let b = unsafe { args.next_arg::<i32>() }; a + b}fn foo() -> i32 { unsafe { sum(0i32, 2i32) }}
The type of ... is VaList,
which is ABI-compatible with the C va_list type across targets. What types can be read from a VaList is guarded by the
VaArgSafe trait.
For more details on c-variadic functions, see the
Reference.
This release also stabilizes support for defining naked variadic functions with
non-"C" ABIs, which must be written via inline assembly.
Layout information from raw pointers
This release settles the safety requirements for retrieving the size and
alignment on raw pointers to both Sized (trivially safe, already possible on
stable) and non-Sized types.
Recommend against round-trip unleaking after Box::leak
While there are no changes to the language semantics in Rust 1.99, we have
updated the documentation on Box::leak to recommend against patterns that
later deallocate that memory. This was done because such code was found to have
problematic interactions with current and future potential compiler optimizations,
and is especially problematic with the upcoming stabilization of custom allocators.
Instead, Box::into_non_null or Box::into_raw should be preferred.
This guidance also applies to other leak functions in the standard library.
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.
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.
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!
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!
This week was fairly positive. We had no pure regressions, and most of the results came from a few architectural improvements with mixed or mostly positive impact. Some improvements also come from addressing previously triaged regression caused by missing no_alias annotation for references in closures.
The biggest improvement this week is in rustdoc, from tackling quadratic behaviour when generating sidebar links. This was reported by a user, but the effect didn't show up in our benchmarks, so we added a special stress test for it.
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.
The community is obnoxiously helpful. I asked a simple question on the Rust community Discord server. What is the best way to read a file in Rust? I expected a straightforward response. Instead, I got back a 2,000-word essay on the inner workings of IO, buffering, error handling, and ownership, plus links to four different blog posts and three different approaches depending on file size, and a working code example.
...
The Rust community has weaponized education against me. I'm now a better engineer than I was yesterday against my will.
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.
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.
If you are a feature implementer and would like your RFC to appear on the above list, add the new 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.
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!
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!
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.
Thanks to Kohei for his work on the newly revised Guided Bug Entry form! If you want to check it out yourself, then click on the Switch to Bugzilla Helper link at the bottom of the enter bug product selection page. Normally users without canconfirm permissions will see the Guided Bug page by default which a large percentage of our Bugzilla users.
Firefox DevTools MCP (firefox-devtools-mcp) 0.10.4 is out on npm.
Changes
This release only contains additional hooks and configuration files to register Firefox DevTools MCP on various third-party aggregators, as well as a new task to automatically register the mcp on https://registry.modelcontextprotocol.io/ on release.
At the end of August, we announced our first Maintainers in Residence, Rust Project contributors who are funded for their upstream contributions and maintenance work from the Rust Foundation Maintainers Fund (RFMF). Since then, the Rust Leadership Council has dedicated more funds from its Project Priorities budget to RFMF, and together with AWS also providing additional funds, this allowed us to open a new full-time Maintainer in Residence (MiR) position to support the Cargo team. We would like to thank the Rust Leadership Council, AWS, and also the Rust Foundation for providing us with this opportunity! If you would like to help us hire more maintainers to improve Rust, consider donating to RFMF.
This post explains why we chose to support the Cargo team specifically, and introduces Scott Schafer, the new Cargo Maintainer in Residence.
Why Cargo?
The new MiR full-time position is dedicated to helping with the maintenance of Cargo, our build system and package manager. The Cargo project is deeply involved in many new Rust features, improvements, and Project Goals. Combined with its cross-cutting nature, where it has to support many different use-cases and integrate with several other tools, it takes a lot of work just to keep up with its maintenance needs, let alone support so many feature requests and proposed changes.
Because of that, the Cargo team has sometimes struggled with meeting its maintenance demands. You might remember that for several years, it actually held a feature freeze, to reduce Cargo's internal tech debt, perform necessary refactorings, go through the issue and pull request backlog, and come up with scalable internal development and design processes, so that they could eventually go back to even thinking about adding new features.
Recently, some changes occurred within the team, which made it more difficult for them to meet their maintenance baseline. Some members of the team left, while others lost their dedicated funding for working on Cargo maintenance and had to scale down their involvement. The Funding team thus considered it very important to support this team, given that we had an opportunity to do so. And thus we decided to hire a full-time maintainer to work on Cargo for (at least) the next 12 months.
Even though we know that a single full-time maintainer will not completely solve the maintenance struggles of the Cargo team, we hope that it will improve the situation, and provide a bit of a relief for the team.
Introducing Scott Schafer
We are very happy to welcome Scott Schafer (@muscraft) into the Maintainer in Residence role! Scott has joined the Cargo team three years ago, and apart from working on Cargo, he is also the lead of the Rust Docker team, which prepares official Docker images for every Rust version.
Apart from working on general maintenance of Cargo, Scott has implemented Cargo's Workspace inheritance feature, and has also spearheaded a complex multi-year effort to switch the rendering of diagnostics in the Rust compiler to use the annotate-snippets crate. This effort has been completed in the Rust 1.93.0 release. Thanks to it, the same diagnostics interface can now be shared between the compiler and Cargo (and also other tools), which amongst other things unblocked further development of the Cargo linting system, which has now been stabilized and will ship in the Rust 1.100.0 release.
Everyone we talked about was very excited about Scott becoming a Cargo Maintainer in Residence, and we share that feeling. We wish Scott all the best in his new role, and we are very happy that we can support his maintenance work.
Here is what Scott thinks about it:
I am incredibly excited to work on Cargo full-time! There have been so many things that I wish I could've worked on over the years, that I will now be able to get to. I hope that my efforts will bring Cargo into a more maintainable state.
Conclusion
We are incredibly happy that we keep getting more funds for the Rust Foundation Maintainers Fund, which allows us to support Rust Project contributors. The funding team will be working with the supported maintainers, and also the funders, to ensure that they are all happy with the arrangement, so that we can secure stable funding for Rust maintenance for years to come.
If you would like to help us support more Rust maintainers, consider donating to RFMF!
The Rust Security Response Team was notified that Miri stores all environment variables to target/, allowing secrets to persist in caches.
While not necessary a vulnerability in and of itself, when paired with GitHub Actions caching behavior, it is possible for this to expose secrets to PRs.
Overview
GitHub Actions makes it possible to cache directories between runs. Typical setups allow CI runs on main (and other branches) to write to cache, and PRs can only read from cache (preventing cache poisoning). Rust projects tend to speed up CI by caching binaries built by cargo install and sometimes the contents of target/.
PR CI can be triggered by anyone who can open PRs on your repository. GitHub requires maintainer approval for the first PR, but future PRs will rerun CI on every push. Anyone who has previously landed a change can trigger a CI run extracting information from cached target/ and then cover their tracks by pushing a second commit to the PR.
GitHub sometimes hides overwritten commits in its UI, making this kind of attack harder to detect. CI run logs and overwritten commits are also deleted after a few months.
When cargo miri is invoked, Miri needs to retain build-relevant environment variables between runs[1]. The current code to do so achieves this by storing all environment variables to target/. This, of course, persists when target/ is cached.
If your environment contained secrets, these can now be accessed by PRs via the cache.
Our fix
Our short term fix for this is to make Miri only preserve CARGO_* environment variables (excepting CARGO_*_TOKEN) and OUT_DIR. In the longer term, Miri and cargo may figure out better ways to inform Miri of the relevant list of environment variables. Note that this patch may not be available on nightly yet.
We also performed an ecosystem scan of GitHub repositories and identified 1 repository with this issue and 7 repositories that do not appear to be vulnerable but should be cautious anyway. We have reached out to those maintainers.
Am I affected?
It is likely that our scan was imperfect, so we recommend you check your own GitHub Actions setups if you run Miri.
The cache is accessible to PRs (common and often the intended use case)
Possible quick fixes include:
Disabling cache for that job.
Scoping secrets to steps in that job that do not call Miri.
Temporarily disabling Miri.
Once done, please clear the cache. Consider rotating any secrets that might have leaked.
The Miri release in the upcoming nightly (2026-09-22) will no longer have this problem.
Even if you do not run Miri, ensure jobs that can write to public caches do not have access to secrets. Many tools do not have special handling for secrets, and assume the entire environment can be written to the filesystem.
Threat model
We consider it bad practice to have a cache that can easily be tainted by secrets.
If caching target/, it is worth making sure that the inputs to processes that create target/ (anything invoking cargo) do not have secrets available. It is generally rare for standard cargo build/test subcommands to need any secrets or tokens[2], so this is mostly a matter of being careful about having secrets exposed as environment variables to the entire job.
Cargo/Miri/Rust does not guarantee that environment variables will be safe from being copied into target/. While we are treating this as a security issue and patching it out of an abundance of caution, this is not something you should rely on in general. Beyond official Rust tooling, it is possible for build scripts to be doing things that lead to the environment being stored in compilation artifacts.
Acknowledgements
Thanks to Predrag Gruevski of OpenAI for reporting this issue to us. Furthermore, the ecosystem scan was performed using Codex access and credits donated by OpenAI, which we also thank them for.
Issue triage and remediation was performed by Manish Goregaokar, Ralf Jung, Ben Kimock, Weihang Lo, Jacob Finkelman, Walter Pearce, Josh Stone, and Mark Rousskov.
Miri is invoked multiple times by cargo miri for complicated reasons ↩
In theory it could come up with build scripts reading from the network ↩
Firefox DevTools MCP (firefox-devtools-mcp) 0.10.3 is out on npm.
New features:
New tool: close_firefox_session — ends the browser session: it releases the connection when the server is attached to an existing Firefox, and closes the browser when the server started it.
New parameter: fullPage for screenshot_page — captures the whole scrollable document instead of the viewport.
New parameter: downloadFolder for set_download_behavior — only used when setting the “allowed” behavior, it sets the folder where downloads should be stored and defaults to ~/.firefox-devtools-mcp/output/downloads.
Bug fixes:
No longer fails to find the Firefox binary on Linux with Flatpak.
restart_firefox now rejects when used against a server started with --connectExisting, whereas it used to restart the WebDriver session without restarting the browser and silently ignored all its configuration parameters.
Other changes:
Servers started with --connectExisting now disconnect after 30 minutes without any tool call.
restart_firefox is now part of the mozilla-internal package, and should only be used in controlled environments, as it allows the agent to restart the browser with a custom configuration. Instead, the new tool close_firefox_session can be used whenever clients do not need to update the browser configuration, and will have a more consistent behavior.
Path checks for the saveTo parameters have been improved to avoid overlapping with existing profile folders
Install:
claude mcp add firefox-devtools npx @mozillamozilla/firefox-devtools-mcp@latest
For internal Firefox development, swap firefox-devtools-mcp with firefox-devtools-mcp-moz in order to benefit from additional tools, such as chrome-privileged script execution.
Special thanks to all the contributors who filed issues and submitted patches for this release: f3tch (github), shoemoney and mightykatun.
We believe that there is an ongoing campaign targeting rust-lang members and
owners of popular crates that is attempting to compromise devices and accounts
in order to use them to publish malware.
What we've seen
A video call is set up for something positive — maybe for a job, maybe for a
project, maybe for a contract opportunity — and then that's used as a vector to
either get the target to install something on their computer (such as a
purportedly missing audio codec) or execute another command (for example, via
putting a command on the clipboard).
These attackers are setting up new but legitimate seeming company profiles,
including plausible LinkedIn presences, in order to pass cursory inspection.
Please take extra care in the near term. Be appropriately suspicious of cold
outreaches, and ensure that any calls you have with new people are on platforms
you trust — ideally, try to be the one who sets up the call on a platform you
already use.
Please also re-check that your accounts look normal: MFA enabled,
no unexpected logins on platforms that can track that, and so on.
If you have any concerns about your accounts, please reach out to
help@crates.io (for crates.io account concerns) and/or
security@rust-lang.org (for any other
concerns). We're very happy to help.
Bug 2069143 - When retrieving comments for a bug(s) via REST API, remove comments that would be collapsed in the web UI unless explicitly asking for them
Bug 2070247 - Support limiting who can needinfo the Hackbot account
Bug 2061445 - Migrate Bugzilla (system info) REST resource to native Mojo API
Bug 1877201 - Post to bug/{bugid}/comment returns wrong comment_id
Bug 2072224 - Update REST API authentication documentation to reflect per-resource migration to native Mojo auth
Bug 2060932 - Support GitHub-style <details>/<summary> collapsible sections in comments
Bug 2072689 - Buglist link on My Dashboard is broken
EDIT: Updated to reference gh correctly. Thanks flod!
Sometimes on Github, I need to fetch a patch from a fork I don't typically see everyday so I can try it out locally. I use the line at the top of the patch which has a copy button next to it because it's convenient.
The common steps for this are:
Click the contributor's branch and go to their github fork repository.
Copy the repository link.
Add a new git remote with an alias (typically their username).
git fetch <alias>
git checkout <alias>/<branch>
Here is a one-liner for it that you can add to your gitconfig:
You might ask, why do all of this when the github gh CLI does this for you?
While it does simplify some tasks, I wanted a solution that was independant to a specific git host. The git@github.com remote that is used in the alias can be changed or made configurable if desired. This is something that I wouldn't be able to do with platform-dependant gh.
The fetch also works well with jj too because the fetch and checkout remain headless.
Formatted and commented, it looks less intimidating:
f() { # Get the repository name from your checkout (assuming it is the # original directory name as the remote). PNAME=$(basename `git rev-parse --show-toplevel`); # Parse out the fork's owner. # Example: `<owner>:<branch>` OWNER=$(echo $1 | cut -d':' -f1); # Parse out the branch name. # Example: `<owner>:<branch>` BRANCH=$(echo $1 | cut -d':' -f2); # Do a fetch of that particular branch using the extracted # information from above. # This assumes the remote is hosted on github. git fetch git@github.com:$OWNER/$PNAME.git $BRANCH; # Checkout using the alias `FETCH_HEAD` which git provides. git checkout FETCH_HEAD;};# Execute the function!# It's easier to build a function that holds variables and execute# rather than in-line it.f
Comments
With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one. Known non-private replies are displayed below.
Learn how this was implemented from the original source here.
<noscript><p>Loading comments relies on JavaScript. Try enabling JavaScript and reloading, or visit <a href="https://mindly.social/@jonalmeida/117279142568496801">the original post</a> on Mastodon.</p></noscript>
<noscript>You need JavaScript to view the comments.</noscript>
Last September, the Servo project announced that long-time maintainer Josh Bowman-Matthews (@jdm) would work part-time on improving the Servo contributor experience, entirely funded by the monthly donations on OpenCollective and GitHub.
In his own words, here is a look back over the past year!
First of all, I am enormously grateful to everyone who financially supports Servo, as those donations have enabled me to devote significant time to a project that I care a lot about.
Some highlights from that funded work that I’m proud of:
On top of that, I spent time diagnosing unexpected failures in others’ PRs and fixed numerous intermittent test failures that made merging PRs more difficult for everyone.
A few pieces of work from this period that stand out to me:
supporting a large scale rewrite of Servo’s JS engine integration to address intermittent panics related to garbage collection—I reviewed lots of pull requests, but also filed many issues that enabled the work addressing the panics to be spread across many other contributors
supporting another contributor’s grant proposal to work on Servo that was approved!
This role I’ve carved out means a lot to me—I’ve found a healthy balance that allows me to spend time with my family as well as make meaningful contributions to Servo, and I get to spend a lot of time looking for ways to make the project more accessible for others.
A big thank you to everybody supporting the project and my work; each individual monthly donation makes a big difference!
I’m excited to see what’s possible in the coming year.
This is the opposite of what I wanted to write about: relocating your MOZ_OBJDIR outside of your source directory will make your IDE faster.
In mozilla-central (the firefox monorepo), a default object files directory is created within the same source directory. This is equivalent to the build/ directory you would typically see in other projects.
While this is typically fine, I've found that Android Studio indexes many of these files and that can be slow when you're not working across all the layers in Gecko and Firefox. I ended up with these build directories that grew over time:
Directory
Last touched
Size
Files
obj-aarch64-unknown-linux-android
2025-11-04
29 G
113,116
objdir-desktop
2025-11-04
19 G
41,010
objdir-frontend
2026-09-09
6.3 G
66,930
obj-aarch64-apple-darwin24.5.0
2025-06-16
4 K
1
That's a lot to index! I figured a way around this problem is to move the OBJDIRs out of the source directory and into something like ~/.mozbuild:
While this does speed up IDE indexing, it's at the cost of developer experience, because now our generated code (e.g. FxNimbus) shows up as red symbols everywhere.
[insert sad trombone sound clip]
I'm uncertain if trimming what we index in these OBJDIRs is worth a large enough performance win, so for now I'll ensure I clean-up my stale copies which I'm not actively using.
One of the biggest challenges Rust developers report in our
annual surveys is a subpar debugging experience. So, back
in February, we ran our first Rust Debugging Survey,
in the hopes of identifying how Rust developers are using debuggers and what
problems they are facing when doing so. We received over 2,300 responses, and
we'd like to thank everyone who took the time to participate in the survey!
In this report, we'll go over some of the results of the survey. If you'd like,
you can also check out the complete results of the survey.
If you'd like to skip ahead to any particular section, you can do so with this
index:
The first step to making sense of the survey results is understanding who took
the survey. We asked respondents to rate their Rust expertise, from "Never used
it" to "Advanced". Over 80% reported themselves as "Advanced" or "Intermediate",
split roughly evenly between the two:
We also asked respondents if they currently use or have used debuggers in Rust.
Over 46% said they currently do, with the remaining responses split between
"have in the past" and "never have". That means that over half of respondents do
not currently use a debugger for Rust!
Categorized by expertise, the responses reveal that roughly half of "beginners"
have never used debuggers in Rust! On the other hand, nearly half of "advanced
users" currently do use debuggers in Rust:
For respondents who indicated they had previously used Rust but no longer did,
we asked if challenges with debugging support were why they stopped. For nearly
3%, the answer was "yes", with an additional 24% reporting debugging issues as
being partially responsible (though mind the small response count; most
respondents were active users of Rust):
Knowing what debuggers developers are using and how is another important part of
understanding the challenges they face. To this end, we asked respondents how
they were debugging their programs. Unsurprisingly, most developers make use of
print debugging and the dbg! macro. Excluding those, using lldb inside an
IDE was the most popular choice, followed by gdb on the command line:
We can get a more detailed breakdown of these results if we include the
operating system on which the respondents use a given debugging approach. We
examine this from two different angles. The first angle being, "On operating
system X, what percent of responses are using debugger Y?". Print debugging and
the dbg! macro are consistently the top two yet again, but looking beyond
that, things get more interesting. On Linux, using gdb on the command line was
the most popular choice by a thin margin, beating lldb in an IDE by only 0.4%.
On Windows, Windows Subsystem for Linux (WSL), and macOS, lldb in an IDE was
the top pick by at least 6%, making it a very popular choice in general. On
Windows, the three least popular choices were the command line debuggers (gdb
CLI, lldb CLI, and BugStalker), and on both Windows and macOS the third most
popular pick was, "I don't know". Those who were debugging on operating systems
not listed (Other) most frequently used some kind of special embedded debugger
or gdb:
The other angle we can look at these responses from is, "For users of debugger
X, what percent of responses are using it on operating system Y?". For most
debuggers, Linux makes up the largest portion of uses, ranging from about 45% to
about 77%, followed by Windows, then macOS. The most notable exceptions are
WinDbg and the Visual Studio debugger, which are primarily used on Windows, and
lldb, which is used more on macOS than Windows in an IDE and on the command
line:
To the 6 respondents who use WinDbg on Linux: we wish you luck!
As for how people actually use their debugger of choice, the aggregate results
are not particularly surprising. Roughly 87% of users are using debuggers for
stepping line-by-line through programs and a little over half of users are using
debuggers to obtain stack traces from hung/crashed processes. Only a quarter of
the respondents use a debugger to debug async code. That might be partially
caused by the async Rust debugging experience being clumsy and incomplete, or it
could just be that users aren't writing much async code:
If we break these results down by expertise, we can learn a bit more about usage
patterns. As users become more experienced with Rust, their use of debuggers for
learning purposes decreases, and they get more stack traces from crashed
processes:
The final bit of insight into how Rustaceans use debuggers is if they are
debugging programs that use Rust alongside other programming languages. For 44%
of respondents, the answer is "yes", which is a pretty high number!
As for which languages those are, C dominates the scene at a little over 70%,
followed by C++ at about 43% and Python at about 20%:
Instead of diving right into asking, "what problems do you face when using
debuggers?", or something to that effect, we first asked respondents why they
decide against using debuggers whenever they do, including for reasons that
aren't necessarily "problems with debuggers".
The most commonly reported reason was that it was easier or faster to use logs
or print debugging to solve problems, reported by a little over 81% of
respondents. This could partially be explained by the open responses, which
featured complaints that debuggers were too difficult to set up and/or use
(especially on Windows, when dealing with Web Assembly, or in embedded
contexts) and sentiment suggesting that small and/or simple problems just don't
really need a debugger. It does leave one wondering if the user experience could
be made convenient enough to dethrone print debugging, but it seems hard to beat
something so intuitive. This is followed by roughly 37% of respondents who write
code that Just Works. Fair enough. After that, about 26% of respondents
indicated that they've decided not to use debuggers in situations where the
language features they were working with had poor support. This is slightly more
than issues with standard library types, at about 22%, which is slightly more
than issues with external library types, at about 20%:
As stepping through code was anticipated to be one of the most common uses for
debuggers, we directly asked respondents if they faced any issues when doing so.
A little over 51% of respondents said they did! Of those who reported that they
experienced issues stepping through code, we asked when they were experiencing
issues. Async code was the most common case reported at slightly over 28%,
followed by code involving macros at about 23%. The least common case reported
was code involving function pointers, at almost 6%:
We also directly asked respondents which types in the standard library were hard
to work with, if any. This was an open-response question, and reading through
the responses, some particularly common complaints were with enums and
collections, particularly std::collections::HashMap and std::vec::Vec. This
is also visible in the word cloud in the full report.
We asked respondents to indicate which pain points, if any, they have
encountered when using debuggers with Rust. At slightly over 74%, poor
representation of values was the most common pain point by a decent margin,
followed by being unable to print variables at just over 55%:
We asked respondents to indicate if they were library authors, and if so, if
they were aware of and using the debugger_visualizer attribute. Nearly 62% of
respondents indicated that they were library authors who were not aware of this
attribute:
For those who indicated that they were library authors who knew about the
attribute but did not use it, we also asked why. This represented a much smaller
fraction of respondents, so keep that in mind! That said, half of these library
authors indicated that they didn't have the time to maintain visualizer
attributes, and just under half indicated they didn't know how to write
visualizer scripts:
For those of you who have been reading this section asking yourself what the
debugger_visualizer attribute is, you can read up on it in
The Rust Reference: Debugger Attributes. The quick
explanation is that the debugger_visualizer attribute can be applied to
modules or the crate root to embed files in the debug information which improve
the display of values with certain debuggers. The two currently supported file
types are Natvis files, used by Microsoft debuggers such as WinDbg, and GDB
"pretty printers", which are structured Python scripts used by GDB.
Closing Remarks
Thanks to your participation in this survey, we've gained some great insights
about how Rustaceans are using debuggers and what issues they are facing. For
example, knowing that such a high number of users are dealing with poor
representation of values pairs well with knowing which standard library types
are causing issues, knowing that many library authors haven't heard of the
debugger_visualizer attribute, and knowing that many of those who have but
don't use it either don't know how or don't have time to maintain visualizer
scripts.
Looking to the future, the survey results have suggested that there are a few
notable ways we could most significantly improve the debugging experience in
Rust, such as:
Fixing the way enums are represented by debuggers so they show actual
variants
Fixing the way collections (such as HashMap) are represented by debuggers so
they show their contents, rather than their implementation details
Fixing the way string types (such as String and CString) are represented
by debuggers so they render as text, rather than their implementation details
Improving the async debugging experience, particularly with stack traces
Improving stepping through certain state machines (such as iterators and
Futures)
Providing documentation on basic set up and use of some common debuggers
A common suggestion that could resolve those first three points is to use the
Debug implementation of types to display them in debuggers. There are
challenges to that approach, such as the fact that the Debug implementation is
not present in the final binary unless it is actually used somewhere in the
program, but it isn't impossible. Notably, this is already supported by the
BugStalker debugger (given the same condition that the Debug
implementation must actually be used), which some of you first heard about from
the survey! It also appears to have some support for async, with plans to
expand.
One notable way the debugger experience is currently being improved is through
the ongoing Google Summer of Code project improving how we test debug info and
visualizer scripts, making it easier to maintain and improve our own visualizer
scripts and general compatibility with visualizer scripts without silent
breakage or regressions.
Once again, we'd like to thank everyone who took the time to participate in the
survey!
Firefox DevTools MCP (firefox-devtools-mcp) 0.10.2 is out on npm.
New tools:
press_key — sends a single key, optionally with modifiers, to a snapshot element or the focused element (Return, Escape, Tab, arrows, ctrl+shift+t, …).
type_text — types text key by key into the focused element, with an optional key to press afterwards.
set_network_cache — bypass or restore the HTTP cache, for the selected tab or browser-wide. Useful before a performance measurement or when verifying a change a cached asset would otherwise hide.
Other changes:
navigate_page and new_page take an optional wait argument (none, interactive, complete), so an agent can wait for the load event before deciding a page is done.
Windows: per-user Firefox installs are now detected, instead of failing with “unable to find binary in default location”.
aarch64 Linux: geckodriver is now resolved on all platforms, fixing “Unable to obtain browser driver” when a native geckodriver is on PATH.
Several Windows path fixes for --output-file, --log-file and saveTo.
For internal Firefox development, swap firefox-devtools-mcp with firefox-devtools-mcp-moz in order to benefit from additional tools, such as chrome-privileged script execution.
The Rust team has published a new point release of Rust, 1.98.1. Rust is a programming language that is empowering everyone to build reliable and efficient software.
If you have a previous version of Rust installed via rustup, getting Rust 1.98.1 is as easy as:
rustup update stable
If you don't have it already, you can get rustup from the appropriate page on our website.
In Rust 1.98.0, in some circumstances, rustc would incorrectly generate a trait object vtable with a null pointer
where a function pointer should be. This leads to undefined behavior in the
emitted code. In some cases this may 'just' cause segfaults due to the null
pointer being loaded, but it is possible for it to be justification for
arbitrary effects (as is typical for UB).
If you'd like to help us out by testing future releases, you might consider
using the beta (rustup default beta) and nightly (rustup default nightly) channels locally and in your CI. Please
report any bugs you
might come across!
Contributors to 1.98.1
Many people came together to create Rust 1.98.1. We couldn't have done it without all of you. Thanks!
The rustup team is happy to announce the release of rustup version 1.29.1.
Rustup is the recommended tool to install Rust, a
programming language that empowers everyone to build reliable and efficient
software.
What's new in rustup 1.29.1
The headlines of this release are:
Concurrency in certain rustup operations has been improved:
When running rustup update, rustup will first check for possible updates in parallel. pr#4752
When running rustup component add with multiple components, they will be installed concurrently. pr#4790
Implicit installation of the active toolchain in rustup-init and rustup invocations has
been deprecated where deemed unnecessary and will now produce a warning. pr#4840
Please see our
blog post
for more details regarding this change.
rustup doc now supports the --serve flag which allows serving the docs over local HTTP.
This should help users with containerized browser and/or rustup setups. pr#4986
Installing i686-pc-windows-* host toolchains on 64-bit Windows now requires --force-non-host.
pr#4935
rustup-init will no longer leave unexpected files on disk after cancelled installations. pr#4996
A bug has been fixed which might cause Windows installation to fail when using rustup-init.sh.
pr#4756
If you have a previous version of rustup installed, getting the new one is as easy as stopping
any programs which may be using rustup (e.g. closing your IDE) and running:
$ rustup self update
Rustup will also automatically update itself at the end of a normal toolchain update:
$ rustup update
If you don't have it already, you can get rustup from the appropriate page on our website.
Rustup's documentation is also available in the rustup book.
Caveats
Rustup releases can come with problems not caused by rustup itself but just due to having a new release.
In particular, anti-malware scanners might block rustup or stop it from creating or copying
files, especially when installing rust-docs which contains many small files.
Issues like this should be automatically resolved in a few weeks when the anti-malware scanners are updated
to be aware of the new rustup release.
Thanks
Thanks again to all the contributors who made this rustup release possible!
Servo 0.5.0 contains all of the changes we landed in July, which came out to 488 commits, and we now publish binaries for Linux aarch64 (@mukilan, #46760)!
If you’re working on a pull request that you think might be interesting for the next monthly update, even if you’re not 100% sure, tell us about it by following the steps below:
You add the monthly update label to your pull request, or comment @servo-highfive monthly update
Highfive posts a comment asking you some questions
You answer those questions in a comment containing @servo-highfive monthly update answer
Most of Gumroad (gumroad.com), except for the landing page, did not render at all in v0.4.0, but as of v0.5.0, pages like the Discover page or this product page render almost perfectly.
The upgrade to Stylo 2026-07-01 brings several changes to built-in CSS functions (@Loirooriol, #46129):
‘alpha()’ is now supported, under --pref layout_css_alpha_color_function_enabled
‘progress()’ is now supported, under --pref layout_css_progress_function_enabled
‘ellipse()’ values ‘closest-corner’ and ‘farthest-corner’ are no longer stable due to spec uncertainty, but they are still experimental, under --pref layout_css_ellipse_corners_enabled
‘attr()’ is more conformant, under --pref layout_css_attr_enabled
WebGPU content can now enjoy better conformance and use GPUExternalTexture and importExternalTexture() on GPUDevice, under --pref dom_webgpu_enabled (@sagudev, #45873, #46178, #46286).
IndexedDB content can now use the name property on IDBIndex, under --pref dom_indexeddb_enabled (@skyz1, #45512).
document.fonts now includes a FontFace for each valid ‘@font-face’, under --pref dom_fontface_enabled (@simonwuelker, #46509, #46537).
All of the features above are enabled in servoshell’s experimental mode.
We’ve started implementing WebVTT for native subtitles and captions, enabled by default (no --pref).
While they don’t render just yet, we can now fetch each <track src>, parse the WebVTT, and expose cues via the track property on HTMLTrackElement (@TimvdLippe, #46289, #46383).
July was a big month for accessibility in Servo, under --pref accessibility_enabled.
The focus for this month has been on performance, with the accessibility tree now supporting incremental updates (@alice, @delan, #45578, #45971, #46589, #46691, #46385), requiring fewer HashMap lookups and tree walks (@alice, @delan, #45798, #46740, #46348), and allowing for faster DOM mutations (@alice, #46348, #46530).
We’ve also started working on the File and Directory Entries API, to allow users to select and upload entire directories via <input type=file> and drag-and-drop.
To that end, we now have webkitGetAsEntry() on DataTransferItem, plus minimal support for FileSystemEntry, FileSystemDirectoryEntry, and FileSystemFileEntry, under --pref dom_entries_api_enabled (@yezhizhen, #46456, #46879, #46832).
servoshell for Android now runs on Android 10+ (91% market share), not just Android 13+ (68% market share), improving adaptability and reducing waste (@jschwe, #46142, #46308).
We’ve also fixed a problem with building for Android on macOS (@jschwe, #46128).
servoshell for Windows is now better behaved when run in a console window, making the command prompt wait until servoshell exits (@yezhizhen, #43010).
When using the Firefox DevTools, the Console tab now supports some basic autocomplete (@freyacodes, #46382).
Inline SVG can now use web fonts defined in the containing page (@yodalee, #45979).
We’re also implementing the SVG DOM, starting with stub interfaces for SVGElement, SVGCircleElement, SVGDefsElement, SVGEllipseElement, SVGLineElement, SVGLinearGradientElement, SVGPathElement, SVGPolygonElement, SVGPolylineElement, SVGRadialGradientElement, SVGStopElement, SVGRectElement, SVGSymbolElement, and SVGUseElement (@mu-mostafa98, #46558).
2D canvas rendering is now multithreaded, improving frame rates by up to 55% and power consumption per frame by up to 42% (@yezhizhen, #46410), and should use a lot less memory too (@jschwe, @sagudev, #46786).
Text rendering is up to 10x faster for cases with the same text and different ‘font-size’ (@Loirooriol, #46129).
Flex layout benchmarks are up to 3% faster, and an improvement to getElementsByClassName() has made some websites up to 1% faster (@Narfinger, @jdm, #46563, #46595, #46594).
Interested in helping build a web browser?
Take a look at our curated list of issues that are good for new contributors!
Donations
Thanks again for your generous support!
We are now receiving 7824 USD/month (+1.8% from June) in recurring donations.
This helps us cover the cost of our speedyCIandbenchmarkingservers, 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 35 GitHub users (same as June) 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.
Unified builds (also know as Jumbo Builds) is a build techniques that aims at
improving build time through the concatenation of several sources as a single
unified source before compilation.
The goal is obtained through implicit caching of header instantiation, although it
implies a trade-off with parallelism.
Let's illustrate this behavior through a simple example, two codes that implement variation of the same approach:
/* algo0.cpp */#include<iostream>#include<string>#include<vector>voidtranslate(std::vector<std::string>&w,void(&t)(std::string&));voidtranslate(std::vector<std::string>&w_out,std::vector<std::string>const&w_in,void(&t)(std::string&)){std::cout<<"[log] through transform\n";w_out=w_in;translate(w_out,t);}/* algo1.cpp */#include<algorithm>#include<iostream>#include<string>#include<vector>voidtranslate(std::vector<std::string>&w,void(&t)(std::string&)){std::cout<<"[log] through for_each\n";std::for_each(w.begin(),w.end(),[&t](std::string&s){t(s);});}
Compiling individual files take the following times:
In that simple case, the weight of headers with respect to actual user code is such that
compilation of the unified file takes almost the same time as the max compilation time among each individual file. That's roughly a 1.97x speedup on compilation time.
That's the promise given by unified builds. And it's a promise held.
Now let's have a look at the consequences of that deal.
Beforehand, we still need to introduce another parameter tied to unified builds: the
unification parameter, say P. That parameter bounds the number of files
that are unified together. Let's imagine we have a hundred of individual source
files compiled with exactly the same compilation flags. Setting P to 5
leads to the generation of 20 unified sources compiled independently.
Remember the parameter P.
Quality of the Generated Code
Let's create a shared object from algo{0,1}.o (this implies a recompilation with -fPIC of the sources):
After stripping, comparing the size of the binaries yield a difference of a few bytes. After disassembling, it turns out the compiler decides to inline the call to void translate(std::vector<std::string>& w, void (&t)(std::string&)) from algo0.cpp when compiling the unified source, something the compiler cannot do when doing split compilation, as it does not know anything about the implementation of that function.
Interestingly, compiling with -flto=thin still lead the compiler instantiation through different optimization path.
Falling back to -flto=full finally yields to the same shared object, which makes sense because Full LTO is very close to performing source unification at the bytecode level and our sources are very simple. It's not a given though because the actual optimisation pipeline is still different in the two scenario.
Why does it matter? Depending on the value of P, the compiler will see
different sets of files per unified file, which will result in different binary
code. It's actually even worse: depending on the way we fill those unification
sets, event with the same parameter P, we end up with different binaries.
Let's call that the reunifying problem.
Even if we have an algorithm that seems to guarantee reproducibility, for
instance working on a sorted list of files with a fixed P, variation can
arise: the introduction of a new source file can lead to changes in every
unified file (e.g. if the split is done by chunks and the new file ends up at
the beginning of the file list).
So unified builds tend to improve performance, but they do not interact in a gentle way with performance reproducibility.
Recompilation Times
Let's denote S as the number of sources and C as the number of CPUs.
Intuitively, setting P=1 yields to the faster recompilation time when a
single file is touched---a usual scenario when developing a new feature.
On the opposite, setting P=S yields to the slower recompilation time (if S >>
C!) under the same scenario as all sources are recompiled under that
scenario.
The form of the curve between those two extreme varies depending on the nature
of the files, and the amount of header sharing between individual sources.
Caching tools like sccache is impacted by the same mechanism: as P gets
greater, more cache misses are hit and more recompilation are done.
Marginally, introducing a new source also pollutes the cache or triggers
recompilation for the unified source it gets added to, and eventually for all
the unified sources derived from the associated file list. The reunifying problem strikes again.
So unified build make compilation faster, but recompilation slower. Setting
P to an acceptable value is important depending on the usage scenario.
Correctness
Unified build changing the compilation unit frontier, which in turns modifies
the semantic of the program. This change can be straight-forward or complex to
debug, and even remain silent. I've listed a few instances of the two first
categories below, and a crafted one for the latter category.
Macro / Symbol Redefinition
This one is trivial to spot (a preprocessor-warning is issued for the macro,
and a compiler error is issued for the symbol redefinition):
/* pi0.cpp */#define PI 3.141593constexprdoublepi(){return3.141593;}/* pi1.cpp */#define PI 3.14159265constexprdoublepi(){return3.14159265;}
The solution usually lies in moving the definition in a shared header, moving the declaration in a shared header and the definition in a single file, or renaming identifiers to avoid the name conflict. Note that depending on the solution we may change the visibility of the symbols, or impact code readability (assuming the identifier name was perfectly chosen in the first place).
Overload Conflicts
This one is also trivial to spot and may hint toward debatable design. But it exists and may be more complex to understand than the above:
The fix is generally to provide a perfect match for the overload, change the call site to avoid the ambiguity, or rename the functions/change their namespace to make the call site explicit.
Using Namespace Confusion
This one tends to creep a lot in codebase where using namespace is used. It generates ambiguity among potential symbols.
A caricatured situation is exhibited with the following situation:
The problem with that category is that the fix is quite unsatisfying: there is no way to limit the scope of a using directive, removing using directive can lead to very verbose codebase, renaming symbols to avoid conflicts goes against the very purpose of namespaces...
Delicatessen
I spent a lot of time nailing that one down, so I wrote a small reproducer to illustrate the problem.
Let me comment that layout a bit: We basically have two different classes named Lock: one lives in the mozilla::dom namespace, and one lives at top-level. In header1.h, although we include the definition of mozilla::dom::Lock, we also get the definition of ::Lock, so a straight reference to Lock is not ambiguous.
Concerning source files, src0.cpp, src1.cpp and src3.cpp just include headers while src2.cpp contains the infamous using namespace modilla::dom; statement.
Let's now consider various partition of the file list src0.cpp, src1.cpp, src2.cpp, src3.cpp:
Isn't that amazing? Some intermediate unification, namely 0,1;2,3 and 0;1,2,3 fail, but other unifications, namely 0,1,2,3 and 0;1,2,3 fail. Did you notice that both non-unified and full unified build succeeds, while some intermediate unification fail? What a disaster. This basically mean that given a set of sources, and without putting restriction on the language (like banning using statement), the only way to be sure that a unified build always succeeds whatever the chosen partition is to test every partition. Not very satisfying.
As a side effect, we can also deduce that adding a new source file to a set of files to be unified can break compilation in files that used to compile fine. That's another instance of the reunifying problem.
Changing Semantic
It is quite easy to derive from the above an example whose semantic change once unified. Let's slightly change the overload conflict example from above:
When compiled independently, this results in a binary that prints a 0 and a 1 on the screen. But when compiled as a unified source, we only get a pair of 0.
Concluding Words
Remember that discussion between Luke and Yoda?
LUKE
Vader. Is the dark side stronger?
YODA
No… no… no. Quicker, easier, more seductive.
That's exactly my thoughts on unified builds: they give you quick wins in
term of cold build speed and give faster builds. That's very good
properties, and you rip the benefit of them very quickly. Then you realize
that you're tied to a monster in terms of maintainability and developer
experience, but you're already addict to the speed it gave you.
While using guest wifi during travels, I was trying to fetch some changes from a github remote and my git fetch is over SSH which happens over port 22. Most (TCP?) ports are blocked on public or hotel wifi, so with some help1 I learnt that Github serves SSH over 443 too.
While working on Writing the Docs: 2026 Edition,
I experimented a bit with updating the Glean Dictionary to
incorporate some of the ideas in the essay, in particular those around reference documentation.
As a sort of demonstration that documentation about an application can be
treated as a data product in and of itself, I hacked up a quick ETL pipeline to dump the JSON output of the Glean Dictionary into a DuckDB database that can be queried standalone.
Since the Glean Dictionary is just a static Netlify site, the DuckDB database can simply
be published alongside as a standard file.
This allows you to do fun things from the DuckDB console like:
From there, you could either build an interface like the Glean Dictionary itself (much easier to do from
a database than the somewhat harebrained dataclass soup I originally came up with) or any number
of other data products or analyses. Here's an example marimo notebook that tracks the metrics added
to each product over time:
As you can see, the number of metrics did a big jump in 2024 (I imagine they finally moved Firefox Desktop over to Glean) with slower and steadier growth since.
There's a popular post going around about Alibaba doing WebAudio fingerprinting and in the process, mucking around with the author's Bluetooth headphones. Here's the tl;dr: browser fingerprinting is a far-too-pervasive method of tracking users across the web, but at least for WebAudio specifically, it's not very effective. Firefox has largely eliminated this fingerprinting vector.
I work on Firefox. Chrome, Brave, Safari probably have defenses against this. I'm pretty sure Chrome's WebAudio code was made approximately constant (like ours) many years ago. It probably still leaks CPU architecture though.
If you want to test Alibaba's WebAudio fingerprint, I had claude extract it into a standalone page. My value is sha256:9a388c0dd04cfdc54314f9d961c7e2d247b972067e28d1cea76bd6060cf1392e, and the other value we saw in an internal survey was sha256:16d3191880ce01f726015ec6a1f9a072a81ebd04bf489098d4685d1d1c0b2711.
99.24% of users belong to one of three values, and .76% of users had that data collection point fail (a value of zero.) So virtually all users have one of these three values but why are there three values??? Why isn't there one?? Well this is why we collected this telemetry - to ensure we really had made things constant the way we thought we had, and if we hadn't, figure out why and fix it.
The improvements we put into 118 eliminated a lot of differences and I wish I had a graph I could show you of what it looked like before (but I don't.) But what about these remaining three buckets? We were able to figure out that the differences between these are CPU-level differences, which is not surprising given the math operations of audio processing. Specifically one value comes from (all) x86 CPUs and x64 CPUs that lack fused multiply-add instructions, one value from x64 with FMA, and one value from CPUs with the NEON instruction set (ARM).
We followed up on that effort to collapse these buckets into one another. Bug 2036977 collapses the x64 (with FMA3) into the x86/x64 (without FMA3) bucket. Bug 2040494 is on file to collapse the remaining bucket into the NEON bucket but I haven't worked on it in a while because it's just not as high a priority as larger improvements we can make like Sanitizing the WebGL Renderer and Vendor (something I talked a bit about a few months ago).
Because if you've read this far you surely care about the rest of the graph also. There is also the long-tail of 23 other values belonging to 48 other users. This is very unfortunate, as it makes these users completely unique, but it is also not terribly unusual - computers are weird and these results could have been caused by bad RAM, a CPU bug, or possibly some crazy architecture (LoongArch??).
But at the end of the day, WebAudio fingerprinting is nearly useless. I don't expect browser fingerprinting to disappear from websites entirely (unless some regulatory action occurs, fingers crossed) - it's still going to be effective against a majority of users on the web, but at least for privacy focused browsers, it should be wildly less effective.
A bug comes in, or someone shows me a page doing something surprising, and the question is always the same one. Is that what the spec says? Not what we all assume it says, and not what Gecko happens to do, but what the algorithm actually does when you follow it step by step. So I open html.spec.whatwg.org, find the entry point, and start reading. Twenty tabs later I have usually lost the thread.
This isn’t an occasional chore. I do it when I’m implementing something new, when I’m working out whether a Gecko bug is a bug in Gecko or a bug in the spec, when I’m reviewing a spec change someone else has proposed, and when I’m trying to design something that has to fit alongside everything that already exists. Four fairly different jobs, and every one of them comes down to the same mechanical work. Start at an algorithm, follow the calls, keep track of where you’ve been.
The specs are built for exactly this, in principle. Every step that invokes another algorithm is a hyperlink, and the whole thing is one large call graph spread across HTML, DOM, Fetch, URL and Infra. The trouble is that walking that graph by hand is slow, and it’s the kind of slow a machine should be doing on your behalf.
Two tools have changed how I do this. One answers questions about the graph, and the other helps me walk it. If you’d rather install them first and read afterwards, they’re both at the end.
webspec-index trace
webspec-index is Jan Jaeschke’s tool for querying web specifications from the command line. It indexes the specs, on demand as it needs them, and lets you ask about definitions, references and IDL. In August it gained a trace subcommand, and that’s the one I keep reaching for.
You give it two anchors and it finds the routes between them. It answers in JSON by default, which is the right choice for feeding it into something else but not for reading, so add --format markdown when the audience is you. Say I want to know how a navigation can end up firing navigateerror:
# trace: `HTML#navigate` -> `HTML#event-navigateerror`
Max depth 6, kind `step`. Found 11 trace(s).
## Trace 1 (5 hop(s))
1) [`HTML#navigate` step 20](…#beginning-navigation:set-the-ongoing-navigation)
calls `HTML#set-the-ongoing-navigation`
> Set the ongoing navigation for navigable to navigationId.
2) [`HTML#set-the-ongoing-navigation` step 2](…#aborting-navigation:inform-the-…)
calls `HTML#inform-the-navigation-api-about-aborting-navigation`
> Inform the navigation API about aborting navigation given navigable.
3) [`HTML#inform-the-navigation-api-…` step 3.1](…#ongoing-navigation-tracking:…)
calls `HTML#abort-the-ongoing-navigation`
- under: While navigation's ongoing navigate event is not null:
> Abort the ongoing navigation given navigation.
4) [`HTML#abort-the-ongoing-navigation` step 7](…#ongoing-navigation-tracking:…)
calls `HTML#abort-a-navigateevent`
> Abort event given error.
5) `HTML#abort-a-navigateevent` step 6 calls `HTML#event-navigateerror`
> Fire an event named navigateerror at navigation using ErrorEvent, with
additional attributes initialized according to errorInfo.
I’ve wrapped those lines and shortened the URLs to fit; in the real output each hop is one long line and every link is a full deep link to that exact call site.
Every hop names the exact step number, quotes the step text, and tells you the condition it sits under. That last part matters more than it looks. Step 3.1 only runs while the ongoing navigate event is non-null, and knowing that is the difference between a path that exists on paper and a path your case actually took.
Eleven routes, though1. Reading eleven of these back to back is not much better than the twenty tabs.
Three zoom levels
This is where webspec-index trace gets genuinely nice, and it’s the part I want to draw attention to, because it’s easy to miss in the help text. The --detail flag changes how much of each route you get, and the three levels turn out to be three different ways of working rather than just three verbosity settings.
Ask the same question again with --detail edges and the prose falls away, leaving the skeleton:
Now you can read all eleven at once, and a shape appears that I could not see in the verbose output at all. There are five places inside navigate where a route starts:
There are exactly two ways into that step, from HTML#abort-the-ongoing-navigation step 7 and from HTML#process-navigate-event-handler-failure step 5. Eleven routes exist because three of the five entry points re-enter the same navigate event firing machinery from different directions, not because there are eleven genuinely different things going on.
That’s a useful thing to know before you start debugging. If a navigation is firing navigateerror and you want to find out why, you don’t instrument eleven places. You instrument the one step they all pass through and work backwards from there.
So the way I use it now is to start at --detail edges to find the shape, then drop back to the default once I know which route I care about and want to read the reasoning. The third level, --detail compact, drops the prose but keeps every anchor as a deep link to the exact call site, which is the form you want when you’re pasting a route into a bug report for someone else to follow.
WebSpec Tracer
webspec-index trace answers a specific kind of question, and it’s worth being precise about which kind. You have to know both ends. That’s fine when you’re asking whether one thing can reach another, but a lot of the time I only know where I’m standing. I have a starting point, a bug in front of me, and no idea yet where the algorithm goes.
There’s also something webspec-index trace can’t do for you in principle. It finds routes through the spec text, statically. It doesn’t know which branch your case took, because that depends on what actually happened at runtime, and only you have that information.
So I wrote WebSpec Tracer, a Firefox extension that walks the specs with you rather than ahead of you. You give it a starting anchor and it shows you that algorithm with every linked term highlighted. Click one and it becomes the next hop in the trace, and the panel moves you into it. Click the wrong one and Back undoes it.
The screenshot is a walk that started at location.assign and has arrived inside navigate. Step 15 is the interesting one. It runs only if four things are all true, that there’s no document resource, no response, the URL matches the current entry’s URL ignoring fragments, and the fragment is non-null. If all four hold, it navigates to a fragment and returns. Otherwise the algorithm keeps going into the full navigation path below.
No tool can decide that for me. Whether my bug went left or right at step 15 depends on the page and on what the user did. But the extension puts the decision in front of me with the alternatives visible, which is precisely what I was failing to do for myself with twenty tabs open.
The trace builds up in the panel underneath as markdown, with every anchor pointing at the exact call site. That box is a display rather than a text field, so there are buttons for getting the trace out of it. Copy does what you’d expect. Insert is the one I actually use, and it’s the reason the extension lives in the sidebar: it writes the markdown straight into whichever field you have focused on the page, so you can have a spec issue open next to the panel and drop the trace into it without going near the clipboard. Reset clears everything and starts over.
Switch the issue over to Preview and there are the same two hops, rendered, each one a link into the spec. That’s most of the reason I built the thing. Explaining a route through an algorithm in prose is miserable, and the version you retype by hand is always slightly wrong. This way what I hand over is exactly what I walked.
One more thing worth knowing, about the anchors themselves. The starting one has to say which spec it’s in, so HTML#dom-location-assign rather than just dom-location-assign, but after that the extension works it out as it goes. Anything that turns up in the trace without a prefix is in the spec you started from, and a prefix appears when the trace crosses a boundary:
That’s the same walk carried a few hops further. Hop 3 is that step 15 decision, taken, and the last line is WEBIDL#wait-for-all. Somewhere in the navigate event firing the trace left HTML entirely, and the prefix is the only thing that tells me so. The panel on the left has stopped showing me HTML too, and is now sitting in WebIDL. That’s easy to lose track of when you’re clicking through by hand, and it’s usually the moment a question stops being about HTML and starts being about something else.
When one call happens twelve times
Sometimes an algorithm invokes another one from many different places, and a trace has to say something sensible about that. This was in the design from the start, and the answer it arrived at is to do what the spec already does. Start at HTML#update-the-image-data and follow it to HTML#abort-the-image-request:
HTML#abort-the-image-request is invoked from twelve separate places inside HTML#update-the-image-data. The extension doesn’t make me choose one, and it doesn’t flatten them into a single link either. It records the hop once and hangs all twelve call sites off it as numbered links, so the line reads #update-the-image-data calls #abort-the-image-request, [2], [3] and onwards to [12], each number pointing at its own anchor.
The numbering isn’t invented. It’s the spec’s own scheme for repeated references: the anchors run #abort-the-image-request, then -2, -3, and so on up to -12. All the trace does is carry that through, so a hop in the output identifies call sites the same way the spec identifies them, and the numbers mean the same thing in both places.
That also keeps the trace honest. The hop happened, and that’s true whichever of the twelve fired, but the fact that there are twelve is worth knowing on its own, and anyone reading can go and check any one of them. Writing this out by hand you would link the first and lose the other eleven without ever noticing. The same thing is visible in the previous screenshot, where hop 3 reads #navigate calls #navigate-fragid, [2], [3].
webspec-index trace deals with the same situation the other way round. Ask it the same question and you get twelve separate one hop traces, each naming its own step number, step 2, then 7.4.2, then 11.1, and so on down the algorithm. Neither is wrong. The command line is telling you which steps, and the extension is keeping your trace to one line while preserving every place it could have come from.
Which one, when
They divide up cleanly enough that I don’t think about it much any more. If I know both ends and want to know what’s between them, or how many different routes there are, that’s webspec-index trace. If I know where I’m starting and want to find out where it goes, that’s the extension.
The one I underused at first was --detail edges. Asking a broad question and then zooming out far enough to see the answer’s shape is a different move from following a single path carefully, and it’s the one that has surprised me most often, usually by showing me that several things I thought were separate all funnel through the same step.
Getting them
webspec-index is on crates.io:
cargo binstall webspec-index
It builds its index as it goes, so the first trace into a corner you haven’t visited before pauses briefly to fetch and index the specs the route touches. After that it’s fast.
WebSpec Tracer is on addons.mozilla.org, and the source is on GitHub. It lives in the sidebar, toggled with Ctrl+Shift+U, so you can keep it open next to whatever you’re reading.
Both are worth a try if you spend any time at all reading specs, whether you’re implementing them, arguing with them, or just trying to find out whether the browser is wrong or you are.
Notes
Eleven is what you get at the default maximum depth of six hops. Raise it and there are more. ↩
I don't think I ever wrote about it explicitly, but in the background I (along with a few others at Mozilla Data) were digging deep into "proposal culture" and how to collaboratively build software together using design documentation.
Thinking hard about knowledge systems is a somewhat idiosyncratic obsession, and something I emphasize a little less day-to-day now. That said, I still think that putting effort into organizing information and making it legible both leads to better workplaces and makes us generally more productive. Fundamentally, it’s an act of care both to yourself and your peers, past and present.
Moreover, the core points I was trying to make at the time (either in what I wrote or what I did) mostly still hold:
Separate the "what" from the "how"
Reference documentation should derive from ground truth
Document your decisions and process
Continuous improvement
As a sort of retrospective, I'd like to revisit these principles 5 years on, with special attention to how they relate to large language models (LLMs).
I’m going to talk about Mozilla a lot because it’s all on the public record and I can talk about it relatively freely. I haven’t really been involved in the community or watching closely what’s been happening there since I left in January 2022. Likely the internal perspective at Mozilla these days is different from my own (hopefully not that different 😂).
Separate the what from the how
This is what I talked about in A principled reorganization of docs.telemetry.mozilla.org. While I wasn’t aware of it at the time, I was basically repeating the principles of the diataxis framework. Since then, diataxis has become my go-to for describing good documentation, because it's so well written and explained.[1]
diataxis breaks documentation down into four categories:
Tutorials: How to do a basic task, step by step: intended for beginners
Explanation: Longer documents describing the "why": intended for all
How-to: How to accomplish a specific task: intended for non-beginners
Reference: Ground truth establishing the "what": intended for all
I think this framework holds up really well, and it’s still the first thing I reach for when thinking about (or explaining) how to write something. Although diataxis was never fully applied, docs.telemetry.mozilla.org became a more useful site for data practitioners after I emphasized tutorials and "getting started" docs, and put the reference and deeper dives later on. It was pretty frequent that I'd be able to answer a question on our internal slack channel and be able to quote a step-by-step link from the docs describing exactly what someone needed to do or know.
My one quibble with diataxis is that sometimes for a complex task you need to mix the different types of documentation. Visualizing Percentiles of a Main Ping Exponential Histogram was written as a very long and sprawling mix of explanation with theory (even the name is a mouthful), but I think that’s one of its strengths? I got repeated feedback that it was extremely helpful by the Firefox engineers. I can't be totally certain it still holds up today, but I'm fairly certain it does. At the very least, no one has felt motivated to take it down.
Do LLMs change anything here? I would argue no. Although some of the things we want to do now are different (and still changing as of this writing), each has proven useful in its own way.
Tutorials and explanations are the easiest case to make: they are meant for general sense making, and the need for that hasn't gone away at all. Indeed I think as we delegate more of the day-to-day work to machines and reduce the amount of hands-on work which generates understanding, our need for these aids has only increased. I occasionally use LLMs to create initial drafts of this type of work and it’s interesting to see what they miss or get wrong: often some vital fact or nuance is not there, and they have a tendency to emphasize incidental details.
One thing that many people find surprising about tutorials is that they were actually never really meant to describe how to do something. Instead, the purpose is primarily just to give some hands-on experience and feeling of success, so they can start feeling more confident in going deeper and doing the more advanced tasks where explanation, how-to, and reference become more important. This need to gain a preliminary understanding of the system has not gone away at all with LLMs-- I maintain that hands-on learning and action is still the best way for most people to learn anything, including how to work with computational systems. Once you've gone beyond the basics, any gaps in understanding or context can be filled in by absorbing the content of deeper articles describing the concepts of a system and why it is the way it is.
Howto documentation (sometimes also called cookbooks or runbooks) are an interesting case, as I feel like this is where things have simultaneously changed the most and the least. Back in the day, I felt that most human written documentation should be exactly this type of thing: it was the most broadly useful and tended to age much better than explanation or reference material that would frequently go out of date. Being able to answer a slack question with a link to this type of material was always satisfying.
Of course now, often it's a large language model or similar executing these types of tasks. Early in 2026, the hot topic was creating Agent Skills which are, at heart, just little markdown docs named SKILL.md which describe how to perform a particular task. I think they are entirely complementary to the approach. You write a detailed howto describing how to perform a task (using Mozilla data as an example, query a table in BigQuery). An LLM can process a well-written markdown file just as easily as a human: just create an appropriate skill file saying as such:
---
name: query-main-ping-telemetry-table*
description: Query the main ping telemetry table in Bigquery* ---
Refer to `docs/howtos/query-telemetry-table-in-bigquery.md`
I'm often surprised at how quickly modern large language models can figure out how to do something essentially from first principles, but if you've arrived at a good solution already, why bother doing that? Just give them a cookbook and save the time and the tokens. And if you need to debug what it's doing (or do part of it yourself), the exact set of steps is right there.
Reference documentation deserves its own treatment. More on it in the next section.
Reference documentation should derive from ground truth
Up until now, our defacto answer has been some combination of “Use the probe dictionary / telemetry.mozilla.org” and/or “refer to docs.telemetry.mozilla.org”. I submit that we’re at the point where these approaches break down: as mentioned above, there are many more types of data we now need to care about than just “probes” (or “metrics”, in Glean-parlance). When we just cared about the main ping, we could write dataset documentation for its recommended access point (main_summary) and the raw number of derived datasets was manageable. But in this new world, where we have N applications times M ping types, the number of canonical ping tables are now so many that documenting them all on docs.telemetry.mozilla.org no longer makes sense.
I was already thinking about the Glean Dictionary and trying to justify working on it to my peers and upper management. In retrospect, this claim is self-evident: if you're dealing with hundreds or thousands of entities (or more), a manual approach to writing the documentation just isn't going to work. Even if you can generate a perfect first draft, you've only postponed the problem: the implementation will change, and independently maintained reference documentation will drift from it.
The solution is to make your reference documentation derive from the actual truth of the thing that you're trying to document. This means some kind of machine readable representation: the source code, metrics definitions, dataset definitions, or similar. Critically, the reference documentation should directly link back to that original source, so you can verify (and examine) where these ground truth assertions are coming from. This is why the Glean Dictionary included a link back to the original schema or artifact which generated the documentation:
This is a pretty old idea, dating back to Sphinx’s autodoc and doxygen, although it hasn’t been super consistently applied in modern data engineering from 2020 to 2022 or so. These days, we tend to emphasize getting to an answer (“How many users did XYZ?”) over how the answer was derived. Mozilla was a bit of an exception in that it had (hopefully still has) a lot of curious engineers who really did care about these details.
When using an LLM, I feel like this type of ground truth is more critical than ever. Much has been written about the tendency of large language models to hallucinate. Conclusions drawn from inaccurate source data can compound, leading to yet more confusion and problems that only get caught at review time (if you're lucky). Having a clear statement of the system-as-it-currently-is (with citations) is the best defense against that. Not only does it reduce the chance of hallucination or confabulation, but it gives a human operator the chance to check the reference and see if it makes sense.
What was maybe a little more controversial (and possibly still is), was my conviction that writing a custom website was the right answer. The world is awash in SaaS companies offering to make sense of a data schema, service architecture, what have you-- why not just use those? At the time I wrote that article, we were considering Google's data catalog and I had to justify why creating a new thing wasn’t Not Invented Here:
A few months ago, I thought that Google’s Data Catalog (billed as offering “a unified view of all your datasets”) might provide a solution, but on further examination it only solves part of the problem: it provides only a view on your BigQuery tables and it isn’t designed to provide detailed information on the domain objects we care about (products, pings, measures, and tools). You can map some of the properties from these objects onto the tables (e.g. adding a probe’s description field to the column representing it in the BigQuery table), but Data Catalog's interface to surfacing and filtering through this information is rather slow and clumsy and requires detailed knowledge of how these higher level concepts relate to BigQuery primitives.
I haven't looked at Google's Data Catalog (or whatever it’s called now) since then, but I suspect that although it’s probably a great schema viewer, it's still going to be just as confusing for someone who doesn’t work as a data engineer day to day.
It's just inherent: unless your product is a database, the domain of a generic data catalog (schemas, tables) is going to be inherently different than that of your business. This is true whether what you care about is the experience of Firefox users or dispatching taxi cabs. Forcing a data user to think in terms of the former is to move them away from asking actual product questions.
The same really goes for any other "data catalog" SaaS product: in order to work across industries, it needs to express a bunch of concepts which are going to be foreign to your domain. If you're going to go to the trouble of mapping your own concepts and systems to the model of a product like this (which is honestly 90% of the effort, in my experience), putting in a little bit of extra effort to produce a custom interface or website might actually be the missing piece to make that work maximally impactful. This is even easier today thanks to large language models and generally evolving technology and practices.
One area where I do think there is still room for development and growth is infrastructure to create these custom tools and data representations. Partly based on my experiences with the Glean Dictionary (and associated tools like probe-scraper and mozilla-schema-generator), I've been working on a new framework called repo-parser which formalizes this concept of extracting this “ground truth”.
Given a git repository and a set of rules (e.g. “convert this set of markdown frontmatter into a set of tags”), it can take a forest of markdown, yaml, and json and produce either a duckdb database or an in-memory representation of the repository’s content. You can use this to populate a data catalog (like the Glean Dictionary), create a dedicated documentation site or any number of other things.
repo-parser’s example site is a mundane example of this. The content in the screenshot is produced from a README.md inside the repository, collected and organized dynamically at build-time:
I'm not certain if repo-parser is exactly the right abstraction for building something like this, or if it's more
like an architectural design pattern where every implementation is bespoke. Time will tell.
Proposals, architectural decision records
Ground truth can tell us what, but doesn’t say much about why. When I introduced the afore-mentioned dictionary product in The Glean Dictionary, embedded within was a link to a Google docs proposal.
This was part of a larger movement towards "design docs" that myself and a few of my coworkers were pushing at the time. The idea was, before embarking on a large piece of work, you would write and circulate a large Google Doc containing a problem statement, goals, non-goals, and a preliminary expression of what you were trying to solve and alternatives considered. Then, discussion could proceed asynchronously over docs comments.
I didn't really write a lot about them because (I think) my coworkers wrote and presented on them extremely effectively and I was more interested in, well, the above. But I think they're just as valuable a kind of writing, they answer so many important product and engineering questions:
Why this design?
Why now? (or why then, if looking at the proposal retroactively)
How does this impact security, reliability, ... ?
What comes next?
I still think proposals are extremely valuable. The obvious benefit is they allow us to reject or modify sub-optimal designs before too much effort is spent building and/or supporting them. But just as importantly, they allow us to distribute understanding of what is being built and why across teams.[2] In software engineering, it’s one of the best ways I’ve found to align a diverse group of human beings around a single objective.
This wisdom was pretty old even then: the influential article on Design Docs at Google dates all the way back to 2015, best I can tell. There are other precursors -- for example, Painless Functional Specifications by Joel Spolsky way back from the year 2000. I'm sure you can find articles about this all the back to the 70s, or even earlier if you look hard enough. However, I've noticed that many organizations struggle to have a good proposal culture. If the ideas are so obvious, why?
Some issues go well beyond the scope of this article: I do think that having an effective proposal requires a strong commitment to psychological safety. If people don't feel like their design or feedback is going to be taken in good faith, the proposal process won't work very well. People will be hesitant to put things out there and to engage, and the opportunity for learning will be reduced. In a healthy workplace, people are constantly being guided towards presenting their best ideas, with the knowledge that their colleagues are there to support them.
Looking at how large language models have affected things, I have two answers: "almost not at all" and “totally".
For the traditional "big google doc" type proposals I talk about above, I lean towards thinking that these should continue to be used (more or less) the way they were before: as a means for human beings to align on the right solution to a problem. A large language model can help with the initial brainstorming producing the first draft, but to be maximally effective both the initial communication and response should be the product of human interaction. That ensures that the proposal is an expression of a fully fleshed out train of thought and represents real effort. Likewise, the responses to it represent a real attempt to engage with and learn from the content.
On the other hand, I think for our day-to-day where the implementation is largely produced by a large language model, I have found the above formula useful in an unexpected way: as a way to converse and convey requirements to a machine.
These days, I spend more of my time prompting an LLM than writing code myself. But since they have the memory of a goldfish, I’ve found getting them to produce the right thing a bit of a crapshoot. Yes, there is AGENTS.md but that can only tell it so much: there’s always more “implicit” context that is needed to solve a problem of any complexity. And even with extremely long context, important facts can be missed or given too little weight.
Since I found that I was encoding these details in a prompt, I thought: hey, why not just make the prompt a design doc? Have a human express the intent, goals, non-goals and a sketch of a solution in a markdown document and then collaborate with an LLM on the detailed design. Then, once you’re happy with the result, proceed with implementation. The result can then be checked in along with the code as a sort of guide to understanding what was done. In a pull request type work flow, it can be critiqued or commented on like any other code artifact.
If you’re familiar with plan mode, a lot of this might sound familiar. The key difference is a fairly deliberate structure (explicit dating and proposal template) and a higher level of human involvement in the earlier stages of design. To incorporate some notion of chronology (since IDRs are meant to be “moments in time”), I used a filename convention inspired by database migration tools like alembic and goose (for example: 202601180308-upload-packages-to-pypi.md) and a design template that should be familiar if you’ve read Design Docs at Google:
# 2026-01-18: Upload packages to PyPI
Owner: Will Lachance <wlach@protonmail.com>
## Overview### Problem Statement
Save maintainer time and avoid security gotchas by using github actions to upload to pypi
### Context (as needed)
This repo ships a Rust CLI as a Python package via \`maturin\` (\`bindings \= "bin"\`). CI already builds wheels on Linux/macOS/Windows and runs a smoke test from the built wheel.
### Goals-Make it easier to publish new releases
-Improve security
### Non-Goals-Automate version bumping (will still entail a seperate commit to bump version)
### Proposed Solution
…
I've found this a nice additional specialization of proposals, and have found it really helpful in my own work. If you're curious about this, check out the idr-tools repository, where I've been exploring this idea further. You can see a few samples of the idea both there and in repo-parser (in the idrs subdirectory).
Always be improving
One through-line which is in the above work which I didn't blog about explicitly: always be improving. Typos, fixing bitrotted instructions, little improvements in clarity: over time this type of enhancement ensures that the documentation is maximally useful both to humans and machines.
You can use an LLM to do this automatically, but obviously you want a human being to review the output. Either way, the end result is the same: better end-user documentation and an (infectious) demonstration of actual care. It’s a way of supporting others, including your future self. After some time, repeated small actions become a habit, others follow along and it fades into the background as “the way we do things around here”.
Conclusion
Although I wouldn't write the above articles in the same way (or possibly even write them at all) today, I do think the conclusions hold up-- in some cases even more strongly-- in the new, strange world of large language models. So long as there are complex systems, I don't think describing them in forms that are meant to explain will go out of style. If anything, it becomes more powerful and needed at all stages of the design and build process.
Sometimes people talk about the divio documentation system, but as I understand it, it’s just a precursor to diataxis (created by the same author). ↩︎
I believe the same is true of code review, but that’s another story. ↩︎
We have tokens that are added to official builds when a CI server is building the production-ready application. These tokens are added to the BuildConfig.java by using the buildConfigField function in a build.gradle.
While testing, you want to quickly regenerate just this file to see changes occur with a new value. Instead of using the more expensive assembleDebug task for your debug variant, you can use generateDebugBuildConfig.
In the Firefox monorepo world for the Fenix module, the fully-qualified task name would look like this:
Lately I’ve been thinking about cyclic trait implementations. This is a problem that I’ve been trying to understand for years and years and I finally feel like I’m geting somewhere. I’m going to try to write out a series of blog posts documenting those explorations and, hopefully, culminating in a design that could be RFC’d. In this first post, I want to talk about one of the interesting questions, what I am going to call “internal” vs “external” proofs. I know that this material can seem abstract, so I’m going to try and connect it to “real Rust” as much as possible! This particular blog post is an introduction, explaining the general problem and giving some motivation for why we care.
What are cyclic trait implementations?
Right now in Rust we require most traits to have non-cyclic, or inductive, implementations. To explain what I mean, let’s consider this trait:
traitDump{fndump(&self);}
Now imagine that we have an impl of this for i32:
// Impl I
implDumpfori32{fndump(&self){println!("{self}");}}
and finally a recursive List type that has an impl as well:
structList<T>{value: Rc<T>,next: Option<Rc<List<T>>>,}// Impl L
impl<T>DumpforList<T>whereT: Dump,{fndump(&self){Dump::dump(&self.value);ifletSome(n)=&self.next{Dump::dump(n);}}}
If I try to show that List<i32>: Dump, I do that by
Applying “impl L” to show that List<i32>: Dump if i32: Dump
Then applying “impl I” to show that i32: Dump
There’s no cycle here – that is, I didn’t have to use impl L to show that impl L is valid.
Cyclic logic sounds bad, but it can be exactly what you want
Now, when I said that “the impl L didn’t have to use the impl L to show that it is valid” that might not have sounded suspicious to you. In fact, it’s a pretty natural idea. After all, generally when you try to establish a logical argument, you aren’t allowed to use cyclic reasoning. That is, you can’t say: I know that Niko likes Rust because Niko likes Rust. So, in the same sense, it seems natural that I should not be able to say “I know that List<i32> implements Dump because List<i32> implements Dump”.
But actually, it would sometimes be really useful to say exactly that. One example is so-called “perfect derive”. In our Dump impl above, we had one where-clause, T: Dump. And if you were to create a custom derive for Dump and write #[derive(Dump)], the impl I showed is typically exactly what you would get. But it’s not necessarily what you want. Consider what you get with #[derive(Clone)]:
// Impl LC1
impl<T>CloneforList<T>whereT: Clone,// <-- generated but not really required!
{fndump(&self){List{value: Clone::clone(&self.value),next: Clone::clone(&self.next),}}}
Here, the derive is going to create an impl that requires T: Clone. But if you look closely, you’ll see that all the fields only use Rc<T>, so in fact, we should be able to clone a List even without T: Clone! But how is the compiler to know this?
You might think that the compiler could do some super smarty-pants analysis on the fields to figure it out. And, in a way, it can: that is what cyclic trait solving is all about. The thing is, while the compiler can do that, the derive cannot – the derive doesn’t have access to the definitions of other types and so forth, and clearly we would need to know things about Option and Rc to figure out whether T: Clone is required here.
But what we could do is to generate a different impl. Instead of adding T: Clone for each type parameter, we could add a where-clause for each field type. This makes sense: after all, we are just going to be calling Clone on every field, so it’s quite logical to say that the impl is valid if every field is cloneable:
Under this formulation, we can see that all we have to be able to do is to clone an Rc<_> and clone an Option<Rc<_>>, neither of which require that T: Clone.
This idea is called perfect derive
We call this idea [perfect derive][] and it’s been a goal for a while. The thing is, cyclic reasoning is tricky to get right. The Clone example is actually an easy one: that one doesn’t really require cyclic reasoning:
To show that List<i32>: Clone we have to show that…
Rc<i32>: Clone, which is easy because impl<T> Clone for Rc<T> doesn’t have any where-clauses1
Option<Rc<List<i32>>>: Clone uses the impl<T> Clone for Option<T> where T: Clone impl which requires…
Rc<List<i32>>: Clone, which is again easy
But it’s not so easy for Dump
But if we use that same “cyclic derive pattern” to generate our Dump impl, things don’t work out so well. Instead of just a T: Dump bound, our Dump impl now has two bounds:
// impl L1
impl<T>DumpforList<T>whereRc<T>: Dump,// <-- Used to be `T: Dump`
Option<Rc<List<T>>>,// <-- This one is new!
{fndump(&self){...}}
Now imagine we try to show List<i32>: Dump. We begin by applying impl L1, which requires us to show that its where clauses hold:
To show List<i32>: Dump we use impl L1, which has two where-clauses:
Rc<i32>: Dump, this one is easy because the Rc impl requires that i32: Dump which is true.
But Option<Rc<List<i32>>>: Dump is tricky. The Option impl requires that…
We need to prove Rc<List<i32>>: Dump, and then the Rc impl requires that…
We need to prove List<i32>: Dump, but that is what we started with! That’s cyclic logic!
Ugh. Something’s tricky here!
We can’t just accept any old cycle because of supertraits
Now, maybe you think we can just accept any cycles. And for these examples, it would be fine: but it’s not correct if you consider supertraits. Consider this trait and impl pair:
traitMagic: Copy{}// Impl M
impl<T>MagicforTwhereT: Magic,{}
If you are naive, this weird trait-impl pair can be used to prove that any type is Copy, regardless of whether it has a Copy impl. For example:
Say we want to prove that String: Copy. We observe that if a type implements Magic, it must implement Copy, so…
We begin by proving String: Magic. We use the impl M, which requires
that we show String: Magic, which is a cycle, so we accept it.
Uh oh, now we did something wrong. We proved that String: Copy even though there is no Copy impl. Something is fishy.
Now clearly we can all see the problem here – the implementation of Magic didn’t really add any information. It was just a tautology, saying that T: Magic if T: Magic. It’s not wrong, but implementing Magic was supposed to tell us more than just the fact that there is an impl of Magic, it was supposed to tell us also that the supertrait Copy is implemented. And that’s not true here.
But if you think about it, it’s hard to decide why we should reject impl M but accept the impl L1 of Dump for List<T>. They both wind up with a cyclic proof. So what’s the difference? This is the question we’ll be exploring over the next few blog posts.
Soundness for traits
This gets at an interesting question: what does it mean for the trait system to be sound. This seems obvious but actually it was a question I found kind of non-obvious for a long time.
We’ve found two satisfactory answers to that question. One of them involves converting to dictionary-passing style. Nadri explained that in a blog post. I think that’s a great post to read. I’m going to give another definition here that doesn’t require converting to a dependently typed program2
My rough definition is this3: the trait system is sound if, whenever it accepts some program P, that program cannot have a function that believes some Trait holds for the T, but there is no impl of Trait that can be used. So in the case of Magic and Copy, it’s easy to write a program that shows simple cyclic trait solving is unsound:
traitMagic: Copy{}impl<T: Magic>MagicforT{}fnis_copy<T: Copy>(){// this function believes `T: Copy`
}fnmain(){// this can be called because we believe that
// * `String: Magic` because
// * `String: Magic`, and we accept cycles.
// And then `String: Magic` implies `String: Copy`.
is_copy::<String>();}
By my definition, any sound type/trait system must reject this program because, if it were to execute, then execution would reach is_copy::<String> and yet there is no Copy impl that is judged to ber applicable to String. Uh oh!
Coming next
As I promised, this post was mostly focused on “setting the scene”. My goal was to explain what the problem is that we are trying to solve – permitting “good cyclic impls” but forbidding bad ones. I didn’t spend a lot of time on the bad ones, but it turns out that there’s a wide variety of unsound things one can do, some of which the compiler currently gets wrong, others of which it would only get wrong if we started permitting cycles.
My motivation for getting into this work is a bit complicated. I want perfect derive. But it’s also a loose end in our trait semantics that I really want to see nailed down before we move onto other tasks. Having auto traits (e.g., Send) work differently from other traits is clearly a “smell”, and without a strong understanding of the logical underpinnings of our trait system it’s easy to get things wrong when we build extensions.
In the next few posts I’ll go a bit deeper into the exploration I and others have been doing. I’ll talk about some of the “false starts” we took along the way and why they don’t work, and then about some of the solutions that are under consideration. Working through this stuff has really helped me to broaden my understanding of various areas of logic. By the time we’re done, we’ll cover4 coinduction and productivity, modal logic and the later modality, and we’ll see how our techniques might even help us with resolving specialization5.
Apart from the default Sized bound, I’m ignoring that here ↩︎
I have found that both the dictionary-passing interpretation and the logic approach I’m using are valuable. In the end, they’re more or less equivalent, which I guess shouldn’t be surprising if you’ve heard of the Curry Howard Correspondence, but I’ll talk about that later perhaps. ↩︎
I would like to, but haven’t, define a simplified version of Rust that includes trait solving and simple type checkoing and show that it cannot “go wrong”. ↩︎
I have been accessing, using the internet since probably around 1991, using my dad's academic account and then schools and then home. I've used the internet before the web existed, sending emails, downloading freeware and sharewares from "huge" ftp repository. Sharing ftp site, using Usenet to find more and share more. This was a time you had to buy Ethernet NuBus card and install both Ethernet drivers and TCP/IP ones.
Then one day something called NCSA Mosaic came into the picture - this changed the way I consumed 'the network'. After a while some friends showed me Netscape, and mosaic faded away. I've used Netscape on Mac, Windows 3.11, NT, on workstations running Unix (hello Dec Alpha).
At the time my gig was either running OS/2, and Linux Slackware on my Pentium machine. I then got into the Beos world, and that was one OS that Netscape didn't ship binaries for. Microsoft had started pushing Internet explorer (IE). I was starting to work and *had* to use Windows. I came up with the idea of using Mozilla at work in order to find bugs that would later be fixed when I'd use it home on my favorite OS.
So I started using Mozilla builds for windows, and then following planet and mozilla.org to be aware of the process. I would now and then file bugzilla issues, often to discover they've been filled already :-) I clearly remember the day tabs were introduced. I walked around the room I was working in showing the great new feature to my colleagues that were using Mozilla. There were no formal releases, at the time CI-CD didn't exist completely yet, so you could keep the same version for a day or for a week depending on how broken the tree was
Then I continued using builds until updates became available, and I choose to use the nightly channel. At that time my motivation had changed, I had moved from dead BeOS to NeXTStep+1 aka Mac OS X. There was no way for me to help My then browser of choice Camino at work. Mac OS X was way less popular in the tech industry that it is now.
The web landscape was dominated by IE , the mac version was unfortunately different meaning that using it made a bunch of websites unusable.
When Firefox got out, Camino started dying and I switched boats.
I then got employed by Mozillamessaging and used both Thunderbird and Firefox nightlies. I was the QA lead and there were no way I could test all the software and hardware combination used by our users. So crowd testing was very valuable to me. At this point I had stopped reporting issues and the only thing I was reporting were crashes.
Since 2010 a bunch of Chrome related browsers have started taking the web over, a bit like in 2006 when Microsoft was running the show, instead now google is. I currently run 2 versions of Firefox, nightly for personal stuff and release for work related ones. I occasionally have to fire a chrome related browser because testing with Ff from the vendors is not being done (I'm looking at DocuSign and FoundryVtt ).
So running nightly software has not been a burden at all, because there are a few crashes here and there, but I always submit, so the stability team at Mozilla can investigate. The idea is that if it crashes on me, it won't crash on my Dad's computer because he runs releases. Once or twice in the course of those 20 years did I have to either reinstall or create a new profile. That's how stable nightly has been for me.
I'm annoyed at some of the decision Mozilla makes, but again they are the most independent browser maker out there with a big enough team to deal with the development surface of a browser. Hence, for some forseable future I'll be running nightlies in hopes of keeping the web open. I'm lurking at Servo and following it's development, reporting issues there because I see it has an alternative to Firefox, but it's far from being ready yet.
I'd encourage some of my readers to try/use Nightly. If you choose to do so, send in crash reports and enable telemetry.
Servo 0.4.0 contains all of the changes we landed in June, which came out to yet another record 558 commits (April: 534, May: 391).
For security fixes, see § Security.
Servo is steadily becoming a bigger and busier project every month, and by June 2026, we’ve been reading through over four times the commits as we did when we started in September 2023.
This is hard work, particularly since there are things we need to know that are often difficult to answer just by reading the changes:
Who does the change affect, if anyone? Does it affect users, Servo developers, embedders, or some other group?
What observable difference does the change make, if any?
Does the feature require any preferences to be enabled, or is it enabled for everyone by default?
Are any real-world websites affected by the change?
What issue or broader project is the change related to? This question is answered by Fixes: #xxxxx or Part of: #xxxxx in the PR description.
Thanks to an initiative by @jdm, it’s now easier than ever for you to help us answer those questions, using the Servo Highfive bot!
If you’re working on a pull request that you think might be interesting for the next monthly update, even if you’re not 100% sure, tell us about it by following the steps below:
You add the monthly update label to your pull request, or comment @servo-highfive monthly update
Highfive posts a comment asking you some questions
You answer those questions in a comment containing @servo-highfive monthly update answer
Several more security bugs in Servo’s JS runtime have been fixed in Servo 0.4.0 with the update to SpiderMonkey 140.12.0 (@jschwe, #45766).
The exact CVEs that apply to us are not yet known, but for more details, see MFSA 2026-58.
RSA operations in SubtleCrypto now do modular exponentiation in constant time (@kkoyung, #45631).
Please note that our RSA implementation is currently vulnerable to the Marvin Attack – for more details, see RUSTSEC-2023-0071.
We’ve fixed an HTML injection bug (XSS) in file:/// directory listings, which affected file names containing </script> (@sahvx655-wq, #45510).
Real world compat
Layout correctness has significantly improved on lichess.org, and many websites have become a lot more readable thanks to our improved handling of variable fonts (@simonwuelker, #45768), including Zulip (servo.zulipchat.com) and Speedtest (speedtest.net).
Many websites worked in Servo even before version 0.4.0, including Google Photos (photos.google.com) and Cash Converters (cashconverters.com.au), and continue to work in version 0.4.0.
Other websites, like Google Maps (maps.google.com) and OpenStreetMap (www.openstreetmap.org), render well but have some issues with interactivity.
We’re implementing the more powerful version of ‘attr()’ that can be used anywhere, not just in ‘content’, under --pref layout_css_attr_enabled (@Loirooriol, #45041, #45421, #45495, #45752).
WebGPU support has improved, under --pref dom_webgpu_enabled:
implemented copyExternalImageToTexture() on GPUQueue (@sagudev, #45646)
implemented createQuerySet() on GPUDevice and resolveQuerySet() on GPUCommandEncoder (@sagudev, #45644)
implemented pushDebugGroup(), popDebugGroup(), and insertDebugMarker() on GPUCommandEncoder, GPUComputePassEncoder, and GPURenderPassEncoder (@jschwe, #45489)
more conformant requestAdapter() on GPU (@sagudev, #45424)
more conformant secure context enforcement (@sagudev, #45279)
All of the features above are enabled in servoshell’s experimental mode.
We’ve made more progress towards accessibility support, under --pref accessibility_enabled (@alice, @delan, #45555, #45554, #44949).
We’ve started implementing visible and interactive text selection (@mrobinson, @SimonSapin, #46107), one of the most long-awaited features of any web browser.
Stay tuned!
We’ve also started working on Web Animations, under --pref dom_web_animations_enabled (@simonwuelker, #45522, #45983), as well as webkitRelativePath on File, under --pref dom_entries_api_enabled (@yezhizhen, #45666).
Rust doesn’t have a stable ABI, so it has generally not been possible to embed Servo in another application without building Servo from source.
To make it possible, we’ve started designing a wrapper C API that will let you consume Servo as a prebuilt shared library using the stable and ubiquitous C ABI (@mukilan, #44984).
Eventually the idea is that we’ll create a wrapper Rust API around that wrapper C API, so you can have both the ergonomics of Rust and the build simplicity of C.
WebView::send_error has been removed (@mukilan, #45502) – this method was always meant to be internal, and has become unused after we introduced the new WebView- and WebViewDelegate-based API
the Android version now requires Android 13+ (@jschwe, #46104)
the desktop version now lets you drag and drop files to open them (@simonwuelker, #45454)
the desktop version now lets the tab bar scroll horizontally if you have too many tabs open, but from one tab hoarder to another, maybe you should reconsider having so many tabs open (@Nylme, #44884)
the desktop version enters fullscreen on the monitor containing the window, even if you’ve moved it to a different monitor (@rhit-kapilaar, #45556)
the desktop UI is more performant, resizes more smoothly, and no longer gets stuck in hovered states (@mrobinson, #45289, #45456, #45290)
<select multiple> should now be interactable on all desktop platforms (@alexcat3, #45419)
localhost:<port> now implies http://in the location bar and on the command line, rather than treating localhost: as an unsupported URL scheme (@SteveSharonSam, #45729, #45832)
When using the Firefox DevTools:
in the Console tab, uncaught exceptions are reported correctly (@jdm, #45549)
in the Console and Debugger tabs, you can now inspect the elements of nested arrays and the entries of Map objects (@atbrakhi, #45435, #45514, #45767)
in the Debugger tab, the Scopes panel now shows any ‘(uninitialized)’ variables, the value of this, and the global scope (@atbrakhi, @eerii, #45824, #45517)
mach try --help now lists all of the kinds of try jobs you can run (@shubhamg13, #45607)
mach test-wpt --update-expectations lets you run Web Platform Tests and update expectations in a single command (@TimvdLippe, #45521), rather than having to run mach test-wpt --log-raw <path> followed by mach update-wpt <path>
More on the web platform
To allow for more performant scrolling, ‘wheel’ events are no longer .cancelable unless there are one or more non-passive event listeners (@kunalmohan, #45667).
Note that like in Firefox, ‘wheel’ events are passive by default.
‘dotted’, ‘dashed’, and ‘wavy’ text decorations are now continuous across element boundaries (@mrobinson, #45726).
We use a RefCell-based mechanism to store many of our DOM types in other DOM types, enforcing Rust’s “aliasing xor mutability” rule at runtime by panicking if the rule is violated.
But when garbage collection happens, we need to borrow() each DomRefCell to trace the references, and this is the source of many panic bugs.
To fix that whole class of bugs, we initially created CanGc, a marker type that would annotate the code paths where GC can occur, in conjunction with custom static analysis (@jdm, #33140).
With the Rust type system we can do even better, if we flip that around and require any borrow_mut() call to prove that GC can not occur by passing a NoGC marker value.
We can then require that a &NoGC must be borrowed from a &JSContext (which blocks GC) and not a &mut JSContext (which allows GC), taking advantage of how Rust references work without needing any custom static analysis.
We have a large codebase that needs to be migrated in parts, so for now we’ve created the new method safe_borrow_mut() (@sagudev, #46050).
We also need to update all of our script-related code to borrow our safe JSContext wrapper, rather than creating an owned JSContext on the spot.
NoGC was designed to prevent dynamic borrow failures, but it also enables some performance optimisations!
If we can prove that garbage collection is impossible in some part of Servo, we can often avoid rooting JavaScript objects when interacting with them within that region of code.
This has allowed us to reduce overheads by over 1% in the layout process and in HTMLCollection (@Narfinger, #46092, #45582).
Our memory usage has improved, with BoxFragment now 17% smaller (288 → 240 bytes on amd64) and ShapeCacheEntry now smaller too (@SimonSapin, @mrobinson, @simonwuelker, #45183, #45496).
Speaking of which, 2D canvases now use up to 23% less power (@yezhizhen, #45301), and we now avoid rasterising the same SVG more than once (@Narfinger, @jschwe, #44805).
Servo now decodes all images asynchronously and fills image caches asynchronously, leaving script threads (web content processes) more time for other work (@Narfinger, #45542, #44483).
On top of that, we’ve improved incremental layout (@mrobinson, @Loirooriol, #45411) and reduced reflows in IntersectionObserver (@jschwe, #45986).
We’ve started working on incremental updates for the stacking context tree, and as a side effect, we’ve made some layout-bound microbenchmarks up to 10% faster (@mrobinson, @Loirooriol, #45208).
Interested in helping build a web browser?
Take a look at our curated list of issues that are good for new contributors!
Donations
Thanks again for your generous support!
We are now receiving 7681 USD/month (+0.2% from May) in recurring donations.
This helps us cover the cost of our speedyCIandbenchmarkingservers, 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 35 GitHub users (same as May) 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.
Today, the IETF held the CURRENT BoF,
where the goal was to develop a new protocol.
That protocol would be substantially like TLS,
reusing its record layer and basic structure,
but it would drop in MLS for key exchange.
This is somewhere between a pretty bad idea
and a horrible idea.
The wholesale replacement of a huge chunk of protocol architectures
is a hallmark of a lot of the AI-generated protocol proposals
that have flooded the IETF.
A small blemish is identified,
then the fix is a whole new protocol,
or a major piece of surgery.
No regard for the wisdom of Chesterton’s Fence
or the accumulated knowledge and usefulness embodied in what exists.
Experienced engineers know that rewriting a code module
is not something you do lightly.
There’s lots of literature out there about why this is a bad idea generally,
and some emerging discussion about how AI might just change that.
The reasons not to rewrite a software component still largely apply
to a protocol component.
The reasons that AI might make it easier to do that safely, less so.
Protocols are different.
Wholesale Change Will Miss Use Cases
Just like with a code change,
a protocol component that changes will miss use cases
that people really care about.
The usual concerns with code apply:
The existing features you know about and can test for
can be handled.
The existing problems you know and care about can be fixed.
You inevitably introduce brand-new problems.
The existing features you don’t know about
get lost.
Unlike code changes, you probably don’t have a test case
for existing features that you didn’t know about.
We found that with HTTP/2,
where a number of use cases got lost in the process
of “upgrading” HTTP.
In HTTP/1.1,
performing client authentication
in the middle of request was possible.
Losing that capability in HTTP/2
affected few enough people
that it was not badly damaging for the ecosystem.
It still sucked.
A lot of work was done to try to find these issues,
but we did not learn about these problems until fairly late in the process.
Proposing a protocol change means asking a whole lot of other people,
many of whom are not invested in your goals,
to do that work.
Changing a protocol by replacing a chunk of it,
no matter how much care is taken,
either asks the entire ecosystem to change with you.
That means asking everyone to move with you.
If they don’t, you are not changing the protocol,
you are forking it.
Forking A Protocol Destroys Interoperability
The real value of having a protocol like TLS
is that a great many things can all talk to each other.
Forking a protocol –
and sometimes profiling a protocol, a subject for another post –
destroys that.
You now have two ways to achieve the same goal,
and a choice to join one of two clubs.
You can join both, but that means constantly translating back and forth,
something that can only get harder over time
as protocol semantics diverge.
And yes, in case you were asking,
this applies to the entirety of the IETF IoT sphere,
which has parallel HTTP, TLS, and other analogues.
Ostensibly, these address the needs of highly constrained hardware,
but the cost is an ecosystem cut off from the mainstream.
But Fixing Protocols Is Hard
Yes, existing protocols come with baggage
or technical debt.
Maybe they aren’t perfectly optimized for your use.
The value that an existing protocol carries
is that you are sharing the burden of its maintenance
with a great many more people.
Fixing it, maybe by adding extensions to support your needs,
comes with opportunities to improve the protocol
even beyond that immediate need.
Every change is a chance to work off some of the accumulated cruft.
Major refreshes, like the TLS 1.3 reworking,
cleared out a ton of cruft in the process.
You get to benefit from the work that others do to improve that protocol too.
Do the Work
It is hard to be a responsible steward for the fabric of the Internet.
We do it because it is worthwhile.
Ignoring the lessons of the past is not helpful.
AI makes code cheap, but it doesn’t make standards cheap.
That is because standards are agreements between people
and those take time to build.
Let’s be honest: nobody wakes up excited to do chores.
For a lot of engineering teams, testing feels exactly like that, a tedious, manual chore tacked onto the end of a long sprint. The developer finishes the feature, throws it over the wall, and prays it doesn’t bounce back with a list of bugs.
When testing feels like an extra burden, people skip it, rush through it, or resent it. And that’s exactly when critical issues slip through into production.
Planet Mozilla
Collected here are the most recent blog posts from all over the Mozilla community.
The content here is unfiltered and uncensored, and represents the views of individual community members.
Individual posts are owned by their authors -- see original source for licensing information.