Home Posts Rust 1.95: Linear Types and the Future of Memory Safety
Systems Engineering

Rust 1.95: Linear Types and the Future of Memory Safety [Deep Dive]

Rust 1.95: Linear Types and the Future of Memory Safety
Dillip Chowdary
Dillip Chowdary
Systems Architect · April 23, 2026 · 12 min read

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 a Project derive replace pin-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; MustMove only 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

PropertyAffine (default)Linear (MustMove)Edge
Catches forgot-to-consume bugsRuntime / Drop side effectsCompile-time errorLinear
Ergonomics for happy-path codeImplicit drop is convenientEvery consumer must be namedAffine
Backward compatibilityAlwaysOpt-in via marker traitAffine (default-safe)
Cancellation safety in asyncDrop runs on .await cancelCancel is itself a use; can be bannedLinear
Runtime costNoneNoneTie (both compile-time)
Useful for irreversible resourcesDiscipline-basedType-system enforcedLinear
Library compatibilityUniversalForces semver-major in some cratesAffine
Drop still runs?YesYes — orthogonal to MustMoveTie

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 self by value. That's how the compiler knows the value has been "used up." A method that takes &self or &mut self doesn't satisfy the linear obligation.
  • Drop still runs. If you somehow do leak the value (e.g., via std::mem::forget), Drop behaves exactly as before. MustMove is a static check, not a runtime mechanism.
  • ? propagation works. When a function returns Result<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-lite 0.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 MustMove opt-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 MustMove is 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. MustMove is 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

  1. rustup update stable. The toolchain bump is the only mandatory step.
  2. Run cargo build against the old workspace. Existing affine code keeps compiling. The only breaks come from dependencies that opted types into MustMove in the same release; those produce explicit E0710 errors.
  3. 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.
  4. Migrate pin-project-lite consumers. Optional but recommended for new code; existing usage continues to work via the language-pin feature.
  5. 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? +
Rust's existing affine types guarantee a value is used at most once — you can drop it implicitly without consuming it. Linear types (via the new 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? +
No. 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? +
No. 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? +
Yes for new code. Rust 1.95 introduces #[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.

Found this useful? Share it.