diff --git a/Cargo.lock b/Cargo.lock index 07821f75264..cf692f00e1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1236,6 +1236,7 @@ dependencies = [ "ahash", "allocator-api2", "anyhow", + "backtrace", "bindgen", "cc", "cfg-if", @@ -1247,6 +1248,7 @@ dependencies = [ "datadog-php-profiling", "dynasmrt", "env_logger 0.11.6", + "gimli 0.31.1", "lazy_static", "libc 0.2.177", "libdd-alloc", @@ -1876,6 +1878,11 @@ name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.12.0", + "stable_deref_trait", +] [[package]] name = "gimli" diff --git a/profiling/.cargo/config.toml b/profiling/.cargo/config.toml index e9934dd2e16..0ac477df410 100644 --- a/profiling/.cargo/config.toml +++ b/profiling/.cargo/config.toml @@ -1,9 +1,16 @@ [target.x86_64-apple-darwin] # Leaving syntax here in case we decide to list them all specifically #rustflags = ["-C", "link-args=-Wl,-U,_zend_register_extension -Wl,-U,_zend_getenv -Wl,-U,_sapi_module -Wl,-U,_executor_globals"] -rustflags = ["-C", "link-args=-undefined dynamic_lookup"] +# -force-frame-pointers: Ensure all functions set up frame pointers for FP-based backtrace tests +rustflags = ["-C", "link-args=-undefined dynamic_lookup", "-C", "force-frame-pointers=yes"] [target.aarch64-apple-darwin] # Leaving syntax here in case we decide to list them all specifically #rustflags = ["-C", "link-args=-Wl,-U,_zend_register_extension -Wl,-U,_zend_getenv -Wl,-U,_sapi_module -Wl,-U,_executor_globals"] -rustflags = ["-C", "link-args=-undefined dynamic_lookup"] +rustflags = ["-C", "link-args=-undefined dynamic_lookup", "-C", "force-frame-pointers=yes"] + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "force-frame-pointers=yes"] + +[target.aarch64-unknown-linux-gnu] +rustflags = ["-C", "force-frame-pointers=yes"] diff --git a/profiling/Cargo.toml b/profiling/Cargo.toml index 4968a33e81b..6587f83a949 100644 --- a/profiling/Cargo.toml +++ b/profiling/Cargo.toml @@ -50,9 +50,13 @@ features = ["env-filter", "fmt", "smallvec", "std"] [dev-dependencies] allocator-api2 = { version = "0.2", default-features = false, features = ["alloc"] } +backtrace = "0.3" criterion = { version = "0.5.1" } datadog-php-profiling = { path = ".", features = ["test"] } +[target.'cfg(any(target_arch = "x86_64", target_arch = "aarch64"))'.dependencies] +gimli = { version = "0.31", features = ["write"] } + [target.'cfg(target_arch = "x86_64")'.dev-dependencies] criterion-perf-events = "0.4.0" perfcnt = "0.8.0" @@ -67,6 +71,13 @@ debug_stats = [] io_profiling = [] stack_walking_tests = [] test = [] +# Tests only (Linux): Link with LLVM's libunwind to use __unw_add_dynamic_eh_frame_section +# for eh_frame registration and enable tests using unw_* APIs. When disabled (default), +# uses libgcc's __register_frame and libunwind API tests are skipped. +# NOTE: Ensure LLVM's libunwind is actually linked (not the nongnu libunwind), otherwise +# _Unwind_Backtrace will come from libgcc_s, which knows nothing about the registration +# through the libunwind interface +libunwind_link = [] # Not for prod: tracing = ["dep:tracing"] diff --git a/profiling/src/wall_time.rs b/profiling/src/wall_time.rs index 6aa7d95a347..ff91e464810 100644 --- a/profiling/src/wall_time.rs +++ b/profiling/src/wall_time.rs @@ -137,31 +137,447 @@ pub extern "C" fn ddog_php_prof_interrupt_function(execute_data: *mut zend_execu } } +/// JIT trampoline generation for frameless function interception. +/// +/// This module provides the core assembly generation for trampolines that: +/// 1. Call an "original" function +/// 2. Tail-call an "interrupt" function afterward +/// +/// The trampoline sets up frame pointers for FP-based unwinding, and generates +/// .eh_frame DWARF CFI data for DWARF-based stack unwinding. +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +pub(crate) mod jit_trampoline { + #[cfg(target_arch = "aarch64")] + use dynasmrt::aarch64::Assembler; + #[cfg(target_arch = "x86_64")] + use dynasmrt::x64::Assembler; + #[cfg(target_arch = "aarch64")] + use dynasmrt::DynasmLabelApi; + use dynasmrt::{dynasm, DynasmApi}; + use gimli::write::{ + Address, CallFrameInstruction, CommonInformationEntry, EhFrame, EndianVec, + FrameDescriptionEntry, FrameTable, + }; + use gimli::{Encoding, Format, LittleEndian, Register}; + use log::error; + use std::ffi::c_void; + + // DWARF register numbers + #[cfg(target_arch = "x86_64")] + const RSP: Register = Register(7); + #[cfg(target_arch = "x86_64")] + const RBP: Register = Register(6); + #[cfg(target_arch = "x86_64")] + const RIP: Register = Register(16); + + #[cfg(target_arch = "aarch64")] + const X29: Register = Register(29); + #[cfg(target_arch = "aarch64")] + const X30: Register = Register(30); + #[cfg(target_arch = "aarch64")] + const SP: Register = Register(31); + + // Size of each trampoline in bytes (fixed, known at compile time) + // x86_64: push rbp (1) + mov rbp,rsp (3) + mov rax,imm64 (10) + call rax (2) + + // pop rbp (1) + mov rax,imm64 (10) + jmp rax (2) = 29 bytes + #[cfg(target_arch = "x86_64")] + const TRAMPOLINE_SIZE: u32 = 29; + + // aarch64: stp (4) + mov x29,sp (4) + ldr x16 (4) + blr x16 (4) + + // ldp (4) + ldr x16 (4) + br x16 (4) + .qword (8) = 36 bytes per trampoline + // Plus shared interrupt_label .qword (8) at end of batch + #[cfg(target_arch = "aarch64")] + const TRAMPOLINE_SIZE: u32 = 36; + + // macOS uses a llvm's libunwind, so __unw_add_dynamic_eh_frame_section is + // always available and required. + // Like in general for llvm's libunwind, the __register_frame on macOS is + // aliased to __unw_add_dynamic_fde which only accepts single FDEs, not full + // .eh_frame sections. + #[cfg(target_os = "macos")] + extern "C" { + fn __unw_add_dynamic_eh_frame_section(eh_frame_start: usize); + fn __unw_remove_dynamic_eh_frame_section(eh_frame_start: usize); + } + + // On Linux, we try __unw_add_dynamic_eh_frame_section first (LLVM libunwind), + // falling back to __register_frame (libgcc) if not available. + // __register_frame is always available, so we can have strong linkage here. + // However, __register_frame from LLVM libunwind doesn't accept a full + // .eh_frame section, so we can't use it. + // See the abandoned https://reviews.llvm.org/D44494 + // For the libunwind symbol, we need to use dlsym (weak linkage attribute + // is unstable) + #[cfg(target_os = "linux")] + extern "C" { + fn __register_frame(begin: *const u8); + fn __deregister_frame(begin: *const u8); + fn dlsym(handle: *mut c_void, symbol: *const std::ffi::c_char) -> *mut c_void; + } + + #[cfg(target_os = "linux")] + const RTLD_DEFAULT: *mut c_void = std::ptr::null_mut(); + + #[cfg(target_os = "linux")] + type EhFrameSectionFn = unsafe extern "C" fn(usize); + + /// Cached function pointers for LLVM libunwind's eh_frame section API. + /// Looked up once via dlsym, then cached for subsequent calls. + #[cfg(target_os = "linux")] + struct UnwDynamicEhFrameFns { + add: Option, + remove: Option, + } + + #[cfg(target_os = "linux")] + static UNW_DYNAMIC_EH_FRAME_FNS: std::sync::OnceLock = + std::sync::OnceLock::new(); + + #[cfg(all(target_os = "linux", feature = "libunwind_link"))] + fn get_unw_dynamic_eh_frame_fns() -> &'static UnwDynamicEhFrameFns { + UNW_DYNAMIC_EH_FRAME_FNS.get_or_init(|| unsafe { + let add_ptr = dlsym(RTLD_DEFAULT, c"__unw_add_dynamic_eh_frame_section".as_ptr()); + let remove_ptr = dlsym( + RTLD_DEFAULT, + c"__unw_remove_dynamic_eh_frame_section".as_ptr(), + ); + UnwDynamicEhFrameFns { + add: if add_ptr.is_null() { + None + } else { + Some(std::mem::transmute(add_ptr)) + }, + remove: if remove_ptr.is_null() { + None + } else { + Some(std::mem::transmute(remove_ptr)) + }, + } + }) + } + + #[cfg(all(target_os = "linux", not(feature = "libunwind_link")))] + fn get_unw_dynamic_eh_frame_fns() -> &'static UnwDynamicEhFrameFns { + // Without libunwind_link feature, always use __register_frame (libgcc) + UNW_DYNAMIC_EH_FRAME_FNS.get_or_init(|| UnwDynamicEhFrameFns { + add: None, + remove: None, + }) + } + + /// Result of generating trampolines for multiple original functions. + pub struct TrampolineBatch { + pub buffer: dynasmrt::mmap::ExecutableBuffer, + pub offsets: Vec, + /// eh_frame data for DWARF unwinding. Must be kept alive as long as the + /// trampolines are in use, as the runtime unwinder references this data. + /// The data itself is not read after construction, but dropping it would + /// invalidate the registered unwind information. + #[allow(dead_code)] + eh_frame_data: Vec, + } + + impl TrampolineBatch { + /// Get the trampoline function pointer for the i-th original function. + /// + /// # Safety + /// The index must be valid (< number of originals passed to generate). + pub unsafe fn get_trampoline(&self, i: usize) -> *mut c_void { + self.buffer.as_ptr().add(self.offsets[i].0) as *mut c_void + } + } + + impl Drop for TrampolineBatch { + fn drop(&mut self) { + // Deregister eh_frame data + if !self.eh_frame_data.is_empty() { + #[cfg(target_os = "macos")] + unsafe { + __unw_remove_dynamic_eh_frame_section(self.eh_frame_data.as_ptr() as usize); + } + #[cfg(target_os = "linux")] + unsafe { + let fns = get_unw_dynamic_eh_frame_fns(); + if let Some(remove_fn) = fns.remove { + remove_fn(self.eh_frame_data.as_ptr() as usize); + } else { + __deregister_frame(self.eh_frame_data.as_ptr()); + } + } + } + // eh_frame_data is dropped here, after deregistration + } + } + + /// Generate trampolines for a batch of original functions. + pub fn generate(originals: &[*mut c_void], interrupt_fn: *const ()) -> Option { + let mut assembler = Assembler::new().unwrap(); + let mut offsets = Vec::with_capacity(originals.len()); + + for orig in originals.iter() { + let start = assembler.offset(); + offsets.push(start); + emit_trampoline(&mut assembler, *orig, interrupt_fn); + + let actual_size = assembler.offset().0 - start.0; + assert_eq!( + actual_size, TRAMPOLINE_SIZE as usize, + "TRAMPOLINE_SIZE mismatch: expected {}, got {}. \ + Update TRAMPOLINE_SIZE constant to match actual generated code.", + TRAMPOLINE_SIZE, actual_size + ); + } + + // to generate a PC relative load of the interrupt function address; + // more efficient than loading the 64-bit immediate address into x16, + // which would require 4 instructions on each trampoline + #[cfg(target_arch = "aarch64")] + dynasm!(assembler + ; interrupt_label: ; .qword interrupt_fn as i64 + ); + + let buffer = match assembler.finalize() { + Ok(buffer) => buffer, + Err(e) => { + error!( + "Failed to finalize FLF trampolines (mprotect PROT_EXEC denied?): {:?}. \ + Frameless functions will not appear in cpu/wall-time profiles. \ + This may be caused by security policies (SELinux, seccomp, etc.).", + e + ); + return None; + } + }; + + // generate and register eh_frame data + let code_base = buffer.as_ptr() as u64; + + let trampoline_addrs: Vec = offsets.iter().map(|o| code_base + o.0 as u64).collect(); + + #[cfg(target_os = "macos")] + let eh_frame_data = { + // macOS always uses libunwind's __unw_add_dynamic_eh_frame_section, + // so we always need the libunwind workaround terminator + let data = generate_eh_frame_section(&trampoline_addrs, TRAMPOLINE_SIZE, true); + if !data.is_empty() { + unsafe { + __unw_add_dynamic_eh_frame_section(data.as_ptr() as usize); + } + } + data + }; + + #[cfg(target_os = "linux")] + let eh_frame_data = { + let fns = get_unw_dynamic_eh_frame_fns(); + let use_libunwind = fns.add.is_some(); + + let data = generate_eh_frame_section(&trampoline_addrs, TRAMPOLINE_SIZE, use_libunwind); + + if !data.is_empty() { + unsafe { + if let Some(add_fn) = fns.add { + add_fn(data.as_ptr() as usize); + } else { + __register_frame(data.as_ptr()); + } + } + } + data + }; + + Some(TrampolineBatch { + buffer, + offsets, + eh_frame_data, + }) + } + + /// Emit trampoline assembly for a single function. + fn emit_trampoline( + assembler: &mut Assembler, + original: *mut c_void, + #[allow(unused_variables)] interrupt_fn: *const (), + ) { + #[cfg(target_arch = "aarch64")] + { + // aarch64 trampoline layout (all instructions 4 bytes): + // 0x00: stp x29, x30, [sp, #-16]! - save FP and LR + // 0x04: mov x29, sp - set up frame pointer + // 0x08: ldr x16, >label - load original function address + // 0x0c: blr x16 - call original + // 0x10: ldp x29, x30, [sp], #16 - restore FP and LR + // 0x14: ldr x16, >interrupt_label - load interrupt address + // 0x18: br x16 - tail call interrupt + // 0x1c: .qword original - 8-byte address + dynasm!(assembler + ; stp x29, x30, [sp, -16]! // save link register and frame pointer + ; mov x29, sp // set up frame pointer + ; ldr x16, >label + ; blr x16 + ; ldp x29, x30, [sp], 16 // restore link register and frame pointer + ; ldr x16, >interrupt_label + ; br x16 // tail call + ; label: ; .qword original as i64 + ); + } + + #[cfg(target_arch = "x86_64")] + { + // x86_64 trampoline layout: + // 0x00: push rbp - 1 byte, save frame pointer + // 0x01: mov rbp, rsp - 3 bytes, establish frame pointer chain + // 0x04: mov rax, imm64 - 10 bytes (REX + opcode + 8-byte imm) + // 0x0e: call rax - 2 bytes + // 0x10: pop rbp - 1 byte, restore frame pointer + // 0x11: mov rax, imm64 - 10 bytes + // 0x1b: jmp rax - 2 bytes + // Total: 29 bytes + let _ = interrupt_fn; // Used via direct embedding below + dynasm!(assembler + ; push rbp // save frame pointer (for FP unwinding) + ; mov rbp, rsp // establish frame pointer chain + ; mov rax, QWORD original as i64 + ; call rax + ; pop rbp // restore frame pointer + ; mov rax, QWORD interrupt_fn as i64 + ; jmp rax // tail call + ); + } + } + + /// Generate eh_frame section for multiple trampolines. + /// + /// The `use_libunwind_workaround` parameter controls the terminator format: + /// - `true`: Use 8-byte terminator to work around LLVM libunwind 17.x bug + /// - `false`: Use standard 4-byte zero terminator (for libgcc's __register_frame) + fn generate_eh_frame_section( + code_addresses: &[u64], + code_size: u32, + use_libunwind_workaround: bool, + ) -> Vec { + if code_addresses.is_empty() { + return Vec::new(); + } + + let encoding = Encoding { + format: Format::Dwarf32, + version: 1, + address_size: 8, + }; + + let mut frame_table = FrameTable::default(); + let cie_id = frame_table.add_cie(create_cie(encoding)); + + for &addr in code_addresses { + let fde = create_fde(addr, code_size); + frame_table.add_fde(cie_id, fde); + } + + let mut eh_frame = EhFrame::from(EndianVec::new(LittleEndian)); + frame_table.write_eh_frame(&mut eh_frame).unwrap(); + let mut data = eh_frame.slice().to_vec(); + + if use_libunwind_workaround { + // IMPORTANT: Use non-zero-length terminator to work around a bug in + // LLVM libunwind 17.x and earlier (fixed in commit 58b33d03, Jan 2024). + // See: https://github.com/llvm/llvm-project/issues/76957 + // When __unw_add_dynamic_eh_frame_section encounters a zero-length entry, + // parseCIE returns success without updating cieLength, causing the loop + // to use stale values and read past the buffer. + // Our terminator (length=4, CIE_ID=0xFFFFFFFF) causes both parseCIE + // ("CIE ID is not zero") and decodeFDE ("CIE start does not match") to + // return errors, cleanly exiting the loop on all libunwind versions. + data.extend_from_slice(&[0x04, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF]); + } else { + // standard 4-byte zero terminator for libgcc's __register_frame + // It crashes with libunwind's workaround... + data.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); + } + data + } + + /// Create the Common Information Entry (CIE) for trampolines. + fn create_cie(encoding: Encoding) -> CommonInformationEntry { + #[cfg(target_arch = "x86_64")] + { + // x86_64: At entry, CFA = RSP + 8, return address at CFA - 8 + let mut cie = CommonInformationEntry::new(encoding, 1, -8, RIP); + cie.add_instruction(CallFrameInstruction::Cfa(RSP, 8)); + cie.add_instruction(CallFrameInstruction::Offset(RIP, -8)); + cie + } + + #[cfg(target_arch = "aarch64")] + { + // aarch64: At entry, CFA = SP + 0, return address in x30 (LR) + let mut cie = CommonInformationEntry::new(encoding, 4, -8, X30); + cie.add_instruction(CallFrameInstruction::Cfa(SP, 0)); + cie + } + } + + /// Create a Frame Description Entry (FDE) for a trampoline. + fn create_fde(code_address: u64, code_size: u32) -> FrameDescriptionEntry { + let mut fde = FrameDescriptionEntry::new(Address::Constant(code_address), code_size); + + #[cfg(target_arch = "x86_64")] + { + // After push rbp (offset 1): CFA = RSP + 16, RBP saved at CFA - 16 + fde.add_instruction(1, CallFrameInstruction::CfaOffset(16)); + fde.add_instruction(1, CallFrameInstruction::Offset(RBP, -16)); + // After mov rbp, rsp (offset 4): CFA is now RBP + 16 + fde.add_instruction(4, CallFrameInstruction::CfaRegister(RBP)); + // After pop rbp + fde.add_instruction(0x11, CallFrameInstruction::Restore(RBP)); + fde.add_instruction(0x11, CallFrameInstruction::CfaRegister(RSP)); + fde.add_instruction(0x11, CallFrameInstruction::CfaOffset(8)); + } + + #[cfg(target_arch = "aarch64")] + { + // After stp x29, x30, [sp, #-16]! (offset 4): + // CFA = SP + 16, x29 at CFA - 16, x30 at CFA - 8 + fde.add_instruction(4, CallFrameInstruction::CfaOffset(16)); + fde.add_instruction(4, CallFrameInstruction::Offset(X29, -16)); + fde.add_instruction(4, CallFrameInstruction::Offset(X30, -8)); + // After mov x29, sp (offset 8): CFA is now X29 + 16 + fde.add_instruction(8, CallFrameInstruction::CfaRegister(X29)); + // After: 0x10: ldp x29, x30, [sp], #16 : CFA is the SP + fde.add_instruction(0x14, CallFrameInstruction::Restore(X29)); + fde.add_instruction(0x14, CallFrameInstruction::Restore(X30)); + fde.add_instruction(0x14, CallFrameInstruction::CfaRegister(SP)); + fde.add_instruction(0x14, CallFrameInstruction::CfaOffset(0)); + } + + fde + } +} + #[cfg(php_frameless)] mod frameless { #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] mod trampoline { - #[cfg(target_arch = "aarch64")] - use dynasmrt::aarch64::Assembler; - #[cfg(target_arch = "aarch64")] - use dynasmrt::DynasmLabelApi; - #[cfg(target_arch = "x86_64")] - use dynasmrt::x64::Assembler; - use dynasmrt::{dynasm, DynasmApi, ExecutableBuffer}; + use super::super::jit_trampoline::TrampolineBatch; + use crate::bindings::{ + zend_flf_functions, zend_flf_handlers, zend_frameless_function_info, + }; + use crate::zend; + use crate::{profiling::Profiler, RefCellExt, REQUEST_LOCALS}; + use log::debug; use std::ffi::c_void; use std::sync::atomic::Ordering; - use log::{debug, error}; - use crate::bindings::{zend_flf_functions, zend_flf_handlers, zend_frameless_function_info}; - use crate::{profiling::Profiler, RefCellExt, REQUEST_LOCALS, zend}; // This ensures that the memory stays reachable and is replaced on apache reload for example static mut INFOS: Vec = Vec::new(); - static mut BUFFER: Option = None; + static mut BATCH: Option = None; pub unsafe fn install() { + use super::super::jit_trampoline; + // Collect frameless functions ahead of time to batch-process them. // Otherwise we get a new memory page per function. - let mut originals = Vec::new(); + let mut originals: Vec<*mut c_void> = Vec::new(); let mut i = 0; loop { let original = *zend_flf_handlers.add(i); @@ -172,59 +588,21 @@ mod frameless { i += 1; } - let mut assembler = match Assembler::new() { - Ok(assembler) => assembler, - Err(e) => { - error!("Failed to create assembler for FLF trampolines: {e}. Frameless functions will not appear in wall-time profiles."); + let interrupt_addr = ddog_php_prof_icall_trampoline_target as *const (); + let batch = match jit_trampoline::generate(&originals, interrupt_addr) { + Some(b) => b, + None => { + // Failed to generate trampolines, frameless functions will not be profiled return; } }; - let interrupt_addr = ddog_php_prof_icall_trampoline_target as *const (); - let mut offsets = Vec::new(); // keep function offsets - for orig in originals.iter() { - offsets.push(assembler.offset()); - // Calls original function, then calls interrupt function. - #[cfg(target_arch = "aarch64")] - { - // We need labels on aarch64 as immediates cannot be more than 16 bits - dynasm!(assembler - ; stp x29, x30, [sp, -16]! // save link register and allow clobber of x29 - ; mov x29, sp // store stack pointer - ; ldr x16, >orig_label - ; blr x16 - ; ldp x29, x30, [sp], 16 // restore link register and x29 - ; ldr x16, >interrupt_label - ; br x16 // tail call - ; orig_label: ; .qword *orig as i64 - ); - } - #[cfg(target_arch = "x86_64")] - dynasm!(assembler - ; push rbp // align stack - ; mov rax, QWORD *orig as i64 - ; call rax - ; pop rbp // restore stack - ; mov rax, QWORD interrupt_addr as i64 - ; jmp rax // tail call - ); - } - #[cfg(target_arch = "aarch64")] - dynasm!(assembler - ; interrupt_label: ; .qword interrupt_addr as i64 ); // Allocate enough space for all frameless_function_infos including trailing NULLs let mut infos = Vec::with_capacity(originals.len() * 2); - let buffer = match assembler.finalize() { - Ok(buffer) => buffer, - Err(_) => { - error!("Failed to finalize FLF trampolines (mprotect PROT_EXEC denied?). Frameless functions will not appear in cpu/wall-time profiles. This may be caused by security policies (SELinux, seccomp, etc.)."); - return; - } - }; let mut last_infos = std::ptr::null_mut(); - for (i, offset) in offsets.iter().enumerate() { - let wrapper = buffer.as_ptr().add(offset.0) as *mut c_void; + for (i, _) in batch.offsets.iter().enumerate() { + let wrapper = batch.get_trampoline(i); *zend_flf_handlers.add(i) = wrapper; let func = &mut **zend_flf_functions.add(i); @@ -258,7 +636,7 @@ mod frameless { } INFOS = infos; - BUFFER = Some(buffer); + BATCH = Some(batch); } #[no_mangle] @@ -283,7 +661,9 @@ mod frameless { }); if let Err(err) = result { - debug!("ddog_php_prof_icall_trampoline_target failed to borrow request locals: {err}"); + debug!( + "ddog_php_prof_icall_trampoline_target failed to borrow request locals: {err}" + ); } } } @@ -324,3 +704,8 @@ pub unsafe fn minit() { #[cfg(not(php_frameless))] execute_internal::minit(); } + +#[cfg(test)] +#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] +#[path = "wall_time_tests.rs"] +mod tests; diff --git a/profiling/src/wall_time_tests.rs b/profiling/src/wall_time_tests.rs new file mode 100644 index 00000000000..f3e4a351030 --- /dev/null +++ b/profiling/src/wall_time_tests.rs @@ -0,0 +1,823 @@ +//! Tests for JIT trampoline unwinding functionality. +//! +//! These tests verify that stack unwinding works correctly through dynamically +//! generated JIT trampolines using various unwinding methods: +//! - Frame pointer (FP) walking +//! - _Unwind_Backtrace (generic unwind API) +//! - libunwind (unw_* API) +//! - backtrace crate +//! - Rust panic unwinding + +use std::cell::Cell; +use std::ffi::{c_void, CStr}; + +// dladdr for symbol resolution +mod dladdr_api { + use std::ffi::{c_char, c_void}; + + #[repr(C)] + pub struct DlInfo { + pub dli_fname: *const c_char, // Pathname of shared object + pub dli_fbase: *mut c_void, // Base address of shared object + pub dli_sname: *const c_char, // Name of nearest symbol + pub dli_saddr: *mut c_void, // Address of nearest symbol + } + + extern "C" { + pub fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32; + } +} + +// Generic unwind API (_Unwind_Backtrace, as seen in libgcc_s or llvm libunwind) +mod generic_unwind { + use std::ffi::c_void; + + #[repr(C)] + pub struct UnwindContext { + _data: [u8; 1024], + } + + #[repr(C)] + #[allow(dead_code)] + pub enum UnwindReasonCode { + NoReason = 0, + ForeignExceptionCaught = 1, + FatalPhase2Error = 2, + FatalPhase1Error = 3, + NormalStop = 4, + EndOfStack = 5, + HandlerFound = 6, + InstallContext = 7, + Continue = 8, + } + + pub type UnwindTraceFn = + extern "C" fn(ctx: *mut UnwindContext, arg: *mut c_void) -> UnwindReasonCode; + + extern "C" { + pub fn _Unwind_Backtrace(trace_fn: UnwindTraceFn, arg: *mut c_void) -> UnwindReasonCode; + pub fn _Unwind_GetIP(ctx: *mut UnwindContext) -> usize; + } +} + +// libunwind API (unw_*) +mod libunwind_api { + #[repr(C)] + pub struct UnwContext { + _data: [u8; 4096], + } + + #[repr(C)] + pub struct UnwCursor { + _data: [u8; 4096], + } + + #[cfg(all( + target_os = "linux", + target_arch = "aarch64", + feature = "libunwind_link" + ))] + #[link(name = "unwind")] + extern "C" { + pub fn unw_getcontext(ctx: *mut UnwContext) -> i32; + pub fn unw_init_local(cursor: *mut UnwCursor, ctx: *mut UnwContext) -> i32; + pub fn unw_step(cursor: *mut UnwCursor) -> i32; + pub fn unw_get_reg(cursor: *mut UnwCursor, reg: i32, val: *mut u64) -> i32; + pub fn unw_get_proc_name( + cursor: *mut UnwCursor, + buf: *mut i8, + buf_len: usize, + offp: *mut u64, + ) -> i32; + } + + #[cfg(all( + target_os = "linux", + target_arch = "x86_64", + feature = "libunwind_link" + ))] + #[link(name = "unwind")] + extern "C" { + pub fn unw_getcontext(ctx: *mut UnwContext) -> i32; + pub fn unw_init_local(cursor: *mut UnwCursor, ctx: *mut UnwContext) -> i32; + pub fn unw_step(cursor: *mut UnwCursor) -> i32; + pub fn unw_get_reg(cursor: *mut UnwCursor, reg: i32, val: *mut u64) -> i32; + pub fn unw_get_proc_name( + cursor: *mut UnwCursor, + buf: *mut i8, + buf_len: usize, + offp: *mut u64, + ) -> i32; + } + + #[cfg(target_os = "macos")] + extern "C" { + pub fn unw_getcontext(ctx: *mut UnwContext) -> i32; + pub fn unw_init_local(cursor: *mut UnwCursor, ctx: *mut UnwContext) -> i32; + pub fn unw_step(cursor: *mut UnwCursor) -> i32; + pub fn unw_get_reg(cursor: *mut UnwCursor, reg: i32, val: *mut u64) -> i32; + pub fn unw_get_proc_name( + cursor: *mut UnwCursor, + buf: *mut i8, + buf_len: usize, + offp: *mut u64, + ) -> i32; + } + + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + pub const UNW_REG_IP: i32 = 32; + + #[cfg(all( + target_arch = "aarch64", + target_os = "linux", + feature = "libunwind_link" + ))] + pub const UNW_REG_IP: i32 = -1; + + #[cfg(all(target_arch = "x86_64", target_os = "macos"))] + pub const UNW_REG_IP: i32 = 16; + + #[cfg(all( + target_arch = "x86_64", + target_os = "linux", + feature = "libunwind_link" + ))] + pub const UNW_REG_IP: i32 = -1; +} + +thread_local! { + static JIT_CODE_START: Cell = const { Cell::new(0) }; + static JIT_CODE_END: Cell = const { Cell::new(0) }; + static FRAMES_FOUND: Cell = const { Cell::new(0) }; + static JIT_FRAME_FOUND: Cell = const { Cell::new(false) }; + static CALLER_FUNC_FOUND: Cell = const { Cell::new(false) }; + static CALLER_FUNC_ADDR: Cell = const { Cell::new(0) }; + static UNWIND_METHOD: Cell = const { Cell::new(0) }; +} + +#[derive(Clone)] +struct FrameInfo { + address: usize, + symbol: Option, + offset: Option, +} + +impl FrameInfo { + fn new(address: usize) -> Self { + Self { + address, + symbol: None, + offset: None, + } + } + + fn with_symbol(address: usize, symbol: String, offset: usize) -> Self { + Self { + address, + symbol: Some(symbol), + offset: Some(offset), + } + } +} + +fn is_in_jit_range(addr: usize) -> bool { + let start = JIT_CODE_START.get(); + let end = JIT_CODE_END.get(); + addr >= start && addr < end +} + +fn resolve_symbol_dladdr(addr: usize) -> FrameInfo { + use dladdr_api::*; + unsafe { + let mut info: DlInfo = std::mem::zeroed(); + if dladdr(addr as *const c_void, &mut info) != 0 && !info.dli_sname.is_null() { + let name = CStr::from_ptr(info.dli_sname) + .to_string_lossy() + .into_owned(); + let offset = addr.saturating_sub(info.dli_saddr as usize); + FrameInfo::with_symbol(addr, name, offset) + } else { + FrameInfo::new(addr) + } + } +} + +// Capture backtrace using generic unwind API (_Unwind_Backtrace) +fn capture_generic_unwind() -> Vec { + use generic_unwind::*; + + extern "C" fn trace_callback(ctx: *mut UnwindContext, arg: *mut c_void) -> UnwindReasonCode { + let frames: &mut Vec = unsafe { &mut *arg.cast() }; + let ip = unsafe { _Unwind_GetIP(ctx) }; + frames.push(ip); + if frames.len() > 50 { + return UnwindReasonCode::NormalStop; + } + UnwindReasonCode::NoReason + } + + let mut addrs = Vec::new(); + unsafe { + _Unwind_Backtrace(trace_callback, (&raw mut addrs).cast()); + } + addrs.into_iter().map(resolve_symbol_dladdr).collect() +} + +// Capture backtrace using libunwind API (unw_* functions) +#[cfg(any(target_os = "macos", feature = "libunwind_link"))] +fn capture_libunwind_unwind() -> Vec { + use libunwind_api::*; + + let mut frames = Vec::new(); + unsafe { + let mut ctx: UnwContext = std::mem::zeroed(); + let mut cursor: UnwCursor = std::mem::zeroed(); + + if unw_getcontext(&mut ctx) != 0 { + return frames; + } + if unw_init_local(&mut cursor, &mut ctx) != 0 { + return frames; + } + + let mut name_buf = [0i8; 512]; + loop { + let mut ip: u64 = 0; + unw_get_reg(&mut cursor, UNW_REG_IP, &mut ip); + + let mut offset: u64 = 0; + let ret = unw_get_proc_name( + &mut cursor, + name_buf.as_mut_ptr(), + name_buf.len(), + &mut offset, + ); + + let frame = if ret == 0 { + let name = CStr::from_ptr(name_buf.as_ptr().cast()) + .to_string_lossy() + .into_owned(); + FrameInfo::with_symbol(ip as usize, name, offset as usize) + } else { + FrameInfo::new(ip as usize) + }; + frames.push(frame); + + let ret = unw_step(&mut cursor); + if ret <= 0 || frames.len() > 50 { + break; + } + } + } + frames +} + +// Capture backtrace using backtrace crate +#[inline(never)] +fn capture_backtrace_crate() -> Vec { + let bt = backtrace::Backtrace::new_unresolved(); + let mut frames = Vec::new(); + for frame in bt.frames() { + let ip = frame.ip() as usize; + frames.push(resolve_symbol_dladdr(ip)); + } + frames +} + +// Capture backtrace using frame pointer walking +#[inline(never)] +fn capture_fp_backtrace() -> Vec { + let mut addrs: Vec = Vec::new(); + + #[cfg(target_arch = "x86_64")] + unsafe { + let mut fp: *const usize; + std::arch::asm!("mov {}, rbp", out(reg) fp); + while !fp.is_null() && (fp as usize) > 0x1000_usize { + let ra = *fp.add(1); + if ra == 0 { + break; + } + addrs.push(ra); + let next_fp = *fp; + if next_fp <= fp as usize || addrs.len() > 50 { + break; + } + fp = next_fp as *const usize; + } + } + + #[cfg(target_arch = "aarch64")] + unsafe { + let mut fp: *const usize; + std::arch::asm!("mov {}, x29", out(reg) fp); + while !fp.is_null() && (fp as usize) > 0x1000 { + let ra = *fp.add(1); + if ra == 0 { + break; + } + addrs.push(ra); + let next_fp = *fp; + if next_fp <= fp as usize || addrs.len() > 50 { + break; + } + fp = next_fp as *const usize; + } + } + + addrs.into_iter().map(resolve_symbol_dladdr).collect() +} + +struct BacktraceAnalysis { + found_jit: bool, + found_caller: bool, +} + +fn is_in_function(addr: usize, func_addr: usize) -> bool { + addr >= func_addr && addr < func_addr + 512 +} + +// XXX: remove +fn is_caller_symbol(symbol: &Option) -> bool { + symbol + .as_ref() + .is_some_and(|s| s.contains("call_trampoline_wrapper")) +} + +fn analyze_backtrace(frames: &[FrameInfo], method_name: &str) -> BacktraceAnalysis { + println!("\n=== {} Backtrace ===", method_name); + println!("Got {} frames:", frames.len()); + + let mut found_jit = false; + let mut found_caller = false; + let caller_addr = CALLER_FUNC_ADDR.get(); + + for (i, frame) in frames.iter().enumerate() { + let in_jit = is_in_jit_range(frame.address); + let in_caller = + is_in_function(frame.address, caller_addr) || is_caller_symbol(&frame.symbol); + + let mut markers = String::new(); + if in_jit { + markers.push_str(" <-- JIT TRAMPOLINE"); + } + if in_caller { + markers.push_str(" <-- CALLER"); + } + + let symbol_info = match (&frame.symbol, frame.offset) { + (Some(name), Some(offset)) => format!("{}+0x{:x}", name, offset), + (Some(name), None) => name.clone(), + (None, _) => "??".to_string(), + }; + + println!( + " [{:2}] 0x{:016x} {}{}", + i, frame.address, symbol_info, markers + ); + + if in_jit { + found_jit = true; + } + if in_caller { + found_caller = true; + } + } + + println!("\nFound JIT frame in backtrace: {}", found_jit); + println!("Found caller function in backtrace: {}", found_caller); + + BacktraceAnalysis { + found_jit, + found_caller, + } +} + +#[derive(Clone, Copy, PartialEq)] +#[repr(usize)] +enum UnwindMethod { + FramePointer = 0, + GenericUnwind = 1, + #[cfg(any(target_os = "macos", feature = "libunwind_link"))] + Libunwind = 2, + BacktraceCrate = 3, +} + +impl UnwindMethod { + fn name(&self) -> &'static str { + match self { + UnwindMethod::FramePointer => "Frame Pointer", + UnwindMethod::GenericUnwind => "_Unwind_Backtrace", + #[cfg(any(target_os = "macos", feature = "libunwind_link"))] + UnwindMethod::Libunwind => "libunwind (unw_*)", + UnwindMethod::BacktraceCrate => "backtrace crate", + } + } +} + +thread_local! { + static TRAMPOLINE_PTR: Cell = const { Cell::new(0) }; +} + +// Call the trampoline function. This is so that we can identify that we +// frames frames beyong the trampoline. If we see this function in the +// backtrace, we know that we have unwound through the trampoline. +#[inline(never)] +fn call_trampoline_wrapper() { + let ptr = TRAMPOLINE_PTR.get() as *mut c_void; + let trampoline: extern "C" fn() = unsafe { std::mem::transmute(ptr) }; + trampoline(); + std::hint::black_box(()); +} + +fn run_production_trampoline_test( + test_name: &str, + capture_from: &str, + unwind_method: UnwindMethod, +) { + use super::jit_trampoline; + + #[cfg(target_arch = "x86_64")] + let arch = "x86_64"; + #[cfg(target_arch = "aarch64")] + let arch = "aarch64"; + + println!("\n======================================================================"); + println!( + "TEST: {} ({}) - capture from {}", + test_name, arch, capture_from + ); + println!("======================================================================"); + println!("Unwinding method: {}", unwind_method.name()); + println!("Backtrace captured from: {} function\n", capture_from); + + FRAMES_FOUND.set(0); + JIT_FRAME_FOUND.set(false); + UNWIND_METHOD.set(unwind_method as usize); + + #[inline(never)] + fn capture_backtrace_by_method() -> Vec { + match UNWIND_METHOD.get() { + 0 => capture_fp_backtrace(), + 1 => capture_generic_unwind(), + #[cfg(any(target_os = "macos", feature = "libunwind_link"))] + 2 => capture_libunwind_unwind(), + 3 => capture_backtrace_crate(), + _ => unreachable!(), + } + } + + fn method_short_name() -> &'static str { + match UNWIND_METHOD.get() { + 0 => "FP", + 1 => "GenericUnwind", + #[cfg(any(target_os = "macos", feature = "libunwind_link"))] + 2 => "libunwind", + 3 => "backtrace_crate", + _ => unreachable!(), + } + } + + #[inline(never)] + extern "C" fn original_fn_with_capture() { + println!(" >>> Inside original_fn (capturing backtrace here) <<<"); + let frames = capture_backtrace_by_method(); + let analysis = analyze_backtrace( + &frames, + &format!("{} from original_fn", method_short_name()), + ); + FRAMES_FOUND.set(frames.len()); + JIT_FRAME_FOUND.set(analysis.found_jit); + CALLER_FUNC_FOUND.set(analysis.found_caller); + } + + extern "C" fn original_fn_no_capture() { + println!(" original_fn called (no capture)"); + } + + #[inline(never)] + extern "C" fn interrupt_fn_with_capture() { + println!(" >>> Inside interrupt_fn (capturing backtrace here) <<<"); + let frames = capture_backtrace_by_method(); + let analysis = analyze_backtrace( + &frames, + &format!("{} from interrupt_fn", method_short_name()), + ); + FRAMES_FOUND.set(frames.len()); + JIT_FRAME_FOUND.set(analysis.found_jit); + CALLER_FUNC_FOUND.set(analysis.found_caller); + } + + extern "C" fn interrupt_fn_no_capture() { + println!(" interrupt_fn called (no capture)"); + } + + let (original_fn, interrupt_fn): (extern "C" fn(), extern "C" fn()) = match capture_from { + "original" => (original_fn_with_capture, interrupt_fn_no_capture), + "interrupt" => (original_fn_no_capture, interrupt_fn_with_capture), + _ => panic!("capture_from must be 'original' or 'interrupt'"), + }; + + let originals = vec![original_fn as *mut c_void]; + let batch = jit_trampoline::generate(&originals, interrupt_fn as *const ()) + .expect("Failed to generate JIT trampoline"); + + let trampoline_ptr = unsafe { batch.get_trampoline(0) }; + let code_start = batch.buffer.as_ptr(); + let code_end = unsafe { code_start.add(batch.buffer.len()) }; + + JIT_CODE_START.set(code_start as usize); + JIT_CODE_END.set(code_end as usize); + + println!("Trampoline buffer: {:p} - {:p}", code_start, code_end); + println!("Trampoline function: {:p}", trampoline_ptr); + + CALLER_FUNC_ADDR.set(call_trampoline_wrapper as usize); + println!( + "Caller wrapper function: {:p}", + call_trampoline_wrapper as *const () + ); + + TRAMPOLINE_PTR.set(trampoline_ptr as usize); + call_trampoline_wrapper(); + + let frames = FRAMES_FOUND.get(); + let found_jit = JIT_FRAME_FOUND.get(); + let found_caller = CALLER_FUNC_FOUND.get(); + + println!("\n--- {} Result ---", test_name); + println!("Captured {} frames", frames); + println!("JIT frame visible: {}", found_jit); + println!("Caller function found: {}", found_caller); + + match capture_from { + "original" => { + if !found_jit { + panic!( + "FAILED: JIT frame not visible when capturing from original_fn \ + ({} total frames).", + frames + ); + } + if !found_caller { + panic!( + "FAILED: Caller function (call_trampoline_wrapper) not found in backtrace. \ + Unwinding stopped at JIT code ({} total frames).", + frames + ); + } + println!( + "SUCCESS: Complete backtrace - JIT frame and caller found, {} total frames", + frames + ); + } + "interrupt" => { + if unwind_method == UnwindMethod::FramePointer { + if frames < 5 { + panic!( + "FAILED: FP backtrace too short ({} frames). FP chain may be broken.", + frames + ); + } + println!( + "SUCCESS: FP backtrace complete ({} frames, caller skipped due to tail call)", + frames + ); + } else { + if !found_caller { + panic!( + "FAILED: Caller function (call_trampoline_wrapper) not found in backtrace. \ + Unwinding failed ({} total frames).", + frames + ); + } + println!( + "SUCCESS: Complete backtrace - caller found, {} total frames", + frames + ); + } + } + _ => unreachable!(), + } +} + +// ==================== Frame Pointer Tests ==================== + +#[test] +fn test_unwind_from_original_fp() { + run_production_trampoline_test( + "FP unwind from original_fn", + "original", + UnwindMethod::FramePointer, + ); +} + +#[test] +fn test_unwind_from_interrupt_fp() { + run_production_trampoline_test( + "FP unwind from interrupt_fn", + "interrupt", + UnwindMethod::FramePointer, + ); +} + +// ==================== _Unwind_Backtrace Tests ==================== + +#[test] +fn test_unwind_from_original_generic() { + run_production_trampoline_test( + "_Unwind_Backtrace from original_fn", + "original", + UnwindMethod::GenericUnwind, + ); +} + +#[test] +fn test_unwind_from_interrupt_generic() { + run_production_trampoline_test( + "_Unwind_Backtrace from interrupt_fn", + "interrupt", + UnwindMethod::GenericUnwind, + ); +} + +// ==================== libunwind (unw_*) Tests ==================== + +#[test] +#[cfg(any(target_os = "macos", feature = "libunwind_link"))] +fn test_unwind_from_original_libunwind() { + run_production_trampoline_test( + "libunwind (unw_*) from original_fn", + "original", + UnwindMethod::Libunwind, + ); +} + +#[test] +#[cfg(any(target_os = "macos", feature = "libunwind_link"))] +fn test_unwind_from_interrupt_libunwind() { + run_production_trampoline_test( + "libunwind (unw_*) from interrupt_fn", + "interrupt", + UnwindMethod::Libunwind, + ); +} + +// ==================== backtrace crate Tests ==================== + +#[test] +fn test_unwind_from_original_backtrace_crate() { + run_production_trampoline_test( + "backtrace crate from original_fn", + "original", + UnwindMethod::BacktraceCrate, + ); +} + +#[test] +fn test_unwind_from_interrupt_backtrace_crate() { + run_production_trampoline_test( + "backtrace crate from interrupt_fn", + "interrupt", + UnwindMethod::BacktraceCrate, + ); +} + +// ==================== Rust Panic Unwinding Tests ==================== + +fn run_panic_unwind_test(test_name: &str, panic_from: &str) { + use super::jit_trampoline; + use std::panic::{self, AssertUnwindSafe}; + + #[cfg(target_arch = "x86_64")] + let arch = "x86_64"; + #[cfg(target_arch = "aarch64")] + let arch = "aarch64"; + + println!("======================================================================"); + println!("TEST: {} ({}) - panic from {}", test_name, arch, panic_from); + println!("======================================================================"); + println!("Testing: Rust panic unwinding through JIT trampoline\n"); + + thread_local! { + static ORIGINAL_CALLED: Cell = const { Cell::new(false) }; + static INTERRUPT_CALLED: Cell = const { Cell::new(false) }; + } + + extern "C-unwind" fn original_fn_panic() { + ORIGINAL_CALLED.set(true); + println!(" >>> Inside original_fn - about to panic! <<<"); + panic!("intentional panic from original_fn"); + } + + extern "C-unwind" fn original_fn_no_panic() { + ORIGINAL_CALLED.set(true); + println!(" original_fn called (no panic)"); + } + + extern "C-unwind" fn interrupt_fn_panic() { + INTERRUPT_CALLED.set(true); + println!(" >>> Inside interrupt_fn - about to panic! <<<"); + panic!("intentional panic from interrupt_fn"); + } + + extern "C-unwind" fn interrupt_fn_no_panic() { + INTERRUPT_CALLED.set(true); + println!(" interrupt_fn called (no panic)"); + } + + let (original_fn, interrupt_fn): (extern "C-unwind" fn(), extern "C-unwind" fn()) = + match panic_from { + "original" => (original_fn_panic, interrupt_fn_no_panic), + "interrupt" => (original_fn_no_panic, interrupt_fn_panic), + _ => panic!("panic_from must be 'original' or 'interrupt'"), + }; + + let originals = vec![original_fn as *mut c_void]; + let batch = jit_trampoline::generate(&originals, interrupt_fn as *const ()) + .expect("Failed to generate JIT trampoline"); + + let trampoline_ptr = unsafe { batch.get_trampoline(0) }; + let code_start = batch.buffer.as_ptr(); + let code_end = unsafe { code_start.add(batch.buffer.len()) }; + + println!("Trampoline buffer: {:p} - {:p}", code_start, code_end); + println!("Trampoline function: {:p}", trampoline_ptr); + + ORIGINAL_CALLED.set(false); + INTERRUPT_CALLED.set(false); + + let trampoline: extern "C-unwind" fn() = unsafe { std::mem::transmute(trampoline_ptr) }; + + let result = panic::catch_unwind(AssertUnwindSafe(|| { + trampoline(); + })); + + println!("\n--- {} Result ---", test_name); + println!("original_fn was called: {}", ORIGINAL_CALLED.get()); + println!("interrupt_fn was called: {}", INTERRUPT_CALLED.get()); + + match result { + Ok(()) => { + panic!( + "FAILED: Trampoline returned normally - expected panic from {}", + panic_from + ); + } + Err(payload) => { + let msg = if let Some(s) = payload.downcast_ref::<&str>() { + s.to_string() + } else if let Some(s) = payload.downcast_ref::() { + s.clone() + } else { + "unknown panic payload".to_string() + }; + + println!("Panic caught successfully!"); + println!("Panic message: {}", msg); + + let expected_msg = format!("intentional panic from {}_fn", panic_from); + if !msg.contains(&expected_msg) { + panic!( + "FAILED: Panic message mismatch. Expected '{}', got '{}'", + expected_msg, msg + ); + } + + match panic_from { + "original" => { + if !ORIGINAL_CALLED.get() { + panic!("FAILED: original_fn was not called before panic"); + } + if INTERRUPT_CALLED.get() { + panic!("FAILED: interrupt_fn was called after original_fn panicked"); + } + } + "interrupt" => { + if !ORIGINAL_CALLED.get() { + panic!("FAILED: original_fn was not called"); + } + if !INTERRUPT_CALLED.get() { + panic!("FAILED: interrupt_fn was not called before panic"); + } + } + _ => unreachable!(), + } + + println!( + "SUCCESS: Panic from {} was caught - unwinding worked through JIT code!", + panic_from + ); + } + } +} + +#[test] +fn test_panic_unwind_from_original() { + run_panic_unwind_test("Rust panic unwind from original_fn", "original"); +} + +#[test] +fn test_panic_unwind_from_interrupt() { + run_panic_unwind_test("Rust panic unwind from interrupt_fn", "interrupt"); +}