Files
bun.sh/src/shell/EnvStr.zig
Jarred Sumner 767c61d355 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>
2025-10-19 22:17:19 -07:00

120 lines
3.3 KiB
Zig

/// Environment strings need to be copied a lot
/// So we make them reference counted
///
/// But sometimes we use strings that are statically allocated, or are allocated
/// with a predetermined lifetime (e.g. strings in the AST). In that case we
/// don't want to incur the cost of heap allocating them and refcounting them
///
/// So environment strings can be ref counted or borrowed slices
pub const EnvStr = packed struct(u128) {
ptr: u48,
tag: Tag = .empty,
len: usize = 0,
const debug = bun.Output.scoped(.EnvStr, .hidden);
const Tag = enum(u16) {
/// no value
empty,
/// Dealloced by reference counting
refcounted,
/// Memory is managed elsewhere so don't dealloc it
slice,
};
pub inline fn initSlice(str: []const u8) EnvStr {
if (str.len == 0)
// Zero length strings may have invalid pointers, leading to a bad integer cast.
return .{ .tag = .empty, .ptr = 0, .len = 0 };
return .{
.ptr = toPtr(str.ptr),
.tag = .slice,
.len = str.len,
};
}
fn toPtr(ptr_val: *const anyopaque) u48 {
const num: [8]u8 = @bitCast(@intFromPtr(ptr_val));
return @bitCast(num[0..6].*);
}
/// Same thing as `initRefCounted` except it duplicates thepassed string
pub fn dupeRefCounted(old_str: []const u8) EnvStr {
if (old_str.len == 0)
return .{ .tag = .empty, .ptr = 0, .len = 0 };
const str = bun.handleOom(bun.default_allocator.dupe(u8, old_str));
return .{
.ptr = toPtr(RefCountedStr.init(str)),
.len = str.len,
.tag = .refcounted,
};
}
pub fn initRefCounted(str: []const u8) EnvStr {
if (str.len == 0)
return .{ .tag = .empty, .ptr = 0, .len = 0 };
return .{
.ptr = toPtr(RefCountedStr.init(str)),
.tag = .refcounted,
};
}
pub fn slice(this: EnvStr) []const u8 {
return switch (this.tag) {
.empty => "",
.slice => this.castSlice(),
.refcounted => this.castRefCounted().byteSlice(),
};
}
pub fn memoryCost(this: EnvStr) usize {
const divisor: usize = brk: {
if (this.asRefCounted()) |refc| {
break :brk refc.refcount;
}
break :brk 1;
};
if (divisor == 0) {
@branchHint(.unlikely);
return 0;
}
return this.len / divisor;
}
pub fn ref(this: EnvStr) void {
if (this.asRefCounted()) |refc| {
refc.ref();
}
}
pub fn deref(this: EnvStr) void {
if (this.asRefCounted()) |refc| {
refc.deref();
}
}
inline fn asRefCounted(this: EnvStr) ?*RefCountedStr {
if (this.tag == .refcounted) return this.castRefCounted();
return null;
}
inline fn castSlice(this: EnvStr) []const u8 {
return @as([*]u8, @ptrFromInt(@as(usize, @intCast(this.ptr))))[0..this.len];
}
inline fn castRefCounted(this: EnvStr) *RefCountedStr {
return @ptrFromInt(@as(usize, @intCast(this.ptr)));
}
};
const bun = @import("bun");
const interpreter = @import("./interpreter.zig");
const RefCountedStr = interpreter.RefCountedStr;