
Rust 1.96.0 landed on May 28 with the range ergonomics fix developers have been waiting years for. The headline is RFC 3550: new Copy-able range types that finally let you store a range inside a #[derive(Copy)] struct without hacking it apart. The release also stabilizes assert_matches!, ships a useful Cargo improvement for teams with private registries, and closes two security vulnerabilities affecting third-party registry users. Full details in the official announcement.
Ranges Can Finally Be Copy
The original std::ops::Range implements Iterator directly, which is why it cannot be Copy. Combining Iterator and Copy on the same type is a footgun: a for-loop silently iterates over a copy of the iterator, leaving the original unadvanced. Clippy has flagged this pattern for years. The practical result was that any struct needing to store a range had to split it into two fields — start: usize and end: usize — and manually reconstruct the range every time it was needed.
Rust 1.96 stabilizes the core::range module from RFC 3550. The new types — core::range::Range, RangeFrom, and RangeInclusive — implement IntoIterator instead of Iterator, which means they can be Copy. The new RangeInclusive also makes its start and end fields public, something the old type could not do because its exhaustion state was stored alongside them.
Here is what is now possible:
use core::range::Range;
#[derive(Clone, Copy)]
pub struct Span(Range<usize>);
impl Span {
pub fn of(self, s: &str) -> &str {
&s[self.0]
}
}
That Span struct was not possible in stable Rust before this release. The 0..1 syntax still produces legacy std::ops::Range types for now — migration to the new types is planned for the Rust 2027 edition, where cargo fix will handle most of it automatically. For library authors writing public APIs today: use impl RangeBounds<T> as your parameter type. It accepts both old and new range types, keeping you forward-compatible without forcing callers to import anything new. The full core::range documentation covers available types and iterators.
assert_matches! Is Now Stable
This one is small, and you will immediately wonder why it took this long. assert_matches! works like assert!(matches!(value, pattern)) but automatically prints the actual value when the assertion fails. Previously, getting a useful failure message required writing assert!(matches!(x, Some(n) if n > 0), "{:?}", x). Now:
use std::assert_matches::assert_matches;
let result: Result<u32, &str> = Err("not found");
assert_matches!(result, Ok(n) if n > 0);
// failure prints: result = Err("not found")
debug_assert_matches! is also stabilized and compiles away entirely in release builds. Replace your manual assert-matches boilerplate in test files now.
Cargo: One Entry for Both git and Registry
If you maintain a private Cargo registry and test against git forks, you know the ritual: comment out the registry line, uncomment the git line, run your tests, reverse it before publishing. That ends in 1.96. A single dependency entry can now specify both sources:
[dependencies]
my-crate = { version = "1.2", registry = "internal", git = "https://github.com/myorg/my-crate" }
Development builds use the git version; cargo publish uses the registry version. Narrow use case, but if you hit it, you hit it constantly.
Two CVEs: Check If You Use Third-Party Registries
Both vulnerabilities affect only users of third-party registries — if you use crates.io exclusively, you are not exposed to either. The full security advisories are published on the Rust Blog for CVE-2026-5223 and CVE-2026-5222.
CVE-2026-5223 (Medium): A maliciously crafted crate tarball could extract a file one level above the crate’s own cache directory, allowing it to overwrite another crate’s cached data from the same registry. Cargo now rejects any tarball containing symlinks, regardless of source.
CVE-2026-5222 (Low): When Cargo normalized registry URLs, it applied git-style .git suffix stripping to sparse HTTPS indexes too. This meant credentials for https://registry.example.com/index could be sent to https://registry.example.com/index.git. Fixed in 1.96 by limiting suffix normalization to the git protocol only. The vulnerability affects every Rust version from 1.68 through 1.95.
If you run a private registry — increasingly common in enterprise Rust shops — upgrade to 1.96 and rotate any credentials that may have been exposed.
What Else Shipped
The WebAssembly story in 1.96 — removal of --allow-undefined from Wasm link invocations — we covered separately when the change was announced. Short version: #[wasm_bindgen] users are unaffected; raw extern "C" blocks without a #[link(wasm_import_module)] annotation will break on upgrade.
The compiler also now accepts expr metavariables inside cfg and cfg_attr invocations — useful for procedural macro authors. Developers building Rust from source should note that LLVM 21 is now the minimum; check your toolchain before upgrading.
Upgrading
Run rustup update stable. The CVE fixes alone make this worth doing today if your team uses any third-party registries. Check the full 1.96.0 changelog for the complete list of stabilizations and compiler changes.













