Skip to content

fix(upgrade): replace Bun.mmap with arrayBuffer on all platforms#343

Merged
BYK merged 1 commit intomainfrom
fix/delta-upgrade-etxtbsy
Mar 5, 2026
Merged

fix(upgrade): replace Bun.mmap with arrayBuffer on all platforms#343
BYK merged 1 commit intomainfrom
fix/delta-upgrade-etxtbsy

Conversation

@BYK
Copy link
Copy Markdown
Member

@BYK BYK commented Mar 5, 2026

Problem

Delta upgrades silently fall back to full binary download on Linux because Bun.mmap() throws ETXTBSY when targeting the running executable.

Bun.mmap() always opens files with O_RDWR internally, regardless of the shared flag. When the target file is the currently-running binary:

PR #340 added a platform-conditional (darwinarrayBuffer, else → mmap) but this was insufficient since mmap fails on both platforms.

Evidence

# mmap always fails on the running binary — even with MAP_PRIVATE
$ bun -e 'Bun.mmap(process.execPath, { shared: false })'
ETXTBSY: text file is busy, open

# arrayBuffer works fine (opens with O_RDONLY)
$ bun -e 'await Bun.file(process.execPath).arrayBuffer()'
# ✓ reads ~100MB successfully

Confirmed via end-to-end test: chain resolution works perfectly (8 patch tags, 1-step chain, 19KB patch, SHA-256 matches), but applyPatch() fails on the Bun.mmap() call, error is caught by attemptDeltaUpgrade()'s catch-all, and logged at debug level (invisible without --verbose).

Fix

  • Remove platform-conditional in bspatch.ts, use new Uint8Array(await Bun.file(oldPath).arrayBuffer()) unconditionally on all platforms
  • Include error message in delta upgrade debug log so --verbose reveals root cause
  • Update JSDoc to reflect the arrayBuffer-only approach

The ~100 MB heap cost is acceptable — it's freed immediately after patching completes, and the alternative (full ~32 MB gzipped download) is much slower.

Performance comparison

Method Download size Total time
Full download ~32 MB .gz ~7.2s
Delta patch ~19 KB ~0.85s

Bun.mmap() always opens files with O_RDWR internally, which fails when
the target is the currently-running executable:
- macOS: AMFI sends uncatchable SIGKILL (writable mapping on signed Mach-O)
- Linux: open() returns ETXTBSY (kernel blocks opening running executables for write)

PR #340 added a platform-conditional (darwin→arrayBuffer, else→mmap) but
this was insufficient — mmap fails on BOTH platforms. The { shared: false }
flag only affects MAP_PRIVATE vs MAP_SHARED, not the O_RDWR open() call.

Fix: use Bun.file().arrayBuffer() unconditionally on all platforms. Costs
~100 MB heap for the old binary (freed immediately after patching) but is
the only approach that works cross-platform.

Also improve delta upgrade error visibility: include the error message in
the debug log so --verbose reveals the root cause.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 5, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Trace

Other

  • (api) Add --data/-d flag and auto-detect JSON body in fields by BYK in #320
  • (formatters) Render all terminal output as markdown by BYK in #297
  • (install) Add Sentry error telemetry to install script by BYK in #334
  • (issue-list) Global limit with fair distribution, compound cursor, and richer progress by BYK in #306
  • (log-list) Add --trace flag to filter logs by trace ID by BYK in #329
  • (logger) Add consola-based structured logging with Sentry integration by BYK in #338
  • (project) Add project create command by betegon in #237
  • (upgrade) Add binary delta patching via TRDIFF10/bsdiff by BYK in #327
  • Improve markdown rendering styles by BYK in #342

Bug Fixes 🐛

Api

  • Use numeric project ID to avoid "not actively selected" error by betegon in #312
  • Use limit param for issues endpoint page size by BYK in #309
  • Auto-correct ':' to '=' in --field values with a warning by BYK in #302

Formatters

  • Expand streaming table to fill terminal width by betegon in #314
  • Fix HTML entities and escaped underscores in table output by betegon in #313

Setup

  • Suppress agent skills and welcome messages on upgrade by BYK in #328
  • Suppress shell completion messages on upgrade by BYK in #326

Upgrade

  • Replace Bun.mmap with arrayBuffer on all platforms by BYK in #343
  • Replace Bun.mmap with arrayBuffer on macOS to prevent SIGKILL by BYK in #340
  • Use MAP_PRIVATE mmap to prevent macOS SIGKILL during delta upgrade by BYK in #339

Other

  • (ci) Generate JUnit XML to silence codecov-action warnings by BYK in #300
  • (install) Fix nightly digest extraction on macOS by BYK in #331
  • (nightly) Push to GHCR from artifacts dir so layer titles are bare filenames by BYK in #301
  • (project create) Auto-correct dot-separated platform to hyphens by BYK in #336
  • (region) Resolve DSN org prefix at resolution layer by BYK in #316
  • (test) Handle 0/-0 in getComparator anti-symmetry property test by BYK in #308
  • (trace-logs) Timestamp_precise is a number, not a string by BYK in #323

Documentation 📚

  • Document SENTRY_URL and self-hosted setup by BYK in #337

Internal Changes 🔧

Api

  • Upgrade @sentry/api to 0.21.0, remove raw HTTP pagination workarounds by BYK in #321
  • Wire listIssuesPaginated through @sentry/api SDK for type safety by BYK in #310

Other

  • (craft) Add sentry-release-registry target by BYK in #325
  • (project create) Migrate human output to markdown rendering system by BYK in #341

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 5, 2026

Codecov Results 📊

2683 passed | Total: 2683 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

❌ Patch coverage is 25.00%. Project has 3108 uncovered lines.
❌ Project coverage is 82.64%. Comparing base (base) to head (head).

Files with missing lines (1)
File Patch % Lines
delta-upgrade.ts 88.03% ⚠️ 42 Missing
Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
- Coverage    82.65%    82.64%    -0.01%
==========================================
  Files          127       127         —
  Lines        17904     17902        -2
  Branches         0         0         —
==========================================
+ Hits         14797     14794        -3
- Misses        3107      3108        +1
- Partials         0         0         —

Generated by Codecov Action

@BYK BYK merged commit 0da9cc9 into main Mar 5, 2026
20 checks passed
@BYK BYK deleted the fix/delta-upgrade-etxtbsy branch March 5, 2026 08:55
BYK added a commit that referenced this pull request Mar 5, 2026
The old file (running binary) cannot be mmap'd directly — Bun always opens
with O_RDWR, which fails on running executables (macOS: SIGKILL, Linux:
ETXTBSY). PR #343 fixed this with arrayBuffer() (~100 MB JS heap).

Improve: copy the old binary to a temp file first, then mmap the copy.
The copy is a regular file with no running process, so mmap succeeds and
the ~100 MB is kernel-managed (no GC pressure). On CoW-capable filesystems
(btrfs, xfs, APFS) the copy is a metadata-only reflink — near-instant
with zero extra disk I/O.

Falls back to arrayBuffer() if copy or mmap fails for any reason.
BYK added a commit that referenced this pull request Mar 5, 2026
The old file (running binary) cannot be mmap'd directly — Bun always opens
with O_RDWR, which fails on running executables (macOS: SIGKILL, Linux:
ETXTBSY). PR #343 fixed this with arrayBuffer() (~100 MB JS heap).

Improve: copy the old binary to a temp file first, then mmap the copy.
The copy is a regular file with no running process, so mmap succeeds and
the ~100 MB is kernel-managed (no GC pressure). On CoW-capable filesystems
(btrfs, xfs, APFS) the copy is a metadata-only reflink — near-instant
with zero extra disk I/O.

Falls back to arrayBuffer() if copy or mmap fails for any reason.
BYK added a commit that referenced this pull request Mar 5, 2026
Delta upgrade failures were completely invisible in Sentry:
- No spans for the delta attempt (only DB queries visible in traces)
- Errors caught and logged at debug level (invisible without --verbose)
- No captureException — errors never reported to Sentry

This made it impossible to diagnose the ETXTBSY/SIGKILL issues
(PRs #339, #340, #343) from telemetry alone — they were found
through code analysis and local reproduction.

Changes:
- Wrap attemptDeltaUpgrade in withTracingSpan for a 'upgrade.delta' span
- Record delta.from_version, delta.to_version, delta.channel as attributes
- On success: record patch_bytes and sha256 prefix
- On unavailable (no patch): record delta.result='unavailable'
- On error: captureException with warning level + delta context tags,
  record delta.result='error' and delta.error message on span
- Upgrade log.debug to log.warn for failure messages so users see them

Now delta failures will appear as:
1. A span in the upgrade trace (with error status + attributes)
2. A warning-level exception in Sentry Issues (with delta context)
3. A visible stderr message to the user
BYK added a commit that referenced this pull request Mar 5, 2026
Delta upgrade failures were completely invisible in Sentry:
- No spans for the delta attempt (only DB queries visible in traces)
- Errors caught and logged at debug level (invisible without --verbose)
- No captureException — errors never reported to Sentry

This made it impossible to diagnose the ETXTBSY/SIGKILL issues
(PRs #339, #340, #343) from telemetry alone — they were found
through code analysis and local reproduction.

Changes:
- Wrap attemptDeltaUpgrade in withTracingSpan for a 'upgrade.delta' span
- Record delta.from_version, delta.to_version, delta.channel as attributes
- On success: record patch_bytes and sha256 prefix
- On unavailable (no patch): record delta.result='unavailable'
- On error: captureException with warning level + delta context tags,
  record delta.result='error' and delta.error message on span
- Upgrade log.debug to log.warn for failure messages so users see them

Now delta failures will appear as:
1. A span in the upgrade trace (with error status + attributes)
2. A warning-level exception in Sentry Issues (with delta context)
3. A visible stderr message to the user
BYK added a commit that referenced this pull request Mar 5, 2026
Delta upgrade failures were completely invisible in Sentry:
- No spans for the delta attempt (only DB queries visible in traces)
- Errors caught and logged at debug level (invisible without --verbose)
- No captureException — errors never reported to Sentry

This made it impossible to diagnose the ETXTBSY/SIGKILL issues
(PRs #339, #340, #343) from telemetry alone — they were found
through code analysis and local reproduction.

Changes:
- Wrap attemptDeltaUpgrade in withTracingSpan for a 'upgrade.delta' span
- Record delta.from_version, delta.to_version, delta.channel as attributes
- On success: record patch_bytes and sha256 prefix
- On unavailable (no patch): record delta.result='unavailable'
- On error: captureException with warning level + delta context tags,
  record delta.result='error' and delta.error message on span
- Upgrade log.debug to log.warn for failure messages so users see them

Now delta failures will appear as:
1. A span in the upgrade trace (with error status + attributes)
2. A warning-level exception in Sentry Issues (with delta context)
3. A visible stderr message to the user
BYK added a commit that referenced this pull request Mar 5, 2026
…ing (#344)

## Changes

### 1. Copy-then-mmap with child process probe for mmap safety

`Bun.mmap()` on the running binary fails fatally on macOS (SIGKILL from
AMFI) and
Linux (ETXTBSY). PR #343 fixed this by using `arrayBuffer()`
unconditionally, but that
spikes ~100 MB onto the JS heap.

This PR restores zero-heap mmap while handling the macOS SIGKILL:

1. **Copy**: `copyFileSync(process.execPath,
tmpdir/sentry-patch-old-{pid})`
   CoW reflinks on btrfs/xfs/APFS for near-instant zero-I/O copy
2. **Probe**: Spawn a child process that tries `Bun.mmap(copy)`  
If macOS AMFI sends SIGKILL, only the child dies — parent survives and
knows mmap is unsafe
3. **Mmap**: If probe exits 0, mmap the copy (zero JS heap,
kernel-managed pages)
4. **Fallback**: If probe fails, read copy via `arrayBuffer()` (~100 MB
heap)

### 2. Instrument delta upgrade with Sentry spans and error capture

Delta failures were completely invisible in Sentry — no spans, no
`captureException`,
errors logged at debug level. The ETXTBSY/SIGKILL bugs (PRs #339#343)
were only
discoverable through code analysis and local reproduction.

- Wraps `attemptDeltaUpgrade` in `withTracingSpan('upgrade.delta')`
- Records `delta.from_version`, `delta.to_version`, `delta.channel`,
`delta.patch_bytes`
- On error: `captureException` with warning level + delta context tags
- Upgrades error log from `log.debug()` to `log.warn()` so users see
failures
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.

1 participant