Repository: bytecodealliance/wasmtime
Pull request #9030 (2024-07-27 18:34:17)

Fix `fmin`/`fmax` cprop miscompilation and add `f16`/`f128` `fmin`/`fmax` cprop support
Currently, constant propagation for `f32`/`f64` `fmin`/`fmax` always outputs the first input when both inputs are zeros. However, the semantics of the `fmin`/`fmax` instructions require that `-0.0` be treated as less than `0.0`. This PR fixes this and adds test cases to ensure the issue does not reoccur. Const prop support for `f16`/`f128` `fmin`/`fmax` is also added.

`f16`/`f128` issue: #8312

github-actions[bot]: #### Subscribe to Label Action

cc @cfallin, @fitzgen

<details>
This issue or pull request has been labeled: "cranelift", "isle"

Thus the following users have been cc'd because of the following labels:

* cfallin: isle
* fitzgen: isle

To subscribe or unsubscribe from this label, edit the <code>.github/subscribe-to-label.json</code> configuration file.

[Learn more.](https://github.com/bytecodealliance/subscribe-to-label-action)
</details>

Patch:
diff --git a/cranelift/codegen/src/ir/immediates.rs b/cranelift/codegen/src/ir/immediates.rs
index f96d678b8..d9efcac27 100644
--- a/cranelift/codegen/src/ir/immediates.rs
+++ b/cranelift/codegen/src/ir/immediates.rs
@@ -512,11 +512,13 @@ macro_rules! ieee_float {
             const SIGN_MASK: $bits_ty = 1 << (Self::EXPONENT_BITS + Self::SIGNIFICAND_BITS);
             const SIGNIFICAND_MASK: $bits_ty = $bits_ty::MAX >> (Self::EXPONENT_BITS + 1);
             const EXPONENT_MASK: $bits_ty = !Self::SIGN_MASK & !Self::SIGNIFICAND_MASK;
+            /// The positive WebAssembly canonical NaN.
+            pub const NAN: Self = Self::with_bits(Self::EXPONENT_MASK | (1 << (Self::SIGNIFICAND_BITS - 1)));
 
             /// Create a new
             #[doc = concat!("`", stringify!($name), "`")]
             /// containing the bits of `bits`.
-            pub fn with_bits(bits: $bits_ty) -> Self {
+            pub const fn with_bits(bits: $bits_ty) -> Self {
                 Self { bits }
             }
 
@@ -550,6 +552,42 @@ macro_rules! ieee_float {
                 Self::with_bits((self.bits() & !Self::SIGN_MASK) | (sign.bits() & Self::SIGN_MASK))
             }
 
+            /// Returns the minimum of `self` and `other`, following the WebAssembly/IEEE 754-2019 definition.
+            pub fn minimum(self, other: Self) -> Self {
+                // FIXME: Replace with Rust float method once it is stabilised.
+                if self.is_nan() || other.is_nan() {
+                    Self::NAN
+                } else if self.is_zero() && other.is_zero() {
+                    if self.is_negative() {
+                        self
+                    } else {
+                        other
+                    }
+                } else if self <= other {
+                    self
+                } else {
+                    other
+                }
+            }
+
+            /// Returns the maximum of `self` and `other`, following the WebAssembly/IEEE 754-2019 definition.
+            pub fn maximum(self, other: Self) -> Self {
+                // FIXME: Replace with Rust float method once it is stabilised.
+                if self.is_nan() || other.is_nan() {
+                    Self::NAN
+                } else if self.is_zero() && other.is_zero() {
+                    if self.is_positive() {
+                        self
+                    } else {
+                        other
+                    }
+                } else if self >= other {
+                    self
+                } else {
+                    other
+                }
+            }
+
             /// Create an
             #[doc = concat!("`", stringify!($name), "`")]
             /// number representing `2.0^n`.
@@ -583,6 +621,11 @@ macro_rules! ieee_float {
                 self.abs().bits() > Self::EXPONENT_MASK
             }
 
+            /// Returns true if `self` has a negative sign, including 0.0, NaNs with positive sign bit and positive infinity.
+            pub fn is_positive(self) -> bool {
+                !self.is_negative()
+            }
+
             /// Returns true if `self` has a negative sign, including -0.0, NaNs with negative sign bit and negative infinity.
             pub fn is_negative(self) -> bool {
                 self.bits() & Self::SIGN_MASK == Self::SIGN_MASK
@@ -641,8 +684,8 @@ macro_rules! ieee_float {
                         // Zeros are always equal regardless of sign.
                         return Some(Ordering::Equal);
                     }
-                    let lhs_positive = !self.is_negative();
-                    let rhs_positive = !rhs.is_negative();
+                    let lhs_positive = self.is_positive();
+                    let rhs_positive = rhs.is_positive();
                     if lhs_positive != rhs_positive {
                         // Different signs: negative < positive
                         return lhs_positive.partial_cmp(&rhs_positive);
diff --git a/cranelift/codegen/src/isle_prelude.rs b/cranelift/codegen/src/isle_prelude.rs
index ce6879cb7..64c343ea6 100644
--- a/cranelift/codegen/src/isle_prelude.rs
+++ b/cranelift/codegen/src/isle_prelude.rs
@@ -938,6 +938,14 @@ macro_rules! isle_common_prelude_methods {
             }
         }
 
+        fn f16_min(&mut self, a: Ieee16, b: Ieee16) -> Option<Ieee16> {
+            a.minimum(b).non_nan()
+        }
+
+        fn f16_max(&mut self, a: Ieee16, b: Ieee16) -> Option<Ieee16> {
+            a.maximum(b).non_nan()
+        }
+
         fn f16_neg(&mut self, n: Ieee16) -> Ieee16 {
             -n
         }
@@ -987,23 +995,11 @@ macro_rules! isle_common_prelude_methods {
         }
 
         fn f32_min(&mut self, a: Ieee32, b: Ieee32) -> Option<Ieee32> {
-            if a.is_nan() || b.is_nan() {
-                None
-            } else if a <= b {
-                Some(a)
-            } else {
-                Some(b)
-            }
+            a.minimum(b).non_nan()
         }
 
         fn f32_max(&mut self, a: Ieee32, b: Ieee32) -> Option<Ieee32> {
-            if a.is_nan() || b.is_nan() {
-                None
-            } else if a >= b {
-                Some(a)
-            } else {
-                Some(b)
-            }
+            a.maximum(b).non_nan()
         }
 
         fn f32_neg(&mut self, n: Ieee32) -> Ieee32 {
@@ -1055,23 +1051,11 @@ macro_rules! isle_common_prelude_methods {
         }
 
         fn f64_min(&mut self, a: Ieee64, b: Ieee64) -> Option<Ieee64> {
-            if a.is_nan() || b.is_nan() {
-                None
-            } else if a <= b {
-                Some(a)
-            } else {
-                Some(b)
-            }
+            a.minimum(b).non_nan()
         }
 
         fn f64_max(&mut self, a: Ieee64, b: Ieee64) -> Option<Ieee64> {
-            if a.is_nan() || b.is_nan() {
-                None
-            } else if a >= b {
-                Some(a)
-            } else {
-                Some(b)
-            }
+            a.maximum(b).non_nan()
         }
 
         fn f64_neg(&mut self, n: Ieee64) -> Ieee64 {
@@ -1086,6 +1070,14 @@ macro_rules! isle_common_prelude_methods {
             a.copysign(b)
         }
 
+        fn f128_min(&mut self, a: Ieee128, b: Ieee128) -> Option<Ieee128> {
+            a.minimum(b).non_nan()
+        }
+
+        fn f128_max(&mut self, a: Ieee128, b: Ieee128) -> Option<Ieee128> {
+            a.maximum(b).non_nan()
+        }
+
         fn f128_neg(&mut self, n: Ieee128) -> Ieee128 {
             -n
         }
diff --git a/cranelift/codegen/src/nan_canonicalization.rs b/cranelift/codegen/src/nan_canonicalization.rs
index 5be41857a..e6a73deaf 100644
--- a/cranelift/codegen/src/nan_canonicalization.rs
+++ b/cranelift/codegen/src/nan_canonicalization.rs
@@ -10,10 +10,6 @@ use crate::ir::{Function, Inst, InstBuilder, InstructionData, Opcode, Value};
 use crate::opts::MemFlags;
 use crate::timing;
 
-// Canonical 32-bit and 64-bit NaN values.
-static CANON_32BIT_NAN: u32 = 0b01111111110000000000000000000000;
-static CANON_64BIT_NAN: u64 = 0b0111111111111000000000000000000000000000000000000000000000000000;
-
 /// Perform the NaN canonicalization pass.
 pub fn do_nan_canonicalization(func: &mut Function, has_vector_support: bool) {
     let _tt = timing::canonicalize_nans();
@@ -95,7 +91,7 @@ fn add_nan_canon_seq(pos: &mut FuncCursor, inst: Inst, has_vector_support: bool)
 
     match val_type {
         types::F32 => {
-            let canon_nan = pos.ins().f32const(Ieee32::with_bits(CANON_32BIT_NAN));
+            let canon_nan = pos.ins().f32const(Ieee32::NAN);
             if has_vector_support {
                 vectorized_scalar_select(pos, canon_nan, types::F32X4);
             } else {
@@ -103,7 +99,7 @@ fn add_nan_canon_seq(pos: &mut FuncCursor, inst: Inst, has_vector_support: bool)
             }
         }
         types::F64 => {
-            let canon_nan = pos.ins().f64const(Ieee64::with_bits(CANON_64BIT_NAN));
+            let canon_nan = pos.ins().f64const(Ieee64::NAN);
             if has_vector_support {
                 vectorized_scalar_select(pos, canon_nan, types::F64X2);
             } else {
@@ -111,12 +107,12 @@ fn add_nan_canon_seq(pos: &mut FuncCursor, inst: Inst, has_vector_support: bool)
             }
         }
         types::F32X4 => {
-            let canon_nan = pos.ins().f32const(Ieee32::with_bits(CANON_32BIT_NAN));
+            let canon_nan = pos.ins().f32const(Ieee32::NAN);
             let canon_nan = pos.ins().splat(types::F32X4, canon_nan);
             vector_select(pos, canon_nan);
         }
         types::F64X2 => {
-            let canon_nan = pos.ins().f64const(Ieee64::with_bits(CANON_64BIT_NAN));
+            let canon_nan = pos.ins().f64const(Ieee64::NAN);
             let canon_nan = pos.ins().splat(types::F64X2, canon_nan);
             vector_select(pos, canon_nan);
         }
diff --git a/cranelift/codegen/src/opts/cprop.isle b/cranelift/codegen/src/opts/cprop.isle
index a9685be75..7eee7d219 100644
--- a/cranelift/codegen/src/opts/cprop.isle
+++ b/cranelift/codegen/src/opts/cprop.isle
@@ -350,19 +350,31 @@
       (if-let r (f64_nearest n))
       (subsume (f64const $F64 r)))
 
+(rule (simplify (fmin $F16 (f16const $F16 n) (f16const $F16 m)))
+      (if-let r (f16_min n m))
+      (subsume (f16const $F32 r)))
 (rule (simplify (fmin $F32 (f32const $F32 n) (f32const $F32 m)))
       (if-let r (f32_min n m))
       (subsume (f32const $F32 r)))
 (rule (simplify (fmin $F64 (f64const $F64 n) (f64const $F64 m)))
       (if-let r (f64_min n m))
       (subsume (f64const $F64 r)))
+(rule (simplify (fmin $F128 (f128const $F128 (ieee128_constant n)) (f128const $F128 (ieee128_constant m))))
+      (if-let r (f128_min n m))
+      (subsume (f128const $F128 (ieee128_constant r))))
 
+(rule (simplify (fmax $F16 (f16const $F16 n) (f16const $F16 m)))
+      (if-let r (f16_max n m))
+      (subsume (f16const $F16 r)))
 (rule (simplify (fmax $F32 (f32const $F32 n) (f32const $F32 m)))
       (if-let r (f32_max n m))
       (subsume (f32const $F32 r)))
 (rule (simplify (fmax $F64 (f64const $F64 n) (f64const $F64 m)))
       (if-let r (f64_max n m))
       (subsume (f64const $F64 r)))
+(rule (simplify (fmax $F128 (f128const $F128 (ieee128_constant n)) (f128const $F128 (ieee128_constant m))))
+      (if-let r (f128_max n m))
+      (subsume (f128const $F128 (ieee128_constant r))))
 
 (rule (simplify (fneg $F16 (f16const $F16 n)))
       (subsume (f16const $F16 (f16_neg n))))
diff --git a/cranelift/codegen/src/prelude.isle b/cranelift/codegen/src/prelude.isle
index 233ab899e..2e7b3ee87 100644
--- a/cranelift/codegen/src/prelude.isle
+++ b/cranelift/codegen/src/prelude.isle
@@ -239,6 +239,10 @@
 
 ;; Floating point operations
 
+(decl pure partial f16_min (Ieee16 Ieee16) Ieee16)
+(extern constructor f16_min f16_min)
+(decl pure partial f16_max (Ieee16 Ieee16) Ieee16)
+(extern constructor f16_max f16_max)
 (decl pure f16_neg (Ieee16) Ieee16)
 (extern constructor f16_neg f16_neg)
 (decl pure f16_abs (Ieee16) Ieee16)
@@ -301,6 +305,10 @@
 (extern constructor f64_abs f64_abs)
 (decl pure f64_copysign (Ieee64 Ieee64) Ieee64)
 (extern constructor f64_copysign f64_copysign)
+(decl pure partial f128_min (Ieee128 Ieee128) Ieee128)
+(extern constructor f128_min f128_min)
+(decl pure partial f128_max (Ieee128 Ieee128) Ieee128)
+(extern constructor f128_max f128_max)
 (decl pure f128_neg (Ieee128) Ieee128)
 (extern constructor f128_neg f128_neg)
 (decl pure f128_abs (Ieee128) Ieee128)
diff --git a/crates/wasmtime/src/engine.rs b/crates/wasmtime/src/engine.rs
index 77b35bfd3..495827514 100644
--- a/crates/wasmtime/src/engine.rs
+++ b/crates/wasmtime/src/engine.rs
@@ -699,6 +699,60 @@ impl Engine {
         code.publish()?;
         Ok(Arc::new(code))
     }
+
+    /// Unload process-related trap/signal handlers and destroy this engine.
+    ///
+    /// This method is not safe and is not widely applicable. It is not required
+    /// to be called and is intended for use cases such as unloading a dynamic
+    /// library from a process. It is difficult to invoke this method correctly
+    /// and it requires careful coordination to do so.
+    ///
+    /// # Panics
+    ///
+    /// This method will panic if this `Engine` handle is not the last remaining
+    /// engine handle.
+    ///
+    /// # Aborts
+    ///
+    /// This method will abort the process on some platforms in some situations
+    /// where unloading the handler cannot be performed and an unrecoverable
+    /// state is reached. For example on Unix platforms with signal handling
+    /// the process will be aborted if the current signal handlers are not
+    /// Wasmtime's.
+    ///
+    /// # Unsafety
+    ///
+    /// This method is not generally safe to call and has a number of
+    /// preconditions that must be met to even possibly be safe. Even with these
+    /// known preconditions met there may be other unknown invariants to uphold
+    /// as well.
+    ///
+    /// * There must be no other instances of `Engine` elsewhere in the process.
+    ///   Note that this isn't just copies of this `Engine` but it's any other
+    ///   `Engine` at all. This unloads global state that is used by all
+    ///   `Engine`s so this instance must be the last.
+    ///
+    /// * On Unix platforms no other signal handlers could have been installed
+    ///   for signals that Wasmtime catches. In this situation Wasmtime won't
+    ///   know how to restore signal handlers that Wasmtime possibly overwrote
+    ///   when Wasmtime was initially loaded. If possible initialize other
+    ///   libraries first and then initialize Wasmtime last (e.g. defer creating
+    ///   an `Engine`).
+    ///
+    /// * All existing threads which have used this DLL or copy of Wasmtime may
+    ///   no longer use this copy of Wasmtime. Per-thread state is not iterated
+    ///   and destroyed. Only future threads may use future instances of this
+    ///   Wasmtime itself.
+    ///
+    /// If other crashes are seen from using this method please feel free to
+    /// file an issue to update the documentation here with more preconditions
+    /// that must be met.
+    pub unsafe fn unload_process_handlers(self) {
+        assert_eq!(Arc::weak_count(&self.inner), 0);
+        assert_eq!(Arc::strong_count(&self.inner), 1);
+
+        crate::runtime::vm::deinit_traps();
+    }
 }
 
 /// A weak reference to an [`Engine`].
diff --git a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs
index c5964491e..46cfe310a 100644
--- a/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/custom/traphandlers.rs
@@ -20,8 +20,15 @@ pub unsafe fn wasmtime_setjmp(
     capi::wasmtime_setjmp(jmp_buf, callback, payload, callee.cast())
 }
 
-pub unsafe fn platform_init(_macos_use_mach_ports: bool) {
-    capi::wasmtime_init_traps(handle_trap);
+pub struct TrapHandler;
+
+impl TrapHandler {
+    pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler {
+        capi::wasmtime_init_traps(handle_trap);
+        TrapHandler
+    }
+
+    pub fn validate_config(&self, _macos_use_mach_ports: bool) {}
 }
 
 extern "C" fn handle_trap(ip: usize, fp: usize, has_faulting_addr: bool, faulting_addr: usize) {
diff --git a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
index 320a17861..0059e4b06 100644
--- a/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/miri/traphandlers.rs
@@ -30,7 +30,15 @@ pub fn wasmtime_longjmp(_jmp_buf: *const u8) -> ! {
 #[allow(missing_docs)]
 pub type SignalHandler<'a> = dyn Fn() + Send + Sync + 'a;
 
-pub unsafe fn platform_init(_macos_use_mach_ports: bool) {}
+pub struct TrapHandler;
+
+impl TrapHandler {
+    pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler {
+        TrapHandler
+    }
+
+    pub fn validate_config(&self, _macos_use_mach_ports: bool) {}
+}
 
 pub fn lazy_per_thread_init() {}
 
diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
index 83a4b948d..9ab83046d 100644
--- a/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/unix/machports.rs
@@ -57,28 +57,49 @@ static mut WASMTIME_PORT: mach_port_name_t = MACH_PORT_NULL;
 
 static mut CHILD_OF_FORKED_PROCESS: bool = false;
 
-pub unsafe fn platform_init() {
-    // Mach ports do not currently work across forks, so configure Wasmtime to
-    // panic in `lazy_per_thread_init` if the child attempts to invoke
-    // WebAssembly.
-    unsafe extern "C" fn child() {
-        CHILD_OF_FORKED_PROCESS = true;
+pub struct TrapHandler {
+    thread: Option<thread::JoinHandle<()>>,
+}
+
+impl TrapHandler {
+    pub unsafe fn new() -> TrapHandler {
+        // Mach ports do not currently work across forks, so configure Wasmtime to
+        // panic in `lazy_per_thread_init` if the child attempts to invoke
+        // WebAssembly.
+        unsafe extern "C" fn child() {
+            CHILD_OF_FORKED_PROCESS = true;
+        }
+        let rc = libc::pthread_atfork(None, None, Some(child));
+        assert_eq!(rc, 0, "failed to configure `pthread_atfork` handler");
+
+        // Allocate our WASMTIME_PORT and make sure that it can be sent to so we
+        // can receive exceptions.
+        let me = mach_task_self();
+        let kret = mach_port_allocate(me, MACH_PORT_RIGHT_RECEIVE, addr_of_mut!(WASMTIME_PORT));
+        assert_eq!(kret, KERN_SUCCESS, "failed to allocate port");
+        let kret =
+            mach_port_insert_right(me, WASMTIME_PORT, WASMTIME_PORT, MACH_MSG_TYPE_MAKE_SEND);
+        assert_eq!(kret, KERN_SUCCESS, "failed to insert right");
+
+        // Spin up our handler thread which will solely exist to service exceptions
+        // generated by other threads. Note that this is a background thread that
+        // we're not very interested in so it's detached here.
+        let thread = thread::spawn(|| handler_thread());
+
+        TrapHandler {
+            thread: Some(thread),
+        }
+    }
+}
+
+impl Drop for TrapHandler {
+    fn drop(&mut self) {
+        unsafe {
+            let kret = mach_port_destroy(mach_task_self(), WASMTIME_PORT);
+            assert_eq!(kret, KERN_SUCCESS, "failed to destroy port");
+            self.thread.take().unwrap().join().unwrap();
+        }
     }
-    let rc = libc::pthread_atfork(None, None, Some(child));
-    assert_eq!(rc, 0, "failed to configure `pthread_atfork` handler");
-
-    // Allocate our WASMTIME_PORT and make sure that it can be sent to so we
-    // can receive exceptions.
-    let me = mach_task_self();
-    let kret = mach_port_allocate(me, MACH_PORT_RIGHT_RECEIVE, addr_of_mut!(WASMTIME_PORT));
-    assert_eq!(kret, KERN_SUCCESS, "failed to allocate port");
-    let kret = mach_port_insert_right(me, WASMTIME_PORT, WASMTIME_PORT, MACH_MSG_TYPE_MAKE_SEND);
-    assert_eq!(kret, KERN_SUCCESS, "failed to insert right");
-
-    // Spin up our handler thread which will solely exist to service exceptions
-    // generated by other threads. Note that this is a background thread that
-    // we're not very interested in so it's detached here.
-    thread::spawn(|| handler_thread());
 }
 
 // Note that this is copied from Gecko at
@@ -133,13 +154,17 @@ unsafe fn handler_thread() {
         let mut request: ExceptionRequest = mem::zeroed();
         let kret = mach_msg(
             &mut request.body.Head,
-            MACH_RCV_MSG,
+            MACH_RCV_MSG | MACH_RCV_INTERRUPT,
             0,
             mem::size_of_val(&request) as u32,
             WASMTIME_PORT,
             MACH_MSG_TIMEOUT_NONE,
             MACH_PORT_NULL,
         );
+        if kret == MACH_RCV_PORT_CHANGED {
+            // this port has been closed;
+            break;
+        }
         if kret != KERN_SUCCESS {
             eprintln!("mach_msg failed with {} ({0:x})", kret);
             libc::abort();
diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs
index fd0d60753..862b3eb44 100644
--- a/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/unix/macos_traphandlers.rs
@@ -4,17 +4,28 @@ static mut USE_MACH_PORTS: bool = false;
 
 pub use super::signals::{wasmtime_longjmp, wasmtime_setjmp, SignalHandler};
 
-pub unsafe fn platform_init(macos_use_mach_ports: bool) {
-    USE_MACH_PORTS = macos_use_mach_ports;
-    if macos_use_mach_ports {
-        super::machports::platform_init();
-    } else {
-        super::signals::platform_init(false);
-    }
+pub enum TrapHandler {
+    Signals(super::signals::TrapHandler),
+    #[allow(dead_code)] // used for its drop
+    MachPorts(super::machports::TrapHandler),
 }
 
-pub fn using_mach_ports() -> bool {
-    unsafe { USE_MACH_PORTS }
+impl TrapHandler {
+    pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler {
+        USE_MACH_PORTS = macos_use_mach_ports;
+        if macos_use_mach_ports {
+            TrapHandler::MachPorts(super::machports::TrapHandler::new())
+        } else {
+            TrapHandler::Signals(super::signals::TrapHandler::new(false))
+        }
+    }
+
+    pub fn validate_config(&self, macos_use_mach_ports: bool) {
+        match self {
+            TrapHandler::Signals(t) => t.validate_config(macos_use_mach_ports),
+            TrapHandler::MachPorts(_) => assert!(macos_use_mach_ports),
+        }
+    }
 }
 
 pub fn lazy_per_thread_init() {
diff --git a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
index 9a12bd460..91db01bab 100644
--- a/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/unix/signals.rs
@@ -31,58 +31,114 @@ static mut PREV_SIGBUS: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
 static mut PREV_SIGILL: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
 static mut PREV_SIGFPE: MaybeUninit<libc::sigaction> = MaybeUninit::uninit();
 
-pub unsafe fn platform_init(macos_use_mach_ports: bool) {
-    // Either mach ports shouldn't be in use or we shouldn't be on macOS,
-    // otherwise the `machports.rs` module should be used instead.
-    assert!(!macos_use_mach_ports || !cfg!(target_os = "macos"));
-
-    let register = |slot: *mut libc::sigaction, signal: i32| {
-        let mut handler: libc::sigaction = mem::zeroed();
-        // The flags here are relatively careful, and they are...
-        //
-        // SA_SIGINFO gives us access to information like the program
-        // counter from where the fault happened.
-        //
-        // SA_ONSTACK allows us to handle signals on an alternate stack,
-        // so that the handler can run in response to running out of
-        // stack space on the main stack. Rust installs an alternate
-        // stack with sigaltstack, so we rely on that.
-        //
-        // SA_NODEFER allows us to reenter the signal handler if we
-        // crash while handling the signal, and fall through to the
-        // Breakpad handler by testing handlingSegFault.
-        handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK;
-        handler.sa_sigaction = trap_handler as usize;
-        libc::sigemptyset(&mut handler.sa_mask);
-        if libc::sigaction(signal, &handler, slot) != 0 {
-            panic!(
-                "unable to install signal handler: {}",
-                io::Error::last_os_error(),
-            );
-        }
-    };
+pub struct TrapHandler;
+
+impl TrapHandler {
+    /// Installs all trap handlers.
+    ///
+    /// # Unsafety
+    ///
+    /// This function is unsafe because it's not safe to call concurrently and
+    /// it's not safe to call if the trap handlers have already been initialized
+    /// for this process.
+    pub unsafe fn new(macos_use_mach_ports: bool) -> TrapHandler {
+        // Either mach ports shouldn't be in use or we shouldn't be on macOS,
+        // otherwise the `machports.rs` module should be used instead.
+        assert!(!macos_use_mach_ports || !cfg!(target_os = "macos"));
+
+        foreach_handler(|slot, signal| {
+            let mut handler: libc::sigaction = mem::zeroed();
+            // The flags here are relatively careful, and they are...
+            //
+            // SA_SIGINFO gives us access to information like the program
+            // counter from where the fault happened.
+            //
+            // SA_ONSTACK allows us to handle signals on an alternate stack,
+            // so that the handler can run in response to running out of
+            // stack space on the main stack. Rust installs an alternate
+            // stack with sigaltstack, so we rely on that.
+            //
+            // SA_NODEFER allows us to reenter the signal handler if we
+            // crash while handling the signal, and fall through to the
+            // Breakpad handler by testing handlingSegFault.
+            handler.sa_flags = libc::SA_SIGINFO | libc::SA_NODEFER | libc::SA_ONSTACK;
+            handler.sa_sigaction = trap_handler as usize;
+            libc::sigemptyset(&mut handler.sa_mask);
+            if libc::sigaction(signal, &handler, slot) != 0 {
+                panic!(
+                    "unable to install signal handler: {}",
+                    io::Error::last_os_error(),
+                );
+            }
+        });
+
+        TrapHandler
+    }
+
+    pub fn validate_config(&self, macos_use_mach_ports: bool) {
+        assert!(!macos_use_mach_ports || !cfg!(target_os = "macos"));
+    }
+}
 
+unsafe fn foreach_handler(mut f: impl FnMut(*mut libc::sigaction, i32)) {
     // Allow handling OOB with signals on all architectures
-    register(PREV_SIGSEGV.as_mut_ptr(), libc::SIGSEGV);
+    f(PREV_SIGSEGV.as_mut_ptr(), libc::SIGSEGV);
 
     // Handle `unreachable` instructions which execute `ud2` right now
-    register(PREV_SIGILL.as_mut_ptr(), libc::SIGILL);
+    f(PREV_SIGILL.as_mut_ptr(), libc::SIGILL);
 
     // x86 and s390x use SIGFPE to report division by zero
     if cfg!(target_arch = "x86_64") || cfg!(target_arch = "s390x") {
-        register(PREV_SIGFPE.as_mut_ptr(), libc::SIGFPE);
+        f(PREV_SIGFPE.as_mut_ptr(), libc::SIGFPE);
     }
 
     // Sometimes we need to handle SIGBUS too:
     // - On Darwin, guard page accesses are raised as SIGBUS.
     if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
-        register(PREV_SIGBUS.as_mut_ptr(), libc::SIGBUS);
+        f(PREV_SIGBUS.as_mut_ptr(), libc::SIGBUS);
     }
 
     // TODO(#1980): x86-32, if we support it, will also need a SIGFPE handler.
     // TODO(#1173): ARM32, if we support it, will also need a SIGBUS handler.
 }
 
+impl Drop for TrapHandler {
+    fn drop(&mut self) {
+        unsafe {
+            foreach_handler(|slot, signal| {
+                let mut prev: libc::sigaction = mem::zeroed();
+
+                // Restore the previous handler that this signal had.
+                if libc::sigaction(signal, slot, &mut prev) != 0 {
+                    eprintln!(
+                        "unable to reinstall signal handler: {}",
+                        io::Error::last_os_error(),
+                    );
+                    libc::abort();
+                }
+
+                // If our trap handler wasn't currently listed for this process
+                // then that's a problem because we have just corrupted the
+                // signal handler state and don't know how to remove ourselves
+                // from the signal handling state. Inform the user of this and
+                // abort the process.
+                if prev.sa_sigaction != trap_handler as usize {
+                    eprintln!(
+                        "
+Wasmtime's signal handler was not the last signal handler to be installed
+in the process so it's not certain how to unload signal handlers. In this
+situation the Engine::unload_process_handlers API is not applicable and requires
+perhaps initializing libraries in a different order. The process will be aborted
+now.
+"
+                    );
+                    libc::abort();
+                }
+            });
+        }
+    }
+}
+
 unsafe extern "C" fn trap_handler(
     signum: libc::c_int,
     siginfo: *mut libc::siginfo_t,
diff --git a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs
index 87159e065..26298e905 100644
--- a/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/sys/windows/traphandlers.rs
@@ -1,5 +1,6 @@
 use crate::runtime::vm::traphandlers::{tls, TrapTest};
 use crate::runtime::vm::VMContext;
+use std::ffi::c_void;
 use std::io;
 use windows_sys::Win32::Foundation::*;
 use windows_sys::Win32::System::Diagnostics::Debug::*;
@@ -23,15 +24,43 @@ extern "C" {
 /// Function which may handle custom signals while processing traps.
 pub type SignalHandler<'a> = dyn Fn(*mut EXCEPTION_POINTERS) -> bool + Send + Sync + 'a;
 
-pub unsafe fn platform_init(_macos_use_mach_ports: bool) {
-    // our trap handler needs to go first, so that we can recover from
-    // wasm faults and continue execution, so pass `1` as a true value
-    // here.
-    if AddVectoredExceptionHandler(1, Some(exception_handler)).is_null() {
-        panic!(
-            "failed to add exception handler: {}",
-            io::Error::last_os_error()
-        );
+pub struct TrapHandler {
+    handle: *mut c_void,
+}
+
+unsafe impl Send for TrapHandler {}
+unsafe impl Sync for TrapHandler {}
+
+impl TrapHandler {
+    pub unsafe fn new(_macos_use_mach_ports: bool) -> TrapHandler {
+        // our trap handler needs to go first, so that we can recover from
+        // wasm faults and continue execution, so pass `1` as a true value
+        // here.
+        let handle = AddVectoredExceptionHandler(1, Some(exception_handler));
+        if handle.is_null() {
+            panic!(
+                "failed to add exception handler: {}",
+                io::Error::last_os_error()
+            );
+        }
+        TrapHandler { handle }
+    }
+
+    pub fn validate_config(&self, _macos_use_mach_ports: bool) {}
+}
+
+impl Drop for TrapHandler {
+    fn drop(&mut self) {
+        unsafe {
+            let rc = RemoveVectoredExceptionHandler(self.handle);
+            if rc == 0 {
+                eprintln!(
+                    "failed to remove exception handler: {}",
+                    io::Error::last_os_error()
+                );
+                libc::abort();
+            }
+        }
     }
 }
 
diff --git a/crates/wasmtime/src/runtime/vm/traphandlers.rs b/crates/wasmtime/src/runtime/vm/traphandlers.rs
index 365354e42..8bec87684 100644
--- a/crates/wasmtime/src/runtime/vm/traphandlers.rs
+++ b/crates/wasmtime/src/runtime/vm/traphandlers.rs
@@ -14,7 +14,7 @@ use crate::prelude::*;
 use crate::runtime::module::lookup_code;
 use crate::runtime::vm::sys::traphandlers;
 use crate::runtime::vm::{Instance, VMContext, VMRuntimeLimits};
-use crate::sync::OnceLock;
+use crate::sync::RwLock;
 use core::cell::{Cell, UnsafeCell};
 use core::mem::MaybeUninit;
 use core::ptr;
@@ -25,6 +25,16 @@ pub use self::tls::{tls_eager_initialize, AsyncWasmCallState, PreviousAsyncWasmC
 
 pub use traphandlers::SignalHandler;
 
+/// Platform-specific trap-handler state.
+///
+/// This state is protected by a lock to synchronize access to it. Right now
+/// it's a `RwLock` but it could be a `Mutex`, and `RwLock` is just chosen for
+/// convenience as it's what's implemented in no_std. The performance here
+/// should not be of consequence.
+///
+/// This is initialized to `None` and then set as part of `init_traps`.
+static TRAP_HANDLER: RwLock<Option<traphandlers::TrapHandler>> = RwLock::new(None);
+
 /// This function is required to be called before any WebAssembly is entered.
 /// This will configure global state such as signal handlers to prepare the
 /// process to receive wasm traps.
@@ -37,18 +47,36 @@ pub use traphandlers::SignalHandler;
 /// This function will also panic if the `std` feature is disabled and it's
 /// called concurrently.
 pub fn init_traps(macos_use_mach_ports: bool) {
-    static INIT: OnceLock<()> = OnceLock::new();
-
-    INIT.get_or_init(|| unsafe {
-        traphandlers::platform_init(macos_use_mach_ports);
-    });
-
-    #[cfg(target_os = "macos")]
-    assert_eq!(
-        traphandlers::using_mach_ports(),
-        macos_use_mach_ports,
-        "cannot configure two different methods of signal handling in the same process"
-    );
+    let mut lock = TRAP_HANDLER.write();
+    match lock.as_mut() {
+        Some(state) => state.validate_config(macos_use_mach_ports),
+        None => *lock = Some(unsafe { traphandlers::TrapHandler::new(macos_use_mach_ports) }),
+    }
+}
+
+/// De-initializes platform-specific state for trap handling.
+///
+/// # Panics
+///
+/// This function will also panic if the `std` feature is disabled and it's
+/// called concurrently.
+///
+/// # Aborts
+///
+/// This may abort the process on some platforms where trap handling state
+/// cannot be unloaded.
+///
+/// # Unsafety
+///
+/// This is not safe to be called unless all wasm code is unloaded. This is not
+/// safe to be called on some platforms, like Unix, when other libraries
+/// installed their own signal handlers after `init_traps` was called.
+///
+/// There's more reasons for unsafety here than those articulated above,
+/// generally this can only be called "if you know what you're doing".
+pub unsafe fn deinit_traps() {
+    let mut lock = TRAP_HANDLER.write();
+    let _ = lock.take();
 }
 
 fn lazy_per_thread_init() {


Test patch:
diff --git a/cranelift/filetests/filetests/egraph/cprop.clif b/cranelift/filetests/filetests/egraph/cprop.clif
index f1ca491f4..f6f0e0176 100644
--- a/cranelift/filetests/filetests/egraph/cprop.clif
+++ b/cranelift/filetests/filetests/egraph/cprop.clif
@@ -313,6 +313,72 @@ block0:
 ; check: v2 = iconst.i64 0xf0de_bc9a_7856_3412
 ; nextln: return v2
 
+function %f16_fmin() -> f16 {
+block0:
+    v1 = f16const -0x1.5p6
+    v2 = f16const -0x1.5p7
+    v3 = fmin v2, v1
+    return v3
+}
+
+; check: v4 = f16const -0x1.500p7
+; check: return v4  ; v4 = -0x1.500p7
+
+function %f16_fmin_zero_1() -> f16 {
+block0:
+    v1 = f16const 0.0
+    v2 = f16const -0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f16const -0.0
+; check: return v4  ; v4 = -0.0
+
+function %f16_fmin_zero_2() -> f16 {
+block0:
+    v1 = f16const -0.0
+    v2 = f16const 0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f16const -0.0
+; check: return v4  ; v4 = -0.0
+
+function %f16_fmax() -> f16 {
+block0:
+    v1 = f16const -0x1.5p6
+    v2 = f16const -0x1.5p7
+    v3 = fmax v2, v1
+    return v3
+}
+
+; check: v4 = f16const -0x1.500p6
+; check: return v4  ; v4 = -0x1.500p6
+
+function %f16_fmax_zero_1() -> f16 {
+block0:
+    v1 = f16const 0.0
+    v2 = f16const -0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f16const 0.0
+; check: return v4  ; v4 = 0.0
+
+function %f16_fmax_zero_2() -> f16 {
+block0:
+    v1 = f16const -0.0
+    v2 = f16const 0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f16const 0.0
+; check: return v4  ; v4 = 0.0
+
 function %f16_fneg() -> f16 {
 block0:
     v1 = f16const 0.0
@@ -449,6 +515,28 @@ block0:
 ; check: v4 = f32const 0x1.500000p6
 ; check: return v4  ; v4 = 0x1.500000p6
 
+function %f32_fmin_zero_1() -> f32 {
+block0:
+    v1 = f32const 0.0
+    v2 = f32const -0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f32const -0.0
+; check: return v4  ; v4 = -0.0
+
+function %f32_fmin_zero_2() -> f32 {
+block0:
+    v1 = f32const -0.0
+    v2 = f32const 0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f32const -0.0
+; check: return v4  ; v4 = -0.0
+
 function %f32_fmax() -> f32 {
 block0:
     v1 = f32const 0x1.5p6
@@ -460,6 +548,28 @@ block0:
 ; check: v4 = f32const 0x1.500000p7
 ; check: return v4  ; v4 = 0x1.500000p7
 
+function %f32_fmax_zero_1() -> f32 {
+block0:
+    v1 = f32const 0.0
+    v2 = f32const -0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f32const 0.0
+; check: return v4  ; v4 = 0.0
+
+function %f32_fmax_zero_2() -> f32 {
+block0:
+    v1 = f32const -0.0
+    v2 = f32const 0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f32const 0.0
+; check: return v4  ; v4 = 0.0
+
 function %f32_fneg() -> f32 {
 block0:
     v1 = f32const 0.0
@@ -596,6 +706,28 @@ block0:
 ; check: v4 = f64const -0x1.5000000000000p7
 ; check: return v4  ; v4 = -0x1.5000000000000p7
 
+function %f64_fmin_zero_1() -> f64 {
+block0:
+    v1 = f64const 0.0
+    v2 = f64const -0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f64const -0.0
+; check: return v4  ; v4 = -0.0
+
+function %f64_fmin_zero_2() -> f64 {
+block0:
+    v1 = f64const -0.0
+    v2 = f64const 0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: v4 = f64const -0.0
+; check: return v4  ; v4 = -0.0
+
 function %f64_fmax() -> f64 {
 block0:
     v1 = f64const -0x1.5p6
@@ -607,6 +739,28 @@ block0:
 ; check: v4 = f64const -0x1.5000000000000p6
 ; check: return v4  ; v4 = -0x1.5000000000000p6
 
+function %f64_fmax_zero_1() -> f64 {
+block0:
+    v1 = f64const 0.0
+    v2 = f64const -0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f64const 0.0
+; check: return v4  ; v4 = 0.0
+
+function %f64_fmax_zero_2() -> f64 {
+block0:
+    v1 = f64const -0.0
+    v2 = f64const 0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: v4 = f64const 0.0
+; check: return v4  ; v4 = 0.0
+
 function %f64_fneg() -> f64 {
 block0:
     v1 = f64const 0.0
@@ -638,6 +792,78 @@ block0:
 ; check: v4 = f64const -NaN
 ; check: return v4  ; v4 = -NaN
 
+function %f128_fmin() -> f128 {
+block0:
+    v1 = f128const 0x1.5p6
+    v2 = f128const 0x1.5p7
+    v3 = fmin v2, v1
+    return v3
+}
+
+; check: const0 = 0x40055000000000000000000000000000
+; check: v4 = f128const const0
+; check: return v4  ; v4 = 0x1.5000000000000000000000000000p6
+
+function %f128_fmin_zero_1() -> f128 {
+block0:
+    v1 = f128const 0.0
+    v2 = f128const -0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: const1 = 0x80000000000000000000000000000000
+; check: v4 = f128const const1
+; check: return v4  ; v4 = -0.0
+
+function %f128_fmin_zero_2() -> f128 {
+block0:
+    v1 = f128const -0.0
+    v2 = f128const 0.0
+    v3 = fmin v1, v2
+    return v3
+}
+
+; check: const0 = 0x80000000000000000000000000000000
+; check: v4 = f128const const0
+; check: return v4  ; v4 = -0.0
+
+function %f128_fmax() -> f128 {
+block0:
+    v1 = f128const 0x1.5p6
+    v2 = f128const 0x1.5p7
+    v3 = fmax v2, v1
+    return v3
+}
+
+; check: const1 = 0x40065000000000000000000000000000
+; check: v4 = f128const const1
+; check: return v4  ; v4 = 0x1.5000000000000000000000000000p7
+
+function %f128_fmax_zero_1() -> f128 {
+block0:
+    v1 = f128const 0.0
+    v2 = f128const -0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: const0 = 0x00000000000000000000000000000000
+; check: v4 = f128const const0
+; check: return v4  ; v4 = 0.0
+
+function %f128_fmax_zero_2() -> f128 {
+block0:
+    v1 = f128const -0.0
+    v2 = f128const 0.0
+    v3 = fmax v1, v2
+    return v3
+}
+
+; check: const1 = 0x00000000000000000000000000000000
+; check: v4 = f128const const1
+; check: return v4  ; v4 = 0.0
+
 function %f128_fneg() -> f128 {
 block0:
     v1 = f128const 0.0
diff --git a/tests/unload-engine.rs b/tests/unload-engine.rs
new file mode 100644
index 000000000..c59af4f9a
--- /dev/null
+++ b/tests/unload-engine.rs
@@ -0,0 +1,29 @@
+//! A single-test executable which only tests `Engine::unload_process_handlers`
+//! is possible.
+//!
+//! It's not safe for this binary to contain any other tests.
+
+use wasmtime::*;
+
+#[test]
+#[cfg_attr(miri, ignore)]
+fn test_unload_engine() {
+    for _ in 0..3 {
+        std::thread::spawn(|| {
+            let engine = Engine::default();
+            {
+                let module =
+                    Module::new(&engine, r#"(module (func (export "x") unreachable))"#).unwrap();
+                let mut store = Store::new(&engine, ());
+                let instance = Instance::new(&mut store, &module, &[]).unwrap();
+                let func = instance.get_typed_func::<(), ()>(&mut store, "x").unwrap();
+                assert!(func.call(&mut store, ()).unwrap_err().is::<Trap>());
+            }
+            unsafe {
+                engine.unload_process_handlers();
+            }
+        })
+        .join()
+        .unwrap();
+    }
+}