Skip to content

fix(appsec): load on musl ZTS; ci: disk envelope for helper-rust coverage - #4180

Draft
Leiyks wants to merge 3 commits into
masterfrom
leiyks/fix-appsec-musl-zts-and-coverage-disk
Draft

fix(appsec): load on musl ZTS; ci: disk envelope for helper-rust coverage#4180
Leiyks wants to merge 3 commits into
masterfrom
leiyks/fix-appsec-musl-zts-and-coverage-disk

Conversation

@Leiyks

@Leiyks Leiyks commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Two independent fixes, one per commit. They share no code and can be reviewed separately.

1. fix(appsec): AppSec cannot load at all on musl + ZTS

PHP_GINIT_FUNCTION(ddappsec) declared __cxa_thread_atexit_impl as a strong symbol and called it unconditionally on Linux. That symbol is glibc-private, 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. ddtrace-zts.so references the same symbol weakly, which is why the tracer is unaffected.

This is a shipped, user-facing regression, not a CI artefact. readelf -sW on the published bundles: absent in 1.24.1, GLOBAL UND in both 1.25.0 and 1.25.1 (introduced by #3725). Every musl+ZTS deployment — FrankenPHP-Alpine is the canonical case, plus Swoole/pthreads on Alpine — silently loses AppSec with a startup warning. The blast radius is precisely musl ∩ ZTS: NTS musl builds emit no reference at all, and glibc ZTS emits the same GLOBAL UND but is saved by glibc ≥ 2.18 exporting the symbol.

The fix declares the symbol __attribute__((weak)) and registers the destructor only when it resolves. registered_thread_local_dtor then stays false on musl, which is exactly the case PHP_GSHUTDOWN_FUNCTION(ddappsec) already documents and handles ("a platform without a thread-exit destructor mechanism"), so tshutdown still runs on the owning thread. glibc behaviour is unchanged — the weak reference resolves there and the destructor is registered as before.

Verified by building ddappsec.so for musl+ZTS in dunglas/frankenphp:php8.3.12-alpine (PHP 8.3.12 ZTS, musl 1.2.5, API 20230831 — the exact image the failing CI sub-test uses), A/B on the same build tree with an incremental rebuild of only this file:

symbol binding extension_loaded("ddappsec")
master NOTYPE GLOBAL DEFAULT UND false, with the verbatim CI Error relocating warning
this PR NOTYPE WEAK DEFAULT UND true, no relocation error

installer tests will not go green when this merges. That job installs the published GitHub release for $(cat VERSION) — its needs supply only *-unknown-linux-gnu bundles, and no musl-ZTS bundle exists as an artifact input — so it keeps downloading the immutable 1.25.1 tarball regardless of what master contains. It stays red until a release containing this fix is published and VERSION points at it, or the sub-test is re-pointed at a locally built musl-ZTS bundle. In other words this fix is release-blocking, not CI-blocking, and the gating decision for that job is separate from this code change. Its single failing sub-test test_alpine_zts_no_zend_signals.sh is also the only failing one: because find ordering varies per run, the union across recent master runs covers 40/40 sub-tests with 39/40 observed passing.

2. ci: disk envelope for helper-rust integration coverage

The job 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. On master it has failed 33/33 runs over the last three days in two consecutive signatures, both in :buildPortableLibdatadogPhp: 17 runs with failed to build archive at .../libdatadog_php.a: No space left on device (os error 28), then 10 runs with Bus error from musl-clang at the final cdylib link. A same-pipeline A/B against the sibling holds image, runner tag, CPU and memory equal and differs only by -PuseHelperRustCoverage and the loopback size; OOM was positively excluded (no OOMKilled/eviction/DiskPressure for the exact pods, and SIGBUS is not what a SIGKILL looks like).

Raised to 50G, since the coverage build is strictly larger than the sibling's 30G. Note that 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.

The second half of the change is measurement rather than mitigation: df -h /, docker system df and df -h on the php-portable-libdatadog-php volume, before and after the build. The ENOSPC is measured; the claim that the Bus error is the same exhaustion one step later is currently an inference, and these numbers settle it in a single cycle. The gradle exit status is preserved so a failing build still fails the job, and the report is emitted on the failure path too — which is the case the numbers exist for. Nothing is || true'd and no allow_failure or flaky-jobs entry is added.

Follow-up, deliberately not in this PR

Scoping -C instrument-coverage to the helper-rust package (-Zprofile-rustflags + profile.tracer-release.package.helper-rust.rustflags) instead of applying it globally on top of -Z build-std would attack the cause of the size rather than the envelope, and would cut build time. It is left out because it is unverified and risks silently emptying the LCOV report under fat LTO; it should land on its own once the df numbers above are in and the report can be diffed against a known-good one.

…n 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
…overage

"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.
@datadog-prod-us1-6

datadog-prod-us1-6 Bot commented Sep 10, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 9 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | PHP language tests: [8.5, amd64, zts] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | check libxml2 version — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | test_extension_ci: [7.2, valgrind] — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

View all 9 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 55.03% (+0.00%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 13be1d2 | Docs | View more details | Give us feedback!

@pr-commenter

pr-commenter Bot commented Sep 10, 2026

Copy link
Copy Markdown

Benchmarks [ appsec ]

Benchmark execution time: 2026-09-10 23:38:36

Comparing candidate commit 13be1d2 in PR branch leiyks/fix-appsec-musl-zts-and-coverage-disk with baseline commit fcbed87 in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 12 metrics, 0 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants