Fix memory leaks & blocking syscall in Bun Shell (#23636)

## Summary

Fixes two critical bugs in Bun Shell:

1. **Memory leaks & incorrect GC reporting**: Shell objects weren't
reporting their memory usage to JavaScriptCore's garbage collector,
causing memory to accumulate unchecked. Also fixes a leak where
`ShellArgs` wasn't being freed in `Interpreter.finalize()`.

2. **Blocking I/O on macOS**: Fixes a bug where writing large amounts of
data (>1MB) to pipes would block the main thread on macOS. The issue:
`sendto()` with `MSG_NOWAIT` flag blocks on macOS despite the flag, so
we now avoid the socket fast path unless the socket is already
non-blocking.

## Changes

- Adds `memoryCost()` and `estimatedSize()` implementations across shell
AST nodes, interpreter, and I/O structures
- Reports estimated memory size to JavaScriptCore GC via
`vm.heap.reportExtraMemoryAllocated()`
- Fixes missing `this.args.deinit()` call in interpreter finalization
- Fixes `BabyList.memoryCost()` to return bytes, not element count
- Conditionally uses socket fast path in IOWriter based on platform and
socket state

## Test plan

- [x] New test: `shell-leak-args.test.ts` - validates memory doesn't
leak during parsing/execution
- [x] New test: `shell-blocking-pipe.test.ts` - validates large pipe
writes don't block the main thread
- [x] Existing shell tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
This commit is contained in:
Jarred Sumner
2025-10-19 22:17:19 -07:00
committed by GitHub
parent e63a897c66
commit 767c61d355
17 changed files with 471 additions and 16 deletions

View File

@@ -12,6 +12,30 @@ export_env: ?EnvMap = null,
quiet: bool = false,
cwd: ?bun.String = null,
this_jsvalue: JSValue = .zero,
estimated_size_for_gc: usize = 0,
fn #computeEstimatedSizeForGC(this: *const ParsedShellScript) usize {
var size: usize = @sizeOf(ParsedShellScript);
if (this.args) |args| {
size += args.memoryCost();
}
if (this.export_env) |*env| {
size += env.memoryCost();
}
if (this.cwd) |*cwd| {
size += cwd.estimatedSize();
}
size += std.mem.sliceAsBytes(this.jsobjs.allocatedSlice()).len;
return size;
}
pub fn memoryCost(this: *const ParsedShellScript) usize {
return this.#computeEstimatedSizeForGC();
}
pub fn estimatedSize(this: *const ParsedShellScript) usize {
return this.estimated_size_for_gc;
}
pub fn take(
this: *ParsedShellScript,
@@ -161,6 +185,7 @@ fn createParsedShellScriptImpl(globalThis: *jsc.JSGlobalObject, callframe: *jsc.
.args = shargs,
.jsobjs = jsobjs,
});
parsed_shell_script.estimated_size_for_gc = parsed_shell_script.#computeEstimatedSizeForGC();
const this_jsvalue = jsc.Codegen.JSParsedShellScript.toJSWithValues(parsed_shell_script, globalThis, marked_argument_buffer);
parsed_shell_script.this_jsvalue = this_jsvalue;