From 7dab3a056981a59345dcfae145538e155d033a1b Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Mon, 8 Sep 2025 14:01:14 +0200 Subject: [PATCH 1/5] extract got code into it's own file --- profiling/src/io/got.rs | 221 ++++++++++++++++++++++++++++ profiling/src/{io.rs => io/mod.rs} | 226 +---------------------------- 2 files changed, 226 insertions(+), 221 deletions(-) create mode 100644 profiling/src/io/got.rs rename profiling/src/{io.rs => io/mod.rs} (71%) diff --git a/profiling/src/io/got.rs b/profiling/src/io/got.rs new file mode 100644 index 00000000000..0934d97df72 --- /dev/null +++ b/profiling/src/io/got.rs @@ -0,0 +1,221 @@ +use crate::bindings::{ + Elf64_Dyn, Elf64_Rela, Elf64_Sym, Elf64_Xword, DT_JMPREL, DT_NULL, DT_PLTRELSZ, DT_STRTAB, + DT_SYMTAB, PT_DYNAMIC, R_AARCH64_JUMP_SLOT, R_X86_64_JUMP_SLOT, +}; +use libc::{c_char, c_int, c_void, dl_phdr_info}; +use log::{error, trace}; +use std::ffi::CStr; +use std::ptr; + +fn elf64_r_type(info: Elf64_Xword) -> u32 { + (info & 0xffffffff) as u32 +} + +fn elf64_r_sym(info: Elf64_Xword) -> u32 { + (info >> 32) as u32 +} + +pub struct GotSymbolOverwrite { + pub symbol_name: &'static str, + pub new_func: *mut (), + pub orig_func: *mut *mut (), +} + +/// Override the GOT entry for symbols specified in `overwrites`. +/// +/// See: https://cs4401.walls.ninja/notes/lecture/basics_global_offset_table.html +/// See: https://bottomupcs.com/ch09s03.html +/// See: https://www.codeproject.com/articles/1032231/what-is-the-symbol-table-and-what-is-the-global-of +/// +/// Safety: Why is anything happening in in here safe? Well generally we can say all of the pointer +/// arithmetics are safe because the dynamic library the `info` is pointing to was loaded by the +/// dynamic linker prior to us messing with the global offset table. If the dynamic library would +/// not be a valid ELF64, the dynamic linker would have not loaded it. +unsafe fn override_got_entry( + info: *mut dl_phdr_info, + overwrites: *mut Vec, +) -> bool { + let phdr = (*info).dlpi_phdr; + + // Locate the dynamic programm header (`PT_DYNAMIC`) + let mut dyn_ptr: *const Elf64_Dyn = ptr::null(); + 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; + } + } + if dyn_ptr.is_null() { + trace!("Failed to locate dynamic section"); + return false; + } + + let mut rel_plt: *mut Elf64_Rela = ptr::null_mut(); + let mut rel_plt_size: usize = 0; + let mut symtab: *mut Elf64_Sym = ptr::null_mut(); + let mut strtab: *const c_char = ptr::null(); + + // The dynamic programm header (`PT_DYNAMIC`) has different sections. We are interessted in the + // procedure linkage table (PLT in `DT_JMPREL`), the size of the PLT (`DT_PLTRELSZ`), the + // symbol table (`DT_SYMTAB`) and the the string table for the symbol names (`DT_STRTAB`). + // + // Addresses in here are sometimes relative, sometimes absolute + // - on musl, addresses are relative + // - 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 { + let d_tag = (*dyn_iter).d_tag as u32; + if d_tag == DT_NULL { + break; + } + match d_tag { + DT_JMPREL => { + // Relocation entries for the PLT (Procedure Linkage Table) + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { + rel_plt = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) + as *mut Elf64_Rela; + } else { + rel_plt = (*dyn_iter).d_un.d_ptr as *mut Elf64_Rela; + } + } + DT_PLTRELSZ => { + // Size of the PLT relocation entries + rel_plt_size = (*dyn_iter).d_un.d_val as usize; + } + DT_SYMTAB => { + // The symbol table + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { + symtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) + as *mut Elf64_Sym; + } else { + symtab = (*dyn_iter).d_un.d_ptr as *mut Elf64_Sym; + } + } + DT_STRTAB => { + // The string table for the symbol names + if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { + strtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) + as *const c_char; + } else { + strtab = (*dyn_iter).d_un.d_ptr as *const c_char; + } + } + _ => {} + } + dyn_iter = dyn_iter.offset(1); + } + + if rel_plt.is_null() || rel_plt_size == 0 || symtab.is_null() || strtab.is_null() { + trace!("Failed to locate required ELF sections (`DT_JMPREL`, `DT_PLTRELSZ`, `DT_SYMTAB` and `DT_STRTAB`)"); + return false; + } + + 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 &mut *overwrites { + 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; + } + + // 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(""); + + 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: + // https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst#5733relocation-operations + 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 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 + { + let err = *libc::__errno_location(); + trace!("mprotect failed: {}", err); + return false; + } + + trace!( + "Overriding GOT entry for {} at offset {:?} (abs: {:p}) pointing to {:p} (orig function at {:p})", + overwrite.symbol_name, + (*rel).r_offset, + got_entry, + *got_entry, + *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 = *got_entry; + } + *got_entry = overwrite.new_func; + continue; + } + } + } + true +} + +/// Callback function that should be passed to `libc::dl_iterate_phdr()` and gets called for every +/// shared object. +pub unsafe extern "C" fn callback(info: *mut dl_phdr_info, _size: usize, data: *mut c_void) -> c_int { + let overwrites = &mut *(data as *mut Vec); + + // 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."); + 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 + return 0; + } + + let name = if (*info).dlpi_name.is_null() || *(*info).dlpi_name == 0 { + "[Executable]" + } else { + CStr::from_ptr((*info).dlpi_name) + .to_str() + .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") { + return 0; + } + + if override_got_entry(info, overwrites) { + trace!("Hooked into {name}"); + } else { + trace!("Hooking {name} failed"); + } + + 0 +} diff --git a/profiling/src/io.rs b/profiling/src/io/mod.rs similarity index 71% rename from profiling/src/io.rs rename to profiling/src/io/mod.rs index e17ed89933a..65637055758 100644 --- a/profiling/src/io.rs +++ b/profiling/src/io/mod.rs @@ -1,16 +1,13 @@ -use crate::bindings::{ - Elf64_Dyn, Elf64_Rela, Elf64_Sym, Elf64_Xword, DT_JMPREL, DT_NULL, DT_PLTRELSZ, DT_STRTAB, - DT_SYMTAB, PT_DYNAMIC, R_AARCH64_JUMP_SLOT, R_X86_64_JUMP_SLOT, -}; +pub mod got; + use crate::profiling::Profiler; use crate::{zend, RefCellExt, REQUEST_LOCALS}; use ahash::{HashMap, HashMapExt}; -use libc::{c_char, c_int, c_void, dl_phdr_info, fstat, stat, S_IFMT, S_IFSOCK}; -use log::{error, trace}; +use got::GotSymbolOverwrite; +use libc::{c_int, c_void, fstat, stat, S_IFMT, S_IFSOCK}; use rand::rngs::ThreadRng; use rand_distr::{Distribution, Poisson}; use std::cell::RefCell; -use std::ffi::CStr; use std::mem::MaybeUninit; use std::os::unix::io::RawFd; use std::ptr; @@ -18,219 +15,6 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use std::time::Instant; -fn elf64_r_type(info: Elf64_Xword) -> u32 { - (info & 0xffffffff) as u32 -} - -fn elf64_r_sym(info: Elf64_Xword) -> u32 { - (info >> 32) as u32 -} - -/// Override the GOT entry for symbols specified in `overwrites`. -/// -/// See: https://cs4401.walls.ninja/notes/lecture/basics_global_offset_table.html -/// See: https://bottomupcs.com/ch09s03.html -/// See: https://www.codeproject.com/articles/1032231/what-is-the-symbol-table-and-what-is-the-global-of -/// -/// Safety: Why is anything happening in in here safe? Well generally we can say all of the pointer -/// arithmetics are safe because the dynamic library the `info` is pointing to was loaded by the -/// dynamic linker prior to us messing with the global offset table. If the dynamic library would -/// not be a valid ELF64, the dynamic linker would have not loaded it. -unsafe fn override_got_entry( - info: *mut dl_phdr_info, - overwrites: *mut Vec, -) -> bool { - let phdr = (*info).dlpi_phdr; - - // Locate the dynamic programm header (`PT_DYNAMIC`) - let mut dyn_ptr: *const Elf64_Dyn = ptr::null(); - 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; - } - } - if dyn_ptr.is_null() { - trace!("Failed to locate dynamic section"); - return false; - } - - let mut rel_plt: *mut Elf64_Rela = ptr::null_mut(); - let mut rel_plt_size: usize = 0; - let mut symtab: *mut Elf64_Sym = ptr::null_mut(); - let mut strtab: *const c_char = ptr::null(); - - // The dynamic programm header (`PT_DYNAMIC`) has different sections. We are interessted in the - // procedure linkage table (PLT in `DT_JMPREL`), the size of the PLT (`DT_PLTRELSZ`), the - // symbol table (`DT_SYMTAB`) and the the string table for the symbol names (`DT_STRTAB`). - // - // Addresses in here are sometimes relative, sometimes absolute - // - on musl, addresses are relative - // - 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 { - let d_tag = (*dyn_iter).d_tag as u32; - if d_tag == DT_NULL { - break; - } - match d_tag { - DT_JMPREL => { - // Relocation entries for the PLT (Procedure Linkage Table) - if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { - rel_plt = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) - as *mut Elf64_Rela; - } else { - rel_plt = (*dyn_iter).d_un.d_ptr as *mut Elf64_Rela; - } - } - DT_PLTRELSZ => { - // Size of the PLT relocation entries - rel_plt_size = (*dyn_iter).d_un.d_val as usize; - } - DT_SYMTAB => { - // The symbol table - if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { - symtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) - as *mut Elf64_Sym; - } else { - symtab = (*dyn_iter).d_un.d_ptr as *mut Elf64_Sym; - } - } - DT_STRTAB => { - // The string table for the symbol names - if ((*dyn_iter).d_un.d_ptr as usize) < ((*info).dlpi_addr as usize) { - strtab = ((*info).dlpi_addr as usize + (*dyn_iter).d_un.d_ptr as usize) - as *const c_char; - } else { - strtab = (*dyn_iter).d_un.d_ptr as *const c_char; - } - } - _ => {} - } - dyn_iter = dyn_iter.offset(1); - } - - if rel_plt.is_null() || rel_plt_size == 0 || symtab.is_null() || strtab.is_null() { - trace!("Failed to locate required ELF sections (`DT_JMPREL`, `DT_PLTRELSZ`, `DT_SYMTAB` and `DT_STRTAB`)"); - return false; - } - - 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 &mut *overwrites { - 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; - } - - // 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(""); - - 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: - // https://github.com/ARM-software/abi-aa/blob/main/aaelf64/aaelf64.rst#5733relocation-operations - 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 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 - { - let err = *libc::__errno_location(); - trace!("mprotect failed: {}", err); - return false; - } - - trace!( - "Overriding GOT entry for {} at offset {:?} (abs: {:p}) pointing to {:p} (orig function at {:p})", - overwrite.symbol_name, - (*rel).r_offset, - got_entry, - *got_entry, - *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 = *got_entry; - } - *got_entry = overwrite.new_func; - continue; - } - } - } - true -} - -/// Callback function that should be passed to `libc::dl_iterate_phdr()` and gets called for every -/// shared object. -unsafe extern "C" fn callback(info: *mut dl_phdr_info, _size: usize, data: *mut c_void) -> c_int { - let overwrites = &mut *(data as *mut Vec); - - // 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."); - 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 - return 0; - } - - let name = if (*info).dlpi_name.is_null() || *(*info).dlpi_name == 0 { - "[Executable]" - } else { - CStr::from_ptr((*info).dlpi_name) - .to_str() - .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") { - return 0; - } - - if override_got_entry(info, overwrites) { - trace!("Hooked into {name}"); - } else { - trace!("Hooking {name} failed"); - } - - 0 -} - -struct GotSymbolOverwrite { - symbol_name: &'static str, - new_func: *mut (), - orig_func: *mut *mut (), -} - static mut ORIG_POLL: unsafe extern "C" fn(*mut libc::pollfd, u64, c_int) -> i32 = libc::poll; /// The `poll()` libc call has only every been observed when reading/writing to/from a socket, /// never when reading/writing to a file. There is two known cases in PHP: @@ -865,7 +649,7 @@ pub fn io_prof_first_rinit() { }, ]; libc::dl_iterate_phdr( - Some(callback), + Some(got::callback), &mut overwrites as *mut _ as *mut libc::c_void, ); }; From a2a96920cac2672b0c0cb302640d5b63d81dbcf4 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 14 Oct 2025 16:53:12 +0200 Subject: [PATCH 2/5] perf: hoist stable_config NULL check outside zai_config_ini_rinit loops MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reduces CPU overhead by checking stable_config availability once before the main loop instead of on every config entry/name iteration. This eliminates hundreds of redundant NULL checks per request. Adds zai_config_stable_file_is_available() to avoid repeated function calls and NULL checks inside the hot loop. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- zend_abstract_interface/config/config_ini.c | 4 +++- zend_abstract_interface/config/config_stable_file.c | 4 ++++ zend_abstract_interface/config/config_stable_file.h | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/zend_abstract_interface/config/config_ini.c b/zend_abstract_interface/config/config_ini.c index 06f03f711c8..4224b460517 100644 --- a/zend_abstract_interface/config/config_ini.c +++ b/zend_abstract_interface/config/config_ini.c @@ -429,6 +429,8 @@ void zai_config_ini_rinit(void) { ZAI_ENV_BUFFER_INIT(buf, ZAI_ENV_MAX_BUFSIZ); + bool has_stable_config = zai_config_stable_file_is_available(); + for (uint16_t i = 0; i < zai_config_memoized_entries_count; ++i) { zai_config_memoized_entry *memoized = &zai_config_memoized_entries[i]; if (memoized->ini_change == zai_config_system_ini_change) { @@ -439,7 +441,7 @@ void zai_config_ini_rinit(void) { if (!env_to_ini_name || !memoized->original_on_modify) { for (uint8_t name_index = 0; name_index < memoized->names_count; name_index++) { zai_str name = ZAI_STR_NEW(memoized->names[name_index].ptr, memoized->names[name_index].len); - zai_config_stable_file_entry *entry = zai_config_stable_file_get_value(name); + zai_config_stable_file_entry *entry = has_stable_config ? zai_config_stable_file_get_value(name) : NULL; if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_FLEET_STABLE_CONFIG && strcpy(buf.ptr, ZSTR_VAL(entry->value)) && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { diff --git a/zend_abstract_interface/config/config_stable_file.c b/zend_abstract_interface/config/config_stable_file.c index c3b8dffbbe0..44068621d1d 100644 --- a/zend_abstract_interface/config/config_stable_file.c +++ b/zend_abstract_interface/config/config_stable_file.c @@ -32,6 +32,10 @@ zai_config_stable_file_entry *zai_config_stable_file_get_value(zai_str name) { return zend_hash_str_find_ptr(stable_config, name.ptr, name.len); } +bool zai_config_stable_file_is_available(void) { + return stable_config != NULL; +} + static void stable_config_entry_dtor(zval *el) { zai_config_stable_file_entry *e = (zai_config_stable_file_entry *)Z_PTR_P(el); zend_string_release(e->value); diff --git a/zend_abstract_interface/config/config_stable_file.h b/zend_abstract_interface/config/config_stable_file.h index 397ff28f9d8..557a0c2e6b8 100644 --- a/zend_abstract_interface/config/config_stable_file.h +++ b/zend_abstract_interface/config/config_stable_file.h @@ -18,3 +18,4 @@ void zai_config_stable_file_minit(void); void zai_config_stable_file_mshutdown(void); zai_config_stable_file_entry *zai_config_stable_file_get_value(zai_str name); +bool zai_config_stable_file_is_available(void); From 3889c1b50f5c90177c6b254716ee8ec00526ac50 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 14 Oct 2025 16:54:34 +0200 Subject: [PATCH 3/5] perf: refactor strcpy calls to avoid side effects in conditionals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructured conditional chains to separate strcpy side effects from boolean checks. This improves code clarity and makes the control flow more explicit. The strcpy is now only executed after the source type check passes, maintaining the same performance characteristics while improving maintainability. Changed from chained && expressions with strcpy side effects to nested if statements with clear separation of concerns. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- zend_abstract_interface/config/config_ini.c | 30 ++++++++++++--------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/zend_abstract_interface/config/config_ini.c b/zend_abstract_interface/config/config_ini.c index 4224b460517..0d81fac1662 100644 --- a/zend_abstract_interface/config/config_ini.c +++ b/zend_abstract_interface/config/config_ini.c @@ -442,22 +442,26 @@ void zai_config_ini_rinit(void) { for (uint8_t name_index = 0; name_index < memoized->names_count; name_index++) { zai_str name = ZAI_STR_NEW(memoized->names[name_index].ptr, memoized->names[name_index].len); zai_config_stable_file_entry *entry = has_stable_config ? zai_config_stable_file_get_value(name) : NULL; - if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_FLEET_STABLE_CONFIG - && strcpy(buf.ptr, ZSTR_VAL(entry->value)) - && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { - memoized->name_index = ZAI_CONFIG_ORIGIN_FLEET_STABLE; - memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); - goto next_entry; - } else if (zai_getenv_ex(name, buf, false) == ZAI_ENV_SUCCESS - && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { - goto next_entry; - } else if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_LOCAL_STABLE_CONFIG - && strcpy(buf.ptr, ZSTR_VAL(entry->value)) + if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_FLEET_STABLE_CONFIG) { + strcpy(buf.ptr, ZSTR_VAL(entry->value)); + if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { + memoized->name_index = ZAI_CONFIG_ORIGIN_FLEET_STABLE; + memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); + goto next_entry; + } + } + if (zai_getenv_ex(name, buf, false) == ZAI_ENV_SUCCESS && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { - memoized->name_index = ZAI_CONFIG_ORIGIN_LOCAL_STABLE; - memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); goto next_entry; } + if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_LOCAL_STABLE_CONFIG) { + strcpy(buf.ptr, ZSTR_VAL(entry->value)); + if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { + memoized->name_index = ZAI_CONFIG_ORIGIN_LOCAL_STABLE; + memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); + goto next_entry; + } + } } if (memoized->env_config_fallback && memoized->env_config_fallback(buf, false) && zai_config_process_runtime_env(memoized, buf, in_startup, i, 0)) { From 0c28ff79f41ee117b1f42761607f12ce892198a7 Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 14 Oct 2025 16:55:14 +0200 Subject: [PATCH 4/5] perf: skip config entries with no names in zai_config_ini_rinit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an early continue for config entries that have names_count == 0, avoiding unnecessary processing and conditional checks for entries that cannot have any configuration values. This reduces loop overhead by skipping empty entries entirely instead of entering the inner loop and immediately exiting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- zend_abstract_interface/config/config_ini.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zend_abstract_interface/config/config_ini.c b/zend_abstract_interface/config/config_ini.c index 0d81fac1662..d152ff9f9df 100644 --- a/zend_abstract_interface/config/config_ini.c +++ b/zend_abstract_interface/config/config_ini.c @@ -437,6 +437,11 @@ void zai_config_ini_rinit(void) { continue; } + // Skip entries with no names to avoid unnecessary processing + if (memoized->names_count == 0) { + continue; + } + // makes only sense to update INIs once, avoid rereading env unnecessarily if (!env_to_ini_name || !memoized->original_on_modify) { for (uint8_t name_index = 0; name_index < memoized->names_count; name_index++) { From 77a818cf71d4f403e39e07c1046c69e98e271f7c Mon Sep 17 00:00:00 2001 From: Florian Engelhardt Date: Tue, 14 Oct 2025 16:57:42 +0200 Subject: [PATCH 5/5] perf: avoid redundant strlen calls in zai_config_process_runtime_env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modified zai_config_process_runtime_env to accept a length parameter instead of calling strlen(buf.ptr) for every config value. When we copy from zend_string sources, we now use ZSTR_LEN to get the length directly, eliminating redundant strlen calls on every iteration. This optimization is particularly effective for the stable config paths (FLEET and LOCAL) where we already have the string length from the zend_string structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- zend_abstract_interface/config/config_ini.c | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/zend_abstract_interface/config/config_ini.c b/zend_abstract_interface/config/config_ini.c index d152ff9f9df..37a643bffa6 100644 --- a/zend_abstract_interface/config/config_ini.c +++ b/zend_abstract_interface/config/config_ini.c @@ -342,14 +342,14 @@ void zai_config_ini_minit(zai_config_env_to_ini_name env_to_ini, int module_numb #endif } -static inline bool zai_config_process_runtime_env(zai_config_memoized_entry *memoized, zai_env_buffer buf, bool in_startup, uint16_t config_index, uint16_t name_index) { +static inline bool zai_config_process_runtime_env(zai_config_memoized_entry *memoized, zai_env_buffer buf, bool in_startup, uint16_t config_index, uint16_t name_index, size_t value_len) { /* * we unconditionally decode the value because we do not store the in-use encoded value * so we cannot compare the current environment value to the current configuration value * for the purposes of short circuiting decode */ if (env_to_ini_name) { - zend_string *str = zend_string_init(buf.ptr, strlen(buf.ptr), in_startup); + zend_string *str = zend_string_init(buf.ptr, value_len, in_startup); zend_ini_entry *ini = memoized->ini_entries[name_index]; if (zend_alter_ini_entry_ex(ini->name, str, PHP_INI_USER, PHP_INI_STAGE_RUNTIME, 0) == SUCCESS) { @@ -448,20 +448,22 @@ void zai_config_ini_rinit(void) { zai_str name = ZAI_STR_NEW(memoized->names[name_index].ptr, memoized->names[name_index].len); zai_config_stable_file_entry *entry = has_stable_config ? zai_config_stable_file_get_value(name) : NULL; if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_FLEET_STABLE_CONFIG) { + size_t value_len = ZSTR_LEN(entry->value); strcpy(buf.ptr, ZSTR_VAL(entry->value)); - if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { + if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index, value_len)) { memoized->name_index = ZAI_CONFIG_ORIGIN_FLEET_STABLE; memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); goto next_entry; } } if (zai_getenv_ex(name, buf, false) == ZAI_ENV_SUCCESS - && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { + && zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index, strlen(buf.ptr))) { goto next_entry; } if (entry && entry->source == DDOG_LIBRARY_CONFIG_SOURCE_LOCAL_STABLE_CONFIG) { + size_t value_len = ZSTR_LEN(entry->value); strcpy(buf.ptr, ZSTR_VAL(entry->value)); - if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index)) { + if (zai_config_process_runtime_env(memoized, buf, in_startup, i, name_index, value_len)) { memoized->name_index = ZAI_CONFIG_ORIGIN_LOCAL_STABLE; memoized->config_id = (zai_str) ZAI_STR_FROM_ZSTR(entry->config_id); goto next_entry; @@ -469,7 +471,7 @@ void zai_config_ini_rinit(void) { } } - if (memoized->env_config_fallback && memoized->env_config_fallback(buf, false) && zai_config_process_runtime_env(memoized, buf, in_startup, i, 0)) { + if (memoized->env_config_fallback && memoized->env_config_fallback(buf, false) && zai_config_process_runtime_env(memoized, buf, in_startup, i, 0, strlen(buf.ptr))) { goto next_entry; } }