Move appsec communication to sidecar - #3725
Conversation
d9cd53f to
373c569
Compare
|
Benchmarks [ tracer ]Benchmark execution time: 2026-08-02 01:27:21 Comparing candidate commit ca267b0 in PR branch Found 4 performance improvements and 2 performance regressions! Performance is the same for 188 metrics, 0 unstable metrics.
|
373c569 to
121733e
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #3725 +/- ##
==========================================
+ Coverage 68.81% 75.84% +7.02%
==========================================
Files 166 66 -100
Lines 19030 13366 -5664
Branches 1797 1189 -608
==========================================
- Hits 13095 10137 -2958
+ Misses 5121 2662 -2459
+ Partials 814 567 -247
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 34 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
af7f0bc to
366de49
Compare
Benchmarks [ appsec ]Benchmark execution time: 2026-08-02 00:41:01 Comparing candidate commit ca267b0 in PR branch Found 3 performance improvements and 0 performance regressions! Performance is the same for 9 metrics, 0 unstable metrics.
|
| mlog(dd_log_debug, "Will exchange message with helper"); | ||
|
|
||
| dd_result res = dd_conn_recv(conn, &imsg->_data, &imsg->_size); | ||
| // dd_result res = dd_conn_roundtripv(conn, iovecs, &imsg->_data, |
There was a problem hiding this comment.
Should this comment go?
8d09873 to
1238aea
Compare
1238aea to
2b4a1c0
Compare
The .gitmodules entry was removed in an earlier WIP commit but the gitlink remained in the tree, causing CI checkout to fail. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2698d89 to
b4cf9f1
Compare
52e3c14 to
d79dce4
Compare
d79dce4 to
3c90bf0
Compare
1c54947 to
6c7685d
Compare
…hickory/reqwest The root Cargo workspace includes appsec/helper-rust (since 3f893e2), which depends on the appsec/third_party/libddwaf-rust submodule as a path dependency. Any cargo invocation across the whole repo now needs that submodule checked out, but GIT_SUBMODULE_PATHS for the tracer pipeline (and the global default inherited by profiler/shared) never included it, breaking nearly every job. Also downgrade hickory-net/proto/resolver (via reqwest 0.13.4 -> 0.13.2) back to the versions used on master, since 0.26.1 requires rustc 1.88 while the pinned toolchain is 1.87.0, breaking GitHub Actions profiling jobs.
RUSTFLAGS didn't pick up -fsanitize=address, so build-script binaries that link ASan-instrumented C objects (e.g. libddwaf-sys) failed with undefined __asan_* symbols in debug-zts-asan compile jobs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bers Cargo.toml's members list was reformatted to one entry per line, so the old `s/, "profiling"//` sed (expecting a single-line array) silently no-oped, leaving profiling in the packaged Cargo.toml even though profiling/ itself is never shipped in the pecl tarball. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
helper-rust is now a library dependency of the sidecar crate rather than its own cdylib, so cargo no longer produces target/release/libddappsec_helper.so, and datadog.appsec.helper_path is no longer read anywhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
16330ba to
5861a71
Compare
5190d19 to
6e94b30
Compare
a048b31 to
3eef763
Compare
The portable (musl-built, glibc-compat) libdatadog_php.so has its libc
DT_NEEDED patched out and relies on the host's libc being in the global
scope. When the sidecar is direct-spawned, ld-linux execs the DSO itself,
so anything it needs beyond libc.so.6 (which ld-linux always brings in)
must be passed with --preload.
add_glibc_direct_preloads() only did that on glibc < 2.34, assuming 2.34
had merged all four DSOs into libc. It did not merge libm: exp/log/pow
(referenced by libdd-ddsketch, libdd-telemetry and datadog-ipc's shm
stats) still live in libm.so.6 on every glibc. On glibc >= 2.34 hosts the
spawned sidecar therefore died at load with
libdatadog_php.so: symbol lookup error: undefined symbol: exp
which made every sidecar connection reset, so AppSec never started
("AppSec sidecar backend is unavailable"). This broke the FrankenPHP SSI
tests (dunglas/frankenphp:1.4-php8.4 is Debian 12, glibc 2.36): 27
FrankenphpClassicTests + 10 FrankenphpWorkerTests failures in the
"helper-rust integration coverage" job.
Preload libm.so.6 whenever the host is glibc, and keep libdl/libpthread/
librt for glibc < 2.34.
Verified with test8.4-release-zts-ssi (glibc 2.36, FrankenPHP): 27
failures before, 0 after; test8.3-release-ssi (glibc 2.31, the < 2.34
path) still green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2377925 to
3051f14
Compare
The portable (glibc-compat) build is built against musl with -C target-feature=-crt-static and then has its musl libc DT_NEEDED patched out, so it binds to whatever libc the host process already has. That left libgcc_s.so.1 as its one and only DT_NEEDED, which is wrong on two counts: bare alpine images don't ship libgcc_s.so.1 (so the library was unloadable there), and it was the only thing keeping ld.so from treating the library as a static binary. Drop it: rustc's unwind crate only emits -lgcc_s because crt-static is off (library/unwind/src/lib.rs); point that -lgcc_s at libgcc_eh.a via a linker script of our own so the unwinder is linked statically instead. -static-libgcc does not help here -- it only affects the specs the GCC driver expands for its own -lgcc, not an explicit -lgcc_s. Then declare the dependencies the library actually has, using the glibc names. musl's loader resolves libc.*/libm.*/libdl.*/libpthread.*/librt.* DT_NEEDED entries to the implementation itself without touching the filesystem, so a single artifact stays correct on both libcs, and the references remain unversioned, so no .gnu.version_r entry is created. This also avoids ending up with zero DT_NEEDED entries: since 2.33 glibc re-execs an object with no DT_NEEDED and no PT_INTERP instead of loading it (rtld_chain_load() in elf/rtld.c), which segfaults the sidecar's direct spawn, where the library itself is the program handed to ld.so. Verified on the real artifact: direct exec via ld.so and dlopen(RTLD_NOW) both work with no preloads at all on bare debian bookworm-slim (glibc 2.36) and bare alpine 3.21, where the previous artifact failed outright. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8a51d1b gave the portable DSO real DT_NEEDED entries (libc.so.6, libm.so.6, libdl.so.2, libpthread.so.0, librt.so.1) instead of stripping them all. That makes both the sidecar's direct-spawn and the SSI loader's dlopen() resolve every dependency from the DSO's own dynamic section before any preload/relocation step runs, so the libm.so.6 (and, on glibc < 2.34, libdl/libpthread/librt) --preload injection is a strict subset of what's now loaded anyway -- a no-op kept only as defense in depth. Verified on the real portable artifact (aarch64), direct-spawn and SSI dlopen with zero preloads, on glibc 2.27/2.28/2.36 and musl 1.2.5: entry point runs and symbols resolve. Negative control: removing libm.so.6 from DT_NEEDED reproduces "undefined symbol: exp" on all of them, confirming the DT_NEEDED entries (not the preloads) are what makes this work. Also reverts the libdatadog submodule's spawn_worker --preload support (2f9d3c1e8), which had no other caller, restoring Config::spawn_without_trampoline and the argument-less SpawnMethod::Direct. loader/dd_library_loader.c is now byte-identical to origin/master. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(ci): isolate PHPTs from Fabric egress proxy * fix(ci): isolate integration tests from Fabric proxy * fix(appsec): unset proxy in test wrapper
Move AppSec communication to the sidecar
Summary
The AppSec helper already runs as a shared library loaded inside the sidecar
process (the sidecar
dlopens it viaddog_sidecar_enable_appsec). But untilnow the extension did not talk to it through the sidecar: the helper opened its
own dedicated Unix domain socket, and the extension connected to that socket
directly, side-channeling past the sidecar's own transport. A lock file was used
to single-instance the helper. This PR removes that side-channel socket and lock
file.
The extension now reaches the helper by sending messages through the existing
sidecar transport — the same channel already used for traces, telemetry, etc.
The AppSec request/response payloads (the
dds-framed msgpack messages:client_init,request_init,request_shutdown, …) are themselves unchanged,but they are no longer exchanged over a raw socket: they are now wrapped as a
new message type inside the existing sidecar communication protocol, and the
sidecar unwraps them and forwards them to the helper (and routes the reply
back).
As a consequence the standalone C++ helper is deleted in its entirety, along
with its build system and third-party dependencies (see "Secondary changes").
How communication works now
The broker API (libdatadog + components-rs)
The actual broker logic lives in libdatadog: this PR bumps the
libdatadogsubmodule, which adds the AppSec message-brokering API to the sidecar. To call
that API from this repo,
components-rs/carries the matching generated Cheaders (
sidecar.h,common.h) and the thinsidecar.rswrapper, and thenew symbols are exported in
datadog.sym. The new entry points are:ddog_sidecar_send_appsec_message(transport, session_id, client_id, data)—called by the extension side. It ships
datato the sidecar, whichroutes it to the helper and returns the helper's reply as a
ddog_AppsecCResponse { ptr, len, capacity, disconnect }. Thedisconnectflag tells the extension the session was torn down on the other end.
ddog_sidecar_appsec_response_drop(response)— frees the response bufferreturned by
send_appsec_message(a libdatadog-allocatedVecin theextension's own process, holding the bytes that came back over the sidecar
transport). This is distinct from
free_responsebelow, which frees thehelper's separate buffer on the sidecar side.
ddog_sidecar_appsec_register_message_handler(on_message, on_disconnect, free_response)— called by the helper when it is loaded into the sidecar, to register
the three C callbacks the sidecar invokes.
Extension side
The socket transport in
network.c/helper_process.cis gone (~600 linesremoved): no more
connect()/retry loops,writev/recvframing,SO_PEERCREDcredential checks, send/recv timeouts, or socket/lock pathcomputation.
dd_connshrinks to{ bool connected; uint64_t client_id; }.Sending a command is now a single round-trip:
dd_conn_roundtripv()(replacing the old send + recv pair) serializes theoutgoing msgpack into one buffer and hands it to a new
dd_trace_send_appsec_message()wrapper inddtrace.c.ddtrace_get_sidecar_transport()export (added todatadog.symand madeZTS-aware in
ext/sidecar.c— it resolves the right thread'sDATADOG_G(sidecar)from a passed-in TSRM cache) and callsddog_sidecar_send_appsec_message(...).ddog_AppsecCResponse, is parsed as msgpack, andfreed with
ddog_sidecar_appsec_response_drop().Two new identifiers carry the routing:
session_id— the extension's formatted sidecar session id (the same onealready used for the sidecar); it scopes a client to a PHP process tree.
client_id— auint64assigned by the helper. The extension's firstmessage (
client_init) is sent withclient_id == 0, meaning "create a newclient"; the helper allocates an id and returns it in the
client_initresponse, and the extension stores it in
conn->client_idfor allsubsequent messages.
New explicit teardown:
commands/client_shutdown.{c,h}introduces aclient_shutdownmessage ({ clean: bool, error?: string }) sent fromGSHUTDOWN/request shutdown so the helper can drop the client promptly insteadof relying on a socket EOF. GSHUTDOWN was also made thread-safe (registers a
thread-local destructor and redirects
TSRMLS_CACHEto the worker's cache).test_mock_transport.{c,h}is new: in phpt tests there is no real sidecar, sothe mock transport intercepts
dd_trace_send_appsec_message()and does rawframed socket I/O against
mock_helperover an fd handed in from PHP(
set_mock_helper_fd()).Helper side
The helper no longer binds a
UnixListener, no longer acquires aflock-basedsingle-instance lock (
lock.rs, –178, deleted), and no longer polls thesidecar for readiness.
config.rsloses the socket and lock-path fields.Instead, at startup it calls
ddog_sidecar_appsec_register_message_handlerandserves traffic entirely through the three callbacks (new
client/sidecar_msg.rs,+718). Client tasks are still async Tokio tasks, but they are now fed by an
mpscchannel instead of a socket stream:on_message(session_id, client_id, data, len) -> AppsecCResponse— thehot path. It looks up the client task for
(session_id, client_id)in aCLIENTSmap; ifclient_id == 0it spawns a new client task and assigns afresh id, writing it back through the
*client_idout-parameter. It forwardsthe bytes to the task over an
mpsc::Sender<HelperRequest>andblock_onsthe
oneshotreply, then returns it as anAppsecCResponse. Errors/timeoutsproduce a
FatalErrorreply withdisconnect: true.on_disconnect(session_id, client_id)— removes the client (or allclients of a session when
client_id == 0) from the map; dropping the sendercloses the task's channel and the task exits.
free_response(ptr, len, capacity)— reconstitutes and drops theVecthat backed a response.
The client message loop (
client.rs) was reworked to decodeCommands fromthe
mpscstream (via the existingprotocol::CommandCodec) and answer over aoneshotsender rather than writing back to a socket;server.rsgains aClientTaskSetthat owns the spawned tasks and drains them on shutdown.So the end-to-end path for one command is:
Secondary changes
appsec/src/helper/**(client, engine, WAFglue, msgpack/network/proto layers) and its tests/benches/fuzzers.
appsec/third_party/{libddwaf,msgpack-c,cpp-base64}submodules and their
.gitmodules/CMake wiring;cmake/helper.cmake,check_fslib.cppand related clang-tidy/clang-format machinery..gitlab/build-appsec-helper.shdeleted, the Rust helper now builds to theunified
libddappsec-helper.so,generate-appsec.php/generate-package.phpdrop the C++ helper compile/asan/coverage jobs, and the final/SSI/debug
artifact scripts stop shipping a second helper binary.
DD_APPSEC_HELPER_RUNTIME_PATHand thehelper_rust_redirectiontoggle (and theirsupported-configurations.jsonentries) — there is no socket/lock path to configure and no C++/Rust choice
to make.
client_initnow only accepts therusthelper runtime.config.w32excludes the three new unix-only sidecar appsecsymbols from the DLL exports (AppSec is not supported on Windows).
ConnectivityTests.groovy(clients exit promptlyon sidecar disconnect, and clean shutdown via
client_shutdownon GSHUTDOWN)and
CrashDetectionTests.groovy(coredump on sidecar SIGSEGV; clean sidecarexit after Apache restart);
TelemetryTestssimplified now that the runtimeis always Rust.