Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ PHP NEWS
. Fixed bug GH-25268 (NULL php_libxml_ref_obj dereference in
dom_objects_store_clone_obj when cloning Dom\Notation). (David Carlier)

- FPM:
. Fixed bug GH-25077 (Worker output lost when catch_workers_output is enabled
and error_log is syslog). (localzet)

- MBString:
. Fixed stack overflow in mb_check_encoding() and mb_convert_encoding() with
deeply nested arrays. (Eyüp Can Akman)
Expand Down
9 changes: 8 additions & 1 deletion sapi/fpm/fpm/fpm_stdio.c
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,14 @@ static void fpm_stdio_child_said(struct fpm_event_s *ev, short which, void *arg)
create_log_stream = !child->log_stream;
if (create_log_stream) {
log_stream = child->log_stream = malloc(sizeof(struct zlog_stream));
zlog_stream_init_ex(log_stream, ZLOG_WARNING, STDERR_FILENO);
int log_fd = STDERR_FILENO;
#ifdef HAVE_SYSLOG_H
/* The FPM master redirects stderr to /dev/null when logging to syslog. */
if (fpm_use_error_log() && fpm_globals.error_log_fd == ZLOG_SYSLOG) {
log_fd = ZLOG_SYSLOG;
}
#endif
zlog_stream_init_ex(log_stream, ZLOG_WARNING, log_fd);
zlog_stream_set_decorating(log_stream, child->wp->config->decorate_workers_output);
zlog_stream_set_wrapping(log_stream, ZLOG_TRUE);
zlog_stream_set_msg_prefix(log_stream, STREAM_SET_MSG_PREFIX_FMT,
Expand Down
97 changes: 97 additions & 0 deletions sapi/fpm/tests/gh25077-syslog-worker-output.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
--TEST--
GH-25077: FPM captured worker output reaches syslog
--SKIPIF--
<?php
require __DIR__ . '/skipif.inc';
if (PHP_OS_FAMILY !== 'Linux') {
die('skip Linux LD_PRELOAD test');
}
if (PHP_INT_SIZE !== 8) {
die('skip 64-bit test shim only');
}
if (!function_exists('exec')) {
die('skip exec is disabled');
}
exec('command -v cc 2>/dev/null', $output, $status);
if ($status !== 0 || !$output) {
die('skip C compiler not available');
}
?>
--FILE--
<?php
require_once __DIR__ . '/tester.inc';

$temp = tempnam(sys_get_temp_dir(), 'fpm-gh25077-');
$library = $temp . '.so';
$log = $temp . '.log';
$source = __DIR__ . '/gh25077_syslog_intercept.c';

try {
exec('cc -shared -fPIC -Wall -Wextra -Werror -o ' . escapeshellarg($library)
. ' ' . escapeshellarg($source) . ' 2>&1', $output, $status);
if ($status !== 0) {
throw new RuntimeException('Failed to compile test syslog interceptor: ' . implode("\n", $output));
}

$config = <<<'CONFIG'
[global]
error_log = syslog
daemonize = no
[unconfined]
listen = {{ADDR}}
pm = static
pm.max_children = 1
catch_workers_output = yes
CONFIG;

$code = <<<'PHP'
<?php
file_put_contents('php://stdout', "GH25077_WORKER_STDOUT\n");
file_put_contents('php://stderr', "GH25077_WORKER_STDERR\n");
PHP;

$env = getenv();
$env['LD_PRELOAD'] = (isset($env['LD_PRELOAD']) ? $env['LD_PRELOAD'] . ' ' : '') . $library;
$env['PHP_FPM_TEST_SYSLOG_CAPTURE'] = $log;

// LD_PRELOAD puts this test shim ahead of the ASan runtime on sanitized builds.
// Allow that ordering for this FPM subprocess without disabling ASan itself.
$asanOptions = $env['ASAN_OPTIONS'] ?? '';
$env['ASAN_OPTIONS'] = ($asanOptions === '' ? '' : $asanOptions . ':')
. 'verify_asan_link_order=0';

$tester = new FPM\Tester($config, $code);
$tester->start(forceStderr: false, envVars: $env);
$tester->checkConnection();
$tester->request()->expectEmptyBody();

$found = false;
for ($i = 0; $i < 100; $i++) {
if (is_file($log)) {
$captured = file_get_contents($log);
if (str_contains($captured, 'GH25077_WORKER_STDOUT')
&& str_contains($captured, 'GH25077_WORKER_STDERR')) {
$found = true;
break;
}
}
usleep(50000);
}
$tester->terminate();
$tester->close();
var_dump($found);
} finally {
@unlink($log);
@unlink($library);
@unlink($temp);
}
?>
Done
--EXPECT--
bool(true)
Done
--CLEAN--
<?php
require_once __DIR__ . '/tester.inc';
FPM\Tester::clean();
?>
65 changes: 65 additions & 0 deletions sapi/fpm/tests/gh25077_syslog_intercept.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* Test-only syslog interceptor; no host syslog service is required. */
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>

static void capture_syslog(const char *format, va_list args)
{
const char *path = getenv("PHP_FPM_TEST_SYSLOG_CAPTURE");
if (path == NULL || *path == '\0') {
return;
}

char message[8192];
int len = vsnprintf(message, sizeof(message), format, args);
if (len < 0) {
return;
}

int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0600);
if (fd < 0) {
return;
}
size_t count = (size_t) len < sizeof(message) ? (size_t) len : sizeof(message) - 1;
(void) write(fd, message, count);
(void) write(fd, "\n", 1);
close(fd);
}

void syslog(int priority, const char *format, ...)
{
(void) priority;
va_list args;
va_start(args, format);
capture_syslog(format, args);
va_end(args);
}

void vsyslog(int priority, const char *format, va_list args)
{
(void) priority;
capture_syslog(format, args);
}

/* glibc uses these symbols when _FORTIFY_SOURCE is enabled. */
void __syslog_chk(int priority, int flag, const char *format, ...)
{
(void) priority;
(void) flag;
va_list args;
va_start(args, format);
capture_syslog(format, args);
va_end(args);
}

void __vsyslog_chk(int priority, int flag, const char *format, va_list args)
{
(void) priority;
(void) flag;
capture_syslog(format, args);
}
Loading