Skip to content

Commit b9a49fa

Browse files
iamlucbwoebi
andauthored
[loader] Fix zombie creation (#3683)
* Add a test to assert the loader doesn't create zombies * Reap child in thread Signed-off-by: Bob Weinand <bob.weinand@datadoghq.com> * Avoid PHP 7.3 syntax Signed-off-by: Bob Weinand <bob.weinand@datadoghq.com> --------- Signed-off-by: Bob Weinand <bob.weinand@datadoghq.com> Co-authored-by: Bob Weinand <bob.weinand@datadoghq.com>
1 parent ec92fdf commit b9a49fa

File tree

3 files changed

+92
-4
lines changed

3 files changed

+92
-4
lines changed

loader/dd_library_loader.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include <php_ini.h>
1010
#include <stdbool.h>
1111
#include <errno.h>
12+
#include <pthread.h>
13+
#include <sys/wait.h>
1214
#include <main/SAPI.h>
1315
#include <ext/standard/basic_functions.h>
1416

@@ -311,6 +313,12 @@ void ddloader_logf(injected_ext *config, log_level level, const char *format, ..
311313
va_end(va);
312314
}
313315

316+
static void *ddloader_reap_child(void *arg) {
317+
pid_t pid = (pid_t)(intptr_t)arg;
318+
waitpid(pid, NULL, 0);
319+
return NULL;
320+
}
321+
314322
/**
315323
* @param error The c-string this is pointing to must not exceed 150 bytes
316324
*/
@@ -405,6 +413,13 @@ static void ddloader_telemetryf(telemetry_reason reason, injected_ext *config, c
405413
return;
406414
}
407415
if (pid > 0) {
416+
// reap the child in a background thread to avoid leaking it
417+
pthread_t reaper;
418+
pthread_attr_t attr;
419+
pthread_attr_init(&attr);
420+
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
421+
pthread_create(&reaper, &attr, ddloader_reap_child, (void *)(intptr_t)pid);
422+
pthread_attr_destroy(&attr);
408423
return; // parent
409424
}
410425

loader/tests/functional/includes/autoload.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
set_exception_handler(function ($ex) {
66
$trace = $ex->getTrace();
7-
$file = $trace[0]['file'] ?: '';
8-
$line = $trace[0]['line'] ?: '';
7+
$file = isset($trace[0]['file']) ? $trace[0]['file'] : '';
8+
$line = isset($trace[0]['line']) ? $trace[0]['line'] : '';
99
$stackTrace = basename($file).':'.$line;
1010

1111
if (basename($file) === 'assert.php') {
12-
$file2 = $trace[1]['file'] ?: '';
13-
$line2 = $trace[1]['line'] ?: '';
12+
$file2 = isset($trace[1]['file']) ? $trace[1]['file'] : '';
13+
$line2 = isset($trace[1]['line']) ? $trace[1]['line'] : '';
1414
$stackTrace = basename($file2).':'.$line2.' > '.$stackTrace;
1515
}
1616

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
require_once __DIR__."/includes/autoload.php";
4+
skip_if_php5();
5+
6+
$telemetryLogPath = tempnam(sys_get_temp_dir(), 'test_loader_');
7+
8+
// Build the command to run PHP with the loader
9+
$cmd = sprintf(
10+
'FAKE_FORWARDER_LOG_PATH=%s DD_TELEMETRY_FORWARDER_PATH=%s php -n -dzend_extension=%s -r "sleep(1);"',
11+
escapeshellarg($telemetryLogPath),
12+
escapeshellarg(__DIR__.'/../../bin/fake_forwarder.sh'),
13+
escapeshellarg(getLoaderAbsolutePath())
14+
);
15+
16+
try {
17+
// Start the PHP process in background and get its PID
18+
$descriptors = [
19+
0 => ['pipe', 'r'],
20+
1 => ['pipe', 'w'],
21+
2 => ['pipe', 'w'],
22+
];
23+
24+
$process = proc_open($cmd . ' & echo $!', $descriptors, $pipes);
25+
if (!is_resource($process)) {
26+
throw new \Exception("Failed to start PHP process");
27+
}
28+
29+
// Get the real PHP PID from the output
30+
$firstLine = fgets($pipes[1]);
31+
$phpPid = (int)trim($firstLine);
32+
33+
if ($phpPid <= 0) {
34+
throw new \Exception("Failed to get PHP PID");
35+
}
36+
37+
if (debug()) {
38+
echo "[debug] PHP PID: $phpPid\n";
39+
}
40+
41+
// Wait for the telemetry fork to happen and complete
42+
usleep(300000); // 300ms
43+
44+
// Check for zombie processes that are children of the PHP process
45+
$zombieCheckCmd = sprintf('ps --ppid %d -o pid,state,comm --no-headers 2>/dev/null || echo "NO_CHILDREN"', $phpPid);
46+
$zombieOutput = shell_exec($zombieCheckCmd);
47+
48+
if (debug()) {
49+
echo "[debug] Children processes:\n" . $zombieOutput . "\n";
50+
}
51+
52+
$zombieCount = substr_count($zombieOutput, ' Z ');
53+
54+
// Wait for the PHP process to finish
55+
$waitCmd = sprintf('wait %d 2>/dev/null; echo $?', $phpPid);
56+
$phpExitCode = (int)trim(shell_exec($waitCmd));
57+
58+
// Read the remaining output and close pipes
59+
$output = stream_get_contents($pipes[1]);
60+
$errors = stream_get_contents($pipes[2]);
61+
fclose($pipes[0]);
62+
fclose($pipes[1]);
63+
fclose($pipes[2]);
64+
proc_close($process);
65+
66+
if ($zombieCount > 0) {
67+
throw new \Exception("FAILED: Zombie process detected after telemetry fork! Found $zombieCount zombie(s)");
68+
}
69+
70+
echo "OK: No zombie processes detected\n";
71+
} finally {
72+
@unlink($telemetryLogPath);
73+
}

0 commit comments

Comments
 (0)