Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 38 additions & 36 deletions src/jsc/bindings/ImportMetaObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,47 +226,49 @@ extern "C" JSC::EncodedJSValue functionImportMeta__resolveSyncPrivate(JSC::JSGlo
if (!isESM) {
if (globalObject) [[likely]] {
if (globalObject->hasOverriddenModuleResolveFilenameFunction) [[unlikely]] {
auto overrideHandler = uncheckedDowncast<JSObject>(globalObject->m_moduleResolveFilenameFunction.getInitializedOnMainThread(globalObject));
if (overrideHandler) [[likely]] {
ASSERT(overrideHandler->isCallable());
JSValue parentModuleObject = globalObject->requireMap()->get(globalObject, from);

JSValue parentID = jsUndefined();
if (auto* parent = dynamicDowncast<Bun::JSCommonJSModule>(parentModuleObject)) {
parentID = parent->filename();
} else {
parentID = from;
}
JSValue overrideHandler = globalObject->m_moduleResolveFilenameOverride.get();
JSC::CallData overrideCallData = JSC::getCallData(overrideHandler);
if (overrideCallData.type == JSC::CallData::Type::None) [[unlikely]] {
return JSC::throwVMTypeError(lexicalGlobalObject, scope, "Module._resolveFilename is not a function"_s);
}

MarkedArgumentBuffer args;
args.append(moduleName);
args.append(parentModuleObject);
auto parentIdStr = parentID.toWTFString(globalObject);
auto bunStr = Bun::toString(parentIdStr);
args.append(jsBoolean(Bun__isBunMain(lexicalGlobalObject, &bunStr)));

// Pass options object with paths if provided
if (!userPathList.isUndefinedOrNull()) {
JSObject* options = JSC::constructEmptyObject(globalObject);
options->putDirect(vm, JSC::Identifier::fromString(vm, "paths"_s), userPathList);
args.append(options);
}
JSValue parentModuleObject = globalObject->requireMap()->get(globalObject, from);

JSValue parentID = jsUndefined();
if (auto* parent = dynamicDowncast<Bun::JSCommonJSModule>(parentModuleObject)) {
parentID = parent->filename();
} else {
parentID = from;
}

JSValue result = JSC::profiledCall(lexicalGlobalObject, ProfilingReason::API, overrideHandler, JSC::getCallData(overrideHandler), parentModuleObject, args);
MarkedArgumentBuffer args;
args.append(moduleName);
args.append(parentModuleObject);
auto parentIdStr = parentID.toWTFString(globalObject);
auto bunStr = Bun::toString(parentIdStr);
args.append(jsBoolean(Bun__isBunMain(lexicalGlobalObject, &bunStr)));

// Pass options object with paths if provided
if (!userPathList.isUndefinedOrNull()) {
JSObject* options = JSC::constructEmptyObject(globalObject);
options->putDirect(vm, JSC::Identifier::fromString(vm, "paths"_s), userPathList);
args.append(options);
}

JSValue result = JSC::profiledCall(lexicalGlobalObject, ProfilingReason::API, overrideHandler, overrideCallData, parentModuleObject, args);
RETURN_IF_EXCEPTION(scope, {});
if (!isRequireDotResolve) {
JSString* string = result.toString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
if (!isRequireDotResolve) {
JSString* string = result.toString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto str = string->value(globalObject);
RETURN_IF_EXCEPTION(scope, {});
WTF::String prefixed = Bun::isUnprefixedNodeBuiltin(str);
if (!prefixed.isNull()) {
return JSValue::encode(jsString(vm, prefixed));
}
return JSC::JSValue::encode(string);
auto str = string->value(globalObject);
RETURN_IF_EXCEPTION(scope, {});
WTF::String prefixed = Bun::isUnprefixedNodeBuiltin(str);
if (!prefixed.isNull()) {
return JSValue::encode(jsString(vm, prefixed));
}
return JSC::JSValue::encode(result);
return JSC::JSValue::encode(string);
}
return JSC::JSValue::encode(result);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,11 @@ class GlobalObject : public Bun::GlobalScope {
\
/* TODO: these should use LazyProperty */ \
\
V(public, LazyPropertyOfGlobalObject<JSCell>, m_moduleResolveFilenameFunction) \
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_moduleResolveFilenameFunction) \
/* Whatever user code assigned to require("module")._resolveFilename. Like Node's plain data */ \
/* property it holds any value; require() throws if it is not callable. Only meaningful while */ \
/* hasOverriddenModuleResolveFilenameFunction is set. */ \
V(public, WriteBarrier<JSC::Unknown>, m_moduleResolveFilenameOverride) \
V(public, LazyPropertyOfGlobalObject<JSCell>, m_moduleRunMainFunction) \
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_modulePrototypeUnderscoreCompileFunction) \
V(public, LazyPropertyOfGlobalObject<JSFunction>, m_commonJSRequireESMFromHijackedExtensionFunction) \
Expand Down
33 changes: 21 additions & 12 deletions src/jsc/modules/NodeModuleModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,9 @@
PropertyName propertyName))
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
if (globalObject->hasOverriddenModuleResolveFilenameFunction) [[unlikely]] {
return JSValue::encode(globalObject->m_moduleResolveFilenameOverride.get());
}
return JSValue::encode(
globalObject->m_moduleResolveFilenameFunction.getInitializedOnMainThread(
globalObject));
Expand All @@ -440,26 +443,32 @@
(JSGlobalObject * lexicalGlobalObject,
EncodedJSValue thisValue, EncodedJSValue encodedValue,
PropertyName propertyName))
{
auto* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto value = JSValue::decode(encodedValue);
if (value.isCell()) {
bool isOriginal = false;
if (value.isCallable()) {
JSC::CallData callData = JSC::getCallData(value);
bool isOriginal = false;
if (value.isCallable()) {
JSC::CallData callData = JSC::getCallData(value);

if (callData.type == JSC::CallData::Type::Native) {
if (callData.native.function.untaggedPtr() == &jsFunctionResolveFileName) {
isOriginal = true;
}
if (callData.type == JSC::CallData::Type::Native) {
if (callData.native.function.untaggedPtr() == &jsFunctionResolveFileName) {
isOriginal = true;
}
}
globalObject->hasOverriddenModuleResolveFilenameFunction = !isOriginal;
globalObject->m_moduleResolveFilenameFunction.set(
lexicalGlobalObject->vm(), globalObject, value.asCell());
}

if (isOriginal) {
globalObject->hasOverriddenModuleResolveFilenameFunction = false;
globalObject->m_moduleResolveFilenameOverride.clear();
} else {
// Any value is accepted, as with Node's plain data property; whether it
// is callable is checked when require() goes to call it.
globalObject->m_moduleResolveFilenameOverride.set(
lexicalGlobalObject->vm(), globalObject, value);
globalObject->hasOverriddenModuleResolveFilenameFunction = true;
}

return true;

Check warning on line 471 in src/jsc/modules/NodeModuleModule.cpp

View check run for this annotation

Claude / Claude Code Review

Sibling Module.runMain override has the same crash class left unfixed

The adjacent `Module.runMain` accessor in this file has the byte-identical pre-fix pattern (`setModuleRunMain` gates on `value.isCell()` and stores any cell; `NodeModuleModule__callOverriddenRunMain` does `uncheckedDowncast<JSObject>` + `profiledCall(..., getCallData(overrideHandler), ...)` with no `CallData::Type::None` check), so `Module.runMain = {}` in a `--require` preload hits the same crash class. Per REVIEW.md ('fix the whole class in the same PR… If a site is intentionally excluded, say
Comment thread
robobun marked this conversation as resolved.
}

PathResolveModule getParent(VM& vm, JSGlobalObject* global, JSValue maybe_parent)
Expand Down Expand Up @@ -1152,7 +1161,7 @@
});

globalObject->m_moduleResolveFilenameFunction.initLater(
[](const Zig::GlobalObject::Initializer<JSCell>& init) {
[](const Zig::GlobalObject::Initializer<JSFunction>& init) {
JSFunction* resolveFilenameFunction = JSFunction::create(
init.vm, init.owner, 2, "_resolveFilename"_s,
jsFunctionResolveFileName, JSC::ImplementationVisibility::Public,
Expand Down
70 changes: 70 additions & 0 deletions test/js/node/module/node-module-module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,76 @@ console.log("survived", require("./late.js"));`,
expect(await proc.exited).toBe(0);
});

test("Overwriting _resolveFilename with a non-callable makes require() throw like Node", async () => {
// Node keeps _resolveFilename as a plain data property: any value can be
// assigned and reads back, and require() throws when it goes to call it.
using dir = tempDir("resolve-filename-non-callable", {
"dep.cjs": `module.exports = "dep";`,
"main.cjs": `
const Module = require("module");
const original = Module._resolveFilename;
const attempt = fn => {
try {
return "returned " + String(fn());
} catch (e) {
return e.constructor.name + ": " + e.message;
}
};
const results = {};
for (const [label, value] of [
["object", {}],
["string", "not a function"],
["undefined", undefined],
["null", null],
["number", 42],
["symbol", Symbol("s")],
]) {
Module._resolveFilename = value;
results[label] = {
readsBack: Object.is(Module._resolveFilename, value),
require: attempt(() => require("./dep.cjs")),
requireResolve: attempt(() => require.resolve("./dep.cjs")),
createRequire: attempt(() => Module.createRequire(__filename)("./dep.cjs")),
};
}
// Callable objects other than plain functions are still honored.
Module._resolveFilename = new Proxy(original, {});
results.callableProxy = attempt(() => require("./dep.cjs"));
Module._resolveFilename = original;
results.restored = {
readsBack: Module._resolveFilename === original,
require: attempt(() => require("./dep.cjs")),
};
console.log(JSON.stringify(results));
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "main.cjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
const notAFunction = {
readsBack: true,
require: "TypeError: Module._resolveFilename is not a function",
requireResolve: "TypeError: Module._resolveFilename is not a function",
createRequire: "TypeError: Module._resolveFilename is not a function",
};
expect(JSON.parse(stdout)).toEqual({
object: notAFunction,
string: notAFunction,
undefined: notAFunction,
null: notAFunction,
number: notAFunction,
symbol: notAFunction,
callableProxy: "returned dep",
restored: { readsBack: true, require: "returned dep" },
});
expect(exitCode).toBe(0);
});

test("Overwriting Module.prototype.require", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "run", path.join(import.meta.dir, "modulePrototypeOverwrite.cjs")],
Expand Down