Skip to content

Register the fork handlers once per process, not on every mi_heap_new - #19

Merged
Jarred-Sumner merged 1 commit into
bun-dev3-v2from
claude/pthread-atfork-once
Aug 14, 2026
Merged

Jarred-Sumner merged 1 commit into
bun-dev3-v2from
claude/pthread-atfork-once

Conversation

@dylan-conway

Copy link
Copy Markdown
Member

The upstream/dev3 merge (27d1636) moved the pthread_atfork(...) call from mi_process_init_once() into mi_process_init(), after the mi_atomic_do_once block. mi_process_init() is reached from _mi_thread_init_with_heap() — i.e. from every mi_heap_new() — so each heap registered another (prepare, parent, child) triple.

macOS caps a process's atfork handler table at one VM page (~680 entries on arm64). Once a heap-heavy workload fills it, every later pthread_atfork in the process fails with ENOMEM. In Bun this showed up as BoringSSL's init_pthread_fork_detection() calling abort() on the first RAND_bytes after ~700 transpiled modules (each transpile job creates a heap) — astro build and vite build both hit it: https://buildkite.com/bun/bun/builds/95095. glibc's list is heap-allocated and unbounded, so Linux only leaked entries (and ran N nested no-op handlers per fork).

The fix moves the call back into mi_process_init_once(), where it was before the merge.

test-fork-user-heap gains case_d: after 2048 mi_heap_new/mi_heap_delete cycles, a fresh pthread_atfork must still succeed. On macOS it returns 12 (ENOMEM) before this change and 0 after; on Linux it passes either way (documented in the test). Full ctest passes locally in Release and Debug (Debug also exercises case_b).

Verified in Bun with a release build: unpatched → astro-post.test.js and a 700-import + randomBytes repro both abort; patched → both pass.

The upstream/dev3 merge (27d1636) moved the pthread_atfork call from
mi_process_init_once() into mi_process_init(), after the do-once block.
mi_process_init() is reached from _mi_thread_init_with_heap() and so
from every mi_heap_new(), which meant each heap registered another
(prepare, parent, child) triple. macOS caps a process's atfork table at
one page (~680 entries on arm64); once a heap-heavy workload filled it,
any other pthread_atfork in the process failed with ENOMEM. In Bun that
surfaced as BoringSSL's init_pthread_fork_detection() aborting on the
first RAND_bytes after ~700 transpiled modules (astro build, vite build).
glibc's list is unbounded so Linux only leaked entries.

Move the call back into mi_process_init_once(). test-fork-user-heap gains
case_d: after 2048 heap create/delete cycles a fresh pthread_atfork must
still succeed (fails with 12/ENOMEM on macOS before this change).
dylan-conway added a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
…in astro/vite builds)

Bumps oven-sh/mimalloc to f201f52a (oven-sh/mimalloc#19). The dev3 sync in
#37367 left mimalloc's pthread_atfork call outside the once-guard in
mi_process_init(), which every mi_heap_new() reaches, so each transpile
job's heap registered another handler triple. macOS caps the atfork table
at one page (~680 entries); once full, BoringSSL's own pthread_atfork got
ENOMEM and init_pthread_fork_detection() aborted on the first RAND_bytes.
That is the SIGABRT in astro-post.test.js and vite-build.test.ts on the
darwin lanes. Linux was unaffected (glibc's list is unbounded).
@Jarred-Sumner
Jarred-Sumner merged commit 6e891cb into bun-dev3-v2 Aug 14, 2026
dylan-conway added a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
…in astro/vite builds)

Bumps oven-sh/mimalloc to f201f52a (oven-sh/mimalloc#19). The dev3 sync in
#37367 left mimalloc's pthread_atfork call outside the once-guard in
mi_process_init(), which every mi_heap_new() reaches, so each transpile
job's heap registered another handler triple. macOS caps the atfork table
at one page (~680 entries); once full, BoringSSL's own pthread_atfork got
ENOMEM and init_pthread_fork_detection() aborted on the first RAND_bytes.
That is the SIGABRT in astro-post.test.js and vite-build.test.ts on the
darwin lanes. Linux was unaffected (glibc's list is unbounded).
dylan-conway added a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
…in astro/vite builds)

Bumps oven-sh/mimalloc to 6e891cbe, the bun-dev3-v2 merge of oven-sh/mimalloc#19. The dev3 sync in
mi_process_init(), which every mi_heap_new() reaches, so each transpile
job's heap registered another handler triple. macOS caps the atfork table
at one page (~680 entries); once full, BoringSSL's own pthread_atfork got
ENOMEM and init_pthread_fork_detection() aborted on the first RAND_bytes.
That is the SIGABRT in astro-post.test.js and vite-build.test.ts on the
darwin lanes. Linux was unaffected (glibc's list is unbounded).
dylan-conway added a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
…in astro/vite builds)

Bumps oven-sh/mimalloc to 6e891cbe, the bun-dev3-v2 merge of
oven-sh/mimalloc#19. The dev3 sync in bun#37367 left mimalloc's
pthread_atfork call outside the once-guard in mi_process_init(), which
every mi_heap_new() reaches, so each transpile job's heap registered
another handler triple. macOS caps the atfork table at one page (~680
entries); once full, BoringSSL's own pthread_atfork got ENOMEM and
init_pthread_fork_detection() aborted on the first RAND_bytes. That is the
SIGABRT in astro-post.test.js and vite-build.test.ts on the darwin lanes.
Linux was unaffected (glibc's list is unbounded).
Jarred-Sumner pushed a commit to oven-sh/bun that referenced this pull request Aug 14, 2026
…in astro/vite builds); build: --local-deps (#38291)

### What was crashing

`test/js/third_party/astro/astro-post.test.js` and
`test/integration/vite-build/vite-build.test.ts` SIGABRT on the darwin
lanes since #37367 ([build
95095](https://buildkite.com/bun/bun/builds/95095)). Stack:

```
abort
init_pthread_fork_detection()      ← BoringSSL aborts when pthread_atfork() != 0
pthread_once
RAND_bytes
bun_runtime::node::crypto::random::__jsc_host_random_bytes
```

The mimalloc dev3 sync left `pthread_atfork(...)` in `mi_process_init()`
*after* its `mi_atomic_do_once` block instead of inside
`mi_process_init_once()`. `mi_process_init()` is reached from every
`mi_heap_new()`, so each transpile job / arena reset registered another
handler triple (~1050 registrations by the time astro's build reaches
its first `randomBytes`). macOS caps a process's atfork table at one
page (~680 entries on arm64); once full, every other `pthread_atfork` in
the process gets `ENOMEM`, and BoringSSL aborts on that. glibc's list is
unbounded, so Linux only leaked entries.

Minimal repro: 700 trivial `.ts` imports followed by
`crypto.randomBytes(4)` → `abort()`; 600 is fine.

### Fix

Bump `oven-sh/mimalloc` to `6e891cbe` — the `bun-dev3-v2` merge of
oven-sh/mimalloc#19 — which moves the call back
under the once-guard and adds a regression case to the fork's
`test-fork-user-heap` (fails with `ENOMEM` on macOS before, passes
after). `process.versions.mimalloc` expectation updated.

Verified locally with `build:release` on macOS arm64, same build config
both ways:

| mimalloc source | 700-import + `randomBytes` | `astro-post.test.js` |
|---|---|---|
| `be7eb3f` (current pin) | abort | abort |
| `6e891cbe` (tree-identical to the tested `f201f52a`) | ok | 4/4 pass
×2 |

### Also: `--local-deps=name=path`

Added while chasing this so a vendored dep can be built from a local
checkout instead of the pinned tarball:

```sh
bun bd --local-deps=mimalloc=~/code/mimalloc test foo.test.ts
```

Works for any `github-archive` dep (`name=path[,name=path]`); no fetch /
`.ref` / patches, banner shows `local:<name>`, unknown or disabled names
fail at configure, and an out-of-repo checkout's objects map onto
`obj/vendor/<name>/`. Editing the checkout rebuilds incrementally (for
mimalloc: just `static.c.o` + link). Local/in-tree `direct` deps also
stop stamping their source *directory* as a PCH dependency (depfiles
already track edits). Default-config `build.ninja` is byte-identical
apart from the pin. Docs in `scripts/build/deps/README.md`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants