Hey folks, Rust 1.93 is here

On January 22, 2026, Rust 1.93 was officially released.

Looking at this version number, you might think: “1.92 just came out a month ago, what big changes could there be?”

Well, there are actually several improvements that feel really nice to use. It’s like when your regular breakfast spot suddenly switches to a better hot sauce - same food, but it just hits different.

First, how to upgrade:

rustup update stable

Done.

Why this version matters

One word to describe Rust 1.93: refinement.

Things that felt awkward before got fixed; places where you had to take detours got straightened out. A few highlights:

All *-linux-musl targets unified to musl 1.2.5, making static linking more reliable. You can now use #[cfg] for conditional compilation inside assembly, no more copy-pasting entire asm blocks. Global allocators can safely use thread_local! - a restriction that’s been blocking people for years.

These are all “hurts when you need it, forget about it when you don’t” kind of changes. Let’s go through them one by one.

musl 1.2.5: Good news for static linking folks

If you’ve done static linking with Rust binaries, you definitely know about musl.

musl is a lightweight C standard library implementation specifically designed for static linking. Simply put, it packages all your program’s dependencies into a single binary file that you can drop on a server and run directly, no need to install libgcc, glibc, and all that.

What scenario is this great for?

Container images. You’ve built images with Alpine Linux, right? That thing is tiny - the whole system is just a few dozen megabytes. But Alpine uses musl, not glibc. If you throw in a standard Rust-compiled binary, it probably won’t run because the two libc implementations don’t match.

Before, you had to compile with targets like x86_64-unknown-linux-musl, but you’d occasionally run into weird issues like DNS resolution acting up or certain symbols not being found.

Now with musl upgraded to 1.2.5, most of these problems are fixed. Especially DNS-related bugs - 1.2.4 and 1.2.5 put in serious work on those.

Potential hiccups

musl 1.2.4 removed some old libc symbols. If one of your dependencies is still using a very old libc version, you might get “undefined reference” errors.

The solution is simple:

cargo update

Run that, update dependencies to the latest versions, and it’s usually solved. The Rust team ran crater tests - not many projects break, mostly old projects that haven’t been updated in years.

To verify your static build works, try this:

rustup target add x86_64-unknown-linux-musl

RUSTFLAGS="-C target-feature=+crt-static" cargo build --target x86_64-unknown-linux-musl --release

If you hit undefined reference to 'open64' issues, upgrade libc to version 0.2.146 or above.

#[cfg] in assembly: No more copy-paste

When writing inline assembly, you might have encountered this need: output different assembly instructions based on different CPU features. Like using vector instructions on machines that support SSE2, and regular instructions on those that don’t.

How did you write it before? Copy the entire asm block and change the condition:

// SSE2 version
#[cfg(target_feature = "sse2")]
unsafe {
    std::arch::asm!(
        "pxor %xmm0, %xmm0",
        options(nostack, preserves_flags)
    );
}

// Non-SSE2 version
#[cfg(not(target_feature = "sse2"))]
unsafe {
    std::arch::asm!(
        "mov $0, %eax",
        options(nostack, preserves_flags)
    );
}

Copy-paste aside, it’s easy to miss changes. If you have ten instructions with only one different, you’d have to copy ten times.

Now it’s better - just write #[cfg] directly in the asm:

use std::arch::asm;

unsafe {
    asm!(
        "nop",
        #[cfg(target_feature = "sse2")]
        "pxor %xmm0, %xmm0",
        options(nostack, preserves_flags)
    );
}

See, just add #[cfg(target_feature = "sse2")], and this line only gets compiled when SSE2 is enabled.

Much cleaner. It’s like the pain of writing C++ templates before if constexpr - you had to write two sets of functions. The finer the granularity of conditional compilation, the cleaner the code. This principle applies to any language.

Who needs this?

If you’re not doing low-level development, operating systems, or cryptography, you might not need it. But for people in those trenches, this feature saves a lot of effort.

Global allocators and TLS: Long-standing debt finally paid

This change has been awaited for a long time.

Before, if you wrote a custom global allocator in Rust and called thread_local! or std::thread::current() in the alloc method, the compiler would error out saying “re-entrancy doom-loop.”

Why? thread_local! itself needs to allocate memory to store thread-local data, and what you’re implementing is precisely the global allocator - it becomes calling itself. Like trying to lift your right hand with your left hand.

But many reasonable allocator designs need to track allocation statistics per thread. Like if you want to know how much memory each thread is using for performance analysis. Before, you had to work around it - either write the allocator in C or use various hacks.

1.93 solved this problem. The standard library internally uses the system allocator as a fallback, so no more infinite loops.

Here’s an example:

use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;

struct MyAlloc;

thread_local! {
    static ALLOCATED: Cell<usize> = Cell::new(0);
}

unsafe impl GlobalAlloc for MyAlloc {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        let p = System.alloc(layout);
        if !p.is_null() {
            // Can now safely use thread_local!
            ALLOCATED.with(|c| c.set(c.get() + layout.size()));
        }
        p
    }
}

#[global_allocator]
static A: MyAlloc = MyAlloc;

This code wouldn’t compile before 1.93, but now it’s fine. Thread-level memory statistics, cache pool management - all doable this way.

Newly stabilized APIs

This release stabilized a batch of APIs. Let me highlight a few practical ones.

Array and slice conversions

The Slices ↔ arrays feature is super useful when parsing protocols.

Before, if you wanted to convert the first four bytes of a slice into an array, you’d write:

let arr: &[u8; 4] = buf[..4].try_into().unwrap();

Now it’s one line:

let arr = buf.get(..4).and_then(|s| s.as_array());

Look at this line - doesn’t it feel like picking up a package from a locker? Before, you had to take the package out of the locker and put it in a basket; now it’s one step, locker becomes basket.

Zero-copy: into_raw_parts

Zero-copy memory operation diagram

String::into_raw_parts and Vec::into_raw_parts are finally stable.

What does this mean? The memory layout of String and Vec is contiguous: pointer, length, capacity. When passing to C code or doing advanced memory operations, before you had to use as_ptr() and len() to get this information separately.

Now it’s one line:

let s = String::from("hello");
let (ptr, len, cap) = s.into_raw_parts();

// ... pass to C, or stash ...

// Can reassemble it
let s2 = unsafe { String::from_raw_parts(ptr, len, cap) };
assert_eq!(s2, "hello");

Note: After into_raw_parts, you’ve taken over the original String - memory is yours to manage. Forget to free it and you get a memory leak, free it too early and you get double-free.

It’s like taking a package from the delivery station to keep yourself - the station doesn’t manage it anymore. You either keep it safe or remember to return it.

VecDeque conditional pop

VecDeque::pop_front_if and pop_back_if are also stable.

Before, if you wanted to pop an element that meets a condition, you’d write:

let front = q.front();
if front.map(|&x| x % 2 == 1).unwrap_or(false) {
    q.pop_front();
}

Now it’s one line:

q.pop_front_if(|&x| x % 2 == 1);

Great for TTL expiration deletion, deduplication, queue merging scenarios.

Duration nanosecond support

Duration::from_nanos_u128 is here too.

Before, if you wanted to create a Duration from a very large nanosecond count, you had to split it yourself:

let nanos = 10_u128.pow(12);
let secs = (nanos / 1_000_000_000) as u64;
let subsec_nanos = (nanos % 1_000_000_000) as u32;
let duration = Duration::new(secs, subsec_nanos);

Now just do:

use std::time::Duration;

let d = Duration::from_nanos_u128(10_u128.pow(12)); // 1,000,000,000,000ns

Exceeding Duration::MAX will panic, which is better than overflowing and getting a completely wrong time.

Upgrade guide

Upgrading to Rust 1.93 is two steps.

Update the toolchain:

rustup update stable
cargo update

If using musl, verify the static build:

# Add musl target
rustup target add x86_64-unknown-linux-musl

# Build test
RUSTFLAGS="-C target-feature=+crt-static" cargo build --target x86_64-unknown-linux-musl --release

That’s it.

Final thoughts

Rust 1.93 isn’t a “wow” major version, but every change is solid:

  • musl 1.2.5 makes static builds more reliable
  • #[cfg] inside asm eliminates duplicate code
  • Global allocators can use TLS now
  • Zero-copy APIs make FFI less convoluted

This kind of version often signals the ecosystem maturing. Those awkward spots are getting smoothed out one by one, and Rust keeps getting more pleasant to write.

Alright, that’s it for today. Give the upgrade a try, and see you in the comments if you have issues.


If you found this useful, give it a like so more people can see it. Share it with your Rust-writing friends too.

Any questions, let’s chat in the comments.