Repository navigation
HTTP/2: process crashes (nil pointer SIGSEGV in x/net/http2 deadline callback) after fastcgi_finish_request() when the request body was not read, with Caddy's default read_body_idle timeout #2694
Description
What happened?
Disclosure: this report was drafted with the assistance of an AI tool. All reproduction steps, test results and logs below were executed and reviewed in our local environment. All suggestions was made by AI Assistance and should be considered only as a guidance.
- Not a known issue: not listed at https://frankenphp.dev/docs/known-issues/.
- Not already reported: closely related to panic: runtime error: invalid memory address or nil pointer dereference #2535 (fixed by fix: don't set request body read deadline after the request is finished #2538 in v1.12.6), but this crash still happens on v1.12.7, through a different path. fix: don't set request body read deadline after the request is finished #2538 skips the read deadline in
go_read_postwhenfc.isDone, but the request body is still read through Caddy'sIdleTimeoutReader(read_body_idle, default 1 min since caddyhttp: mitigate slowloris via idle read/write deadlines caddyserver/caddy#7913), which callsSetReadDeadlineitself. No open issue was found forSetWriteDeadline/fastcgi_finish_request+ HTTP/2. - Dependency: the deadline call comes from Caddy's idle-timeout wrapper, but it only happens because the request body is read after FrankenPHP has finished the request (
ServeHTTPalready returned). The Gonet/httpcontract forbids using theRequest.Body/ResponseWriterafter the handler returns, so FrankenPHP seems the right place to fix it, e.g. by not readingfc.request.Bodyat all oncefc.isDone. Happy to move this to Caddy if maintainers prefer.
What happened?
With HTTP/2 enabled (the Caddy default), FrankenPHP in classic mode (no worker) crashes the whole process when a PHP script calls fastcgi_finish_request() (or frankenphp_finish_request()) before the request body has been read. Laravel/Symfony do this on every request: Response::send() calls fastcgi_finish_request() when the function exists, and the body is often read later or not at all.
The process exits with:
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xc5754e]
Expected: finishing the request early must never crash the server; at worst, late reads should return EOF/0 bytes.
Minimal reproduction (no framework, no database)
probe.php:
<?php
// read=none -> finish without reading the body (crashes)
// read=after -> finish, then read php://input (crashes)
// read=before -> read php://input before finishing (no crash)
// read=nofinish -> never call fastcgi_finish_request() (no crash)
$read = $_GET['read'] ?? 'after';
http_response_code(204);
if ($read === 'before') {
file_get_contents('php://input');
}
if ($read !== 'nofinish') {
fastcgi_finish_request();
}
if ($read === 'after' || $read === 'before') {
file_get_contents('php://input');
}Caddyfile:
{
admin off
auto_https disable_redirects
servers {
protocols h1 h2
}
frankenphp {
num_threads 4
max_threads 4
}
}
https://localhost:18443 {
tls internal
root * /diagnostic
php_server
}Steps:
-
Start FrankenPHP with the files above mounted in
/diagnostic:docker run -d --name probe --network none \ -v "$PWD":/diagnostic:ro \ <frankenphp-image> frankenphp run --config /diagnostic/Caddyfile --adapter caddyfile
-
Send concurrent HTTP/2 requests from inside the container:
docker exec probe curl -ksS --http2 --parallel --parallel-max 16 -o /dev/null \ "https://localhost:18443/probe.php?read=none&n=[1-500]"
-
The container exits with code
2after only a few requests and logs the panic above.
Results
Each line is a fresh container, 500 requests, concurrency 16, same image.
| Scenario | Result |
|---|---|
HTTP/1.1 only (protocols h1), read=after (earlier build of the same probe) |
200/200 OK, no crash |
HTTP/2, read=nofinish |
200/200 OK, no crash |
HTTP/2, read=none, default Caddy timeouts |
crash after 4 requests (4/4 runs at concurrency 16; 90 requests in a run at concurrency 8) |
HTTP/2, read=after |
crash after 39 requests |
HTTP/2, read=before, GET and POST JSON |
500/500 OK (3 runs each, 3,000 requests total) |
HTTP/2, read=none, timeouts { write_idle -1s } |
crash after 4 requests (2/2 runs) |
HTTP/2, read=none, timeouts { read_body_idle -1s } |
500/500 OK (2/2 runs) |
HTTP/2, read=none, both idle timeouts disabled |
500/500 OK (2/2 runs) |
In the real application (Laravel 11 on the same image), the server crashed after 2 HTTP/2 requests. With $request->getContent() called before $response->send(), it handled 486 HTTP/2 requests without crashing.
Analysis
- The crash only happens with HTTP/2. As explained in fix: don't set request body read deadline after the request is finished #2538, HTTP/2 deadlines live on the stream, which is torn down (
handlerDone()setsresponseWriterStateto nil) onceServeHTTPreturns. On HTTP/1 they live on thenet.Conn. - Disabling Caddy's
read_body_idlealone removes the crash; disablingwrite_idlealone does not. This suggests the body is read after the request is finished, during PHP request shutdown (we did not confirm the exact caller; likely SAPI deactivation draining unread input throughgo_read_post→fc.request.Body.Read). That read goes through Caddy'sIdleTimeoutReader.Read, which callshttp.ResponseController.SetReadDeadlineon the finished HTTP/2 stream. go_read_postalready skips its own deadline whenfc.isDone(fix: don't set request body read deadline after the request is finished #2538), but still readsfc.request.Body, which is wrapped by Caddy.- Consuming the body before finishing (
file_get_contents('php://input')) avoids any late body read, so the crash disappears.
Symbolization caveat: go tool addr2line on the stripped binary maps pc=0xc5754e to golang.org/x/net/http2.(*responseWriter).SetWriteDeadline.func1 (golang.org/x/net@v0.59.0/http2/server.go:2744), the closure run on the HTTP/2 serve loop. This conflicts with the timeout experiment above, which points to the read wrapper. We could not capture a goroutine stack: with GOTRACEBACK=system or GOTRACEBACK=all, only the two panic lines are printed. The exact call chain is therefore unconfirmed.
Suggested fix
In go_read_post, return 0 without touching fc.request.Body once fc.isDone, since the body must not be used after ServeHTTP returns. Alternatively, avoid draining the request body during request shutdown after an early finish.
Workarounds we confirmed
- In the application, read the body before finishing, e.g. in Laravel
public/index.php:$request->getContent();before$response->send();. - Disable Caddy's body idle timeout:
servers { timeouts { read_body_idle -1s } }. This loses slowloris protection on request bodies. - Serve the PHP backend over HTTP/1.1 only (
protocols h1). disable_functions = fastcgi_finish_request,frankenphp_finish_requestalso avoids it, but then terminable middleware runs before the response completes, adding latency.
Build Type
Custom Docker image based on Chainguard (Wolfi, glibc 2.44). PHP is built by Chainguard's melange toolchain (see Build System in the phpinfo() below). We have not confirmed whether the FrankenPHP binary is Chainguard's package or a separate build. It reports:
- FrankenPHP
v1.12.7 - Caddy
v2.11.6 - Go
go1.26.8 golang.org/x/netv0.59.0- PHP
8.2.33ZTS (Zend Max Execution Timers => enabled,Zend Signal Handling => disabled)
We could not test the official dunglas/frankenphp image: our network blocks Docker Hub. Since the trigger is Caddy's default read_body_idle together with HTTP/2, we expect it to reproduce there, but this is not verified.
Operating System
- Reproduction above: Linux container (Chainguard/Wolfi) on Docker/Rancher Desktop. The
x86_64image was emulated with Rosetta on an Apple Silicon host. The crash does not depend on Rosetta: disablingread_body_idleor reading the body before finishing removes it under identical emulation. - The same panic message was observed by our team in production on native Linux
x86_64(Azure). We have not confirmed the crash address there.
Build Type
Custom (tell us more in the description)
Worker Mode
No
Operating System
Other (tell us more in the description)
CPU Architecture
x86_64
PHP configuration
Sanitized excerpt from CLI phpinfo(), not a full dump. Credentials, connection details, session security settings, environment variables, host identifiers and unrelated configuration have been omitted. CLI values may differ from the effective web request configuration.
Sanitized PHP configuration (CLI): build, extensions and request handling
phpinfo()
PHP Version => 8.2.33
System => Linux <redacted-host> <kernel> x86_64
Build Date => Oct 1 2026 23:02:09
Build System => Linux chainguard 6.18.54-r1-qemu-melange #Chainguard SMP PREEMPT_DYNAMIC Sun Sep 27 17:18:15 UTC 2026 x86_64 Linux
Configure Command => './configure' '--prefix=/usr' '--libdir=/usr/lib/php' '--datadir=/usr/share/php' '--sysconfdir=/etc/php' '--with-config-file-path=/etc/php' '--with-config-file-scan-dir=/etc/php/conf.d' '--with-layout=GNU' '--with-pic' '--enable-embed' '--enable-zts' '--disable-zend-signals' '--enable-zend-max-execution-timers' '--disable-cgi' '--disable-fpm' '--disable-phpdbg' '--enable-cli' '--with-curl' '--with-openssl' '--with-readline' '--with-sodium' '--with-zlib' '--enable-opcache' '--enable-mbstring' '--enable-sockets' '--enable-pcntl' '--enable-mysqlnd' '--with-pdo-mysql=mysqlnd' '--with-pdo-sqlite' '--enable-pdo' '--enable-intl' '--enable-xml' '--enable-dom' '--enable-simplexml' '--enable-xmlreader' '--enable-xmlwriter' '--enable-ftp' '--enable-bcmath' '--enable-ctype' '--with-bz2' '--enable-calendar' '--enable-exif' '--with-gettext' '--with-gmp' '--enable-phar' '--enable-fileinfo' '--enable-posix' '--with-external-pcre' '--with-pear'
Server API => Command Line Interface
Virtual Directory Support => enabled
PHP API => 20220829
PHP Extension => 20220829
Zend Extension => 420220829
Zend Extension Build => API420220829,TS
PHP Extension Build => API20220829,TS
Debug Build => no
Thread Safety => enabled
Thread API => POSIX Threads
Zend Signal Handling => disabled
Zend Memory Manager => enabled
Zend Multibyte Support => provided by mbstring
Zend Max Execution Timers => enabled
IPv6 Support => enabled
DTrace Support => disabled
Extensions shown in the original CLI output (names only):
bcmath, bz2, calendar, ctype, curl, date, dom, exif, fileinfo, filter,
ftp, gd, gettext, gmp, hash, iconv, intl, json, ldap, libxml, mbstring,
mysqlnd, openssl, opentelemetry, pcntl, pcre, PDO, pdo_mysql, pdo_pgsql,
pdo_sqlite, pgsql, Phar, posix, random, readline, redis, Reflection,
session, SimpleXML, soap, sockets, sodium, SPL, sqlite3, standard,
tokenizer, xml, xmlreader, xmlwriter, Zend OPcache, zip, zlib
Core (selected directives)
Directive => Local Value => Master Value
disable_functions => no value => no value
enable_post_data_reading => On => On
file_uploads => On => On
hard_timeout => 2 => 2
ignore_user_abort => Off => Off
implicit_flush => On => On
max_execution_time => 0 => 0
max_input_time => -1 => -1
memory_limit => 1024M => 1024M
output_buffering => 0 => 0
output_handler => no value => no value
post_max_size => 80M => 80M
upload_max_filesize => 80M => 80M
Zend OPcache v8.2.33 (CLI)
Opcode Caching => Disabled
Optimization => Disabled
SHM Cache => Enabled
File Cache => Disabled
JIT => On
Startup Failed => Opcode Caching is disabled for CLI
Directive => Local Value => Master Value
opcache.enable => On => On
opcache.enable_cli => Off => Off
opcache.jit => tracing => tracing
opcache.jit_buffer_size => 0 => 0
zlib (selected directives)
Directive => Local Value => Master Value
zlib.output_compression => Off => Off
zlib.output_compression_level => -1 => -1
zlib.output_handler => no value => no valueRelevant log output
Relevant log output
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0xc5754e]
Container exit code `2` (`OOMKilled=false`). No goroutine stack is printed, even with `GOTRACEBACK=system` or `GOTRACEBACK=all`.
`go tool addr2line` on the same binary:
golang.org/x/net/http2.(*responseWriter).SetWriteDeadline.func1
/root/go/pkg/mod/golang.org/x/net@v0.59.0/http2/server.go:2744
Activity