Skip to content

Portable builds in CI - #4239

Open
cataphract wants to merge 32 commits into
masterfrom
glopes/portable-builds
Open

cataphract wants to merge 32 commits into
masterfrom
glopes/portable-builds

Conversation

@cataphract

@cataphract cataphract commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Description

Every Linux artifact used to be built twice: once against glibc on the old CentOS 7 images, and once against musl on Alpine. That doubled the compile jobs for each component and PHP version, and kept an ageing toolchain alive only to get an old enough glibc.

This PR builds each extension, the sidecar library and the loader once per architecture, against musl, and links them in a way that loads in both glibc and musl processes. The build scripts refuse any output that picks up a glibc symbol version. The builds now run in the parent pipeline, so the test, appsec, profiler and packaging child pipelines all use the binaries that actually ship, instead of rebuilding their own.

A few runtime fixes came with it, because the musl build changed assumptions the code had been relying on implicitly:

  • The backtrace functions are looked up at runtime. datadog.log_backtrace now warns when they're unavailable instead of being silently ignored.
  • A small trampoline sets PHP's pinned VM registers before ddtrace calls a VM handler directly.
  • The Rust-side session-id buffers are aligned the way C expects.

The CentOS 7 images are no longer needed and are removed.

With one binary per architecture, the release bundles no longer carry a libc in their name. That is the one change users may notice:

Release asset names changed

Linux builds are now portable across glibc and musl, so the separate -linux-gnu and -linux-musl release assets are replaced by one archive per architecture:

  • dd-library-php-<version>-<arch>-linux-gnu.tar.gz and …-linux-musl.tar.gz → dd-library-php-<version>-<arch>-linux.tar.gz
  • …-linux-gnu-<API>[-zts|-debug].tar.gz and …-linux-musl-<API>[-zts].tar.gz → …-linux-<API>[-zts|-debug].tar.gz

datadog-setup.php already uses the new names. Scripts that download these archives directly must be updated.

Summary of job changes

Reviewer checklist

  • Test coverage seems ok.
  • Appropriate labels assigned.

@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Pipelines  Tests

✨ Unblock PR with BitsAI

❌ Errors

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

🚦 8 Pipeline jobs failed

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

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | merge-gate

View more details · View in GitLab

DataDog/apm-reliability/dd-trace-php | publish docker image for system tests

View more details · View in GitLab

View all 8 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: 68.26% (+0.00%)

Useful? React with 👍 / 👎

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

@pr-commenter

pr-commenter Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-09-30 10:47:14

Comparing candidate commit 31a3cfb in PR branch glopes/portable-builds with baseline commit d98de1e in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 194 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 ----------------------------------'

@cataphract
cataphract force-pushed the glopes/portable-builds branch from 43a8a23 to a38379b Compare September 29, 2026 15:10
@cataphract
cataphract marked this pull request as ready for review September 29, 2026 15:21
@cataphract
cataphract requested review from a team as code owners September 29, 2026 15:21
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T15:27:52.685760Z a38379b Draft marked ready
🔒 Security Review ✅ Completed 2026-09-29T15:27:35.496227Z a38379b Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

cataphract and others added 15 commits September 30, 2026 15:55
Fetch generated bridge sources in downstream AppSec jobs and preserve glibc backtraces in musl-built portable tracer extensions.
The portable-bundle rework stopped producing the per-API tarballs
(dd-library-php-X-{arch}-linux-gnu-20230831.tar.gz and friends), and
datadog-setup.php dropped the logic that downloaded them. Every install
therefore fetched the full bundle, which is about 1.5 GB per
architecture, instead of a ~50 MB subset. With ext-curl the download is
bounded by a 50 s CURLOPT_TIMEOUT, which that size cannot meet on most
connections, and the file_get_contents() fallback would read the whole
archive into memory.

Bring the per-version tarballs back, for Linux and Windows, but name
them after the portable platform (x86_64-linux-20230831.tar.gz) rather
than after a libc: the binaries are identical for glibc and musl, so a
libc-specific pair would only duplicate the release assets. They are
cut from the already assembled full bundle instead of being staged
separately, so their contents cannot drift from it.

datadog-setup.php regains the previous behaviour: when all selected
binaries share one PHP API and configuration it asks for the
per-version tarball, and falls back to the full bundle on a 404.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Portable builds are compiled against musl and look up backtrace() and
backtrace_symbols() as weak symbols, so on a musl system the SIGSEGV
handler has nothing to print. Setting datadog.log_backtrace there was
silently ignored, or, with crashtracking on (the default), reported as
a conflict with the crashtracker, which is not the reason it does
nothing.

Check availability first and say so explicitly, then carry on as if
the setting were off. That also lets the crashtracker conflict be
decided in one place: the portable-build change had added a second copy
of that check ahead of the original one.

The new test only runs on Alpine; tracer .phpt jobs run on glibc
images, so it is exercised by local Alpine builds, which take the same
weak-import path as the portable build.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SIGSEGV handler is also installed for health metrics, where it sends
datadog.tracer.uncaught_exceptions{class:sigsegv}. It is skipped while
the crashtracker is installed, since it neither saves nor chains to the
crashtracker's handler. In that case the warning blamed
datadog.log_backtrace, even when the user had only enabled health
metrics.

The two settings are not really exclusive: the heartbeat metric is
still sent, and the crashtracker sends its own crash report. Say
exactly what is lost instead. Letting both run by installing ddtrace's
handler first, so the crashtracker chains to it, was considered and
rejected: it would bring back a metric the crash report already covers,
at the cost of running the handler's backtrace() calls, which are not
async-signal-safe, on every crash.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The trampoline that pins PHP's VM frame and instruction registers
around the nested handler call is file-scope inline assembly. It
switched to .text with a bare directive, behind the compiler's back:
GCC tracks the current section itself and only emits a directive when
it thinks one is needed, so anything it emitted next could land in
.text instead of where it belonged. Use .pushsection/.popsection so the
compiler's section state is left as it was.

On aarch64 the trampoline also saved x30 without building a frame
record. The CFI was correct, so gdb and libunwind were unaffected, but
frame-pointer walkers (perf --call-graph fp and similar) followed the
handler's saved x29 straight past zai_interceptor_declare_jit_handler.
Save x29/x30 as a pair and point x29 at it, keeping x27/x28 in the same
32-byte frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fpm produces an invalid apk when the after-install script's size falls
in a range ending at 7680 bytes (jordansissel/fpm#1866). The file
carries a comment about this, but nothing enforced it, and dropping the
Alpine-specific extension naming shrank the script from 7751 to 7571
bytes, into the failing range. The portable-build change padded the
comment back up; the margin is now about 250 bytes, and the next edit
that trims the script would fail both APK jobs with an unrelated-looking
tar error.

Check the size in the .apk.% target before running fpm, so the failure
names the cause, and make the comment state the threshold and point at
the check. The check is limited to apk because deb, rpm and tar.gz
packaging are not affected.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cataphract and others added 10 commits September 30, 2026 15:55
Release artifacts are now built in the portable musl image, and the
CentOS 7 build images this series removed are no longer produced. A
few things still existed only to accommodate them: fallback
definitions of HWCAP2_BTI and PROT_BTI for CentOS 7's kernel headers in
the loader's telemetry reaper, the X-centos7 services in
docker-compose.yml, and dockerh's translation of --php variants for
centos-7 images.

Both the portable image and bookworm-11 define the BTI constants; the
reaper compiles with branch protection enabled against either. dockerh
keeps its BASH_ENV redirect, which other images still need, with the
comment no longer pointing at centos-7 and devtoolset-7.

CentOS 7 as a runtime target is untouched: it still has to load the
portable binaries, so the package verification and randomized-test
jobs that run on it stay, as do runtime workarounds for old glibc such
as calling gettid through syscall().

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The oldest supported glibc is 2.17 (CentOS 7), per the loader's
requirements.json and check_glibc_version.sh, and nothing in CI verifies
on CentOS 6 any more. What was left of it:

- The CentOS 6 targets, compose services and verify_rpm_centos6.sh in
  dockerfiles/verify_packages, reachable from no job.
- dockerfiles/packaging/Makefile, removed as a whole rather than just
  its CentOS 6 targets: every target in it points at services or
  Dockerfiles that no longer exist (jessie, stretch, PHP 5.x), and
  nothing invokes it. fpm_packaging/ next to it is still used.
- Two comments that gave CentOS 6 as the reason for code that is still
  needed for other reasons. The empty Expect header in coms.c remains
  necessary because CURLOPT_EXPECT_100_TIMEOUT_MS needs curl 7.36 and
  CentOS 7 ships 7.29; the stack-sample _Static_assert stays out of
  the header because the header is included from C++ in tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removing the CentOS 7 toolchain took the image definitions in
dockerfiles/ci/centos/7 with it, but those images are not only a
toolchain: the randomized tests run the shipped portable binaries in
datadog/dd-trace-ci:php-randomizedtests-centos7-*, which
tests/randomized/docker/centos.Dockerfile builds on top of
php-X.Y_centos-7. That is the only coverage of the release artifacts
under real web servers on glibc 2.17, the oldest glibc we support.

The published images keep that working for now, but without their
sources and the ci-images jobs nobody could rebuild them to pick up a
new PHP patch release or fix the base. Bring both back unchanged from
master. php-profiling.Dockerfile stays deleted: nothing in the compose
file builds it, and it only served profiler builds on CentOS 7.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The CentOS 7 CI image now exists only as a runtime environment: the
randomized tests run the shipped portable binaries on top of it. It
still carried everything needed to compile ddtrace itself on CentOS 7,
which nothing does any more: LLVM/clang/lld built from source (the
libclang for bindgen, and the bulk of the image's build time), the Rust
toolchain with rust-src, Catch2 for the C component tests that no
longer run on this image, the securebits.h stand-in that only
ddtrace's config.m4 needed, and librdkafka-devel.

What the image still needs stays: devtoolset-7, the autotools and the
from-source libraries used to build PHP itself, CMake for libzip, and
the compilers and -devel packages pecl needs in the randomized image.
postgresql-devel looked removable but is still used by PHP 7.0-7.3's
pdo_pgsql.

Built on amd64 with PHP 8.3 and the randomized image on top; it behaves
as the published image does (same extensions loaded, same web server
configs valid), while php-8.3_centos-7 shrinks from 5.0
to 2.6 GB.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The base image enables devtoolset-7 through BASH_ENV, but Docker runs
RUN steps with /bin/sh, and bash ignores BASH_ENV in that mode. pecl
therefore compiled the extensions with CentOS 7's GCC 4.8. That went
unnoticed until redis 6.3.0, which no longer compiles with it, so the
Dockerfile could no longer be rebuilt at all; the published image from
master fails the same way.

Pinning redis would fix today's failure but leave the next extension
release to break the same way. Running the steps under bash puts them
on the compiler the base image intends, which is also what the PHP
builds in that image already use.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Portable builds link libdatadog's spawn trampoline against musl, and
its PT_INTERP named musl's loader, so on glibc hosts every spawn that
went through the trampoline failed with ENOENT. The default direct
exec avoided it, but DD_SPAWN_WORKER_USE_EXEC and
DD_SIDECAR_DISABLE_DIRECT_EXEC both select the trampoline: the sidecar
never started, and every request logged "Failed starting AppSec in
sidecar". The randomized-test images set DD_SPAWN_WORKER_USE_EXEC,
which is how it surfaced on CentOS 7.

Pick up the spawn_worker change that rewrites the trampoline's
interpreter to the running process's loader at spawn time, rather than
steering the sidecar away from the trampoline: that keeps both
variables working, on musl and glibc alike. spawn_worker keeps it
behind the opt-in trampoline-host-loader feature, since only a
trampoline linked against musl with glibc-compatible library names
needs it; the portable sidecar build enables it through a datadog-php
feature of the same name.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every randomized scenario, on master too, failed its prepare step
waiting for Elasticsearch, and the jobs still passed because analyze
runs in after_script. The base services (Elasticsearch, MySQL, the
test agent, ...) never started: run-base-docker-compose.sh only runs
docker-compose up while their network is missing, and since the move
to GitLab the job created that network itself beforehand.

It did so to paper over a race: the base services were just another
prerequisite of the scenarios Makefile's test target, so make --jobs
started the scenarios alongside them, and a scenario's
`docker-compose up` failed on the not-yet-created external network.
Start the base services before any scenario instead, shut them down
afterwards whatever the outcome, and drop the network creation.

Once they did start, Elasticsearch 7.17.4 exited at once on cgroup v2
hosts (a JDK NullPointerException in its cgroup detection); 7.17.28
starts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With the base services up, two thirds of the randomized web requests
returned 500 and the long-running CLI script died on its first
message: the app requests "httpbin/get?...", which Guzzle rejects
("URI must include a scheme and host") since 6.5.8. curl accepted the
scheme-less URL, so only the Guzzle snippet broke. The same happens
with master's bundle; it went unnoticed because the scenarios never
got this far.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The randomized tests ran analyze in after_script, whose failures
GitLab ignores, so the jobs passed while every scenario failed: that
is how the missing base services, the Elasticsearch crash and the
broken Guzzle URL all went unnoticed. Run it as part of the script,
and keep the results as artifacts on failure too, since that is when
they are needed.

The no-asan jobs also generated scenarios for the buster images, whose
PHP is a debug-zts-asan build that the regular bundle does not ship,
so the installer refused every one of them. Restrict those jobs to
centos7, where the bundle's NTS extension loads.

The asan jobs have no such platform yet: their debug-zts bundle is
built on bookworm, CentOS 7 has no debug-zts PHP and buster's glibc is
too old. They will fail until bookworm-based randomized images exist.
Both job families stay out of the merge gate (flaky-jobs.txt), so this
makes the failures visible without blocking merges.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Disabled test_c runs fail whenever a test's output matches
"Segmentation fault", which is how they catch crashes. The warning
added for health metrics with the crashtracker began "Segmentation
faults will not be reported ...", and the dogstatsd tests that enable
health metrics print it, so those runs failed on PHP 8.0, 8.4 and 8.5
without anything having crashed.

Name the signal instead. Narrowing the grep would have weakened a
crash check to accommodate a log message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cataphract
cataphract force-pushed the glopes/portable-builds branch from 31a3cfb to 9601f9d Compare September 30, 2026 14:57
@realFlowControl

Copy link
Copy Markdown
Member

I'm skeptical about this PR for two reasons

First, I’m not convinced our current test coverage validates the build change. Two examples from a spot check:

  • The main profiler PHPT suite still builds native profiler binaries separately on Alpine and Debian, rather than testing the portable musl-built profiler on glibc.
  • The green benchmark result comes from tracer microbenchmarks that rebuild their own binaries rather than using the portable release artifacts.

These two checks therefore do not validate the portability and performance of the binaries we would ship. I know other jobs consume the portable artifacts.

Second (and that is my biggest concernt), this moves libc compatibility assumptions from the standard toolchain into a compatibility layer we need to maintain and validate.

For exmaple on ARM64, pthread_attr_t is 56 bytes in musl but 64 bytes in glibc. We patch the C headers, but I don’t see a fix for Rust’s libc definition. Without that fix, Rust allocates a 56-byte object, while glibc’s pthread_attr_init() writes 64 bytes into it, 8 bytes past the end. So far it looks like we are lucky, because those extra 8 bytes are in stack slots not used during that call, but that is pure luck and not a guarantee.

I would prefer to keep relying on Rust’s supported target/libc pairing rather than take on the ongoing responsibility of validating the standard library and our dependencies against a different libc.

Build PHPT, Rust, correctness, sanitizer, and benchmark inputs with
the portable musl-to-glibc toolchain.

Patch project and build-std libc layouts on ARM64. Verify pthread_attr_t
bounds with an ASAN interposer on glibc.
Ensure fresh ARM64 CI checkouts have the parent directory required for
the patched Cargo sources.
libdatadog now derives the listener PID from the current process. Drop the stale PID arguments while retaining explicit PIDs for worker connections.
Skip the pre-8.2 x86_64 musl ZTS JIT cases whose PHP runtime has
broken TLS access.

Exclude PHP 7.4 ARM64 language tests whose stock release image has an
incorrect double-to-long configure result.
Exclude old intl expectations that depend on undefined float-to-int
behavior outside x86.

Exclude PHP 7.2 bundled Oniguruma tests that crash in stock ARM64
NTS and ZTS runtimes.
@cataphract
cataphract requested a review from a team as a code owner October 1, 2026 14:22
@cataphract

Copy link
Copy Markdown
Contributor Author

@realFlowControl I believe I've addresses most of your point, though the more abstract one, ofc remains. Please see also https://chonk.us1.prod.dog/v2/get/chonk-uploads-prod/gustavo.lopes/9d9b5f3e-4b01-4d60-884e-19bb327fffc6 for the summary of the job changes

@pr-commenter

pr-commenter Bot commented Oct 1, 2026

Copy link
Copy Markdown

Benchmarks [ profiler ]

Benchmark execution time: 2026-10-01 15:12:41

Comparing candidate commit b0ebefa in PR branch glopes/portable-builds with baseline commit 457aa53 in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 1 performance regressions! Performance is the same for 25 metrics, 10 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 ----------------------------------'

scenario:walk_stack/1

  • 🟥 wall_time [+276.193ns; +279.495ns] or [+2.262%; +2.289%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:php-profiler-exceptions-control

  • unstable cpu_system_time [-0.024ms; +8.222ms] or [-0.057%; +19.276%]
  • unstable cpu_user_time [-8.097ms; +0.304ms] or [-13.079%; +0.491%]

scenario:php-profiler-exceptions-with-profiler

  • unstable cpu_system_time [-7.775ms; +2.934ms] or [-16.259%; +6.136%]
  • unstable cpu_usage_percentage [+19.753%; +57.843%]
  • unstable cpu_user_time [-5242.899µs; +7168.899µs] or [-6.888%; +9.419%]
  • unstable execution_time [-119.698ms; -42.606ms] or [-60.687%; -21.601%]

scenario:php-profiler-exceptions-with-profiler-and-timeline

  • unstable cpu_system_time [-8.409ms; +1.122ms] or [-16.746%; +2.235%]
  • unstable cpu_user_time [-0.092ms; +9.061ms] or [-0.124%; +12.191%]

scenario:php-profiler-timeline-memory-control

  • unstable cpu_system_time [-4341.816µs; +3175.016µs] or [-9.788%; +7.157%]

scenario:php-profiler-timeline-memory-with-profiler

  • unstable cpu_system_time [-29.156ms; +20.331ms] or [-6.856%; +4.781%]

gh-worker-dd-mergequeue-cf854d Bot pushed a commit to DataDog/libdatadog that referenced this pull request Oct 2, 2026
The trampoline executable embeds the program interpreter of the libc it was linked against. dd-trace-php portable builds link it against musl with glibc-compatible library names, so the binary can otherwise run on both libcs. On glibc hosts, however, execve() fails because its musl PT_INTERP path does not exist.

Only portable builds need this adaptation. Gate it behind the opt-in `trampoline-host-loader` feature so other spawn_worker consumers keep the trampoline and build behavior unchanged.

No interpreter path exists on both libcs. With the feature enabled, the build script relinks the trampoline with its default interpreter padded with leading slashes to a fixed 256 bytes. The padded path is equivalent, so an unpatched trampoline behaves as before. When writing the trampoline out, the spawner replaces it with the running process PT_INTERP, which is always the right loader for the host. Invoking the trampoline through that loader would avoid patching, but comes with its own set of complications.

Tested with a portable dd-trace-php build: the sidecar starts through the trampoline on Alpine, Debian bookworm (glibc 2.36), and CentOS 7 (glibc 2.17), where it previously panicked on glibc.

See also DataDog/dd-trace-php#4239


Co-authored-by: gustavo.lopes <gustavo.lopes@datadoghq.com>
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