|
6 | 6 | * minimal memory usage during CLI self-upgrades: |
7 | 7 | * |
8 | 8 | * - Old binary: copy-then-mmap for 0 JS heap (CoW on btrfs/xfs/APFS), |
9 | | - * with child-process probe for mmap safety (macOS AMFI sends uncatchable |
10 | | - * SIGKILL on signed Mach-O), falling back to `arrayBuffer()` if unsafe |
| 9 | + * falling back to `arrayBuffer()` if copy/mmap fails |
11 | 10 | * - Diff/extra blocks: streamed via `DecompressionStream('zstd')` |
12 | 11 | * - Output: written incrementally to disk via `Bun.file().writer()` |
13 | 12 | * - Integrity: SHA-256 computed inline via `Bun.CryptoHasher` |
@@ -228,88 +227,38 @@ type OldFileHandle = { |
228 | 227 | cleanup: () => void; |
229 | 228 | }; |
230 | 229 |
|
231 | | -/** |
232 | | - * Probe whether `Bun.mmap()` works on a file by spawning a child process. |
233 | | - * |
234 | | - * macOS AMFI sends uncatchable SIGKILL when a process creates a writable |
235 | | - * memory mapping of a code-signed Mach-O binary — even a copy. Since |
236 | | - * SIGKILL terminates the process inside the syscall (before any catch |
237 | | - * block runs), we cannot detect the failure in-process. |
238 | | - * |
239 | | - * This probe spawns a minimal child that attempts `Bun.mmap()` on the |
240 | | - * target file. If the child exits 0, mmap is safe. If it's killed |
241 | | - * (SIGKILL) or exits non-zero, we fall back to `arrayBuffer()`. |
242 | | - * |
243 | | - * @param filePath - Path to the file to probe (should be the temp copy) |
244 | | - * @returns true if mmap is safe on this file, false otherwise |
245 | | - */ |
246 | | -async function probeMmapSafe(filePath: string): Promise<boolean> { |
247 | | - try { |
248 | | - const child = Bun.spawn( |
249 | | - [ |
250 | | - process.execPath, |
251 | | - "-e", |
252 | | - `Bun.mmap(${JSON.stringify(filePath)}, { shared: false })`, |
253 | | - ], |
254 | | - { stdout: "ignore", stderr: "ignore" } |
255 | | - ); |
256 | | - const exitCode = await child.exited; |
257 | | - return exitCode === 0; |
258 | | - } catch { |
259 | | - return false; |
260 | | - } |
261 | | -} |
262 | | - |
263 | 230 | /** |
264 | 231 | * Load the old binary for read access during patching. |
265 | 232 | * |
266 | | - * Strategy: copy to temp file, probe mmap safety via child process, |
267 | | - * then mmap the copy if safe. This avoids `Bun.mmap()` on files that |
268 | | - * would trigger uncatchable SIGKILL (macOS AMFI on signed Mach-O) while |
269 | | - * keeping zero JS heap on platforms where mmap works. On CoW filesystems |
270 | | - * (btrfs, xfs, APFS) the copy is a metadata-only reflink (near-instant). |
| 233 | + * Strategy: copy to temp file, then try mmap on the copy. The copy is a |
| 234 | + * regular file (no running process), so `Bun.mmap()` succeeds on both |
| 235 | + * Linux and macOS — ETXTBSY (Linux) and AMFI SIGKILL (macOS) only affect |
| 236 | + * the running binary's inode, not a copy. On CoW filesystems (btrfs, xfs, |
| 237 | + * APFS) the copy is a metadata-only reflink (near-instant). |
271 | 238 | * |
272 | | - * Falls back to `Bun.file().arrayBuffer()` (~100 MB heap) if the probe |
273 | | - * fails or if copy/mmap errors for any reason. |
| 239 | + * Falls back to `Bun.file().arrayBuffer()` (~100 MB heap) if copy or |
| 240 | + * mmap fails for any reason. |
274 | 241 | */ |
275 | 242 | async function loadOldBinary(oldPath: string): Promise<OldFileHandle> { |
276 | 243 | const tempCopy = join(tmpdir(), `sentry-patch-old-${process.pid}`); |
277 | 244 | try { |
278 | 245 | copyFileSync(oldPath, tempCopy); |
279 | 246 |
|
280 | | - // Probe mmap safety in a child process — macOS AMFI sends uncatchable |
281 | | - // SIGKILL on writable mappings of signed Mach-O, even for copies. |
282 | | - const mmapSafe = await probeMmapSafe(tempCopy); |
283 | | - |
284 | | - if (mmapSafe) { |
285 | | - const data = Bun.mmap(tempCopy, { shared: false }); |
286 | | - return { |
287 | | - data, |
288 | | - cleanup: () => { |
289 | | - try { |
290 | | - unlinkSync(tempCopy); |
291 | | - } catch { |
292 | | - // Best-effort cleanup — OS will reclaim on reboot |
293 | | - } |
294 | | - }, |
295 | | - }; |
296 | | - } |
297 | | - |
298 | | - // mmap probe failed — read copy into JS heap, then clean up temp file |
299 | | - const data = new Uint8Array(await Bun.file(tempCopy).arrayBuffer()); |
300 | | - try { |
301 | | - unlinkSync(tempCopy); |
302 | | - } catch { |
303 | | - // Best-effort cleanup |
304 | | - } |
| 247 | + // mmap the copy — safe because it's a separate inode, not the running |
| 248 | + // binary. MAP_PRIVATE avoids write-back to disk. |
| 249 | + const data = Bun.mmap(tempCopy, { shared: false }); |
305 | 250 | return { |
306 | 251 | data, |
307 | 252 | cleanup: () => { |
308 | | - // Data is in JS heap — no temp file to clean up |
| 253 | + try { |
| 254 | + unlinkSync(tempCopy); |
| 255 | + } catch { |
| 256 | + // Best-effort cleanup — OS will reclaim on reboot |
| 257 | + } |
309 | 258 | }, |
310 | 259 | }; |
311 | 260 | } catch { |
312 | | - // Copy failed — fall back to reading original directly into JS heap |
| 261 | + // Copy or mmap failed — fall back to reading into JS heap |
313 | 262 | try { |
314 | 263 | unlinkSync(tempCopy); |
315 | 264 | } catch { |
|
0 commit comments