Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
b16592435a Fix LazyProperty assertion by preventing circular util.inspect access
Fixes circular dependency that caused LazyProperty assertion when console.log(buffer)
was called during util.inspect initialization.

The problem:
1. console.log(buffer) triggers util.inspect LazyProperty initialization
2. During module loading, Buffer's custom inspect function is called
3. Buffer's custom inspect tries to access util.inspect for property formatting
4. LazyProperty assertion fails: !(initializer.property.m_pointer & lazyTag)

The solution:
- ZigGlobalObject.h: Added utilInspectFunctionIfInitialized() method that checks
  LazyProperty::isInitialized() without triggering initialization
- UtilInspect.cpp: JSC__JSValue__callCustomInspectFunction checks if util.inspect
  is initialized before calling it; returns original value if not ready (triggers
  fallback to default formatting)
- ConsoleObject.zig: When processing custom inspect result, use getAdvanced() with
  disable_inspect_custom=true to prevent infinite recursion if custom inspect
  returns the same object

Test case: Buffer() as function in recursive context with console.log would crash,
now works correctly by gracefully falling back to default formatting during initialization.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-18 05:13:28 +00:00
4 changed files with 63 additions and 4 deletions

View File

@@ -2260,11 +2260,13 @@ pub const Formatter = struct {
this.max_depth,
enable_ansi_colors,
});
// Strings are printed directly, otherwise we recurse. It is possible to end up in an infinite loop.
// Strings are printed directly, otherwise we recurse
// Disable custom inspect when formatting the result to prevent infinite recursion
// if the custom inspect returns the same object or an object with its own custom inspect
if (result.isString()) {
writer.print("{}", .{result.fmtString(this.globalThis)});
} else {
try this.format(try ConsoleObject.Formatter.Tag.get(result, this.globalThis), Writer, writer_, result, this.globalThis, enable_ansi_colors);
try this.format(try ConsoleObject.Formatter.Tag.getAdvanced(result, this.globalThis, .{ .disable_inspect_custom = true }), Writer, writer_, result, this.globalThis, enable_ansi_colors);
}
},
.Symbol => {

View File

@@ -49,11 +49,16 @@ extern "C" JSC::EncodedJSValue JSC__JSValue__callCustomInspectFunction(
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
// Check if util.inspect is initialized to avoid circular dependency during LazyProperty initialization
// If it's not ready, return the original value so default formatting will be used
JSFunction* inspectFn = globalObject->utilInspectFunctionIfInitialized();
if (!inspectFn) {
return encodedThisValue;
}
JSObject* options = Bun::createInspectOptionsObject(vm, globalObject, max_depth, colors);
RETURN_IF_EXCEPTION(scope, {});
JSFunction* inspectFn = globalObject->utilInspectFunction();
RETURN_IF_EXCEPTION(scope, {});
auto callData = JSC::getCallData(functionToCall);
MarkedArgumentBuffer arguments;
arguments.append(jsNumber(depth));

View File

@@ -272,6 +272,10 @@ public:
JSC::Structure* utilInspectOptionsStructure() const { return m_utilInspectOptionsStructure.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectFunction() const { return m_utilInspectFunction.getInitializedOnMainThread(this); }
// Check if util.inspect is initialized without triggering initialization (for circular dependency prevention)
JSC::JSFunction* utilInspectFunctionIfInitialized() const {
return m_utilInspectFunction.isInitialized() ? m_utilInspectFunction.getInitializedOnMainThread(this) : nullptr;
}
JSC::JSFunction* utilInspectStylizeColorFunction() const { return m_utilInspectStylizeColorFunction.getInitializedOnMainThread(this); }
JSC::JSFunction* utilInspectStylizeNoColorFunction() const { return m_utilInspectStylizeNoColorFunction.getInitializedOnMainThread(this); }

View File

@@ -0,0 +1,48 @@
import { expect, test } from "bun:test";
// Regression test for LazyProperty assertion failure when Buffer's custom inspect
// is called during util.inspect initialization
//
// The issue occurred when:
// 1. console.log(buffer) triggers util.inspect LazyProperty initialization
// 2. During node:util module loading, Buffer's custom inspect is invoked
// 3. Buffer's custom inspect needs util.inspect to format properties
// 4. LazyProperty assertion: ASSERTION FAILED: !(initializer.property.m_pointer & lazyTag)
//
// Fix: JSC__JSValue__callCustomInspectFunction checks if util.inspect is initialized
// before calling it, returns the original value if not ready (fallback to default formatting)
test("Buffer as function in recursive context with console.log should not crash", () => {
const cent = Buffer.from([194]);
let callCount = 0;
const maxCalls = 5; // Limit recursion to avoid infinite loop
function f6() {
const v8 = Buffer("/posts/:slug([a-z]+)");
if (callCount < maxCalls) {
callCount++;
try {
cent.forEach(f6);
} catch (e) {}
}
// This console.log would trigger util.inspect lazy initialization
// The fix ensures custom inspect doesn't cause circular LazyProperty access
console.log(v8);
}
f6();
// If we got here without crashing, the test passes
expect(callCount).toBe(maxCalls);
});
test("Buffer as function should work like Buffer.from for strings", () => {
const b1 = Buffer("hello");
const b2 = Buffer.from("hello");
expect(b1.toString()).toBe("hello");
expect(b2.toString()).toBe("hello");
expect(b1.equals(b2)).toBe(true);
});