Compare commits

..

1 Commits

Author SHA1 Message Date
Claude Bot
e7164128c6 fix(child_process): remove self-referencing cycle in execSync/execFileSync errors
In `checkExecSyncError`, `ObjectAssign(err, ret)` copies `ret.error`
onto `err`, but `err` IS `ret.error`, creating `err.error === err`.
This causes `JSON.stringify(err)` to throw with a cyclic structure error.

Delete the `.error` property after the assign to break the cycle.

Closes #26844

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-09 23:25:35 +00:00
4 changed files with 61 additions and 282 deletions

View File

@@ -69,7 +69,6 @@
#include "ZigSourceProvider.h"
#include <JavaScriptCore/FunctionPrototype.h>
#include "JSCommonJSModule.h"
#include <JavaScriptCore/JSModuleLoader.h>
#include <JavaScriptCore/JSModuleNamespaceObject.h>
#include <JavaScriptCore/JSSourceCode.h>
#include <JavaScriptCore/LazyPropertyInlines.h>
@@ -684,19 +683,6 @@ JSC_DEFINE_CUSTOM_SETTER(setterUnderscoreCompile,
return true;
}
static BunLoaderType loaderFromFilename(const String& filename)
{
if (filename.endsWith(".ts"_s))
return BunLoaderTypeTS;
if (filename.endsWith(".tsx"_s))
return BunLoaderTypeTSX;
if (filename.endsWith(".jsx"_s))
return BunLoaderTypeJSX;
// _compile always receives JavaScript/TypeScript source code.
// Default to JS for unknown extensions (e.g. .custom, .node, etc.)
return BunLoaderTypeJS;
}
JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * globalObject, CallFrame* callframe))
{
auto* moduleObject = jsDynamicCast<JSCommonJSModule*>(callframe->thisValue());
@@ -714,95 +700,46 @@ JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * glo
String filenameString = filenameValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
String wrappedString;
auto* zigGlobalObject = jsCast<Zig::GlobalObject*>(globalObject);
if (zigGlobalObject->hasOverriddenModuleWrapper) [[unlikely]] {
wrappedString = makeString(
zigGlobalObject->m_moduleWrapperStart,
sourceString,
zigGlobalObject->m_moduleWrapperEnd);
} else {
wrappedString = makeString(
"(function(exports,require,module,__filename,__dirname){"_s,
sourceString,
"\n})"_s);
}
// Determine loader from filename extension.
BunLoaderType loader = loaderFromFilename(filenameString);
moduleObject->sourceCode = makeSource(
WTF::move(wrappedString),
SourceOrigin(URL::fileURLWithFileSystemPath(filenameString)),
JSC::SourceTaintedOrigin::Untainted,
filenameString,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
// Transpile the source through Bun's transpiler. This handles:
// - TypeScript syntax (.ts, .tsx)
// - JSX syntax (.jsx, .tsx)
// - ESM import/export → CJS conversion (when code uses CJS features)
BunString specifier = Bun::toStringRef(filenameString);
BunString referrer = BunStringEmpty;
auto sourceUTF8 = sourceString.utf8();
ZigString sourceZig = { reinterpret_cast<const unsigned char*>(sourceUTF8.data()), sourceUTF8.length() };
ErrorableResolvedSource transpileResult = {};
Bun__transpileVirtualModule(globalObject, &specifier, &referrer, &sourceZig, loader, &transpileResult);
EncodedJSValue encodedFilename = JSValue::encode(filenameValue);
#if OS(WINDOWS)
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, true, &encodedFilename, 1));
#else
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, false, &encodedFilename, 1));
#endif
RETURN_IF_EXCEPTION(throwScope, {});
if (!transpileResult.success) {
if (!throwScope.exception()) {
throwException(globalObject, throwScope, createError(globalObject, "Failed to transpile source in Module._compile"_s));
}
return {};
}
String dirnameString = dirnameValue.toWTFString(globalObject);
ResolvedSource& source = transpileResult.result.value;
if (source.isCommonJSModule) {
// The transpiler detected CJS usage and already wrapped the code in
// (function(exports, require, module, __filename, __dirname) { ... }).
String wrappedString = source.source_code.toWTFString(BunString::ZeroCopy);
if (zigGlobalObject->hasOverriddenModuleWrapper) [[unlikely]] {
// Strip the default wrapper and re-wrap with the custom one.
auto trimStart = wrappedString.find('\n');
if (trimStart != WTF::notFound) {
wrappedString = makeString(
zigGlobalObject->m_moduleWrapperStart,
wrappedString.substring(trimStart, wrappedString.length() - trimStart - 4),
zigGlobalObject->m_moduleWrapperEnd);
}
}
moduleObject->sourceCode = makeSource(
WTF::move(wrappedString),
SourceOrigin(URL::fileURLWithFileSystemPath(filenameString)),
JSC::SourceTaintedOrigin::Untainted,
filenameString,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
EncodedJSValue encodedFilename = JSValue::encode(filenameValue);
#if OS(WINDOWS)
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, true, &encodedFilename, 1));
#else
JSValue dirnameValue = JSValue::decode(Bun__Path__dirname(globalObject, false, &encodedFilename, 1));
#endif
RETURN_IF_EXCEPTION(throwScope, {});
String dirnameString = dirnameValue.toWTFString(globalObject);
evaluateCommonJSModuleOnce(
vm,
zigGlobalObject,
moduleObject,
jsString(vm, dirnameString),
jsString(vm, filenameString));
RETURN_IF_EXCEPTION(throwScope, {});
} else {
// The source contains ESM syntax (import/export). The transpiler kept it as ESM
// (with TS/JSX stripped). Provide the transpiled source to JSC's module loader
// and evaluate it as ESM, then populate the CJS module's exports.
auto provider = Zig::SourceProvider::create(zigGlobalObject, source, JSC::SourceProviderSourceType::Module, false);
zigGlobalObject->moduleLoader()->provideFetch(globalObject, filenameValue, JSC::SourceCode(WTF::move(provider)));
RETURN_IF_EXCEPTION(throwScope, {});
// Use requireESMFromHijackedExtension to synchronously evaluate the ESM module
// and populate the CJS module's exports from the ESM namespace.
JSC::JSFunction* requireESM = zigGlobalObject->requireESMFromHijackedExtension();
JSC::MarkedArgumentBuffer args;
args.append(filenameValue);
JSC::CallData callData = JSC::getCallData(requireESM);
NakedPtr<JSC::Exception> returnedException = nullptr;
JSC::profiledCall(globalObject, JSC::ProfilingReason::API, requireESM, callData, moduleObject, args, returnedException);
if (returnedException) [[unlikely]] {
throwException(globalObject, throwScope, returnedException->value());
return {};
}
}
WTF::NakedPtr<JSC::Exception> exception;
evaluateCommonJSModuleOnce(
vm,
jsCast<Zig::GlobalObject*>(globalObject),
moduleObject,
jsString(vm, dirnameString),
jsString(vm, filenameString));
RETURN_IF_EXCEPTION(throwScope, {});
return JSValue::encode(jsUndefined());
}

View File

@@ -1016,6 +1016,9 @@ function checkExecSyncError(ret, args, cmd?) {
if (ret.error) {
err = ret.error;
ObjectAssign(err, ret);
// ObjectAssign copies ret.error onto err, but err IS ret.error,
// creating a self-referencing cycle (err.error === err). Remove it.
delete err.error;
} else if (ret.status !== 0) {
let msg = "Command failed: ";
msg += cmd || ArrayPrototypeJoin.$call(args, " ");

View File

@@ -0,0 +1,24 @@
import { expect, test } from "bun:test";
import { execFileSync, execSync } from "child_process";
test("execFileSync error should not have self-referencing cycle", () => {
try {
execFileSync("nonexistent_binary_xyz_123");
expect.unreachable();
} catch (err: any) {
// err.error should not be the same object as err (self-referencing cycle)
expect(err.error).not.toBe(err);
// JSON.stringify should not throw due to cyclic structure
expect(() => JSON.stringify(err)).not.toThrow();
}
});
test("execSync error should not have self-referencing cycle", () => {
try {
execSync("nonexistent_binary_xyz_123");
expect.unreachable();
} catch (err: any) {
expect(err.error).not.toBe(err);
expect(() => JSON.stringify(err)).not.toThrow();
}
});

View File

@@ -1,185 +0,0 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("Module._compile handles ESM import/export syntax", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
import path from "node:path";
import { fileURLToPath } from "node:url";
const config = {
value: 42,
};
export default config;
\`;
const m = new Module('/tmp/test-26874.ts');
m.filename = '/tmp/test-26874.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874.ts');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"value":42}');
expect(exitCode).toBe(0);
});
test("Module._compile handles TypeScript syntax", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
interface Config {
value: number;
name: string;
}
const config: Config = {
value: 123,
name: "test",
};
module.exports = config;
\`;
const m = new Module('/tmp/test-26874-ts.ts');
m.filename = '/tmp/test-26874-ts.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-ts.ts');
console.log(JSON.stringify(m.exports));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"value":123,"name":"test"}');
expect(exitCode).toBe(0);
});
test("Module._compile still works with plain CJS source", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
const x = 10;
const y = 20;
module.exports = { sum: x + y };
\`;
const m = new Module('/tmp/test-26874-cjs.js');
m.filename = '/tmp/test-26874-cjs.js';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-cjs.js');
console.log(JSON.stringify(m.exports));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"sum":30}');
expect(exitCode).toBe(0);
});
test("Module._compile handles ESM with .js filename", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
export const hello = "world";
export default { greeting: "hi" };
\`;
const m = new Module('/tmp/test-26874-esm.js');
m.filename = '/tmp/test-26874-esm.js';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-esm.js');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"greeting":"hi"}');
expect(exitCode).toBe(0);
});
test("Module._compile handles ESM TypeScript with imports and exports", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const Module = require('module');
const source = \`
import path from "node:path";
import { fileURLToPath } from "node:url";
interface Config {
turbopack: { root: string };
}
const config: Config = {
turbopack: {
root: "/test",
},
};
export default config;
\`;
const m = new Module('/tmp/test-26874-esm-ts.ts');
m.filename = '/tmp/test-26874-esm-ts.ts';
m.paths = Module._nodeModulePaths('/tmp/');
m._compile(source, '/tmp/test-26874-esm-ts.ts');
console.log(JSON.stringify(m.exports.default));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout.trim()).toBe('{"turbopack":{"root":"/test"}}');
expect(exitCode).toBe(0);
});