Skip to content

Windows shutdown crash: TLS destructor accesses memory released by VirtualVector #885

Closed

Description

@DamoyY

Summary

On Windows, a Rust process using snmalloc-rs as its global allocator can crash during process shutdown when a thread-local value is dropped. The crash is reproducible with a thread-local Arc and does not require Tokio or any application-specific code.

This appears to be an ordering problem: snmalloc's Windows VirtualVector reservation destructor releases allocator memory before Rust's Windows TLS destructor drops the thread-local value. The TLS drop then performs an atomic decrement through memory that has already been released.

Environment

  • snmalloc commit: e9f7b2ebd11fcadd5913c95d90770902e8126646 (snmalloc-rs 0.7.5)
  • Rust: rustc 1.100.0-nightly (5ceaf6608 2026-09-25)
  • Target: x86_64-pc-windows-msvc
  • MSVC toolchain
  • Windows 11 Insider Preview, build 10.0.29671

Minimal reproduction

use std::sync::Arc;

#[global_allocator]
static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;

thread_local! {
    static VALUE: Arc<usize> = Arc::new(42);
}

fn main() {
    VALUE.with(|value| assert_eq!(**value, 42));
    println!("main completed");
}

Cargo.toml dependency:

[dependencies]
snmalloc-rs = { git = "https://github.com/microsoft/snmalloc.git", rev = "e9f7b2ebd11fcadd5913c95d90770902e8126646" }

Build and run on Windows:

cargo run

Observed output:

main completed
error: process didn't exit successfully (...snmalloc_tls.exe)
(exit code: 0xc0000005, STATUS_ACCESS_VIOLATION)

The process exits successfully when the #[global_allocator] declaration is removed.

Debugger evidence

The crash is deterministic in both debug and release builds. The failing instruction is Rust's atomic decrement while dropping the TLS value:

snmalloc_tls!core::sync::atomic::atomic_sub<usize,usize>+0x60
snmalloc_tls!alloc::sync::impl$42::drop<usize,alloc::alloc::Global>
snmalloc_tls!core::ptr::drop_glue<alloc::sync::Arc<usize,alloc::alloc::Global>>
snmalloc_tls!std::sys::thread_local::native::lazy::destroy<alloc::sync::Arc<usize,alloc::alloc::Global>>
ntdll!RtlpFlsDataCleanup

The accessed address is already in a MEM_FREE region. A breakpoint on VirtualFree shows the preceding calls originate from:

snmalloc::VirtualVector::~VirtualVector
snmalloc::`dynamic atexit destructor for 'reservations''

Skipping the VirtualVector destructor in the debugger makes the same binary exit with code 0, which confirms that the shutdown ordering is involved.

Questions

Is this shutdown ordering expected to be supported for Rust TLS values? If so, could the Windows reservation cleanup be deferred until after Rust TLS destructors, or otherwise avoid releasing allocator metadata while TLS destructors can still allocate or deallocate? A documented opt-out for allocator finalization would also be useful for applications where the OS will reclaim memory at process exit.

This is related to #809, but the reproduction here is an immediate use-after-free crash rather than slow post-main cleanup.

Activity

  1. mjp41 commented on Sep 29, 2026

    @mjp41
    Member

    Interesting. This was added by
    #773

    Neil Monday (@NeilMonday) the tricks you applied in that PR involved specific segments to give correct teardown ordering. I'm guessing that the Rust compiler is either not using those pragmas, or the Rust routine has different order.

    Schrodinger ZHU Yifan (@SchrodingerZhu) do you know much about the Rust teardown ordering.

  2. mjp41 commented on Sep 30, 2026

    @mjp41
    Member

    #886 is working on this

  3. mjp41 commented on Sep 30, 2026

    @mjp41
    Member

    The problem is not Rust specific.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions