Latest in branch 1.96
1.96.0
Released 28 May 2026
(2 days ago)
SoftwareRust
Version1.96
Initial release1.96.0
28 May 2026
(2 days ago)
Latest release1.96.0
28 May 2026
(2 days ago)
Support statusSupported
Release noteshttps://github.com/rust-lang/rust/releases/tag/1.96.0
Source codehttps://github.com/rust-lang/rust/tree/1.96.0
Downloadhttps://github.com/rust-lang/rust/releases/tag/1.96.0
Rust 1.96 ReleasesView full list

What's New In Rust 1.96

Category Highlights
New Features New core::range types (Range, RangeFrom, RangeInclusive) that implement Copy and IntoIterator; assert_matches! and debug_assert_matches! macros stabilized; From<T> impls for AssertUnwindSafe, LazyCell, and LazyLock
Breaking Changes WebAssembly targets no longer pass --allow-undefined to the linker; undefined symbols are now hard linker errors instead of auto-imports from the "env" module
Bug Fixes / Security CVE-2026-5223 (medium): tarball extraction with symlinks via third-party registries; CVE-2026-5222 (low): authentication with normalized URLs via third-party registries. crates.io users are not affected by either.

Why can't Rust's Range types be Copy, and does 1.96 finally fix that?

Yes -- Rust 1.96 stabilizes a new family of range types in core::range that are both Copy and usable as slice indices, ending a long-standing ergonomics pain point. The root cause was architectural: the original std::ops::Range implements Iterator directly, and making a type simultaneously Iterator and Copy is a recognized footgun (Clippy even has a lint for it, copy_iterator). Because the iterator carries internal mutable state, giving it Copy semantics would silently duplicate that state on every copy, producing deeply surprising iteration behavior.

The solution, described in RFC 3550, was to create new range types that implement IntoIterator rather than Iterator. This separates the concept of a range (a pure value describing a span) from the concept of an iterator over that range. The new types stabilized in 1.96 are:

  • core::range::Range<T>
  • core::range::RangeFrom<T>
  • core::range::RangeInclusive<T>
  • Associated iterator types: RangeIter, RangeFromIter, RangeToInclusiveIter

In practice, this means you can now store a range inside a #[derive(Copy, Clone)] struct without splitting it into separate start and end fields -- a common workaround you no longer need:

use core::range::Range;

#[derive(Clone, Copy)]
pub struct Span(Range<usize>);

impl Span {
    pub fn of(self, s: &str) -> &str {
        &s[self.0]
    }
}

Watch out for one important detail: range literal syntax like 0..1 still produces the legacy std::ops::Range type. The edition migration to make literals produce core::range types is planned for a future edition. For library authors, the practical guidance right now is: accept impl RangeBounds<T> in public APIs (which accommodates both old and new ranges), and where you need a concrete type, prefer the new core::range variants to stay ahead of the eventual default.

The new RangeInclusive also makes its start and end fields public, unlike the legacy type which hid the exhausted iterator state. Since the new type is not an iterator, there is no such state to worry about.

How do the new assert_matches! and debug_assert_matches! macros improve test diagnostics?

assert_matches! and debug_assert_matches! are now stable in core/std, giving you pattern-based assertions with far more useful failure output than the assert!(matches!(..))) equivalent. When the assertion fails, the macro prints the Debug representation of the actual value, which makes diagnosing failures much faster than seeing a bare assertion failed with no context.

use core::assert_matches;

fn get_status_code() -> u32 {
    404
}

fn test_status() {
    // On failure, prints the actual value, not just "assertion failed"
    assert_matches!(get_status_code(), 200 | 201 | 204);
}

The behavior difference from a plain assert!(matches!(..)) is purely in the panic message. The macro is especially helpful in data-driven tests where you want to assert a value falls into a range or enum variant set without binding it. debug_assert_matches! follows the same strip-in-release semantics as debug_assert!, so it costs nothing in optimized builds.

Most teams upgrading from a third-party assert_matches crate will find the behavior compatible, but note that the standard library macros are not added to the prelude. You must explicitly use core::assert_matches; or use std::assert_matches; in each module. This was a deliberate decision to avoid breakage for projects already depending on the popular third-party crate of the same name.

What does the WebAssembly linker change in Rust 1.96 break, and how do I fix it?

Rust 1.96 removes the implicit --allow-undefined linker flag for all WebAssembly targets, turning previously silent undefined-symbol situations into hard linker errors. This is a meaningful behavioral change: before 1.96, any undefined symbol at link time was automatically converted into a WebAssembly import from the "env" module, so the module would happily link and only fail at runtime when the host did not provide the import. Now the same scenario fails at compile time.

This matters if you:

  • Rely on implicit imports from a host runtime (e.g., a WASI-adjacent environment with custom host functions)
  • Have a misconfigured build where a symbol is accidentally undefined
  • Use a C library with symbols your link step doesn't fully resolve

If the old behavior was intentional -- you genuinely want certain symbols to be WebAssembly imports -- there are two paths:

// Option 1: Re-enable globally via RUSTFLAGS
// RUSTFLAGS=-Clink-arg=--allow-undefined cargo build --target wasm32-unknown-unknown

// Option 2: Declare the import explicitly in source code
#[link(wasm_import_module = "env")]
extern "C" {
    fn my_host_function(x: i32) -> i32;
}

In practice, option 2 (explicit declarations) is the right approach for production code. It makes host-provided imports visible and auditable in source, prevents accidental symbol name mismatches, and aligns with the Wasm component model's emphasis on explicit interface definitions. The RUSTFLAGS escape hatch is useful for quickly unblocking CI while you update the codebase, but should not be a long-term solution.

This change was pre-announced on the Rust blog in April 2026 to give teams time to prepare, so it should not be a surprise. If your Wasm builds broke on upgrade to 1.96, audit for undefined symbols with wasm-objdump -x or your linker's verbose output.

What are the Cargo security vulnerabilities fixed in Rust 1.96, and who is affected?

Rust 1.96 bundles fixes for two Cargo CVEs that affect users of third-party crate registries. Importantly, users of crates.io are not affected by either vulnerability.

  • CVE-2026-5223 (Medium severity) -- A flaw in how Cargo extracts crate tarballs containing symlinks when using a third-party registry. A maliciously crafted tarball could potentially use symlinks to write files outside the intended extraction directory. This is a supply-chain relevant concern for teams running private registries or pulling from untrusted third-party sources.
  • CVE-2026-5222 (Low severity) -- An authentication issue triggered by URL normalization differences when authenticating against a third-party registry. Under specific conditions, normalized URLs could cause credentials to be sent to an unintended endpoint.

Most teams using only crates.io do not need to take any action beyond the standard rustup update stable. Teams running or consuming from private registries should treat this upgrade as higher priority and review their registry authentication configuration after updating.

Frequently Asked Questions about Rust 1.96

Do I need to update my code when upgrading to Rust 1.96?
Most existing code compiles without changes, but WebAssembly targets that relied on implicit undefined symbol imports will now produce linker errors that must be resolved either with explicit extern declarations or the --allow-undefined flag.

Will range literals like 0..10 produce the new core::range types in Rust 1.96?
No, range literal syntax still produces the legacy std::ops::Range types in 1.96. The migration of literals to the new core::range types is planned for a future edition, so existing code using range literals is unaffected for now.

How do I use the new assert_matches! macro in my tests?
Add use std::assert_matches; or use core::assert_matches; at the top of the module, then call it like assert_matches!(value, Pattern) -- the macro is not in the prelude, so the explicit import is required. For example: use std::assert_matches; in your test module, then assert_matches!(response.status(), 200..=299); in your test body.

Is my project affected by the two Cargo CVEs in Rust 1.96?
Only projects using third-party crate registries other than crates.io are affected; teams relying solely on crates.io are not impacted by either CVE-2026-5223 or CVE-2026-5222.

Can I store the new core::range::Range in a Copy struct and use it to index slices?
Yes, that is precisely the intended use case -- core::range::Range implements both Copy and the SliceIndex trait, so you can derive Copy on a struct containing one and call &my_slice[span.0] directly.

What is the recommended migration path for library authors now that new Range types exist?
Accept impl RangeBounds in public API signatures to remain compatible with both legacy and new range types during the transition period, and prefer concrete new core::range types over legacy std::ops types when you need to store or return a specific range, since the new types will eventually become the edition default.

Releases In Branch 1.96

VersionRelease date
1.96.028 May 2026
(2 days ago)