Skip to content

Commit b7ed2cc

Browse files
authored
fix(upgrade): use MAP_PRIVATE mmap to prevent macOS SIGKILL during delta upgrade (#339)
On macOS, `sentry cli upgrade` is killed with SIGKILL when the delta upgrade system tries to memory-map the running binary for patch application. ## Root Cause `Bun.mmap()` defaults to `{ shared: true }` (MAP_SHARED with PROT_WRITE). macOS's code signing enforcement (AMFI) sends an uncatchable SIGKILL when a MAP_SHARED writable mapping targets a code-signed Mach-O binary — which every Bun-compiled binary is (ad-hoc signed). On Linux, ELF binaries have no such restriction. Because SIGKILL terminates the process inside the `mmap(2)` syscall, the `try/catch` fallback in `attemptDeltaUpgrade()` never executes — the process is dead before JavaScript can run any error handling. ## Fix Pass `{ shared: false }` to `Bun.mmap()` in `src/lib/bspatch.ts` to use MAP_PRIVATE (copy-on-write). macOS allows MAP_PRIVATE on signed binaries because writes go to private pages, not the file. Since we only read from the mapping, no COW pages are allocated — performance is identical.
1 parent c8aa617 commit b7ed2cc

File tree

2 files changed

+10
-2
lines changed

2 files changed

+10
-2
lines changed

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,9 @@ mock.module("./some-module", () => ({
665665
<!-- lore:019cb8c2-d7b5-780c-8a9f-d20001bc198f -->
666666
* **Install script: BSD sed and awk JSON parsing breaks OCI digest extraction**: The install script parses OCI manifests with awk (no jq dependency). Key trap: \`sed 's/},{/}\n{/g'\` doesn't insert newlines on macOS BSD sed (\`\n\` is literal). Also, the first layer shares a line with the config block after \`\[{\` split. Fix: use a single awk pass tracking last-seen \`"digest"\` value, printing it when \`"org.opencontainers.image.title"\` matches target. Works because \`digest\` always precedes \`annotations\` within each OCI layer object. This avoids sed entirely and handles both GNU/BSD awk. The config digest (\`sha256:44136fa...\`) is a 2-byte \`{}\` blob — downloading it instead of the real binary causes \`gunzip: unexpected end of file\`. The install script now has fire-and-forget Sentry telemetry via \`die()\` + ERR trap, which would catch such failures automatically.
667667
668+
<!-- lore:019cb963-cb63-722d-9365-b34336f4766d -->
669+
* **macOS SIGKILL on MAP\_SHARED mmap of signed Mach-O binaries**: macOS AMFI (code signing enforcement) sends SIGKILL when \`MAP\_SHARED\` with \`PROT\_WRITE\` is used on a code-signed Mach-O binary. \`Bun.mmap()\` defaults to \`{ shared: true }\` (MAP\_SHARED). In \`src/lib/bspatch.ts\`, \`Bun.mmap(process.execPath)\` kills the process on macOS during delta upgrades because the running CLI binary is ad-hoc signed (all Bun binaries are). Fix: pass \`{ shared: false }\` for MAP\_PRIVATE. Since the mapping is read-only in practice, no COW pages are allocated — identical performance. Linux ELF binaries have no such restriction.
670+
668671
<!-- lore:019c969a-1c90-7041-88a8-4e4d9a51ebed -->
669672
* **Multiple mockFetch calls replace each other — use unified mocks for multi-endpoint tests**: Bun test mocking gotchas: (1) \`mockFetch()\` replaces \`globalThis.fetch\` — calling it twice replaces the first mock. Use a single unified fetch mock dispatching by URL pattern. (2) \`mock.module()\` pollutes the module registry for ALL subsequent test files. Tests using it must live in \`test/isolated/\` and run via \`test:isolated\`. (3) For \`Bun.spawn\`, use direct property assignment in \`beforeEach\`/\`afterEach\`.
670673

src/lib/bspatch.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,8 +243,13 @@ export async function applyPatch(
243243
);
244244
const extraReader = createZstdStreamReader(patchData.subarray(extraStart));
245245

246-
// Memory-map old file: OS manages pages, 0 JS heap allocation
247-
const oldFile = Bun.mmap(oldPath);
246+
// Memory-map old file: OS manages pages, 0 JS heap allocation.
247+
// MAP_PRIVATE (shared: false) is required on macOS: the kernel's code
248+
// signing enforcement (AMFI) sends SIGKILL when a MAP_SHARED writable
249+
// mapping is created on a code-signed Mach-O binary (which the running
250+
// CLI is). Since we only read from the mapping, no COW pages are ever
251+
// allocated — identical performance to MAP_SHARED.
252+
const oldFile = Bun.mmap(oldPath, { shared: false });
248253

249254
// Streaming output: write directly to disk, no output buffer in memory
250255
const writer = Bun.file(destPath).writer();

0 commit comments

Comments
 (0)