Fix assertion failure in JSTranspiler (#22409)

* Fix assertion failure when calling `bun.destroy` on a
partially-initialized `JSTranspiler`.
* Add a new method, `RefCount.clearWithoutDestructor`, to make this
pattern possible.
* Enable ref count assertion in `bun.destroy` for CI builds, not just
debug.

(For internal tracking: fixes STAB-1123, STAB-1124)
This commit is contained in:
taylor.fish
2025-09-04 19:45:05 -07:00
committed by GitHub
parent d5431fcfe6
commit e2161e7e13
2 changed files with 30 additions and 4 deletions

View File

@@ -649,8 +649,12 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
.transpiler = undefined, .transpiler = undefined,
.scan_pass_result = ScanPassResult.init(bun.default_allocator), .scan_pass_result = ScanPassResult.init(bun.default_allocator),
}); });
errdefer bun.destroy(this); errdefer {
errdefer this.arena.deinit(); this.config.log.deinit();
this.arena.deinit();
this.ref_count.clearWithoutDestructor();
bun.destroy(this);
}
const config_arg = if (arguments.len > 0) arguments.ptr[0] else .js_undefined; const config_arg = if (arguments.len > 0) arguments.ptr[0] else .js_undefined;
const allocator = this.arena.allocator(); const allocator = this.arena.allocator();

View File

@@ -175,11 +175,21 @@ pub fn RefCount(T: type, field_name: []const u8, destructor: anytype, options: O
/// The count is 0 after the destructor is called. /// The count is 0 after the destructor is called.
pub fn assertNoRefs(count: *const @This()) void { pub fn assertNoRefs(count: *const @This()) void {
if (enable_debug) { if (comptime bun.Environment.ci_assert) {
bun.assert(count.raw_count == 0); bun.assert(count.raw_count == 0);
} }
} }
/// Sets the ref count to 0 without running the destructor.
///
/// Only use this if you're about to free the object (e.g., with `bun.destroy`).
///
/// Don't modify the ref count or create any `RefPtr`s after calling this method.
pub fn clearWithoutDestructor(count: *@This()) void {
count.assertSingleThreaded();
count.raw_count = 0;
}
fn assertSingleThreaded(count: *@This()) void { fn assertSingleThreaded(count: *@This()) void {
count.thread.lockOrAssert(); count.thread.lockOrAssert();
} }
@@ -282,11 +292,23 @@ pub fn ThreadSafeRefCount(T: type, field_name: []const u8, destructor: fn (*T) v
/// The count is 0 after the destructor is called. /// The count is 0 after the destructor is called.
pub fn assertNoRefs(count: *const @This()) void { pub fn assertNoRefs(count: *const @This()) void {
if (enable_debug) { if (comptime bun.Environment.ci_assert) {
bun.assert(count.raw_count.load(.seq_cst) == 0); bun.assert(count.raw_count.load(.seq_cst) == 0);
} }
} }
/// Sets the ref count to 0 without running the destructor.
///
/// Only use this if you're about to free the object (e.g., with `bun.destroy`).
///
/// Don't modify the ref count or create any `RefPtr`s after calling this method.
pub fn clearWithoutDestructor(count: *@This()) void {
// This method should only be used if you're about the free the object. You shouldn't
// be freeing the object if other threads might be using it, and no memory order can
// help with that, so .monotonic is sufficient.
count.raw_count.store(0, .monotonic);
}
fn getRefCount(self: *T) *@This() { fn getRefCount(self: *T) *@This() {
return &@field(self, field_name); return &@field(self, field_name);
} }