1. What did you do
We run a Swoole\WebSocket\Server in production (Docker) on multiple hosts:
Swoole\WebSocket\Server, dispatch_mode => 2, 16 worker processes, max_request => 0
Runtime::enableCoroutine(SWOOLE_HOOK_ALL)
- Long-lived per-connection consumer coroutines that block on
Swoole\Coroutine\Channel::pop(1.0) in a loop (producer/consumer queue)
Swoole\Timer::tick() / Timer::after() timers per connection
- A
Channel-based connection pool (phpredis + runtime hooks)
No specific user action triggers the crash — it happens spontaneously in
production, roughly once every few weeks per host.
2. What did you expect
Worker processes should shut down cleanly, even when Channels that still have
blocked consumers are destroyed during shutdown.
3. What actually happened
Worker processes crash with SIGSEGV. Kernel logs from two independent hosts
over ~2 months show four crashes, all at the identical offset 0x3376f inside
the executable segment of swoole.so, faulting address 0x18:
[Wed Apr 29 19:26:00 2026] host-04: php[1650455]: segfault at 18 ip 00007927e784876f sp 00005bd9aae397f0 error 4 in swoole.so[7927e7815000+170000]
[Sun May 10 20:24:02 2026] host-04: php[1650447]: segfault at 18 ip 00007927e784876f sp 00005bd92f887c10 error 4 in swoole.so[7927e7815000+170000]
[Thu Jul 2 12:50:21 2026] host-01: php[1505854]: segfault at 18 ip 000071bd1999576f sp 00005e73345605f0 error 4 in swoole.so[71bd19962000+170000]
[Thu Jul 2 16:40:27 2026] host-04: php[1008159]: segfault at 18 ip 00007ef0b7d5276f sp 00005a582946e750 error 4 in swoole.so[7ef0b7d1f000+170000]
Symbol resolution
The executable LOAD segment of our swoole.so starts at vaddr 0x75000
(readelf -lW), so the faulting vaddr is 0x75000 + 0x3376f = 0xa876f:
$ gdb -batch -ex 'info symbol 0xa876f' swoole.so
swoole::PHPCoroutine::create_context(swoole::PHPCoroutine::Args*) + 239 in section .text
Disassembly around the faulting instruction
a8712: call <_emalloc_large@plt> ; allocate new vm stack (0x2000)
...
a8739: mov 0x209e38(%rip),%rdx ; rdx = executor_globals
a8748: mov 0x1e8(%rdx),%rdi ; rdi = EG field at offset 0x1e8 == NULL
a874f: mov 0x1a0(%rdx),%r8
a8756: mov %rax,0x1d8(%rdx) ; install new vm stack into EG
a875d: mov %rsi,0x1d0(%rdx)
a8764: movq $0x2000,0x1e0(%rdx)
a876f: mov 0x18(%rdi),%rdi ; <<< SIGSEGV: NULL->0x18
a8773: movups %xmm0,0x80(%rax)
a877a: mov %rdi,0x88(%rax) ; would be saved into the new context
So create_context() reads a pointer field from executor_globals
(offset 0x1e8) that is NULL, and dereferences its member at offset 0x18 —
matching the kernel's segfault at 18 exactly, in all four crashes.
Correlation with the server log
The host-01 crash correlates to the second with the Swoole server log: the worker
was destroying coroutine Channels that still had blocked consumers, then
segfaulted:
[2026-07-02 12:50:21 *209.14] WARNING Channel::~Channel() (ERRNO 10003): channel is destroyed, 1 consumers will be discarded
[2026-07-02 12:50:21 *209.14] WARNING Channel::~Channel() (ERRNO 10003): channel is destroyed, 1 consumers will be discarded
[2026-07-02 12:50:22 $17.0] WARNING Worker::report_error(): worker(pid=209, id=14) abnormal exit, status=0, signal=11
A bug occurred in Swoole-v6.0.2, please report it.
(The pid differs from the kernel log because the server runs inside a container
with its own PID namespace; timestamps match exactly.)
Our hypothesis: during worker shutdown, after the PHP executor state has been
partially torn down (the EG field at 0x1e8 already cleared), something still
triggers the creation of a new coroutine (e.g. destructors of objects holding
Channels wake up / re-schedule work, with SWOOLE_HOOK_ALL enabled), and
create_context() dereferences the NULL EG field without a check. Since all four
crashes across two hosts hit the same instruction with the same faulting address,
this is a deterministic NULL dereference on that code path, not random memory
corruption.
We do not have a minimal reproducer yet. Core dumps are now enabled on the
affected hosts; we will attach a full gdb backtrace as soon as the next crash
occurs.
4. Environment
Swoole version : 6.0.2
PHP version : 8.2.20 (NTS)
OS : Linux 6.8.0-40-generic #40-Ubuntu SMP PREEMPT_DYNAMIC x86_64 (Ubuntu, inside Docker)
GCC : 12.2.0
OpenSSL : OpenSSL 3.0.15 3 Sep 2024
Output of php --ri swoole:
swoole
Swoole => enabled
Author => Swoole Team <team@swoole.com>
Version => 6.0.2
Built => May 15 2025 10:01:06
coroutine => enabled with boost asm context
epoll => enabled
eventfd => enabled
signalfd => enabled
cpu_affinity => enabled
spinlock => enabled
rwlock => enabled
sockets => enabled
openssl => OpenSSL 3.0.15 3 Sep 2024
dtls => enabled
http2 => enabled
json => enabled
curl-native => enabled
curl-version => 7.88.1
zlib => 1.2.13
brotli => E16777225/D16777225
mutex_timedlock => enabled
pthread_barrier => enabled
futex => enabled
mysqlnd => enabled
Directive => Local Value => Master Value
swoole.enable_library => On => On
swoole.enable_fiber_mock => Off => Off
swoole.enable_preemptive_scheduler => Off => Off
swoole.display_errors => On => On
swoole.use_shortname => On => On
swoole.unixsock_buffer_size => 8388608 => 8388608
1. What did you do
We run a
Swoole\WebSocket\Serverin production (Docker) on multiple hosts:Swoole\WebSocket\Server,dispatch_mode => 2, 16 worker processes,max_request => 0Runtime::enableCoroutine(SWOOLE_HOOK_ALL)Swoole\Coroutine\Channel::pop(1.0)in a loop (producer/consumer queue)Swoole\Timer::tick()/Timer::after()timers per connectionChannel-based connection pool (phpredis + runtime hooks)No specific user action triggers the crash — it happens spontaneously in
production, roughly once every few weeks per host.
2. What did you expect
Worker processes should shut down cleanly, even when Channels that still have
blocked consumers are destroyed during shutdown.
3. What actually happened
Worker processes crash with SIGSEGV. Kernel logs from two independent hosts
over ~2 months show four crashes, all at the identical offset
0x3376finsidethe executable segment of swoole.so, faulting address
0x18:Symbol resolution
The executable LOAD segment of our swoole.so starts at vaddr
0x75000(
readelf -lW), so the faulting vaddr is0x75000 + 0x3376f = 0xa876f:Disassembly around the faulting instruction
So
create_context()reads a pointer field fromexecutor_globals(offset
0x1e8) that is NULL, and dereferences its member at offset0x18—matching the kernel's
segfault at 18exactly, in all four crashes.Correlation with the server log
The host-01 crash correlates to the second with the Swoole server log: the worker
was destroying coroutine Channels that still had blocked consumers, then
segfaulted:
(The pid differs from the kernel log because the server runs inside a container
with its own PID namespace; timestamps match exactly.)
Our hypothesis: during worker shutdown, after the PHP executor state has been
partially torn down (the EG field at
0x1e8already cleared), something stilltriggers the creation of a new coroutine (e.g. destructors of objects holding
Channels wake up / re-schedule work, with
SWOOLE_HOOK_ALLenabled), andcreate_context()dereferences the NULL EG field without a check. Since all fourcrashes across two hosts hit the same instruction with the same faulting address,
this is a deterministic NULL dereference on that code path, not random memory
corruption.
We do not have a minimal reproducer yet. Core dumps are now enabled on the
affected hosts; we will attach a full
gdbbacktrace as soon as the next crashoccurs.
4. Environment
Output of
php --ri swoole: