Luther Monson, Staff Software Engineer at SUSE

Luther Monson

Go by way of PHP · Containers · VMs · Windows · Kubernetes

Step Debugging a Worker That Never Ends

A persistent PHP worker never finishes its request, so Xdebug never sees your trigger. Here's how ePHPm routes the debugged request to its own fresh worker instead of trying to reset anything.

12-Minute Read

Diagram of the ePHPm router sending untriggered requests to the warm worker pool and Xdebug-triggered requests to a one-request debug lane

Persistent workers are the big performance win in modern PHP. You boot Laravel or Symfony once, keep the container in memory, and serve thousands of requests out of the same process. ePHPm has had a worker mode for a while. Like most worker runtimes it had a quiet hole in it. You couldn’t step debug a request with Xdebug. I wanted that fixed without patching Xdebug and without giving up the warm pool. The answer ended up being to stop fighting the constraint and route around it. The debug…

An Upstream PHP JIT Use-After-Free, and Why Our Default Flipped Twice

ePHPm turned PHP's tracing JIT on by default, then off three days later when stock Laravel died after three requests. The bug was upstream, and this is the bisect, the root cause and where the fix landed.

10-Minute Read

Bar chart of requests served before a crash per PHP version: 8.4.14, 8.5.0 and 8.5.4 never crash; 8.4.24 dies at 7, 8.5.5 at 1, 8.5.7 and 8.5.9 at 2

In August, ePHPm turned PHP’s tracing JIT on by default in single-site serve mode. Three days later it went back off on every platform, and it’s still off. The reason turned out to be a use-after-free in PHP’s tracing JIT that has nothing to do with ePHPm. Stock php -S with a stock Laravel skeleton dies on it in a couple of requests. The fix is now released upstream. So this seemed like the right time to write up how we got there, and what still has to happen before the default…

os/exec's StdoutPipe Footgun: How cmd.Wait() Ate My Last Frame (Twice)

Calling Wait while a StdoutPipe reader is still draining throws away the child's last output. I shipped this bug twice in shipmates.

9-Minute Read

Timeline of a child process exiting, Wait closing the pipe, and the reader losing buffered frames

shipmates spends most of its life spawning claude (and codex) processes and reading newline-delimited JSON off their stdout. The last frame of a turn is the important one. It’s a result frame with the cost, the model and the final text. Stderr matters too, because that’s where Claude Code tells you Invalid API key · Please run /login. For a while I was throwing both away under load. The cause is documented right there in the os/exec godoc. I fixed it in one package and then found the…

OPcache on Windows: Two Silent Failures

An ASLR fatal on Windows looked like a memory-size lottery. Separately, Windows PHP 8.3 builds never had an opcode cache, and the fix sat unpicked for three weeks.

8-Minute Read

Two ePHPm processes computing the same OPcache shared-memory name; the second reattaches and dies on the ASLR check

Windows is the platform where ePHPm gets the least traffic and the most surprises. Last week closed out two OPcache bugs that only exist there. Both were quiet in their own way. One killed the server at startup with nothing in our own logs. The other didn’t kill anything. It just ran PHP 8.3 with no opcode cache at all. For three weeks after a fixed SDK was published, every check stayed green.

Requests That End Wrong: A 200 That Was Really a Crash

Three ePHPm bugs lived in how an embedded PHP request starts and ends. A bailout returned 200, $_POST came up empty and a SIGTERM corrupted the heap.

10-Minute Read

Diagram of a bare zend_bailout being absorbed by php_execute_script's own zend_try instead of reaching ePHPm's guard

The scariest response a server can send isn’t a 500. It’s a 200 that should have been a 500. A crash is loud. The pod restarts, an alert fires, somebody looks. A truncated page with a success status code is quiet. Your CDN caches it and the health check calls the box healthy. A client parses half a document as if it were the whole thing.

Rescuing a Windows Dynamic Disk Over USB with WSL2 and USB/IP

Windows refused to bring an old dynamic disk online over USB. Handing the raw USB device to WSL2's Linux kernel with usbipd-win got every file back.

10-Minute Read

GPT layout of the 1 TB drive with LDM metadata and a 930 GiB LDM data partition, and the two paths: the Windows storage stack refusing it and usbipd passing it to WSL2

I pulled a 1 TB NVMe out of an old Windows box, dropped it in a USB enclosure and plugged it in. Windows showed nothing. No drive letter, no “initialize this disk” prompt, nothing in Explorer. Disk Management listed it as Invalid. The enclosure’s booklet said to “bring the disk online”, which silently did nothing. The data was completely intact. Windows was refusing on policy grounds, not because anything was broken. Getting it back meant going around the Windows…

PHP Signal Safety - The Init Ordering Bug Nobody Warns You About

Embedding PHP in a multithreaded server means PHP's timeout signals can land on any thread. How ePHPm got that under control.

11-Minute Read

Thread timeline: the main thread installs PHP's signal handlers during php_embed_init, a PHP thread later arms ITIMER_PROF, and SIGPROF is delivered to a tokio worker that has no PHP state

Last week I wrote about why ePHPm embeds PHP from Rust and promised the signal bug its own post. Here it is. It’s the kind of bug where the fix is about ten lines and the explanation is the whole post. Nothing in PHP’s embed documentation (there’s barely any) tells you that initializing PHP has process-wide side effects. Those side effects assume PHP is the only thing in the process.

Recent Posts

Categories

About

Luther is a 20+ year software veteran specializing in backend APIs and architecting products. Employed at SUSE as the Team Lead for the Containers on Windows Team working on Rancher + Kubernetes