diff --git a/profiling/src/io/got_elf64.rs b/profiling/src/io/got_elf64.rs index f89a08e4ef..6b60abf3e4 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 @@ -35,16 +36,22 @@ 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 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; - break; + 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; } @@ -63,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; @@ -112,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: @@ -140,14 +146,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); @@ -160,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(), @@ -182,7 +188,11 @@ unsafe fn override_got_entry( replacement: overwrite.new_func as usize, }); *got_entry = overwrite.new_func; - continue; + + if is_relro { + libc::mprotect(aligned_addr as *mut c_void, page_size, libc::PROT_READ); + } + break; } } } @@ -198,13 +208,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 @@ -222,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; } diff --git a/profiling/src/io/got_macho.rs b/profiling/src/io/got_macho.rs index db410ee753..464210afaa 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 @@ -507,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(), @@ -663,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 da30557bac..0e4bf1f2cf 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; @@ -32,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, } } @@ -56,7 +57,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,8 +99,36 @@ 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; +#[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; + + 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: @@ -115,57 +143,49 @@ 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 = ORIG_POLL(fds, nfds, timeout); + let ret = libc::poll(fds, nfds, timeout); 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); } } 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, length: usize, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::recv(socket, buf, length, flags); + } + 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(); @@ -184,16 +204,17 @@ 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 { + if !is_zend_thread() { + return libc::recvmsg(socket, msg, flags); + } + 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(); @@ -212,15 +233,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, @@ -229,8 +241,12 @@ 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 = 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(); @@ -249,16 +265,18 @@ 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, length: usize, flags: c_int, ) -> isize { + if !is_zend_thread() { + return libc::send(socket, buf, length, flags); + } + 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(); @@ -277,15 +295,17 @@ 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 { + if !is_zend_thread() { + return libc::sendmsg(socket, msg, flags); + } + 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(); @@ -304,20 +324,18 @@ 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, nobj: usize, stream: *mut libc::FILE, ) -> usize { + if !is_zend_thread() { + return libc::fwrite(ptr, size, nobj, stream); + } + 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(); @@ -325,20 +343,23 @@ 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 } -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 { + if !is_zend_thread() { + return libc::write(fd, buf, count); + } + 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(); @@ -375,8 +396,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. @@ -386,8 +405,12 @@ 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 = ORIG_FREAD(ptr, size, nobj, stream); + let len = libc::fread(ptr, size, nobj, stream); let _errno_backup = ErrnoBackup::new(); let duration = start.elapsed(); @@ -395,20 +418,23 @@ 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 } -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 { + if !is_zend_thread() { + return libc::read(fd, buf, count); + } + 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(); @@ -443,10 +469,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(); @@ -625,15 +650,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; @@ -696,57 +712,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(); @@ -784,7 +789,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 +824,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)); + } }