Stream: git-wasmtime

Topic: wasmtime / issue #12787 Signal handler violates async-sig...


view this post on Zulip Wasmtime GitHub notifications bot (Mar 16 2026 at 18:24):

jbachorik opened issue #12787:

Summary

Wasmtime's SIGSEGV/SIGBUS trap handler (trap_handler in crates/wasmtime/src/runtime/vm/sys/unix/signals.rs) violates POSIX async-signal-safety requirements by:

  1. Accessing thread-local storage via tls::with(), which can trigger __tls_get_addr
  2. __tls_get_addr calls malloc() on first TLS access for a thread
  3. malloc() is not async-signal-safe and can deadlock if the interrupted code was holding malloc's internal lock

Reproduction

This causes deadlocks when wasmtime is used alongside other software that:

The deadlock occurs when:

1. Thread A is inside malloc() holding its internal lock
2. SIGSEGV arrives (from safefetch or other intentional fault)
3. Wasmtime's trap_handler runs
4. tls::with() → __tls_get_addr → malloc()
5. malloc() tries to acquire the lock already held → DEADLOCK

Stack trace from deadlock

  #7  __tls_get_addr () at ../sysdeps/x86_64/tls_get_addr.S:55
  #8  wasmtime_runtime::traphandlers::tls::raw::get ()
  #9  wasmtime_runtime::traphandlers::unix::trap_handler ()
  #10 <signal handler called>
  #11 safefetch64_impl ()  // intentional SIGSEGV for safe memory access
  ...
  #27 sysmalloc ()
  #28 _int_malloc ()
  #29 __GI___libc_malloc ()  // original malloc call that holds the lock

The code comment acknowledges this

From signals.rs:
"The main current requirement of the signal handler in terms of stack space is that malloc/realloc are called to create a Backtrace of wasm frames."

Suggested fix

1. Use tls_eager_initialize() on thread creation to ensure TLS is initialized before any signal can arrive
2. Consider using a signal-safe TLS mechanism (e.g., dedicated pthread key with pre-allocated storage)
3. Avoid any allocation in the signal handler fast path - defer backtrace creation

Environment

- wasmtime-jni 0.19.0
- Linux x86_64
- glibc

view this post on Zulip Wasmtime GitHub notifications bot (Mar 16 2026 at 19:58):

bjorn3 commented on issue #12787:

FWIW the rust standard library already does tls accesses in its SIGSEGV handler. I believe the latest glibc release also no longer does lazy allocation of tls slots. Musl has never done lazy allocation.

Use tls_eager_initialize() on thread creation to ensure TLS is initialized before any signal can arrive

This is something you can do yourself.

Consider using a signal-safe TLS mechanism (e.g., dedicated pthread key with pre-allocated storage)

Pthread keys don't have pre-allocated storage AFAIK. If anything they are more likely to be lazily allocated I would think. Unlike std::thread_local!(), they are not visible to libc at startup time when the initial tls storage is allocated.

Avoid any allocation in the signal handler fast path - defer backtrace creation

How? After the longjmp out of the signal handler, the wasm stack frames are gone and thus can't be included in any backtrace we generate afterwards.

view this post on Zulip Wasmtime GitHub notifications bot (Mar 16 2026 at 23:01):

alexcrichton commented on issue #12787:

I'd agree that I don't really know exactly what we can do about this. Apart from @jbachorik the bindings to wasmtime explicitly calling Engine::tls_eager_initialize I think that's sort of the best solution here.

Our signal handler in general isn't async-signal safe, and that's a known property. This includes TLS but if it's a fault in Wasmtime we go so far as to lock data structures as well. This works out in Wasmtime because wasmtime-generated signals aren't happening asynchronously, but if an external system is generating asynchronous signals that causes problems.

Being able to access TLS is a pretty fundamental part of our signal handler and I'm not sure how we'd paper over that or remove it. With native TLS not being async-signal safe, and pthread-realted functions also not being async-signal-safe, I'm not sure what we could do.

That's what leads me to: @jbachorik is it possible to use tls_eager_initialize?

view this post on Zulip Wasmtime GitHub notifications bot (Mar 17 2026 at 13:05):

jbachorik commented on issue #12787:

FWIW the rust standard library already does tls accesses in its SIGSEGV handler

I don't think this is true, according to https://doc.rust-lang.org/src/std/sys/pal/unix/stack_overflow/thread_info.rs.html

//! Unfortunately, because thread local storage isn't async-signal-safe, we
//! cannot soundly use it in our stack overflow handler. While this works
//! without problems on most platforms, it can lead to undefined behaviour
//! on others (such as GNU/Linux). Luckily, the POSIX specification documents
//! two thread-specific values that can be accessed in asynchronous signal
//! handlers: the value of `pthread_self()` and the address of `errno`. As
//! `pthread_t` is an opaque platform-specific type, we use the address of
//! `errno` here. As it is thread-specific and does not change over the
//! lifetime of a thread, we can use `&errno` as a key for a `BTreeMap`
//! that stores thread-specific data.

In general, POSIX says that if an asynchronous signal handler calls outside the Section 2.4 “Signal Concepts” safe set, behavior is undefined. In real libcs, many “normal” functions rely on internal locks and mutable global state (stdio buffers, allocator metadata). If the signal interrupts code while those internals are locked or mid-update, a handler that calls back into them can deadlock (lock re-entry) or corrupt state, which can later surface as crashes.

In asynchronously executed signal handlers, restrict code to async-signal-safe operations only. POSIX specifies undefined behavior if a handler calls POSIX functions outside the safe set (POSIX.1-2017 §2.4 “Signal Concepts”; see signal()), and sigaction(3p) notes POSIX does not define behavior when an unsafe handler interrupts unsafe library code. ISO C is stricter: in an asynchronous handler, calling most of the standard library is undefined behavior (C11 draft N1570 §7.14.1.1). In practice, violations can deadlock (re-entering libc while it holds internal locks) or cause corruption/crashes (re-entering while internal state is mid-update).


This works out in Wasmtime because wasmtime-generated signals aren't happening asynchronously

Ok, so you expect to be reacting only to signals your runtime generated in a controlled manner? In that case, the signal handler should immediately pass the signal up the handler chain when it detects the signal does not belong to it.

Maybe you are already doing that; but there is one weird caveat with the compiler-based TLS values I noticed in our profiler code - sometimes the compiler decides to hoist the TLS initialization before the check and branch and you end up with the TLS initialized unconditionally even if you are not processing the signal. Would be probably worth checking.

view this post on Zulip Wasmtime GitHub notifications bot (Mar 17 2026 at 13:23):

bjorn3 commented on issue #12787:

I don't think this is true, according to https://doc.rust-lang.org/src/std/sys/pal/unix/stack_overflow/thread_info.rs.html

Looks like that was changed in https://github.com/rust-lang/rust/commit/84bb0f07e6c3e920db567ff95a5f8365cf042c75.

In that case, the signal handler should immediately pass the signal up the handler chain when it detects the signal does not belong to it.

It does, bubble up the signal if it is not for us, unless StoreExt::set_signal_handler was used to override the signal handler for a specific store, in which case it unconditionally runs said signal handler. To know if a signal handler override is present for the store Wasmtime needs to lookup the current store in thread local storage: https://github.com/bytecodealliance/wasmtime/blob/a1a74d3a174e52ee71e33a076b8c55801a166dbe/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs#L163-L165, https://github.com/bytecodealliance/wasmtime/blob/a1a74d3a174e52ee71e33a076b8c55801a166dbe/crates/wasmtime/src/runtime/vm/traphandlers.rs#L1027-L1036 This API was added in https://github.com/bytecodealliance/wasmtime/pull/620. I'm not sure if it is necessary anymore after https://github.com/bytecodealliance/wasmtime/pull/1577.

view this post on Zulip Wasmtime GitHub notifications bot (Mar 18 2026 at 05:41):

alexcrichton commented on issue #12787:

That's an... interesting solution for libstd, but not really in the cards for us. Libstd has a one-time setup of signal handling state whereas in Wasmtime we're constantly frobbing the TLS slots here, so I don't think we can take the perf hit of inserting a BTreeMap around that.

Looking at other engines, it looks like v8 hits a thread local and SpiderMonkey does the same.


StoreExt::set_signal_handler I don't think would help here. My assumption is that if a thread has executed wasm it's fine because it's initialized TLS. The problem is that Wasmtime's TLS access means that for threads that Wasmtime hasn't touched it'll initialize TLS when Wasmtime's signal handler doesn't need to run at all. If this is something we want to fix, I don't see how we'd solve this without jettisoning our use of TLS entirely.

view this post on Zulip Wasmtime GitHub notifications bot (Mar 18 2026 at 06:51):

bjorn3 commented on issue #12787:

I mean if we get rid of set_signal_handler we can delay the tls access until after we have checked that the signal was caused by wasm code.


Last updated: Mar 23 2026 at 16:19 UTC