Luther Monson, Staff Software Engineer at SUSE

Luther Monson

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

Cross-Platform Process Containment: Job Objects vs Process Groups vs cgroups

How shipmates bounds and tears down agent process trees on Windows, Linux and macOS. It also covers why I deleted 1,206 lines of cgroup code and the grandchild that escaped the memory cap everywhere except my dev box.

8-Minute Read

Agent process tree with the scope of kill and measurement on Windows, Linux and macOS

Every turn a shipmates crew member takes is a subprocess. It’s claude or the codex app-server. That in turn spawns shells, go test, npm or whatever the task calls for. Two things have to be true about that tree. When a turn ends or gets killed, the whole tree goes, including the grandchildren. And if somebody sets a memory or CPU budget, the budget applies to the whole tree, not just the process I happened to start.

Bind Mounts Across Mount Namespaces: Four PRs and a Container Escape

Making GitHub Actions container: jobs work through ephemerd's Docker API shim took four tries, then a TOCTOU race turned the working version into an escape.

8-Minute Read

Sequence diagram of the bind mount TOCTOU race and the openat2 plus staging fix

GitHub Actions has a container: key that runs a job’s steps inside an image of your choosing. It’s popular because it gives you a reproducible toolchain without building a custom runner image. On a self-hosted runner it works by having the runner drive Docker. On ephemerd, “Docker” is a shim that speaks the Docker API and creates containers in containerd. Getting bind mounts right through that shim took four PRs in a week. A couple of months later the version that worked…

A 1 TB Test: Building a Memory Watchdog for go test

A runaway test binary hit about 1 TB of commit charge and took my desktop down. Here's the PowerShell and POSIX sh wrapper I put around go test, and the bug in it that the Windows box couldn't show me.

8-Minute Read

go test process tree with per-package test binaries watched by the safe-test wrapper

On August 16th a server.test.exe from shipmates climbed to roughly 1 TB of commit charge on my Windows workstation. Windows Terminal was hosting several agent sessions in tabs. It hard-faulted with E_OUTOFMEMORY, and every one of those sessions died at the same moment. It wasn’t the first time either. An earlier one hit about 236 GB through an infinite filepath.Dir loop.

Kata Containers for CI: Boot Cost Isn't the Problem, File I/O Is

Kata containers are safer for CI, but slower. Starting one takes 12x longer, which is fine. Reading small files is about 40x slower for the entire job, which isn't.

7-Minute Read

Bar chart of Kata slowdown versus runc for each measurement

Of the three platforms ephemerd runs jobs on, Linux has the weakest isolation. Windows jobs get a Hyper-V isolated container with its own kernel. macOS jobs get a whole VM. A Linux job is an ordinary runc container sharing the host kernel. A kernel escape from a Linux job is a host compromise. Kata Containers fixes that by putting each container in a lightweight VM with its own kernel. It plugs into containerd as just another runtime handler. So I added it in #155, measured it, and left it off…

Per-Tenant Network Policy in One Process: ePHPm's eBPF Thread Tags

Every site on an ePHPm server runs inside the same process, so a normal firewall can't tell them apart. A small eBPF program tags each thread with its site. It keeps sites from reaching each other's local ports for under a microsecond per connection.

17-Minute Read

Flow from a PHP pool thread writing its vhost tag into a BPF map, through the bind and connect cgroup hooks, to allow, rewrite or EPERM

In multi-tenant mode ePHPm serves every site in sites_dir from one process. Every tenant’s PHP runs as the same OS user on the same pool of ZTS threads. A thread that just served blog.example will serve shop.example a few milliseconds later. That’s the density win. It also means the kernel has no idea which tenant is behind a given connect(). As far as Linux is concerned it’s all ephpm-web.

My iptables Rule Matched 0 Packets in 9.8 GB

Container-to-host traffic goes through INPUT, not FORWARD, so ephemerd's carefully written allow-list never matched a single packet.

8-Minute Read

Netfilter flow showing container traffic to the host taking INPUT instead of FORWARD

After a week of Windows firewall rules that listed fine and enforced nothing, I went back to the Linux side of ephemerd feeling pretty good about it. iptables is the boring, well-understood one. Then I looked at the packet counters on a live node. I found a rule that had never matched a packet in its life. That node had pushed 9.8 GB through the same chain.

My Benchmark Was 4x Wrong Because of WSL

Launching the server with its working directory on /mnt/c cost ePHPm about 4x on a light workload. The report built a wrong explanation on top of it, and the strace shows why.

8-Minute Read

Bar chart of ePHPm requests per second by working directory: native ext4 far ahead of DrvFs, with DrvFs throughput falling as the cwd path gets deeper

I do most of my benchmarking on a Windows box in WSL2. It’s the machine on my desk with 32 threads and 62 GB of RAM. Everybody who does that learns the first rule early: don’t put your data on /mnt/c. DrvFs is the 9p-backed mount that exposes the Windows drives inside WSL, and it’s slow for file-heavy work. So the databases, the docroot and the build output all live on the WSL ext4 disk. My scaling harness for ePHPm even says so in capital letters in its README.

Escaping WSL: Booting My Own Linux VM Through Hyper-V's HCS API

WSL2 refuses to run under LocalSystem, so ephemerd boots an Alpine kernel directly through vmcompute.dll instead.

9-Minute Read

Diagram of ephemerd booting a Linux VM through HCS and reading its IP from the serial console

I’ve been building ephemerd, a daemon that runs self-hosted GitHub Actions runners with a fresh environment per job. On a Windows host, Windows jobs run in Hyper-V isolated containers. Linux jobs still need a Linux kernel from somewhere. The obvious answer on Windows in 2026 is WSL2, and that’s what I used first. It worked great from my terminal and fell over the moment ephemerd became a real Windows service.

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