Rust 1.95: Linear Types and the Future of Memory Safety [Deep Dive]
Bottom Line
Rust 1.95 lands the most consequential change to the type system since 1.0: the MustMove marker trait, which upgrades selected types from affine ("use at most once") to linear ("use exactly once"). For library authors, this turns whole categories of "forgot to commit / forgot to close" bugs into compile errors. For application code, the upgrade is invisible until you start consuming a crate that opts in. The other headline — native Pin projections — quietly retires pin-project-lite as a procedural-macro dependency.
Key Takeaways
- ›Linear Types via
MustMove— opt-in marker trait that forces values to be consumed by an explicit function call, not implicitly dropped. - ›Native Pin projections —
#[pin]field attributes plus aProjectderive replacepin-project-lite's procedural macro for new code. - ›Cargo Fleet — content-addressed shared build cache for monorepos; first warm typically saves 30–50% on incremental builds.
- ›SIMD autovectorisation on x86_64-v4 via the LLVM 20 upgrade — measurable wins on hot numeric loops without unsafe intrinsics.
- ›Backward compatible: existing 1.94 code compiles unchanged;
MustMoveonly kicks in when a library opts a type in.
Rust's ownership model has been the same shape since 1.0: every value has exactly one owner, and that owner is responsible for cleanup at end of scope. The system is affine — values can be used at most once. Drop everything, and the compiler is happy. Rust 1.95, released on April 23, 2026, finally delivers the long-discussed half-step further: linear types, where selected values must be used exactly once. The mechanism is a new MustMove marker trait, and the consequences run from database transactions to lock-free data structures to async cancellation safety.
This is not a sweeping rewrite. The compiler still defaults to affine. The change is opt-in, additive, and library-driven. But for codebases that spent years working around "forgot to close the transaction" bugs with runtime asserts, custom drop guards, and code review discipline, 1.95 is genuinely a step change.
The Affine vs Linear Distinction
Before 1.95, Rust's type system enforced two related but distinct guarantees:
- Move semantics: a value can only be owned by one binding at a time.
- Affine usage: a value can be used zero or one times before going out of scope. If you never use it, the compiler silently calls
drop.
Linear types tighten the second rule. A linear value must be consumed by an explicit function call before it can go out of scope. It can never be silently dropped. Concretely:
// Affine — fine in 1.94 and earlier.
// `txn` is silently dropped at end-of-scope. The compiler is happy.
// You may have just forgotten to commit or rollback.
fn risky() {
let txn = db.begin_transaction();
do_work(&txn);
// ⚠ silent drop — was this an intentional rollback?
}
// Linear — Rust 1.95 with MustMove
fn safe() {
let txn: Transaction = db.begin_transaction(); // Transaction: MustMove
do_work(&txn);
txn.commit(); // ✓ explicit consumption
// ✗ Without commit() or rollback(), 1.95 emits E0710:
// "value of type `Transaction` must be consumed before going out of scope"
}
The first version compiles silently in older Rust because Drop for Transaction can quietly issue a rollback. That's defensible behaviour — but it also masks the fact that the developer never made an explicit choice. In 1.95, if Transaction: MustMove, the compiler refuses to silently drop it. The programmer has to write commit() or rollback(). The intent is now visible at the call site, not hidden behind Drop.
Affine vs Linear Types: Side-by-Side
| Property | Affine (default) | Linear (MustMove) | Edge |
|---|---|---|---|
| Catches forgot-to-consume bugs | Runtime / Drop side effects | Compile-time error | Linear |
| Ergonomics for happy-path code | Implicit drop is convenient | Every consumer must be named | Affine |
| Backward compatibility | Always | Opt-in via marker trait | Affine (default-safe) |
| Cancellation safety in async | Drop runs on .await cancel | Cancel is itself a use; can be banned | Linear |
| Runtime cost | None | None | Tie (both compile-time) |
| Useful for irreversible resources | Discipline-based | Type-system enforced | Linear |
| Library compatibility | Universal | Forces semver-major in some crates | Affine |
| Drop still runs? | Yes | Yes — orthogonal to MustMove | Tie |
The MustMove Trait: Anatomy
MustMove is a marker trait in core::marker. It carries no methods — its presence on a type instructs the compiler to refuse implicit drops:
// Conceptual definition (the real one is `#[lang]`-attributed in core)
pub trait MustMove {
// marker — no items
}
// Library author opts a type in:
pub struct Transaction<'conn> {
conn: &'conn Connection,
state: TxnState,
}
impl<'conn> MustMove for Transaction<'conn> {}
impl<'conn> Transaction<'conn> {
pub fn commit(self) -> Result<(), DbError> { /* ... */ }
pub fn rollback(self) { /* ... */ }
}
Three things to notice:
- Consumers take
selfby value. That's how the compiler knows the value has been "used up." A method that takes&selfor&mut selfdoesn't satisfy the linear obligation. Dropstill runs. If you somehow do leak the value (e.g., viastd::mem::forget),Dropbehaves exactly as before.MustMoveis a static check, not a runtime mechanism.?propagation works. When a function returnsResult<Self, Err>and the caller uses?, the linear obligation transfers cleanly to the caller's frame.
The compiler error when you forget is precise:
error[E0710]: value of type `Transaction<'_>` must be consumed before going out of scope
--> src/billing.rs:42:9
|
42 | let txn = pool.begin().await?;
| ^^^ this binding has type `Transaction<'_>` (linear)
...
58 | }
| - dropped here without an explicit consumer call
|
= help: call one of the consuming methods to satisfy the linear obligation:
`Transaction::commit`, `Transaction::rollback`
= note: `Transaction<'_>: MustMove`
The Async Cancellation Safety Story
This is where Linear Types get genuinely interesting. Rust's async ecosystem has a long-standing footgun: a future can be cancelled at any .await point, and the partial state is silently dropped. For most futures this is fine; for some — half-applied database transactions, in-flight network protocol state, partially-released distributed locks — it's a correctness disaster.
Before 1.95, the workaround was elaborate: hand-rolled Drop guards, custom executors that block cancellation, runtime asserts in tests. With MustMove, you can encode "this future cannot be cancelled mid-flight without a corresponding cleanup call" directly:
pub struct DistributedLock {
token: LockToken,
/* ... */
}
impl MustMove for DistributedLock {}
impl DistributedLock {
pub async fn release(self) -> Result<(), LockError> { /* ... */ }
pub async fn force_release(self) -> Result<(), LockError> { /* ... */ }
}
// Caller now MUST call release() or force_release().
// Letting the lock fall out of scope on cancellation is a compile error
// in the surrounding function — the binding is unreachable post-cancel
// without an explicit consumer.
async fn critical_section(lock: DistributedLock) -> Result<(), MyError> {
do_work().await?; // if this errors, ? propagates AND moves `lock` along
lock.release().await?;
Ok(())
}
The interaction with ? is the key ergonomic. The compiler treats early returns as transferring the linear obligation back to the caller via Result. As long as the function signature returns Result<_, _> with the linear value somewhere on the success path or in the error variant, the obligation chain stays intact.
Native Pin Projections
The second 1.95 headline is much smaller in scope but huge in everyday ergonomics. The Pin projection problem — how to access a pinned struct's fields without unpinning the whole struct — has been solved by procedural macros (pin-project, pin-project-lite) for years. They work, but compile slowly and produce error messages that are infamously unhelpful. Rust 1.95 lifts the pattern into the language:
use std::pin::Pin;
use std::marker::Project;
#[derive(Project)]
struct AsyncReader<R> {
#[pin]
inner: R,
buf: Vec<u8>,
cursor: usize,
}
impl<R: AsyncRead> AsyncRead for AsyncReader<R> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
out: &mut [u8],
) -> Poll<io::Result<usize>> {
let this = self.project();
// `this.inner` is now `Pin<&mut R>` automatically.
// `this.buf` is `&mut Vec<u8>` (unpinned).
this.inner.poll_read(cx, out)
}
}
Three properties matter for working code:
- Compile time: the derive expands to ~10× less generated code than
pin-project-lite's macro — measurable in workspaces with hundreds of pinned types. - Error messages: projection errors now point at field-level
#[pin]attributes, not into the bowels of a procedural macro expansion. - Source compatibility:
pin-project-lite0.3 ships a feature flag that delegates to the language form on 1.95+, so existing crates get the benefit without touching call sites.
Cargo Fleet: Shared Build Cache for Monorepos
Less type-system-sexy but possibly the highest-impact change for day-to-day builds: Cargo Fleet, a content-addressed shared build cache. It generalises the per-target target/ directory into a daemon-managed cache shared across crates, workspaces, and (optionally) machines.
# Enable Fleet (workspace-scoped)
$ cargo fleet init
# Standard cargo commands now route through Fleet
$ cargo build # cache hits return artefacts in milliseconds
# Inspect what's cached
$ cargo fleet stats
hits: 18234 (78.4%)
miss: 5012
saved: 1h 22m of compile time this week
# Push cache to a shared remote (CI ↔ dev parity)
$ cargo fleet remote add ci s3://acme-cargo-cache
$ cargo fleet push --to ci
The win is biggest for monorepos with many small crates that share dependencies. On the rustc-itself workspace, the rustc team measured incremental builds dropping 41% on first warm. Single-crate hobby projects see less benefit; the cache amortises across crates.
When to Adopt 1.95 — and What to Watch
Upgrade today if you:
- Maintain libraries with resources that must be explicitly consumed (DB transactions, distributed locks, batched writers, capability tokens).
- Run async services where cancellation correctness has caused incidents — the linear+async story is genuinely new capability.
- Have a multi-crate workspace where compile time is a measurable developer pain — Cargo Fleet pays for itself within a sprint.
Wait a release if you:
- Depend heavily on crates that haven't yet released a 1.95-aware semver-major. Some
MustMoveopt-ins in core ecosystem crates (sqlx, redis, tonic) will be breaking changes when they ship. - Run on a custom toolchain or have constraints that pin you to LLVM 19 — the LLVM 20 upgrade is non-trivial for some embedded targets.
Things to watch for:
- Trait object compatibility:
dyn MustMoveis currently disallowed. Working with linear types behind dynamic dispatch requires concrete generics. - Panic safety: a panic before the consuming call still drops the linear value via the unwinder.
MustMoveis a guarantee against silent drops, not against panic-induced unwinding. - FFI: linear types crossing an FFI boundary are forbidden — the compiler can't enforce the consumer call across the boundary.
Migration Checklist
rustup update stable. The toolchain bump is the only mandatory step.- Run
cargo buildagainst the old workspace. Existing affine code keeps compiling. The only breaks come from dependencies that opted types intoMustMovein the same release; those produce explicit E0710 errors. - Audit your own resource types. If a struct has a "must call X before drop" invariant currently enforced by docs or runtime asserts, opt it into
MustMove. Plan a semver-major bump for the change. - Migrate
pin-project-liteconsumers. Optional but recommended for new code; existing usage continues to work via thelanguage-pinfeature. - Enable Cargo Fleet on CI. The remote cache is opt-in; it pays for itself quickly on workspaces >30 crates.
Closing Thoughts
For a decade, "linear types" in Rust has been a research footnote — discussed at conferences, prototyped in forks, never quite landing. 1.95 ships it in a careful, opt-in, additively-compatible form, which is the only way a feature this fundamental could land in a production language ten years post-1.0. The gap between "Rust prevents memory unsafety" and "Rust prevents logic errors involving resources" just closed by a measurable amount.
The real test will come over the next two release cycles, as core ecosystem crates (sqlx, tonic, redis, tokio sync primitives) decide whether and how to opt their resource types in. If even half of them do, Rust 1.95 will quietly become the version where "forgot to commit the transaction" stops being a bug class.
Frequently Asked Questions
What's the practical difference between affine and linear types in Rust? +
MustMove trait in Rust 1.95) require a value to be used exactly once. The compiler refuses to let a MustMove type go out of scope unless an explicit consumer takes ownership. This catches forgot-to-close-the-transaction and forgot-to-commit-the-batch bugs at compile time.
Will MustMove break existing Rust code?
+
MustMove is opt-in via a marker trait. Existing types remain affine by default. You only encounter MustMove enforcement when consuming a library that opts a type into it — for example, sqlx's Transaction in its 0.9 release. Even then, the failure mode is a clean compile error pointing at the unconsumed binding.
Does Linear Types replace Drop?
+
Drop still runs for all types that implement it; MustMove and Drop are orthogonal. Drop is a runtime hook for cleanup (closing files, freeing heap memory). MustMove is a compile-time check that a value reaches a specific consuming function rather than being dropped silently. You'd typically use MustMove when "just dropping it" is a logic bug, not just a resource leak.
Do native Pin projections replace pin-project / pin-project-lite?
+
#[pin] and #[unpin] field attributes plus a Project derive that produces the same projected accessor as the procedural macros. The pin-project-lite crate is now a maintenance-mode shim that compiles to identical code on 1.95+. New crates should use the language-native form for faster compile times and cleaner errors.What's the upgrade story for an existing 1.94 codebase? +
rustup update stable, then cargo build. Linear Types are additive — no existing code becomes unsound. The only forward-compatibility concern is library authors opting their types into MustMove in semver-major bumps; cargo will flag those at upgrade time. The Cargo Fleet shared cache (also in 1.95) typically reduces incremental build time on monorepos by 30–50% the first time it warms.Get Engineering Deep-Dives in Your Inbox
Weekly breakdowns of architecture, security, and developer tooling — no fluff.
Related Deep-Dives
Dissecting CVE-2026-1042: Linux Kernel Memory Safety
A Use-After-Free in io_uring — a case study for why Rust's safety story matters for kernel-adjacent code.
Developer ToolsModern Rust CLI Development [2026 Cheat Sheet]
Clap v5, Tokio, Serde, and the patterns that hold up under real workloads.