LogoAnyDoor7
  • Blog
  • About
LogoAnyDoor7
BlogAbout
All Posts
2026/10/16

AI agents pushed my Mac into a kernel panic: tracing it to one join_all

Two macOS kernel panics with launchd dying of SIGBUS. The stackshot led to unbounded concurrent writes in pagefind that filled the system file table. The fix is upstream, with a self-check and an open-source triage tool, panicsight.

Between the afternoon of October 15 and the early hours of October 16, my MacBook Pro froze and rebooted twice, 8.5 hours apart. The only useful line in the report that popped up after each reboot was initproc exited -- exit reason namespace 2 subcode 0xa. At the end of the trail, the killer turned out to be one line of concurrency code inside an open-source tool downloaded 9 million times a month, and what gave that line its chance to go wrong was me letting AI agents run far too many jobs at once on this machine. This post records the debugging process, and leaves a self-check procedure for anyone who runs coding agents heavily on a Mac.

Parallel AI agent builds made pagefind write hundreds of thousands of files at once, and with the file table exhausted, launchd could not page a code file back in, took SIGBUS, and the kernel panicked.

Two identical crashes

The crashes happened at 16:14 and 00:43, on an M5 Max machine with 128 GB of memory, running macOS 27.0.1. The signatures in the two panic reports were exactly the same: process number 1 exited after receiving signal 10. Process 1 is launchd, which starts and supervises every other service on the system; once it exits, the only thing the kernel can do is panic. Signal 10 is SIGBUS.

The investigation took two wrong turns before finding the trail. First I suspected an external drive waking from sleep: a 20 TB hard disk at home goes to sleep every night, but by the time of the second crash a scheduled task had already unmounted both of its volumes for three hours, and launchd could not reach the disk at all. Then I went looking in the system logs; several queries in a row came back empty, and for a moment I thought logging had gone silent before the crash. It turned out that zsh has a built-in command also named log, which shadows the system's /usr/bin/log, so queries through it return nothing without any error. With the absolute path, the logs were entirely normal.

Memory checked out. Both panic reports show no memory pressure and plenty of swap headroom.

The answer to where exactly launchd died is in the stackshot attached to the panic report. It is a snapshot the kernel takes at the instant of the crash, covering every process and every thread, including the user-space call-stack addresses. Using the same-version dyld and launchd binaries from this machine, I turned the addresses on launchd's crashed thread back into function names. Frame for frame, the two panics match:

  1. launchd received a request to launch some on-demand service, and the launch call returned an error code outside the expected set.
  2. launchd ran an assertion over that error code. The assertion is harmless in itself: all it does is request a log line.
  3. To write that log, it called dladdr to find out which binary the caller belongs to, so the name could go into the log.
  4. dladdr walks the loaded images inside dyld, and the very first memory-reading instruction, ldr w9, [x0], took a SIGBUS.

The difference between SIGBUS and SIGSEGV is what matters at this layer. On macOS, touching an unmapped address gets you SIGSEGV; SIGBUS is what you get when the address is mapped but the page cannot be fetched this time. The most common reason a page cannot be fetched is a failed page-in: the page is backed by a file on disk, it has gone unused for so long that the in-memory copy was reclaimed long ago, and this time the system had to read it back from disk, and that read failed.

My guess is that the Loader object launchd was reading sat on exactly such a cold page. On a normally running system, page-ins almost never fail, so the real question became this: at that moment, what stopped the kernel from reading back even a single file page.

The culprit was hiding in the stackshot

A build log provided the first direct evidence. Before the panic, an agent had been building a static site of roughly 516,000 pages, and the last line of the log read ENFILE: file table overflow, timestamped 16:14:45. ENFILE means the system-wide file table is full and nothing on the machine can open any file. Four seconds after that line, at 16:14:49, the machine panicked.

The file table limit on this machine is 491,520. Because of an earlier crash, I already had a monitor running that samples every two minutes. It shows the system normally sits at about 11,000 open files; that day it jumped past 200,000 several times within a two-minute window, free vnodes, the kernel entries that track open files, dropped to between 12% and 17% of the total, and then recovered on their own. The sampling was too coarse to catch the peak at the exact moment of the panic.

The second piece of evidence was sitting in the stackshot. I counted, per process, how many threads were in uninterruptible waits with the APFS filesystem driver on their kernel stacks. The two panics came out nearly identical: 549 and 531 such threads across the whole system, of which 512 belonged to one process, pagefind_extended. In both crashes, 511 of those 512 threads were waiting on the same wait object, meaning the same lock; in the second panic, one thread each from Finder, node, and a Python process was also queued behind that lock. The remaining threads stuck in APFS belonged to system processes such as fseventsd, Finder, and kernel_task, stuck alongside everything else.

The pagefind process lived for 473 seconds in the first crash and 72 seconds in the second. The second timeline goes like this: site rendering finished at 00:42:15, pagefind finished indexing and switched to writing results to disk at 00:43:09, wrote for about 18 seconds, and the machine panicked.

One join_all line and a cap of 32

Pagefind is a full-text search tool for static sites, open-sourced by CloudCannon and written in Rust. Once a site build finishes, it scans every page and produces sharded index files, so a purely statically hosted site can offer search. The pagefind package on npm has been downloaded about 9.1 million times over the last 30 days, and Starlight, Astro's documentation framework, uses it by default.

The problem lives in the code that writes the index to disk. Reading through the source, the trouble is three things stacked on top of each other.

  1. It creates one async task per output file, then waits for all of them at once with join_all. A site with hundreds of thousands of pages becomes hundreds of thousands of file-writing tasks starting simultaneously.
  2. Each task creates the file first, then does the highest-level gzip compression, and closes the file only after the write finishes. The file descriptor stays occupied from creation until completion.
  3. When file creation fails, it retries every 100 milliseconds, without ever raising an error; a full file table just means it keeps retrying.

Rust's async runtime tokio executes this kind of file operation on a thread pool whose default cap is exactly 512 threads. The 512 threads stuck in APFS in the stackshot are what that pool looks like once it fills up. As of when I submitted the fix, upstream's latest code still worked this way, and nobody had reported the problem.

The fix changes three things. At most 32 files are written at the same time; content is compressed first, the file is opened after that, and it closes as soon as the write is flushed; retries apply only to transient errors such as a full file table, at most 50 attempts, and afterwards the failure comes back with the file path attached.

I ran a comparison on another machine, a Mac mini with 16 GB of memory:

ScenarioStock thread peakStock open files peakPatched thread peakPatched open files peak
Synthetic site, 30,000 pages52929,1388938

For the same 30,000 pages, the patched build went from 45 seconds to 29, and its 30,000 index fragments came out byte-for-byte identical to the stock version's. On a synthetic 500,000-page site, the patched build held at 38 open files the whole way through, finished in 109 seconds, and produced 500,000 files. The site behind all of this, which had by then grown to about 518,000 pages, also finished indexing on the mini with the patched build. I have sent the fix upstream: pagefind PR #1380.

The same signature, in other people's reports

While researching I found that this exact panic signature had already shown up several times this year. The triggers vary; the mechanism is the same one.

  • Claude Code issue #101116: launchd again took a SIGBUS inside dladdr, set off by a directory-walking tool holding a large number of directory descriptors.
  • Claude Code issue #35556: free vnodes had dropped to 0.82%, and doubling kern.maxvnodes made the panics stop.
  • mcp-language-server issue #155: a file watcher held about 200,000 descriptors; its log first showed FS pagein error: 23 Too many open files in system, and then launchd crashed.
  • a Japanese write-up: Spotlight indexing node_modules pushed vnodes to the ceiling, with the same panic.

My read is that AI coding agents changed the shape of the load a development machine carries. A single person used to run one build at a time; now several agents build, index, watch files, and spawn child processes at once, and the machine may go days without a reboot. Unbounded concurrency in one tool, harmless in ordinary use, can push kernel tables that a desktop system never normally touches up to their limits under this load. And once a limit is hit, the process that dies first can be any process that happens to need one page of code read back; if that process is launchd, the whole machine goes down with it.

If your Mac ever shows initproc exited -- exit reason namespace 2 subcode 0xa, three things to do first:

  1. Open the newest panic-full-*.panic under /Library/Logs/DiagnosticReports/ and confirm the signature matches.
  2. In the report's stackshot, count how many threads of each process are stuck inside the filesystem driver; the top entry is very likely the culprit.
  3. Make a habit of checking file table headroom with sysctl kern.num_files kern.maxfiles, and keep an eye on it while running large builds or indexing jobs.

Doing the second step by hand is slow and fiddly, so I packaged the scripts I used this time into an open-source tool, panicsight. On my two panic reports, the first line of its output already pointed at pagefind_extended. v0.1 reads a panic report and gives the crash type and the most suspicious process, and panicsight check shows how full the file and vnode tables are right now. A continuous watch mode is still in development, and issues are welcome.

All Posts

More Posts

Opus 4.8's real upgrade is the ability to admit it is not done

Claude Opus 4.8's official System Card shows that frontier agents are starting to compete on state honesty, not only output quality.

2026/05/28

Verification Is Free Now: The Real Phase Change of the AI Era

How YC CEO Garry Tan's Complexity Ratchet reveals that 90% test coverage went from heroic effort to Tuesday

2026/05/12
© 2026 AnyDoor7All Rights Reserved.