From dd7c8f88694acfa94f3fecac11ee073373b6b655 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 14:21:57 +0200 Subject: [PATCH 1/9] fix(profiling): correct poll event evaluation and nfds bounds check --- profiling/src/io/mod.rs | 130 +++++++++++++++++++++++++++++++--------- 1 file changed, 102 insertions(+), 28 deletions(-) diff --git a/profiling/src/io/mod.rs b/profiling/src/io/mod.rs index da30557bac..0f8de3dbad 100644 --- a/profiling/src/io/mod.rs +++ b/profiling/src/io/mod.rs @@ -102,6 +102,32 @@ unsafe fn restore_slot_if_owned(restore: &GotSlotRestore) -> bool { static mut ORIG_POLL: unsafe extern "C" fn(*mut libc::pollfd, libc::nfds_t, c_int) -> i32 = libc::poll; +fn eval_poll_events(ret: i32, fds: &[libc::pollfd]) -> (bool, bool) { + let mut has_read = false; + let mut has_write = false; + + for pfd in fds { + let mask = match ret { + 0 => pfd.events, + _ if pfd.revents == 0 => continue, + _ if pfd.revents & (libc::POLLIN | libc::POLLOUT) == 0 => pfd.events, + _ => pfd.revents, + }; + + if (mask & libc::POLLIN) != 0 { + has_read = true; + } + if (mask & libc::POLLOUT) != 0 { + has_write = true; + } + if has_read && has_write { + break; + } + } + + (has_read, has_write) +} + /// The `poll()` libc call has only every been observed when reading/writing to/from a socket, /// never when reading/writing to a file. There are two known cases in PHP: /// - the PHP stream layer (e.g. `file_get_contents("proto://url")`) @@ -120,36 +146,22 @@ unsafe extern "C" fn observed_poll( let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); - if !fds.is_null() { + if ret >= 0 && nfds > 0 && !fds.is_null() { let duration_nanos = duration.as_nanos() as u64; - if (*fds).revents & 1 == 1 { - // requested events contains reading - if SOCKET_READ_TIME_PROFILING_STATS - .borrow_mut_or_false(|io| io.should_collect(duration_nanos)) - { - collect_socket_read_time(duration_nanos); - } - } else if (*fds).revents & 4 == 4 { - // requested events contains writing - if SOCKET_WRITE_TIME_PROFILING_STATS - .borrow_mut_or_false(|io| io.should_collect(duration_nanos)) - { - collect_socket_write_time(duration_nanos); - } - } else if (*fds).events & 1 == 1 { - // socket became readable - if SOCKET_READ_TIME_PROFILING_STATS + let slice = unsafe { std::slice::from_raw_parts(fds, nfds as usize) }; + let (has_read, has_write) = eval_poll_events(ret, slice); + + if has_read + && SOCKET_READ_TIME_PROFILING_STATS .borrow_mut_or_false(|io| io.should_collect(duration_nanos)) - { - collect_socket_read_time(duration_nanos); - } - } else if (*fds).events & 4 == 4 { - // socket became writeable - if SOCKET_WRITE_TIME_PROFILING_STATS + { + collect_socket_read_time(duration_nanos); + } + if has_write + && SOCKET_WRITE_TIME_PROFILING_STATS .borrow_mut_or_false(|io| io.should_collect(duration_nanos)) - { - collect_socket_write_time(duration_nanos); - } + { + collect_socket_write_time(duration_nanos); } } @@ -784,7 +796,9 @@ pub fn io_prof_mshutdown() -> bool { #[cfg(test)] mod tests { - use super::{restore_matches_image, slot_fits_range, ErrnoBackup, GotSlotRestore}; + use super::{ + eval_poll_events, restore_matches_image, slot_fits_range, ErrnoBackup, GotSlotRestore, + }; use static_assertions::assert_not_impl_any; assert_not_impl_any!(ErrnoBackup: Send, Sync); @@ -817,4 +831,64 @@ mod tests { #[cfg(target_os = "macos")] assert!(unsafe { super::got_macho::restore_symbols(&mut restores) }); } + + #[test] + fn test_eval_poll_events_ready() { + let fds = [libc::pollfd { + fd: 3, + events: libc::POLLIN | libc::POLLOUT, + revents: libc::POLLIN, + }]; + assert_eq!(eval_poll_events(1, &fds), (true, false)); + + let fds_both = [libc::pollfd { + fd: 3, + events: libc::POLLIN | libc::POLLOUT, + revents: libc::POLLIN | libc::POLLOUT, + }]; + assert_eq!(eval_poll_events(1, &fds_both), (true, true)); + } + + #[test] + fn test_eval_poll_events_timeout() { + let fds = [libc::pollfd { + fd: 3, + events: libc::POLLIN, + revents: 0, + }]; + assert_eq!(eval_poll_events(0, &fds), (true, false)); + } + + #[test] + fn test_eval_poll_events_hangup() { + let fds = [libc::pollfd { + fd: 3, + events: libc::POLLOUT, + revents: libc::POLLHUP, + }]; + assert_eq!(eval_poll_events(1, &fds), (false, true)); + } + + #[test] + fn test_eval_poll_events_multiple_fds() { + let fds = [ + libc::pollfd { + fd: 3, + events: libc::POLLIN, + revents: 0, + }, + libc::pollfd { + fd: 4, + events: libc::POLLOUT, + revents: libc::POLLOUT, + }, + ]; + assert_eq!(eval_poll_events(1, &fds), (false, true)); + } + + #[test] + fn test_eval_poll_events_empty() { + assert_eq!(eval_poll_events(0, &[]), (false, false)); + assert_eq!(eval_poll_events(1, &[]), (false, false)); + } } From 461d32f56250d05a50888f2cd2520581505b1afe Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 14:41:34 +0200 Subject: [PATCH 2/9] fix(profiling): record bytes instead of item count in fread and fwrite --- profiling/src/io/mod.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/profiling/src/io/mod.rs b/profiling/src/io/mod.rs index 0f8de3dbad..fbd21e8195 100644 --- a/profiling/src/io/mod.rs +++ b/profiling/src/io/mod.rs @@ -337,11 +337,11 @@ unsafe extern "C" fn observed_fwrite( if FILE_WRITE_TIME_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(duration_nanos)) { collect_file_write_time(duration_nanos); } - if len > 0 { - let len_u64 = len as u64; - if FILE_WRITE_SIZE_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(len_u64)) { - collect_file_write_size(len_u64); - } + let bytes = (len as u64).saturating_mul(size as u64); + if bytes > 0 + && FILE_WRITE_SIZE_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(bytes)) + { + collect_file_write_size(bytes); } len @@ -407,11 +407,11 @@ unsafe extern "C" fn observed_fread( if FILE_READ_TIME_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(duration_nanos)) { collect_file_read_time(duration_nanos); } - if len > 0 { - let len_u64 = len as u64; - if FILE_READ_SIZE_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(len_u64)) { - collect_file_read_size(len_u64); - } + let bytes = (len as u64).saturating_mul(size as u64); + if bytes > 0 + && FILE_READ_SIZE_PROFILING_STATS.borrow_mut_or_false(|io| io.should_collect(bytes)) + { + collect_file_read_size(bytes); } len From 4dce09dee39eb37a3832ef65d823010be4dd49c4 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 14:58:35 +0200 Subject: [PATCH 3/9] fix(profiling): restore GNU_RELRO page protection after patching GOT entries --- profiling/src/io/got_elf64.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/profiling/src/io/got_elf64.rs b/profiling/src/io/got_elf64.rs index f89a08e4ef..8cdc485723 100644 --- a/profiling/src/io/got_elf64.rs +++ b/profiling/src/io/got_elf64.rs @@ -35,13 +35,17 @@ unsafe fn override_got_entry( ) -> bool { let phdr = (*info).dlpi_phdr; - // Locate the dynamic programm header (`PT_DYNAMIC`) + // Locate the dynamic program header (`PT_DYNAMIC`) and RELRO segment (`PT_GNU_RELRO`) let mut dyn_ptr: *const Elf64_Dyn = ptr::null(); + let mut relro_range: Option<(usize, usize)> = None; for i in 0..(*info).dlpi_phnum { let phdr_i = phdr.offset(i as isize); if (*phdr_i).p_type == PT_DYNAMIC { dyn_ptr = ((*info).dlpi_addr as usize + (*phdr_i).p_vaddr as usize) as *const Elf64_Dyn; - break; + } else if (*phdr_i).p_type == libc::PT_GNU_RELRO { + let start = (*info).dlpi_addr as usize + (*phdr_i).p_vaddr as usize; + let end = start + (*phdr_i).p_memsz as usize; + relro_range = Some((start, end)); } } if dyn_ptr.is_null() { @@ -140,14 +144,21 @@ unsafe fn override_got_entry( let got_entry = ((*info).dlpi_addr as usize + (*rel).r_offset as usize) as *mut *mut (); - // Change memory protection so we can write to the GOT entry + let is_relro = if let Some((start, end)) = relro_range { + (got_entry as usize) >= start && (got_entry as usize) < end + } else { + false + }; + + // Change memory protection so we can write to the GOT entry if protected by RELRO let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize; let aligned_addr = (got_entry as usize) & !(page_size - 1); - if libc::mprotect( - aligned_addr as *mut c_void, - page_size, - libc::PROT_READ | libc::PROT_WRITE, - ) != 0 + if is_relro + && libc::mprotect( + aligned_addr as *mut c_void, + page_size, + libc::PROT_READ | libc::PROT_WRITE, + ) != 0 { let err = *libc::__errno_location(); trace!("mprotect failed: {}", err); @@ -182,6 +193,10 @@ unsafe fn override_got_entry( replacement: overwrite.new_func as usize, }); *got_entry = overwrite.new_func; + + if is_relro { + libc::mprotect(aligned_addr as *mut c_void, page_size, libc::PROT_READ); + } continue; } } From fa8178f03e554face027c3f1a2430f8ee71186ad Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 15:08:15 +0200 Subject: [PATCH 4/9] perf(profiling): optimize ELF relocation scan, bound dynamic loop, and cache base address --- profiling/src/io/got_elf64.rs | 59 ++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/profiling/src/io/got_elf64.rs b/profiling/src/io/got_elf64.rs index 8cdc485723..2002238ce8 100644 --- a/profiling/src/io/got_elf64.rs +++ b/profiling/src/io/got_elf64.rs @@ -9,6 +9,7 @@ use libc::{c_char, c_int, c_void, dl_phdr_info}; use log::{error, trace}; use std::ffi::CStr; use std::ptr; +use std::sync::OnceLock; fn elf64_r_type(info: Elf64_Xword) -> u32 { (info & 0xffffffff) as u32 @@ -37,18 +38,20 @@ unsafe fn override_got_entry( // Locate the dynamic program header (`PT_DYNAMIC`) and RELRO segment (`PT_GNU_RELRO`) let mut dyn_ptr: *const Elf64_Dyn = ptr::null(); + let mut dyn_count: usize = 0; let mut relro_range: Option<(usize, usize)> = None; for i in 0..(*info).dlpi_phnum { let phdr_i = phdr.offset(i as isize); if (*phdr_i).p_type == PT_DYNAMIC { dyn_ptr = ((*info).dlpi_addr as usize + (*phdr_i).p_vaddr as usize) as *const Elf64_Dyn; + dyn_count = (*phdr_i).p_memsz as usize / std::mem::size_of::(); } else if (*phdr_i).p_type == libc::PT_GNU_RELRO { let start = (*info).dlpi_addr as usize + (*phdr_i).p_vaddr as usize; let end = start + (*phdr_i).p_memsz as usize; relro_range = Some((start, end)); } } - if dyn_ptr.is_null() { + if dyn_ptr.is_null() || dyn_count == 0 { trace!("Failed to locate dynamic section"); return false; } @@ -67,7 +70,7 @@ unsafe fn override_got_entry( // - on glibc, addresses are absolutes // https://elixir.bootlin.com/glibc/glibc-2.36/source/elf/get-dynamic-info.h#L84 let mut dyn_iter = dyn_ptr; - loop { + for _ in 0..dyn_count { let d_tag = (*dyn_iter).d_tag as u32; if d_tag == DT_NULL { break; @@ -116,27 +119,26 @@ unsafe fn override_got_entry( let num_relocs = rel_plt_size / std::mem::size_of::(); - // For each symbol we want to overwrite (from `overwrites`), we scan the relocation entries. - // Once the matching symbol name is found, patch its GOT entry to point to our new function. - for overwrite in state.overwrites.iter_mut() { - for i in 0..num_relocs { - let rel = rel_plt.add(i); - let r_type = elf64_r_type((*rel).r_info); + // Scan relocation entries once and match against symbols we want to overwrite. + for i in 0..num_relocs { + let rel = rel_plt.add(i); + let r_type = elf64_r_type((*rel).r_info); - // Only handle JUMP_SLOT relocations - if r_type != R_AARCH64_JUMP_SLOT && r_type != R_X86_64_JUMP_SLOT { - continue; - } + // Only handle JUMP_SLOT relocations + if r_type != R_AARCH64_JUMP_SLOT && r_type != R_X86_64_JUMP_SLOT { + continue; + } - // Get the symbol index for this relocation, then the symbol struct - let sym_index = elf64_r_sym((*rel).r_info) as usize; - let sym = symtab.add(sym_index); + // Get the symbol index for this relocation, then the symbol struct + let sym_index = elf64_r_sym((*rel).r_info) as usize; + let sym = symtab.add(sym_index); - // Access the symbol name via the string table - let name_offset = (*sym).st_name as isize; - let name_ptr = strtab.offset(name_offset); - let name = CStr::from_ptr(name_ptr).to_str().unwrap_or(""); + // Access the symbol name via the string table + let name_offset = (*sym).st_name as isize; + let name_ptr = strtab.offset(name_offset); + let name = CStr::from_ptr(name_ptr).to_str().unwrap_or(""); + for overwrite in state.overwrites.iter_mut() { if name == overwrite.symbol_name { // Calculate the GOT entry address. Per the ELF spec, `r_offset` for pointer-sized // relocations (such as GOT entries) is guaranteed to be pointer-aligned, see: @@ -197,7 +199,7 @@ unsafe fn override_got_entry( if is_relro { libc::mprotect(aligned_addr as *mut c_void, page_size, libc::PROT_READ); } - continue; + break; } } } @@ -213,13 +215,20 @@ pub unsafe extern "C" fn callback( ) -> c_int { let state = &mut *(data as *mut GotHookState); - // detect myself ... - let mut my_info: libc::Dl_info = std::mem::zeroed(); - if libc::dladdr(callback as *const c_void, &mut my_info) == 0 { - error!("Did not find my own `dladdr` and therefore can't hook into the GOT."); + // detect myself (cached once across iterations) + static MY_BASE_ADDR: OnceLock = OnceLock::new(); + let my_base_addr = *MY_BASE_ADDR.get_or_init(|| { + let mut my_info: libc::Dl_info = unsafe { std::mem::zeroed() }; + if unsafe { libc::dladdr(callback as *const c_void, &mut my_info) } == 0 { + error!("Did not find my own `dladdr` and therefore can't hook into the GOT."); + 0 + } else { + my_info.dli_fbase as usize + } + }); + if my_base_addr == 0 { return 0; } - let my_base_addr = my_info.dli_fbase as usize; let module_base_addr = (*info).dlpi_addr as usize; if module_base_addr == my_base_addr { // "this" lib is actually me: skipping GOT hooking for myself From 333aa0a494413252a401a41217410ee248272a1d Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 15:20:34 +0200 Subject: [PATCH 5/9] fix(profiling): mask local/abs symbol flags and bounds check Mach-O tables --- profiling/src/io/got_macho.rs | 44 ++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/profiling/src/io/got_macho.rs b/profiling/src/io/got_macho.rs index db410ee753..94f64dd509 100644 --- a/profiling/src/io/got_macho.rs +++ b/profiling/src/io/got_macho.rs @@ -332,8 +332,9 @@ unsafe fn rebind_symbols_for_image( // file. At runtime, __LINKEDIT is mapped at (vmaddr + slide). By subtracting // the file offset of __LINKEDIT itself, we get a base we can add any file // offset to in order to get a valid runtime pointer. - linkedit_base = - (slide as usize).wrapping_add(seg.vmaddr as usize) - seg.fileoff as usize; + linkedit_base = (slide as usize) + .wrapping_add(seg.vmaddr as usize) + .wrapping_sub(seg.fileoff as usize); linkedit_found = true; } } @@ -397,7 +398,10 @@ unsafe fn rebind_symbols_for_image( slide, symtab, strtab, + (*symtab_cmd).nsyms as usize, + (*symtab_cmd).strsize as usize, indirect_symtab, + (*dysymtab_cmd).nindirectsyms as usize, &mut *state.overwrites, &mut *state.restores, segname == "__DATA_CONST", @@ -432,7 +436,10 @@ unsafe fn rebind_symbols_in_section( slide: isize, symtab: *const Nlist64, strtab: *const c_char, + nsyms: usize, + strsize: usize, indirect_symtab: *const u32, + nindirectsyms: usize, overwrites: &mut [GotSymbolOverwrite], restores: &mut Vec, is_data_const: bool, @@ -443,7 +450,14 @@ unsafe fn rebind_symbols_in_section( // The indirect symbol table entries for this section start at index `section.reserved1`. // Entry `indirect_sym_indices[i]` tells us which symbol table entry corresponds to slot `i`. - let indirect_sym_indices = indirect_symtab.add(section.reserved1 as usize); + let indirect_sym_start = section.reserved1 as usize; + let Some(indirect_sym_end) = indirect_sym_start.checked_add(num_indirect_syms) else { + return false; + }; + if indirect_sym_end > nindirectsyms { + return false; + } + let indirect_sym_indices = indirect_symtab.add(indirect_sym_start); // The actual pointer slots in memory (adjusted by ASLR slide). let symbol_ptrs = ((slide as usize).wrapping_add(section.addr as usize)) as *mut *mut c_void; @@ -456,19 +470,27 @@ unsafe fn rebind_symbols_in_section( let symtab_index = *indirect_sym_indices.add(i); // Skip special entries that don't refer to real external symbols - if symtab_index == INDIRECT_SYMBOL_LOCAL - || symtab_index == INDIRECT_SYMBOL_ABS - || symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS) - { + if (symtab_index & (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) != 0 { + continue; + } + + if symtab_index as usize >= nsyms { continue; } // Step 2: Look up the symbol in the symbol table to get its name let nlist = &*symtab.add(symtab_index as usize); - let name_ptr = strtab.add(nlist.n_strx as usize); - let name = match CStr::from_ptr(name_ptr).to_str() { - Ok(n) => n, - Err(_) => continue, + let name_offset = nlist.n_strx as usize; + if name_offset >= strsize { + continue; + } + let name_bytes = + std::slice::from_raw_parts(strtab.add(name_offset) as *const u8, strsize - name_offset); + let Ok(name) = CStr::from_bytes_until_nul(name_bytes) else { + continue; + }; + let Ok(name) = name.to_str() else { + continue; }; // Step 3: Strip the Mach-O leading underscore (e.g. "_recv" → "recv") so we can From 012c2adb7accf8e2403cdfc2f11b64b410ac2c41 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 15:26:42 +0200 Subject: [PATCH 6/9] refactor(profiling): call libc directly and eliminate static mut ORIG function pointers --- profiling/src/io/got_elf64.rs | 9 +---- profiling/src/io/got_macho.rs | 6 +--- profiling/src/io/mod.rs | 67 ++++++----------------------------- 3 files changed, 13 insertions(+), 69 deletions(-) diff --git a/profiling/src/io/got_elf64.rs b/profiling/src/io/got_elf64.rs index 2002238ce8..69b7fcf3fe 100644 --- a/profiling/src/io/got_elf64.rs +++ b/profiling/src/io/got_elf64.rs @@ -173,20 +173,13 @@ unsafe fn override_got_entry( } trace!( - "Overriding GOT entry for {} at offset {:?} (abs: {:p}) pointing to {:p} (orig function at {:p})", + "Overriding GOT entry for {} at offset {:?} (abs: {:p}) pointing to {:p}", overwrite.symbol_name, (*rel).r_offset, got_entry, original, - *overwrite.orig_func ); - // This works for musl based linux distros, but not for libc once - *overwrite.orig_func = libc::dlsym(libc::RTLD_NEXT, name_ptr) as *mut (); - if (*overwrite.orig_func).is_null() { - // libc linux fallback - *overwrite.orig_func = original; - } state.restores.push(GotSlotRestore { image: (*info).dlpi_addr as usize, image_name: image_name.into(), diff --git a/profiling/src/io/got_macho.rs b/profiling/src/io/got_macho.rs index 94f64dd509..0eb9207d19 100644 --- a/profiling/src/io/got_macho.rs +++ b/profiling/src/io/got_macho.rs @@ -529,16 +529,12 @@ unsafe fn rebind_symbols_in_section( } trace!( - "Overriding symbol pointer for {} at {:p} pointing to {:p} (orig function at {:p})", + "Overriding symbol pointer for {} at {:p} pointing to {:p}", overwrite.symbol_name, slot, *slot, - *overwrite.orig_func, ); - // Keep the existing single call-through pointer, but record the exact value of every - // slot so MSHUTDOWN can restore images which resolve this symbol differently. - *overwrite.orig_func = original as *mut (); restores.push(GotSlotRestore { image, image_name: image_name.into(), diff --git a/profiling/src/io/mod.rs b/profiling/src/io/mod.rs index fbd21e8195..d1071e7d31 100644 --- a/profiling/src/io/mod.rs +++ b/profiling/src/io/mod.rs @@ -12,7 +12,6 @@ use rustc_hash::FxHashMap; use std::cell::RefCell; use std::mem::MaybeUninit; use std::os::unix::io::RawFd; -use std::ptr; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::Instant; @@ -56,7 +55,6 @@ impl Drop for ErrnoBackup { pub struct GotSymbolOverwrite { pub symbol_name: &'static str, pub new_func: *mut (), - pub orig_func: *mut *mut (), } pub struct GotHookState<'a> { @@ -99,9 +97,6 @@ unsafe fn restore_slot_if_owned(restore: &GotSlotRestore) -> bool { true } -static mut ORIG_POLL: unsafe extern "C" fn(*mut libc::pollfd, libc::nfds_t, c_int) -> i32 = - libc::poll; - fn eval_poll_events(ret: i32, fds: &[libc::pollfd]) -> (bool, bool) { let mut has_read = false; let mut has_write = false; @@ -142,7 +137,7 @@ unsafe extern "C" fn observed_poll( timeout: c_int, ) -> i32 { let start = Instant::now(); - let ret = ORIG_POLL(fds, nfds, timeout); + let ret = libc::poll(fds, nfds, timeout); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -168,8 +163,6 @@ unsafe extern "C" fn observed_poll( ret } -static mut ORIG_RECV: unsafe extern "C" fn(c_int, *mut c_void, usize, c_int) -> isize = libc::recv; - unsafe extern "C" fn observed_recv( socket: c_int, buf: *mut c_void, @@ -177,7 +170,7 @@ unsafe extern "C" fn observed_recv( flags: c_int, ) -> isize { let start = Instant::now(); - let len = ORIG_RECV(socket, buf, length, flags); + let len = libc::recv(socket, buf, length, flags); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -196,16 +189,13 @@ unsafe extern "C" fn observed_recv( len } -static mut ORIG_RECVMSG: unsafe extern "C" fn(c_int, *mut libc::msghdr, c_int) -> isize = - libc::recvmsg; - unsafe extern "C" fn observed_recvmsg( socket: c_int, msg: *mut libc::msghdr, flags: c_int, ) -> isize { let start = Instant::now(); - let len = ORIG_RECVMSG(socket, msg, flags); + let len = libc::recvmsg(socket, msg, flags); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -224,15 +214,6 @@ unsafe extern "C" fn observed_recvmsg( len } -static mut ORIG_RECVFROM: unsafe extern "C" fn( - c_int, - *mut c_void, - usize, - c_int, - *mut libc::sockaddr, - *mut libc::socklen_t, -) -> isize = libc::recvfrom; - unsafe extern "C" fn observed_recvfrom( socket: c_int, buf: *mut c_void, @@ -242,7 +223,7 @@ unsafe extern "C" fn observed_recvfrom( address_len: *mut libc::socklen_t, ) -> isize { let start = Instant::now(); - let len = ORIG_RECVFROM(socket, buf, length, flags, address, address_len); + let len = libc::recvfrom(socket, buf, length, flags, address, address_len); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -261,8 +242,6 @@ unsafe extern "C" fn observed_recvfrom( len } -static mut ORIG_SEND: unsafe extern "C" fn(c_int, *const c_void, usize, c_int) -> isize = - libc::send; unsafe extern "C" fn observed_send( socket: c_int, buf: *const c_void, @@ -270,7 +249,7 @@ unsafe extern "C" fn observed_send( flags: c_int, ) -> isize { let start = Instant::now(); - let len = ORIG_SEND(socket, buf, length, flags); + let len = libc::send(socket, buf, length, flags); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -289,15 +268,13 @@ unsafe extern "C" fn observed_send( len } -static mut ORIG_SENDMSG: unsafe extern "C" fn(c_int, *const libc::msghdr, c_int) -> isize = - libc::sendmsg; unsafe extern "C" fn observed_sendmsg( socket: c_int, msg: *const libc::msghdr, flags: c_int, ) -> isize { let start = Instant::now(); - let len = ORIG_SENDMSG(socket, msg, flags); + let len = libc::sendmsg(socket, msg, flags); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -316,12 +293,6 @@ unsafe extern "C" fn observed_sendmsg( len } -static mut ORIG_FWRITE: unsafe extern "C" fn( - *const c_void, - usize, - usize, - *mut libc::FILE, -) -> usize = libc::fwrite; unsafe extern "C" fn observed_fwrite( ptr: *const c_void, size: usize, @@ -329,7 +300,7 @@ unsafe extern "C" fn observed_fwrite( stream: *mut libc::FILE, ) -> usize { let start = Instant::now(); - let len = ORIG_FWRITE(ptr, size, nobj, stream); + let len = libc::fwrite(ptr, size, nobj, stream); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -347,10 +318,9 @@ unsafe extern "C" fn observed_fwrite( len } -static mut ORIG_WRITE: unsafe extern "C" fn(c_int, *const c_void, usize) -> isize = libc::write; unsafe extern "C" fn observed_write(fd: c_int, buf: *const c_void, count: usize) -> isize { let start = Instant::now(); - let len = ORIG_WRITE(fd, buf, count); + let len = libc::write(fd, buf, count); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -387,8 +357,6 @@ unsafe extern "C" fn observed_write(fd: c_int, buf: *const c_void, count: usize) len } -static mut ORIG_FREAD: unsafe extern "C" fn(*mut c_void, usize, usize, *mut libc::FILE) -> usize = - libc::fread; // So far there seems to be only one situation where a file is read using `fread()` instead of // `read()` in PHP and that is when compiling a PHP file, triggered by it being the start file or a // userland call to `include()`/`require()` functions. @@ -399,7 +367,7 @@ unsafe extern "C" fn observed_fread( stream: *mut libc::FILE, ) -> usize { let start = Instant::now(); - let len = ORIG_FREAD(ptr, size, nobj, stream); + let len = libc::fread(ptr, size, nobj, stream); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -417,10 +385,9 @@ unsafe extern "C" fn observed_fread( len } -static mut ORIG_READ: unsafe extern "C" fn(c_int, *mut c_void, usize) -> isize = libc::read; unsafe extern "C" fn observed_read(fd: c_int, buf: *mut c_void, count: usize) -> isize { let start = Instant::now(); - let len = ORIG_READ(fd, buf, count); + let len = libc::read(fd, buf, count); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -455,10 +422,9 @@ unsafe extern "C" fn observed_read(fd: c_int, buf: *mut c_void, count: usize) -> len } -static mut ORIG_CLOSE: unsafe extern "C" fn(i32) -> i32 = libc::close; /// The sole purpose of this function is to remove the `fd` from the `FD_CACHE` unsafe extern "C" fn observed_close(fd: i32) -> i32 { - let ret = ORIG_CLOSE(fd); + let ret = libc::close(fd); let _errno_backup = ErrnoBackup::new(); let cache = FD_CACHE.get_or_init(|| Mutex::new(FxHashMap::default())); let mut cache = cache.lock().unwrap(); @@ -708,57 +674,46 @@ pub fn io_prof_first_rinit() { GotSymbolOverwrite { symbol_name: "recv", new_func: observed_recv as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_RECV) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "recvmsg", new_func: observed_recvmsg as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_RECVMSG) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "recvfrom", new_func: observed_recvfrom as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_RECVFROM) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "send", new_func: observed_send as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_SEND) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "sendmsg", new_func: observed_sendmsg as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_SENDMSG) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "write", new_func: observed_write as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_WRITE) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "read", new_func: observed_read as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_READ) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "fwrite", new_func: observed_fwrite as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_FWRITE) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "fread", new_func: observed_fread as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_FREAD) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "close", new_func: observed_close as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_CLOSE) as *mut _ as *mut *mut (), }, GotSymbolOverwrite { symbol_name: "poll", new_func: observed_poll as *mut (), - orig_func: ptr::addr_of_mut!(ORIG_POLL) as *mut _ as *mut *mut (), }, ]; let mut restores = GOT_SLOT_RESTORES.lock().unwrap(); From 3be9dc9cae3598f27789e1a11f5a2a1e46a6a5e5 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 16:46:13 +0200 Subject: [PATCH 7/9] perf(profiling): fast-path bypass on non-PHP threads for all IO hooks --- profiling/src/io/mod.rs | 54 ++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/profiling/src/io/mod.rs b/profiling/src/io/mod.rs index d1071e7d31..8d7d4dd845 100644 --- a/profiling/src/io/mod.rs +++ b/profiling/src/io/mod.rs @@ -97,6 +97,11 @@ unsafe fn restore_slot_if_owned(restore: &GotSlotRestore) -> bool { true } +#[inline] +fn is_zend_thread() -> bool { + REQUEST_LOCALS.borrow_or_false(|locals| !locals.vm_interrupt_addr.is_null()) +} + fn eval_poll_events(ret: i32, fds: &[libc::pollfd]) -> (bool, bool) { let mut has_read = false; let mut has_write = false; @@ -136,6 +141,10 @@ unsafe extern "C" fn observed_poll( nfds: libc::nfds_t, timeout: c_int, ) -> i32 { + if !is_zend_thread() { + return libc::poll(fds, nfds, timeout); + } + let start = Instant::now(); let ret = libc::poll(fds, nfds, timeout); let _errno_backup = ErrnoBackup::new(); @@ -169,6 +178,10 @@ unsafe extern "C" fn observed_recv( length: usize, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::recv(socket, buf, length, flags); + } + let start = Instant::now(); let len = libc::recv(socket, buf, length, flags); let _errno_backup = ErrnoBackup::new(); @@ -194,6 +207,10 @@ unsafe extern "C" fn observed_recvmsg( msg: *mut libc::msghdr, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::recvmsg(socket, msg, flags); + } + let start = Instant::now(); let len = libc::recvmsg(socket, msg, flags); let _errno_backup = ErrnoBackup::new(); @@ -222,6 +239,10 @@ unsafe extern "C" fn observed_recvfrom( address: *mut libc::sockaddr, address_len: *mut libc::socklen_t, ) -> isize { + if !is_zend_thread() { + return libc::recvfrom(socket, buf, length, flags, address, address_len); + } + let start = Instant::now(); let len = libc::recvfrom(socket, buf, length, flags, address, address_len); let _errno_backup = ErrnoBackup::new(); @@ -248,6 +269,10 @@ unsafe extern "C" fn observed_send( length: usize, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::send(socket, buf, length, flags); + } + let start = Instant::now(); let len = libc::send(socket, buf, length, flags); let _errno_backup = ErrnoBackup::new(); @@ -273,6 +298,10 @@ unsafe extern "C" fn observed_sendmsg( msg: *const libc::msghdr, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::sendmsg(socket, msg, flags); + } + let start = Instant::now(); let len = libc::sendmsg(socket, msg, flags); let _errno_backup = ErrnoBackup::new(); @@ -299,6 +328,10 @@ unsafe extern "C" fn observed_fwrite( nobj: usize, stream: *mut libc::FILE, ) -> usize { + if !is_zend_thread() { + return libc::fwrite(ptr, size, nobj, stream); + } + let start = Instant::now(); let len = libc::fwrite(ptr, size, nobj, stream); let _errno_backup = ErrnoBackup::new(); @@ -319,6 +352,10 @@ unsafe extern "C" fn observed_fwrite( } unsafe extern "C" fn observed_write(fd: c_int, buf: *const c_void, count: usize) -> isize { + if !is_zend_thread() { + return libc::write(fd, buf, count); + } + let start = Instant::now(); let len = libc::write(fd, buf, count); let _errno_backup = ErrnoBackup::new(); @@ -366,6 +403,10 @@ unsafe extern "C" fn observed_fread( nobj: usize, stream: *mut libc::FILE, ) -> usize { + if !is_zend_thread() { + return libc::fread(ptr, size, nobj, stream); + } + let start = Instant::now(); let len = libc::fread(ptr, size, nobj, stream); let _errno_backup = ErrnoBackup::new(); @@ -386,6 +427,10 @@ unsafe extern "C" fn observed_fread( } unsafe extern "C" fn observed_read(fd: c_int, buf: *mut c_void, count: usize) -> isize { + if !is_zend_thread() { + return libc::read(fd, buf, count); + } + let start = Instant::now(); let len = libc::read(fd, buf, count); let _errno_backup = ErrnoBackup::new(); @@ -603,15 +648,6 @@ impl IOProfilingStats { } fn should_collect(&mut self, value: u64) -> bool { - let zend_thread = - REQUEST_LOCALS.borrow_or_false(|locals| !locals.vm_interrupt_addr.is_null()); - if !zend_thread { - // `curl_exec()` for example will spawn a new thread for name resolution. GOT hooking - // follows threads and as such we might sample from another (non PHP) thread even in a - // NTS build of PHP. We have observed crashes for these cases, so instead of crashing - // (or risking a crash) we refrain from collection I/O. - return false; - } if let Some(next_sample) = self.next_sample.checked_sub(value) { self.next_sample = next_sample; return false; From 5078e6d9c589ec45f00c3a674ea2e9e2a64d1d67 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 16:52:11 +0200 Subject: [PATCH 8/9] refactor(profiling): make ErrnoBackup::new safe and document safety invariants --- profiling/src/io/got_macho.rs | 5 +++-- profiling/src/io/mod.rs | 10 ++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/profiling/src/io/got_macho.rs b/profiling/src/io/got_macho.rs index 0eb9207d19..464210afaa 100644 --- a/profiling/src/io/got_macho.rs +++ b/profiling/src/io/got_macho.rs @@ -681,8 +681,9 @@ pub unsafe fn restore_symbols(restores: &mut Vec) -> bool { fn seg_name(seg: &libc::segment_command_64) -> &str { let bytes = &seg.segname; let len = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - // SAFETY: segment names are always ASCII; cast from &[i8] to &[u8] is safe - // because i8 and u8 have the same size and alignment. + // SAFETY: `seg.segname` is a fixed 16-byte array that outlives the returned reference. + // Casting from `*const c_char` (`*const i8`) to `*const u8` is safe because `i8` and `u8` + // have identical size (1 byte) and alignment, and `len` is bounded by the array length. let bytes: &[u8] = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u8, len) }; std::str::from_utf8(bytes).unwrap_or("") } diff --git a/profiling/src/io/mod.rs b/profiling/src/io/mod.rs index 8d7d4dd845..0e4bf1f2cf 100644 --- a/profiling/src/io/mod.rs +++ b/profiling/src/io/mod.rs @@ -31,13 +31,15 @@ struct ErrnoBackup { impl ErrnoBackup { /// Snapshots the current `errno` value. #[inline] - unsafe fn new() -> Self { + fn new() -> Self { + // SAFETY: libc::__errno_location() (Linux) / libc::__error() (macOS) returns a valid, + // non-null pointer to the calling thread's errno lvalue, safe for reading. #[cfg(target_os = "linux")] - let location = libc::__errno_location(); + let location = unsafe { libc::__errno_location() }; #[cfg(target_os = "macos")] - let location = libc::__error(); + let location = unsafe { libc::__error() }; Self { - errno: *location, + errno: unsafe { *location }, location, } } From 8d724d8ed600f9c47acd992f1bff87b0d45f5efa Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Thu, 3 Sep 2026 17:33:03 +0200 Subject: [PATCH 9/9] fix(profiling): skip ld-musl dynamic linker when hooking ELF GOT --- profiling/src/io/got_elf64.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/profiling/src/io/got_elf64.rs b/profiling/src/io/got_elf64.rs index 69b7fcf3fe..6b60abf3e4 100644 --- a/profiling/src/io/got_elf64.rs +++ b/profiling/src/io/got_elf64.rs @@ -239,9 +239,9 @@ pub unsafe extern "C" fn callback( std::str::from_utf8(image_name).unwrap_or("[Unknown]") }; - // I guess if we try to hook into GOT from `linux-vdso` or `ld-linux` our best outcome will be - // that nothing happens, but most likely we'll crash and we should avoid that. - if name.contains("linux-vdso") || name.contains("ld-linux") { + // I guess if we try to hook into GOT from `linux-vdso`, `ld-linux` or `ld-musl` our best + // outcome will be that nothing happens, but most likely we'll crash and we should avoid that. + if name.contains("linux-vdso") || name.contains("ld-linux") || name.contains("ld-musl") { return 0; }