[php-src] PR #24554: Fix FPM worker output prefix longer than its buffer or log_limit
| From: | Marti2203 | Date: | Thu, 22 Oct 2026 15:55:33 +0000 |
| Subject: | [php-src] PR #24554: Fix FPM worker output prefix longer than its buffer or log_limit | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39716@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24554
Author: Marti2203
With
catch_workers_output = yes, the master logs each worker output line with the
prefix [pool NAME] child PID said into stderr: .
zlog_stream_set_msg_prefix() formats that prefix into a 512-byte stack buffer, then
copies as many bytes as vsnprintf() *would* have written:
```c
char buf[MAX_WRAPPING_PREFIX_LENGTH];
...
len = vsnprintf(buf, MAX_WRAPPING_PREFIX_LENGTH - 1, fmt, args);
...
memcpy(stream->msg_prefix, buf, len);
stream->msg_prefix_len = len;
```
With a pool name longer than about 470 characters, this has two effects:
- The prefix includes stack memory past buf. That memory is written to the error log.
- If the time/level prefix plus the message prefix reaches log_limit (default 1024,
minimum 512), zlog_stream_buf_append() and zlog_stream_unbuffered_write()
compute available_len = zlog_limit - reserved_len - 1, which underflows. In buffered
mode, the master then overflows its log buffer and crashes. In direct mode (log_buffering =
no), it writes memory to the log until it fails.
Only pool configuration can trigger this, because the pool name has to be very long.
### Reproducer
A pool named with N p characters, pm = static, pm.max_children =
1, catch_workers_output = yes, and one request to a script that does
file_put_contents('php://stderr', "hello from worker\n"). Release
build:
| pool name | log_limit | log_buffering | before | after |
|---|---|---|---|---|
| 600 | 1024 | yes | log line contains stack memory | prefix cut at 511 bytes, message intact |
| 600 | 512 | yes | master segfault | prefix cut at 256 bytes, message intact |
| 1100 | 1024 | yes | master segfault | prefix cut at 511 bytes, message intact |
| 1100 | 512 | yes | master segfault | prefix cut at 256 bytes, message intact |
| 1100 | 512 | no | master writes memory to the log without end (stopped at a 10 MB file size limit)
| prefix cut at 256 bytes, message intact |
With ASan, the 600 and 1100 character cases report stack-buffer-overflow ... READ of size
640 / READ of size 1140 in zlog_stream_set_msg_prefix(). With the
fix there are no reports, including for messages of 2000 and 1000 bytes that wrap over several
lines. Each wrapped line stays below log_limit and no message bytes are lost.
### Fix
- zlog_stream_set_msg_prefix(): use only the bytes that were written into
buf, and limit the prefix to half of log_limit. This always leaves room
for the time/level prefix, the quotes, the final suffix and at least part of the message, so the
length arithmetic in the append and wrap paths no longer underflows. Pool names short enough for the
prefix to fit are not affected at the default log_limit. With a small
log_limit (for example 512), a prefix longer than half of it (about 250 or more
characters of pool name) is now truncated where it used to fit; if you prefer an exact bound based
on the space actually left in the line, I can change it.
- zlog_stream_vformat() has the same vsnprintf() pattern with a 1024-byte
buffer. It has no callers at the moment, but the length is clamped the same way.
### Test
- sapi/fpm/tests/log-bwd-long-pool-name.phpt: buffered mode, default
log_limit, 600-character pool name. Checks that the log line is the prefix cut at 511
bytes followed by the quoted message.
- sapi/fpm/tests/log-dwd-long-pool-name-limit-512.phpt: direct mode, log_limit =
512, 1100-character pool name. Checks that the prefix is cut at 256 bytes.
Results:
- Without the fix: the first test fails because the log line contains stack bytes. The second test
does not finish: the master blocks writing an endless log line, and run-tests times out.
- With the fix: both pass, also with ASan.
- sapi/fpm/tests: 121 passed, 16 skipped, 1 warned (an existing XFAIL that passes). Two
tests failed (bug68391-conf-include-order.phpt and
bug68423-multi-pool-all-pms.phpt) because another process on my shared test machine was
already using ports 9010-9023. The failures are bind(): Address already in use and are
unrelated to this change. Under ASan, all sapi/fpm/tests/log-*.phpt tests pass.
This issue was found by an LLM-Enhanced Static Analysis Engine and verified manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)