Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/prof_asan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ on:

jobs:
prof-asan:
name: PHP ${{ matrix.php-version }} ${{ matrix.php-build }} (${{ matrix.runner }})
runs-on: ${{ matrix.runner }}
strategy:
matrix:
php-version: [8.3, 8.4, 8.5]
php-build: [nts-asan, debug-zts-asan]
runner: [arm-8core-linux, ubuntu-8-core-latest]
env:
CARGO_HOME: /rust/cargo
Expand Down Expand Up @@ -42,12 +44,12 @@ jobs:
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: /tmp/build-cargo/
key: ${{ runner.os }}-${{ runner.arch }}-cargo-target-asan-${{ matrix.php-version }}-${{ env.RUST_TOOLCHAIN }}-${{ github.sha }}-${{ hashFiles('.github/workflows/prof_asan.yml') }}
key: ${{ runner.os }}-${{ runner.arch }}-cargo-target-asan-${{ matrix.php-version }}-${{ matrix.php-build }}-${{ env.RUST_TOOLCHAIN }}-${{ github.sha }}-${{ hashFiles('.github/workflows/prof_asan.yml') }}

- name: Build and install profiler
run: |
set -eux
switch-php nts-asan
switch-php ${{ matrix.php-build }}
cd profiling
export CC=clang-19
export CFLAGS='-fsanitize=address -fno-omit-frame-pointer'
Expand All @@ -60,7 +62,7 @@ jobs:
- name: Run phpt tests
run: |
set -eux
switch-php nts-asan
switch-php ${{ matrix.php-build }}
cd profiling/tests
cp -v $(php-config --prefix)/lib/php/build/run-tests.php .
export DD_PROFILING_OUTPUT_PPROF=/tmp/pprof
Expand Down
66 changes: 66 additions & 0 deletions profiling/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ fn main() {
cfg_php_major_version(vernum);
cfg_php_feature_flags(vernum);
cfg_zts();
cfg_php_debug();
apple_linker_flags();
}

Expand Down Expand Up @@ -462,6 +463,71 @@ int main() {
}
}

fn cfg_php_debug() {
println!("cargo::rustc-check-cfg=cfg(php_debug)");

let output = Command::new("php-config")
.arg("--include-dir")
.output()
.expect("Unable to run `php-config`. Is it in your PATH?");

if !output.status.success() {
match String::from_utf8(output.stderr) {
Ok(stderr) => panic!("`php-config --include-dir` failed: {stderr}"),
Err(err) => panic!("`php-config --include-dir` failed, not utf8: {err}"),
}
}

let include_dir = std::str::from_utf8(output.stdout.as_slice())
.expect("`php-config`'s stdout to be valid utf8")
.trim();

let out_dir = env::var("OUT_DIR").unwrap();
let probe_path = Path::new(&out_dir).join("php_debug_probe.c");
fs::write(
&probe_path,
r#"
#include "main/php_config.h"
#include <stdio.h>
int main() {
#if ZEND_DEBUG
printf("1");
#else
printf("0");
#endif
return 0;
}
"#,
)
.expect("Failed to write PHP debug probe file");

let compiler = cc::Build::new().get_compiler();
let probe_exe = Path::new(&out_dir).join("php_debug_probe");
let compile_status = Command::new(compiler.path())
.arg(format!("-I{}", include_dir))
.arg(&probe_path)
.arg("-o")
.arg(&probe_exe)
.status()
.expect("Failed to compile PHP debug probe");

if !compile_status.success() {
panic!("Failed to compile PHP debug probe");
}

let probe_output = Command::new(&probe_exe)
.output()
.expect("Failed to run PHP debug probe");

let debug_value = std::str::from_utf8(&probe_output.stdout)
.expect("PHP debug probe output not UTF-8")
.trim();

if debug_value == "1" {
println!("cargo:rustc-cfg=php_debug");
}
}

/// On macOS (Apple targets), the cdylib has undefined symbols that are
/// resolved at load time by the PHP process. In debug builds, LTO is off
/// which produces more unresolved symbols--we fall back to
Expand Down
79 changes: 79 additions & 0 deletions profiling/src/allocation/allocation_ge84.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use libc::{c_char, c_int, c_void, size_t};
use log::{debug, trace, warn};
use std::sync::atomic::Ordering::Relaxed;

#[cfg(php_debug)]
use libc::c_uint;

#[cfg(feature = "debug_stats")]
use crate::allocation::{ALLOCATION_PROFILING_COUNT, ALLOCATION_PROFILING_SIZE};

Expand Down Expand Up @@ -271,7 +274,24 @@ unsafe fn restore_zend_heap(heap: *mut zend::_zend_mm_heap, custom_heap: c_int)
ptr::write(heap as *mut c_int, custom_heap);
}

#[cfg(not(php_debug))]
unsafe extern "C" fn alloc_prof_malloc(len: size_t) -> *mut c_void {
alloc_prof_malloc_impl(len)
}

#[cfg(php_debug)]
unsafe extern "C" fn alloc_prof_malloc(
len: size_t,
_file: *const c_char,
_line: c_uint,
_orig_file: *const c_char,
_orig_line: c_uint,
) -> *mut c_void {
alloc_prof_malloc_impl(len)
}

#[inline(always)]
unsafe fn alloc_prof_malloc_impl(len: size_t) -> *mut c_void {
#[cfg(feature = "debug_stats")]
ALLOCATION_PROFILING_COUNT.fetch_add(1, Relaxed);
#[cfg(feature = "debug_stats")]
Expand Down Expand Up @@ -300,6 +320,11 @@ unsafe fn alloc_prof_prev_alloc(len: size_t) -> *mut c_void {
// neighboring extension could misbehave. If that happens, we want a proper
// panic with backtrace for debugging rather than undefined behavior.
let alloc = tls_zend_mm_state_get!(prev_custom_mm_alloc).unwrap();
#[cfg(php_debug)]
{
return alloc(len, ptr::null(), 0, ptr::null(), 0);
}
#[cfg(not(php_debug))]
alloc(len)
}

Expand All @@ -308,14 +333,34 @@ unsafe fn alloc_prof_orig_alloc(len: size_t) -> *mut c_void {
// handlers only point to this function after successful init. Using `unwrap_unchecked()` is
// safe here as we have full control over ZendMM with no neighboring extensions.
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
#[cfg(php_debug)]
return zend::_zend_mm_alloc(heap, len, ptr::null(), 0, ptr::null(), 0);
#[cfg(not(php_debug))]
zend::_zend_mm_alloc(heap, len)
}

/// This function exists because when calling `zend_mm_set_custom_handlers()`,
/// you need to pass a pointer to a `free()` function as well, otherwise your
/// custom handlers won't be installed. We cannot just point to the original
/// `zend::_zend_mm_free()` as the function definitions differ.
#[cfg(not(php_debug))]
unsafe extern "C" fn alloc_prof_free(ptr: *mut c_void) {
alloc_prof_free_impl(ptr);
}

#[cfg(php_debug)]
unsafe extern "C" fn alloc_prof_free(
ptr: *mut c_void,
_file: *const c_char,
_line: c_uint,
_orig_file: *const c_char,
_orig_line: c_uint,
) {
alloc_prof_free_impl(ptr);
}

#[inline(always)]
unsafe fn alloc_prof_free_impl(ptr: *mut c_void) {
tls_zend_mm_state_get!(free)(ptr);
}

Expand All @@ -327,6 +372,11 @@ unsafe fn alloc_prof_prev_free(ptr: *mut c_void) {
// neighboring extension could misbehave. If that happens, we want a proper
// panic with backtrace for debugging rather than undefined behavior.
let free = tls_zend_mm_state_get!(prev_custom_mm_free).unwrap();
#[cfg(php_debug)]
{
return free(ptr, core::ptr::null(), 0, core::ptr::null(), 0);
}
#[cfg(not(php_debug))]
free(ptr)
}

Expand All @@ -335,10 +385,31 @@ unsafe fn alloc_prof_orig_free(ptr: *mut c_void) {
// handlers only point to this function after successful init. Using `unwrap_unchecked()` is
// safe here as we have full control over ZendMM with no neighboring extensions.
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
#[cfg(php_debug)]
return zend::_zend_mm_free(heap, ptr, core::ptr::null(), 0, core::ptr::null(), 0);
#[cfg(not(php_debug))]
zend::_zend_mm_free(heap, ptr);
}

#[cfg(not(php_debug))]
unsafe extern "C" fn alloc_prof_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_void {
alloc_prof_realloc_impl(prev_ptr, len)
}

#[cfg(php_debug)]
unsafe extern "C" fn alloc_prof_realloc(
prev_ptr: *mut c_void,
len: size_t,
_file: *const c_char,
_line: c_uint,
_orig_file: *const c_char,
_orig_line: c_uint,
) -> *mut c_void {
alloc_prof_realloc_impl(prev_ptr, len)
}

#[inline(always)]
unsafe fn alloc_prof_realloc_impl(prev_ptr: *mut c_void, len: size_t) -> *mut c_void {
#[cfg(feature = "debug_stats")]
ALLOCATION_PROFILING_COUNT.fetch_add(1, Relaxed);
#[cfg(feature = "debug_stats")]
Expand Down Expand Up @@ -367,6 +438,11 @@ unsafe fn alloc_prof_prev_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_
// neighboring extension could misbehave. If that happens, we want a proper
// panic with backtrace for debugging rather than undefined behavior.
let realloc = tls_zend_mm_state_get!(prev_custom_mm_realloc).unwrap();
#[cfg(php_debug)]
{
return realloc(prev_ptr, len, ptr::null(), 0, ptr::null(), 0);
}
#[cfg(not(php_debug))]
realloc(prev_ptr, len)
}

Expand All @@ -375,6 +451,9 @@ unsafe fn alloc_prof_orig_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_
// handlers only point to this function after successful init. Using `unwrap_unchecked()` is
// safe here as we have full control over ZendMM with no neighboring extensions.
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
#[cfg(php_debug)]
return zend::_zend_mm_realloc(heap, prev_ptr, len, ptr::null(), 0, ptr::null(), 0);
#[cfg(not(php_debug))]
zend::_zend_mm_realloc(heap, prev_ptr, len)
}

Expand Down
10 changes: 10 additions & 0 deletions profiling/src/allocation/allocation_le83.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,9 @@ unsafe fn alloc_prof_orig_alloc(len: size_t) -> *mut c_void {
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
let (prepare, restore) = tls_zend_mm_state_get!(prepare_restore_zend_heap);
let custom_heap = prepare(heap);
#[cfg(php_debug)]
let ptr: *mut c_void = zend::_zend_mm_alloc(heap, len, ptr::null(), 0, ptr::null(), 0);
#[cfg(not(php_debug))]
let ptr: *mut c_void = zend::_zend_mm_alloc(heap, len);
restore(heap, custom_heap);
ptr
Expand All @@ -345,6 +348,9 @@ unsafe fn alloc_prof_orig_free(ptr: *mut c_void) {
// Safety: `ZEND_MM_STATE.heap` will be initialised in `alloc_prof_rinit()` and custom ZendMM
// handlers are only installed and pointing to this function if initialization was succesful.
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
#[cfg(php_debug)]
zend::_zend_mm_free(heap, ptr, core::ptr::null(), 0, core::ptr::null(), 0);
#[cfg(not(php_debug))]
zend::_zend_mm_free(heap, ptr);
}

Expand Down Expand Up @@ -383,6 +389,10 @@ unsafe fn alloc_prof_orig_realloc(prev_ptr: *mut c_void, len: size_t) -> *mut c_
let heap = tls_zend_mm_state_get!(heap).unwrap_unchecked();
let (prepare, restore) = tls_zend_mm_state_get!(prepare_restore_zend_heap);
let custom_heap = prepare(heap);
#[cfg(php_debug)]
let ptr: *mut c_void =
zend::_zend_mm_realloc(heap, prev_ptr, len, ptr::null(), 0, ptr::null(), 0);
#[cfg(not(php_debug))]
let ptr: *mut c_void = zend::_zend_mm_realloc(heap, prev_ptr, len);
restore(heap, custom_heap);
ptr
Expand Down
18 changes: 18 additions & 0 deletions profiling/src/bindings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,27 @@ pub type VmZendThrowExceptionHook = unsafe extern "C" fn(*mut zval);
#[cfg(php8)]
pub type VmZendThrowExceptionHook = unsafe extern "C" fn(*mut zend_object);

#[cfg(not(all(php_debug, php_zend_mm_set_custom_handlers_ex)))]
pub type VmMmCustomAllocFn = unsafe extern "C" fn(size_t) -> *mut c_void;
#[cfg(all(php_debug, php_zend_mm_set_custom_handlers_ex))]
pub type VmMmCustomAllocFn =
unsafe extern "C" fn(size_t, *const c_char, c_uint, *const c_char, c_uint) -> *mut c_void;
#[cfg(not(all(php_debug, php_zend_mm_set_custom_handlers_ex)))]
pub type VmMmCustomReallocFn = unsafe extern "C" fn(*mut c_void, size_t) -> *mut c_void;
#[cfg(all(php_debug, php_zend_mm_set_custom_handlers_ex))]
pub type VmMmCustomReallocFn = unsafe extern "C" fn(
*mut c_void,
size_t,
*const c_char,
c_uint,
*const c_char,
c_uint,
) -> *mut c_void;
#[cfg(not(all(php_debug, php_zend_mm_set_custom_handlers_ex)))]
pub type VmMmCustomFreeFn = unsafe extern "C" fn(*mut c_void);
#[cfg(all(php_debug, php_zend_mm_set_custom_handlers_ex))]
pub type VmMmCustomFreeFn =
unsafe extern "C" fn(*mut c_void, *const c_char, c_uint, *const c_char, c_uint);
#[cfg(php_zend_mm_set_custom_handlers_ex)]
pub type VmMmCustomGcFn = unsafe extern "C" fn() -> size_t;
#[cfg(php_zend_mm_set_custom_handlers_ex)]
Expand Down
6 changes: 3 additions & 3 deletions profiling/src/php_ffi.c
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,9 @@ void ddog_php_prof_copy_long_into_zval(zval *dest, long num) {
}

void ddog_php_prof_zend_mm_set_custom_handlers(zend_mm_heap *heap,
void* (*_malloc)(size_t),
void (*_free)(void*),
void* (*_realloc)(void*, size_t)) {
ddog_php_prof_zend_mm_malloc _malloc,
ddog_php_prof_zend_mm_free _free,
ddog_php_prof_zend_mm_realloc _realloc) {
zend_mm_set_custom_handlers(heap, _malloc, _free, _realloc);
#if PHP_VERSION_ID < 70300
if (!_malloc && !_free && !_realloc) {
Expand Down
16 changes: 13 additions & 3 deletions profiling/src/php_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,20 @@ void ddog_php_prof_copy_long_into_zval(zval *dest, long num);
* `use_custom_heap` flag back to normal when null pointers are being passed
* in on those PHP versions.
*/
#if PHP_VERSION_ID >= 80400 && ZEND_DEBUG
typedef void *(*ddog_php_prof_zend_mm_malloc)(size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
typedef void (*ddog_php_prof_zend_mm_free)(void * ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
typedef void *(*ddog_php_prof_zend_mm_realloc)(void *, size_t ZEND_FILE_LINE_DC ZEND_FILE_LINE_ORIG_DC);
#else
typedef void *(*ddog_php_prof_zend_mm_malloc)(size_t);
typedef void (*ddog_php_prof_zend_mm_free)(void *);
typedef void *(*ddog_php_prof_zend_mm_realloc)(void *, size_t);
#endif

void ddog_php_prof_zend_mm_set_custom_handlers(zend_mm_heap *heap,
void* (*_malloc)(size_t),
void (*_free)(void*),
void* (*_realloc)(void*, size_t));
ddog_php_prof_zend_mm_malloc _malloc,
ddog_php_prof_zend_mm_free _free,
ddog_php_prof_zend_mm_realloc _realloc);

zend_execute_data* ddog_php_prof_get_current_execute_data();

Expand Down
Loading