# Protecting the Rust standard library from accidental breakage

_Published: 2026-08-15_

*Accidental breakage [can happen in any codebase](/blog/2023-09-07-semver-violations-are-common-better-tooling-is-the-answer/). The Rust standard library isn't magically exempt from this — so [it too now uses `cargo-semver-checks`](https://github.com/rust-lang/rust/pull/159671) [to prevent accidental breakage](https://github.com/rust-lang/rust/pull/160253). Here's why this took months of work by multiple Rustaceans, dozens of pull requests, and 15,000+ lines of code across the Rust repo, `cargo-semver-checks`, and its component libraries.*



In September 2020, [an unstable required method was added to a stable `std` trait](https://github.com/rust-lang/rust/pull/76110).
The seemingly innocuous change [broke `async-std`](https://github.com/async-rs/async-std/issues/883) [on nightly](https://github.com/rust-lang/rust/issues/77089), and [was promptly reverted](https://github.com/rust-lang/rust/pull/77090).

In June 2021, [a generic method was added to `core`'s `BuildHasher` trait](https://github.com/rust-lang/rust/pull/86151). The method accidentally did not have a `where Self: Sized` guard, so it [made `BuildHasher` no longer `dyn`-safe](https://github.com/rust-lang/rust/issues/87991). The problem was discovered during Rust 1.55-beta's crater run, and [required a fix](https://github.com/rust-lang/rust/pull/88031) to avoid breaking stable Rust.

In July 2022, [a soundness fix for iterators like `ChunksMut` was merged into `core`](https://github.com/rust-lang/rust/pull/94247). The new implementation [accidentally no longer implemented the `Send` and `Sync` auto-traits](https://github.com/rust-lang/rust/issues/100014) and [needed to be patched](https://github.com/rust-lang/rust/pull/100023) to avoid breaking stable Rust.

In March 2026, `tokio` maintainers [found that their test suite did not compile in Rust 1.94 on Windows](https://github.com/rust-lang/rust/issues/153486). Another `std` trait [had gained unstable methods](https://github.com/rust-lang/rust/pull/149718), and the breakage was sufficiently painful that [a fix was shipped in the Rust 1.94.1 point release](https://blog.rust-lang.org/2026/03/26/1.94.1-release/).

I could go on.[^sn-1]

Humans simply cannot reliably catch accidental breakage. I reviewed each of the breakage-inducing PRs above, and I do not believe I could have spotted the problem on my own. Neither did the much more experienced authors and reviewers who originally participated in those PRs! Our best effort is not enough, so we turn to tooling.

**`cargo-semver-checks` can catch all of these issues today. We have chosen to not let them happen again!**

- [Stability, breakage, and stability breakage](#stability-breakage-and-stability-breakage)
    - [The straightforward case: item stability](#the-straightforward-case-item-stability)
    - [Partial stability makes everything harder](#partial-stability-makes-everything-harder)
- [Plugging stability info into `cargo-semver-checks`](#plugging-stability-info-into-cargo-semver-checks)
- [How we got here and what lies ahead](#how-we-got-here-and-what-lies-ahead)

*Thanks to [Jakub Beránek (kobzol)](https://github.com/kobzol), the [rustdoc team](https://rust-lang.org/governance/teams/#team-rustdoc), the [library](https://rust-lang.org/governance/teams/#team-libs) and [library contributors](https://rust-lang.org/governance/teams/#team-libs-contributors) teams, the [RustWeek 2026 and Rust All Hands organizers](https://rustnl.org/about/), and the many other Rustaceans who put their time, energy, and goodwill toward accomplishing this goal 🦀 `cargo-semver-checks` stands on the shoulders of giants.*

## Stability, breakage, and stability breakage

The Rust standard library uses _stability_ as a mechanism to separate APIs usable in regular Rust releases from those that are experimental and only usable in nightly Rust on an opt-in basis.

As the name suggests, unstable APIs offer no stability or SemVer guarantees and may change at any time. Meanwhile, stable APIs behave exactly like the public API of any other Rust library.

To start, applying `cargo-semver-checks` to the standard library required understanding the difference, lest we frustrate maintainers by making CI complain about API breakage of explicitly-unstable APIs.[^sn-2]

There's another class of breakage too: _de-stabilizing_ a previously-stable API. Sadly, this is also not merely a hypothetical case: [this breakage flavor has precedent too](https://github.com/rust-lang/rust/issues/103306). We wanted to catch this too — and we did.

Finally, items' _name and existence_ can be stable but some of their facets, like `const` or a default value, may not be stable. `cargo-semver-checks` had to model this as well.

We needed to solve two sets of challenges: exposing stability information in rustdoc JSON so `cargo-semver-checks` can read it, and making stability fit into the `cargo-semver-checks` linting data model without needing to rewrite hundreds and hundreds of lints.

Let's discuss stability and rustdoc JSON first.

### The straightforward case: item stability

Check out this example:

```rust
#[stable(feature = "example", since = "1.0.0")]
pub struct Example {
    #[stable(feature = "example", since = "1.0.0")]
    pub stable_field: u32,

    #[unstable(feature = "example_unstable_field", issue = "none")]
    pub unstable_field: u32,
}
```

As you can see, `stable_field` can be used on stable Rust, while `unstable_field` requires nightly Rust and an explicit opt-in with <span class="nobr"><code>#![feature(example_unstable_field)]</code></span>.[^sn-3]

Both removing `stable_field` and making it <span class="nobr"><code>#[unstable]</code></span> would be breaking changes of stable Rust APIs which we need to catch. Changes affecting only `unstable_field` are permitted, provided they do not alter stable properties of the containing type — for example, by removing a stable auto-trait implementation.

To export this data in rustdoc JSON, [this PR added an `Item::stability` field](https://github.com/rust-lang/rust/pull/158230/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6R301-R320) that can be populated with the <span class="nobr"><code>#[stable]</code></span> or <span class="nobr"><code>#[unstable]</code></span> attribute data of standard library items.

### Partial stability makes everything harder

Item stability isn't the whole story.
An item's name and existence can be stable while only _some_ of its capabilities are stable.

Take `const` functions, for example:

```rust
#[stable(feature = "example", since = "1.0.0")]
#[rustc_const_unstable(feature = "example_const", issue = "none")]
pub const fn answer() -> u32 {
    42
}
```

Outside of `const` contexts, `answer()` can be called normally on stable Rust.
But calling it inside `const` is unstable, requiring nightly Rust and an explicit <span class="nobr"><code>#![feature(example_const)]</code></span> opt-in.

Removing `const` from this function therefore isn't a breaking change from the perspective of stable Rust.
To break stable Rust, it would have had to be <span class="nobr"><code>#[rustc_const_stable]</code></span> instead.[^sn-4]

Analogously to item stability, we [added an `Item::const_stability` field to rustdoc JSON](https://github.com/rust-lang/rust/pull/158343/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6R324-R332).

Trait items with provided defaults[^sn-5] have a similar concept of _default stability_:

```rust
#[stable(feature = "example", since = "1.0.0")]
pub trait Example {
    #[stable(feature = "example", since = "1.0.0")]
    #[rustc_default_body_unstable(
        feature = "example_default",
        issue = "none"
    )]
    fn answer_in_trait(&self) -> u32 {
        42
    }
}
```

The `answer_in_trait()` method is stable, but its default implementation isn't.
Implementations of `Example` in stable Rust must provide their own `answer_in_trait()` method, while nightly Rust users who opt into <span class="nobr"><code>#![feature(example_default)]</code></span> may rely on the default.

Since stable downstream trait implementations cannot rely on an unstable default, removing it isn't a source-breaking change to stable Rust.

To expose default stability to rustdoc JSON, [our PR added](https://github.com/rust-lang/rust/pull/158468/changes#diff-ede26372490522288745c5b3df2b6b2a1cc913dcd09b29af3a49935afe00c7e6) `default_unstable` fields to `Function`, `ItemEnum::AssocConst`, and `ItemEnum::AssocType`.

## Plugging stability info into `cargo-semver-checks`

Making stability info available in rustdoc JSON is only half the story. How do we make use of it when linting for breakage?

Rewriting (or worse, duplicating) every lint is completely out of the question! [Exponential growth over several years](/blog/2026-01-11-cargo-semver-checks-2025-year-in-review/) has produced hundreds of them, and we're still adding more!

To find the answer, compare these two cases:
```rust
// In a regular crate on crates.io:
pub struct UserExample {
    pub visible: u32,

    #[doc(hidden)]
    pub unstable: u32,
}

// In the Rust standard library:
#[stable(feature = "example", since = "1.0.0")]
pub struct StdlibExample {
    #[stable(feature = "example", since = "1.0.0")]
    pub visible: u32,

    #[unstable(feature = "example_unstable_field", issue = "none")]
    pub unstable: u32,
}
```

How is `UserExample::unstable` different than `StdlibExample::unstable`? How is `UserExample` different than `StdlibExample`?

Both `unstable` fields have opted out of being stable public API.

Both structs _technically_ have all-public fields. But neither struct's _public API_ supports initialization with `Example { visible, unstable }` struct literal syntax because that requires naming the `unstable` field, which lies outside the SemVer-guaranteed public API.

Breaking unstable APIs is allowed in non-major versions. Breaking <span class="nobr"><code>#[doc(hidden)]</code></span> APIs is also allowed in non-major versions.

**Stability attributes are another flavor of public API marker!** We've [already invested substantial effort](/blog/2023-11-18-checking-semver-for-doc-hidden-items/) into handling <span class="nobr"><code>#[doc(hidden)]</code></span>. We can reuse almost all of that infrastructure here too 🎉
- Items marked <span class="nobr"><code>#[unstable]</code></span> are considered non-public API, exactly as if they were <span class="nobr"><code>#[doc(hidden)]</code></span>.
- If an item is const-unstable, `cargo-semver-checks` considers it non-const.
- If a provided default is unstable, `cargo-semver-checks` pretends the default isn't provided.

Structurally, this does everything we want: breakage of stable items is reported correctly, destabilizations are considered removals from public API, and unstable items' own breakage is never reported.[^sn-6]

But here's my favorite part: the lints are *blissfully unaware of all this.*

The reason `cargo-semver-checks` has had [such a Cambrian explosion of lints](/blog/2026-01-11-cargo-semver-checks-2025-year-in-review/) is that writing lints remains *relatively easy* — both in general, and especially when compared to existing precedents in static analysis tooling. Writing a new lint is even [the recommended onboarding task for new contributors](https://github.com/obi1kenobi/cargo-semver-checks/blob/c47d6c3e5fee2e63fff191f741352e2570f2be0e/CONTRIBUTING.md#L9)!

With this approach for handling stability info, that continues to be the case: newly-written lints will _just work_. In correctly handling <span class="nobr"><code>#[doc(hidden)]</code></span>, their stability-handling will be correct by construction too. They [fall into the pit of success](https://blog.codinghorror.com/falling-into-the-pit-of-success/).

## How we got here and what lies ahead

At the start of this year [I wrote that I'm choosing to reject "number of lints" as a benchmark](https://predr.ag/blog/cargo-semver-checks-2025-year-in-review/#the-path-forward-for-2026-and-beyond) and instead seek out ways to maximize our positive impact on the Rust ecosystem.

This is a great example of the kind of work I had in mind!

The opportunity presented itself in a series of fortuituous conversations at [RustWeek 2026 and the All Hands meetings](https://2026.rustweek.org/) with folks working on Rust's standard library.
What started as an off-hand comment in a hallway chat quickly grew into a sketch of an idea, then into a concrete proposal over the course of several consecutive days of Rust-themed talks, meetings, dinners, and bus rides around the week's events.

Incidentally, this is why having All Hands immediately after a major conference like RustWeek is a phenomenal idea. It maximizes the odds of *precisely this sort of lucky coincidence* happening — and getting sufficient momentum to make it past all the "reasons it won't work" that often come up when a bold new idea is first born. So [many other ideas benefited from this too](https://blog.rust-lang.org/inside-rust/2026/07/31/all-hands-2026-retrospective/)! My hat is off to the RustWeek and All Hands organizers for a job tremendously well done!

Then it was a matter of implementing everything that had been (broadly speaking) agreed upon in those in-person conversations.
This took a while, and the work is still not fully done — we've only made it to the point where adopting `cargo-semver-checks` in Rust CI was definitely preferable to the status quo ante. There are still more things to iron out, and we'll keep working on those.[^sn-7]

Even though there's more work to do, we still have much to celebrate!

We've cut down on the amount of accidental breakage Rustaceans might have to suffer, report, triage, and fix.

Every `cargo-semver-checks` improvement from now on will directly benefit not just the crates.io library ecosystem but Rust itself as well.

The positive impacts of RustWeek and the All Hands continue unabated.

There's never been a better time to write Rust 🦀

*If you liked this essay, consider [subscribing](/subscribe/) or following me on [Mastodon](https://hachyderm.io/@predrag) or [Bluesky](https://bsky.app/profile/predr.ag). You can also fund my writing and work on `cargo-semver-checks` via [GitHub Sponsors](https://github.com/sponsors/obi1kenobi), for which I'd be most grateful ❤*

*Discuss on [r/rust](https://www.reddit.com/r/rust/comments/1vpxqls/protecting_the_rust_standard_library_from/) or [lobste.rs](https://lobste.rs/s/hnx6id/protecting_rust_standard_library_from).*

[^sn-1]: There are [two more](https://github.com/rust-lang/rust/issues/146087) [instances](https://github.com/rust-lang/rust/issues/103306) I've found since 2020. My search was not exhaustive. Likely there are more.

[^sn-2]: Of course, there's a difference between _intended_ breakage of unstable APIs, and _unintentional_ breakage of such APIs. We haven't built this _yet_ so there's room for an even deeper integration here! But generally, breakage of unstable APIs should be reported as "here's what changed, please make sure you intended this" without blocking CI.

[^sn-3]: As a consequence, stable code cannot create a fresh `Example` from field expressions alone and must use `..` in patterns, even though the struct isn't formally <span class='nobr'><code>#[non_exhaustive]</code></span>. Functional update syntax like `Example { stable_field, ..existing }` using an existing `Example` still works.

[^sn-4]: We also had to account for `const trait` declarations, `const` trait impls, and the const behavior of their associated methods — all currently unstable as of Rust 1.97.1. The in-depth research required to discover and properly handle cases like this was part of the challenge of pulling this off.

[^sn-5]: This includes functions, associated consts, and associated types. Once again, associated type default values are themselves an unstable Rust feature, making even discovering this edge case part of the challenge here.

[^sn-6]: A minor bit of UX polishing is due next: [destabilizations are reported as additions of <span class='nobr'><code>#[doc(hidden)]</code></span> specifically](https://github.com/obi1kenobi/cargo-semver-checks/issues/1672), even though there now are several more attributes that could have caused that. We'll fix that too!

[^sn-7]: For example: [better UX around stability breakage](https://github.com/obi1kenobi/cargo-semver-checks/issues/1672), catching breakage on more platforms and not just x86 Linux, edge cases around [how exactly glob imports interact](https://rust-lang.zulipchat.com/#narrow/channel/266220-t-rustdoc/topic/Can.20non-public.20API.20glob.20re-export.20produce.20public.20API.20items.3F/with/616735517) with stability and <span class='nobr'><code>#[doc(hidden)]</code></span>, etc.

Copyright (C) Predrag Gruevski 2026. [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en)
