From 8b91eda77717a10f7d3c0b8c3596df1b651bbbbf Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Thu, 10 Sep 2026 17:17:29 +0200 Subject: [PATCH 1/2] fix(appsec): weakly reference __cxa_thread_atexit_impl so musl ZTS can load The TLS destructor registration in PHP_GINIT_FUNCTION(ddappsec) declared __cxa_thread_atexit_impl as a strong symbol and called it unconditionally on Linux. The symbol is glibc-private and musl exports it nowhere, and musl resolves relocations eagerly, so dlopen("ddappsec.so") fails outright with "Error relocating ...: __cxa_thread_atexit_impl: symbol not found". AppSec is therefore dead on musl + ZTS (FrankenPHP-Alpine, Swoole/pthreads on Alpine) in 1.25.0 and 1.25.1. ddtrace-zts.so references the same symbol weakly, which is why the tracer is unaffected. Declare it weak and register the destructor only when it resolves. registered_thread_local_dtor then stays false on musl, which is the case PHP_GSHUTDOWN_FUNCTION(ddappsec) already handles ("a platform without a thread-exit destructor mechanism"), so tshutdown still runs on the owning thread. glibc is unchanged: the weak reference resolves there and the destructor is registered as before. Verified on dunglas/frankenphp:php8.3.12-alpine (PHP 8.3.12 ZTS, musl 1.2.5), same tree, incremental rebuild of only this file: before: NOTYPE GLOBAL DEFAULT UND __cxa_thread_atexit_impl extension_loaded("ddappsec") => false, "Error relocating" warning after: NOTYPE WEAK DEFAULT UND __cxa_thread_atexit_impl extension_loaded("ddappsec") => true, no relocation error --- appsec/src/extension/ddappsec.c | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/appsec/src/extension/ddappsec.c b/appsec/src/extension/ddappsec.c index 672f6f7d41..821f0b1924 100644 --- a/appsec/src/extension/ddappsec.c +++ b/appsec/src/extension/ddappsec.c @@ -195,15 +195,19 @@ static PHP_GINIT_FUNCTION(ddappsec) if (!is_main_thread) { # if defined(__linux__) extern void *__dso_handle; - // adds a dependency on glibc 2.18 - extern int __cxa_thread_atexit_impl( - void (*func)(void *), void *arg, void *dso_handle); - __cxa_thread_atexit_impl(_tshutdown_handler, NULL, __dso_handle); + // Weak because musl exports no __cxa_thread_atexit_impl (it is a glibc + // 2.18 internal); a strong reference makes dlopen() of this DSO fail. + extern int __cxa_thread_atexit_impl(void (*func)(void *), void *arg, + void *dso_handle) __attribute__((weak)); + if (__cxa_thread_atexit_impl) { + __cxa_thread_atexit_impl(_tshutdown_handler, NULL, __dso_handle); + ddappsec_globals->registered_thread_local_dtor = true; + } # elif defined(__APPLE__) extern void _tlv_atexit(void (*termFunc)(void *), void *objAddr); _tlv_atexit(_tshutdown_handler, NULL); -# endif ddappsec_globals->registered_thread_local_dtor = true; +# endif } #endif } From 8555102bf35bfa580574a7c10f6e9a74a554ec62 Mon Sep 17 00:00:00 2001 From: Alexandre Rulleau Date: Thu, 10 Sep 2026 17:17:41 +0200 Subject: [PATCH 2/2] ci: raise and measure the disk envelope for helper-rust integration coverage "helper-rust integration coverage" never set DOCKER_LOOPBACK_SIZE, so it ran on the DinD default of ~20G while its sibling .appsec_integration_tests sets 30G and "push appsec images" sets 100G. 17 consecutive master runs died in :buildPortableLibdatadogPhp with "failed to build archive at .../libdatadog_php.a: No space left on device (os error 28)", and the 10 runs after that with "Bus error" from musl-clang at the final cdylib link. A same-pipeline A/B against the sibling holds everything else equal - same image, runner tag, CPU and memory - and differs only by -PuseHelperRustCoverage and the loopback size. Raise it to 50G: the coverage build is strictly larger than the sibling's 30G. The existing rm -rf /vol/cargo-target mitigation sits after the build, so it never runs when the build itself is what exhausts the volume. Also report df -h /, docker system df and the php-portable-libdatadog-php volume before and after the build. The ENOSPC is measured; the "Bus error" being the same exhaustion one step later is still an inference, and these numbers settle it in one cycle. The gradle exit status is preserved so a failing build still fails the job, and the report is emitted on that path too, which is the case the numbers are for. --- .gitlab/generate-appsec.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.gitlab/generate-appsec.php b/.gitlab/generate-appsec.php index ce9113d68b..21bf7f8197 100644 --- a/.gitlab/generate-appsec.php +++ b/.gitlab/generate-appsec.php @@ -340,6 +340,9 @@ KUBERNETES_CPU_REQUEST: 8 KUBERNETES_MEMORY_REQUEST: 24Gi KUBERNETES_MEMORY_LIMIT: 30Gi + # Coverage instrumentation makes this build strictly bigger than the 30G + # its non-coverage sibling needs, and the DinD helper only defaults to 20G. + DOCKER_LOOPBACK_SIZE: 50G ARCH: amd64 GRADLE_USER_HOME: "$CI_PROJECT_DIR/.gradle-home" before_script: @@ -355,8 +358,21 @@ TERM=dumb ./gradlew loadCaches --info fi + echo "=== disk before buildPortableLibdatadogPhp ===" + df -h / + docker system df + docker run --rm -v php-portable-libdatadog-php:/vol alpine df -h /vol + + # Keep the exit status but always report disk after the build: an + # exhausted loopback is the hypothesis these numbers exist to settle. TERM=dumb ./gradlew buildPortableLibdatadogPhp \ - --info -Pbuildscan --scan -PuseHelperRustCoverage + --info -Pbuildscan --scan -PuseHelperRustCoverage && rc=0 || rc=$? + + echo "=== disk after buildPortableLibdatadogPhp (gradle exit $rc) ===" + df -h / + docker system df + docker run --rm -v php-portable-libdatadog-php:/vol alpine df -h /vol + [ "$rc" -eq 0 ] || exit "$rc" # Coverage-instrumented artifacts are bulky: this leaves ~6G of cargo # intermediates in php-portable-libdatadog-php, over a quarter of the