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, ); }; diff --git a/zend_abstract_interface/config/config_ini.c b/zend_abstract_interface/config/config_ini.c index 06f03f711c8..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) { @@ -429,36 +429,49 @@ 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) { 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++) { 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); - 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)) - && 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); + 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, 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, 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, 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; + } + } } - 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; } } 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);