Skip to content

Commit c8f85f0

Browse files
committed
Misc fixes
1 parent 6e94b30 commit c8f85f0

10 files changed

Lines changed: 119 additions & 24 deletions

File tree

appsec/helper-rust/coverage_init.c

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
//
33
// The embedded sidecar calls this module explicitly from its entrypoint. It
44
// reinitializes the LLVM profiling runtime to use the correct output path.
5+
// Every other process that loads the library is left pointing at the path the
6+
// harness passes in LLVM_PROFILE_FILE, which is /dev/null: none of them runs
7+
// helper-rust, so their profiles cover nothing that is reported on.
58
//
69
// Coverage data is flushed when the process receives SIGUSR1, as well as
710
// at normal process exit via atexit handler.
@@ -15,13 +18,16 @@
1518
#include <signal.h>
1619
#include <fcntl.h>
1720
#include <stdarg.h>
21+
#include <limits.h>
22+
#include <sys/stat.h>
1823
#include <time.h>
1924
#include <errno.h>
2025

2126
// LLVM profiling runtime API (from compiler-rt/lib/profile/)
2227
extern void __llvm_profile_initialize_file(void);
2328
extern void __llvm_profile_set_filename(const char *);
2429
extern int __llvm_profile_write_file(void);
30+
extern void __llvm_profile_reset_counters(void);
2531
extern const char *__llvm_profile_get_filename(void);
2632

2733
static int log_fd = -1;
@@ -55,10 +61,17 @@ static void coverage_log(const char *fmt, ...) {
5561
write(log_fd, buf, offset);
5662
}
5763

64+
// Writing in merge mode adds the file's counters back into the in-process
65+
// ones, so a process that writes more than once counts everything it has
66+
// already written a second time and the totals double per flush. This process
67+
// flushes on SIGUSR1 as well as at exit, and the runtime registers a writer of
68+
// its own on top of that, so the counters have to be dropped once they are on
69+
// disk.
5870
static void coverage_flush(void) {
5971
coverage_log("coverage_flush() called, flushing coverage data");
6072

6173
int ret = __llvm_profile_write_file();
74+
__llvm_profile_reset_counters();
6275
coverage_log("__llvm_profile_write_file() returned %d (errno=%d: %s)",
6376
ret, errno, strerror(errno));
6477

@@ -79,16 +92,67 @@ static void coverage_signal_handler(int signo) {
7992
(void)signo;
8093
coverage_log("SIGUSR1 received, flushing coverage data");
8194
int ret = __llvm_profile_write_file();
95+
__llvm_profile_reset_counters();
8296
coverage_log("__llvm_profile_write_file() returned %d", ret);
8397
}
8498

99+
// %m selects merge mode, where the runtime serializes on an fcntl write lock
100+
// and accumulates into one shared file per instrumented binary instead of
101+
// writing one file per process. The runtime creates that file itself, at the
102+
// first write, with open(..., 0666); the container's 022 umask turns it into
103+
// 0644. Every container in a run shares the file, and a sidecar inherits the
104+
// user of the SAPI that spawned it, so whoever writes first would otherwise
105+
// end up owning a file the other users cannot open. Get there first and create
106+
// it with the bits the umask would have dropped, after which the runtime only
107+
// ever opens a file that everyone can already write. A zero-length file is
108+
// what merge mode expects to find when there is nothing to merge yet.
109+
//
110+
// Only the runtime can expand the name, since %m stands for a hash of the
111+
// instrumented binary, hence __llvm_profile_get_filename() rather than a path
112+
// built by the caller.
113+
//
114+
// Created under a private name and hard-linked into place, so the file is
115+
// never reachable under its final name while it is still 0644; link() fails
116+
// with EEXIST if another process got there first, which is the wanted
117+
// create-only-if-absent. The staging name deliberately does not end in
118+
// .profraw, so a leftover cannot reach the coverage report.
119+
static void coverage_precreate_profile_file(void) {
120+
const char *filename = __llvm_profile_get_filename();
121+
if (!filename || !filename[0]) {
122+
return;
123+
}
124+
125+
char staging[PATH_MAX];
126+
int len = snprintf(staging, sizeof(staging), "%s.new.%d", filename,
127+
(int)getpid());
128+
if (len < 0 || len >= (int)sizeof(staging)) {
129+
return;
130+
}
131+
132+
int fd = open(staging, O_RDWR | O_CREAT | O_EXCL, 0666);
133+
if (fd < 0) {
134+
return;
135+
}
136+
if (fchmod(fd, 0666) != 0) {
137+
coverage_log("Failed to make %s group/other-writable: %s", staging,
138+
strerror(errno));
139+
} else if (link(staging, filename) != 0 && errno != EEXIST) {
140+
coverage_log("Failed to create %s: %s", filename, strerror(errno));
141+
}
142+
close(fd);
143+
unlink(staging);
144+
}
145+
85146
void ddappsec_helper_coverage_init(void) {
86147
coverage_log("ddappsec_helper_coverage_init() called");
87148

88-
const char *profile_path = getenv("_DD_HELPER_RUST_COVERAGE_FILE");
89-
if (!profile_path) {
90-
profile_path = "/helper-rust/coverage/helper-%h-%p-%m.profraw";
91-
}
149+
// Hardcoded rather than read from the environment on purpose. Only about
150+
// a third of the sidecars in a run inherit the ambient environment at all,
151+
// so a variable naming this path would reach some of them and not others,
152+
// and pointing it anywhere else would split the merge pool in two instead
153+
// of moving it. %m rather than %p so that repeatedly respawned sidecars
154+
// keep merging into the one file.
155+
const char *profile_path = "/helper-rust/helper-%m.profraw";
92156
coverage_log("Helper coverage profile = %s", profile_path);
93157

94158
const char *filename_before = __llvm_profile_get_filename();
@@ -103,6 +167,8 @@ void ddappsec_helper_coverage_init(void) {
103167
coverage_log("Profile filename AFTER reinit: %s",
104168
filename_after ? filename_after : "(null)");
105169

170+
coverage_precreate_profile_file();
171+
106172
signal(SIGUSR1, coverage_signal_handler);
107173
coverage_log("Signal handler installed for SIGUSR1");
108174

appsec/helper-rust/src/service/sampler.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ impl SchemaSampler {
204204
}
205205

206206
// most recent at the top
207-
entries.sort_by(|a, b| b.data.last_accessed.cmp(&a.data.last_accessed));
207+
entries.sort_by_key(|entry| std::cmp::Reverse(entry.data.last_accessed));
208208

209209
let count = std::cmp::min(entries.len(), MAX_ITEMS * 2 / 3);
210210
for ce in entries.into_iter().take(count) {

appsec/helper-rust/src/service/waf_diag.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ pub fn report_diagnostics_errors(
3232
use libddwaf::object::WafObjectType;
3333

3434
let path_label = rc_path.map_or("(bundled rules)", |p| p.as_str());
35-
let parsed_key = match rc_path.map(|p| rc::ParsedConfigKey::from_rc_path(p)) {
35+
let parsed_key = match rc_path.map(rc::ParsedConfigKey::from_rc_path) {
3636
Some(None) => {
3737
warning!("Failed to parse config key for {:?}", path_label);
3838
return;

appsec/src/extension/commands_helpers.c

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,23 +244,22 @@ static ATTR_WARN_UNUSED dd_result _imsg_recv(dd_imsg *nonnull imsg,
244244
{
245245
mlog(dd_log_debug, "Will exchange message with helper");
246246

247-
dd_helper_response response;
248247
dd_result res = dd_conn_roundtrip(
249-
conn, buffer->data, buffer->final_msg_size, &response);
248+
conn, buffer->data, buffer->final_msg_size, &imsg->_response);
250249
if (res) {
251250
return res;
252251
}
253-
imsg->_response = response;
254252

255-
mpack_tree_init(&imsg->_tree, response.data, response.data_len);
253+
mpack_tree_init(
254+
&imsg->_tree, imsg->_response.data, imsg->_response.data_len);
256255
mpack_tree_parse(&imsg->_tree);
257256
imsg->root = mpack_tree_root(&imsg->_tree);
258257
mpack_error_t err = mpack_tree_error(&imsg->_tree);
259258
if (err != mpack_ok) {
260259
mlog(dd_log_warning, "Error parsing msgpack message: %s",
261260
mpack_error_to_string(err));
262-
_dump_in_msg(dd_log_debug, response.data, response.data_len);
263-
dd_helper_response_destroy(&response);
261+
_dump_in_msg(
262+
dd_log_debug, imsg->_response.data, imsg->_response.data_len);
264263
UNUSED(_imsg_destroy(imsg));
265264
return dd_helper_say_goobye;
266265
}

appsec/src/extension/ddappsec.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ static PHP_GINIT_FUNCTION(ddappsec)
194194
if (!is_main_thread) {
195195
# if defined(__linux__)
196196
extern void *__dso_handle;
197+
// adds a dependency on glibc 2.18
197198
extern int __cxa_thread_atexit_impl(
198199
void (*func)(void *), void *arg, void *dso_handle);
199200
__cxa_thread_atexit_impl(_tshutdown_handler, NULL, __dso_handle);

appsec/src/extension/network.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ dd_result dd_conn_roundtrip(dd_conn *nonnull conn, char *nonnull request,
100100
dd_header h;
101101
memcpy(&h, response.ptr, sizeof(h));
102102
if (memcmp(h.code, "dds", 4) != 0) {
103-
mlog(dd_log_warning, "Helper response has invalid magic: %s", h.code);
103+
mlog(dd_log_warning, "Helper response has invalid magic: %.*s",
104+
(int)sizeof(h), h.code);
104105
ret = dd_helper_say_goobye;
105106
goto error;
106107
}

appsec/src/extension/request_lifecycle.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -262,10 +262,6 @@ retry:;
262262
dd_duration_waf_ext_account(&start);
263263
}
264264

265-
if (rbe) {
266-
zend_string_release(rbe);
267-
}
268-
269265
// we might have been disabled by request_init
270266

271267
zend_array *spec = NULL;
@@ -298,6 +294,10 @@ retry:;
298294
goto retry;
299295
}
300296

297+
if (rbe) {
298+
zend_string_release(rbe);
299+
}
300+
301301
dd_request_abort_destroy_block_params(&req_info.req_info.block_params);
302302

303303
return spec;

appsec/tests/integration/src/main/groovy/com/datadog/appsec/php/docker/AppSecContainer.groovy

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -559,8 +559,18 @@ class AppSecContainer<SELF extends AppSecContainer<SELF>> extends GenericContain
559559
withFileSystemBind(
560560
coverageDirectory, '/helper-rust',
561561
BindMode.READ_WRITE)
562-
withEnv '_DD_HELPER_RUST_COVERAGE_FILE',
563-
'/helper-rust/helper-%h-%p-%m.profraw'
562+
// Only the sidecar runs helper-rust, but the SSI loader maps
563+
// the coverage-instrumented libdatadog_php.so into *every* PHP
564+
// process, and the profiling runtime in each of them writes
565+
// the whole 34M counter table out on exit. Those profiles
566+
// cover none of the reported sources, so send them nowhere:
567+
// the report is built from the helper's profile alone, and
568+
// ddappsec_helper_coverage_init() names that one itself, in
569+
// /helper-rust, rather than taking it from the environment.
570+
// Leaving this unset is not an option -- the runtime would
571+
// fall back to its default name and drop a default_*.profraw
572+
// per process into the cwd instead.
573+
withEnv 'LLVM_PROFILE_FILE', '/dev/null'
564574
}
565575
withEnv 'USE_SSI', '1'
566576
withEnv 'DD_LOADER_PACKAGE_PATH', '/tmp/dd-package'

sidecar/src/lib.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,37 @@ fn add_glibc_direct_preloads(config: &mut Config) {
1919
return;
2020
};
2121

22-
let is_glibc = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"gnu_get_libc_version".as_ptr()) };
23-
if is_glibc.is_null() {
22+
if !glibc_version().is_some_and(|version| version < (2, 34)) {
2423
return;
2524
}
2625

2726
// Direct spawn asks ld-linux to load the entrypoint DSO itself, before the
28-
// sidecar can run any initialization code. Supply the old-glibc providers
29-
// as loader --preload arguments.
27+
// sidecar can run any initialization code. Before 2.34, glibc provided
28+
// these symbols in separate libraries, so load them explicitly.
3029
direct_spawn
3130
.preloads
3231
.extend(["libdl.so.2", "libm.so.6", "libpthread.so.0", "librt.so.1"].map(String::from));
3332
}
3433

34+
#[cfg(all(feature = "glibc-compat", target_os = "linux"))]
35+
fn glibc_version() -> Option<(u32, u32)> {
36+
let symbol = unsafe { libc::dlsym(libc::RTLD_DEFAULT, c"gnu_get_libc_version".as_ptr()) };
37+
if symbol.is_null() {
38+
return None;
39+
}
40+
41+
let get_version: unsafe extern "C" fn() -> *const libc::c_char =
42+
unsafe { std::mem::transmute(symbol) };
43+
let version = unsafe { get_version() };
44+
if version.is_null() {
45+
return None;
46+
}
47+
48+
let version = unsafe { std::ffi::CStr::from_ptr(version) }.to_str().ok()?;
49+
let (major, minor) = version.split_once('.')?;
50+
Some((major.parse().ok()?, minor.parse().ok()?))
51+
}
52+
3553
pub fn start_or_connect_to_sidecar(config: Config) -> anyhow::Result<SidecarTransport> {
3654
#[cfg(all(feature = "glibc-compat", target_os = "linux"))]
3755
let mut config = config;

0 commit comments

Comments
 (0)