mirror of
https://github.com/oven-sh/bun
synced 2026-02-19 23:31:45 +00:00
fix(require): prevent Object.prototype setters from triggering during module loading (#24336)
When `Object.prototype[0]` has a setter defined, `require('http')` and
`require('url')` would trigger it multiple times during module
initialization. Node.js does not exhibit this behavior.
Two root causes:
1. `createNodeURLBinding` in NodeURL.cpp used `putByIndexInline` to
populate an array, which walks the prototype chain and triggers
setters. Changed to `putDirectIndex` which sets properties directly.
2. TypeScript `const enum` declarations in `internal/http.ts` were being
compiled to the standard enum IIFE pattern with reverse mappings
(e.g. `Enum[Enum["x"] = 0] = "x"`), which writes to numeric indices
on plain objects inheriting from `Object.prototype`. Replaced with
plain `as const` object literals that don't generate reverse mappings.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -152,16 +152,18 @@ JSC::JSValue createNodeURLBinding(Zig::GlobalObject* globalObject)
|
||||
ASSERT(domainToAsciiFunction);
|
||||
auto domainToUnicodeFunction = JSC::JSFunction::create(vm, globalObject, 1, "domainToUnicode"_s, jsDomainToUnicode, ImplementationVisibility::Public);
|
||||
ASSERT(domainToUnicodeFunction);
|
||||
binding->putByIndexInline(
|
||||
binding->putDirectIndex(
|
||||
globalObject,
|
||||
(unsigned)0,
|
||||
domainToAsciiFunction,
|
||||
false);
|
||||
binding->putByIndexInline(
|
||||
0,
|
||||
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
|
||||
binding->putDirectIndex(
|
||||
globalObject,
|
||||
(unsigned)1,
|
||||
domainToUnicodeFunction,
|
||||
false);
|
||||
0,
|
||||
JSC::PutDirectIndexMode::PutDirectIndexLikePutDirect);
|
||||
return binding;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,43 +92,55 @@ const kDeferredTimeouts = Symbol("deferredTimeouts");
|
||||
|
||||
const kEmptyObject = Object.freeze(Object.create(null));
|
||||
|
||||
export const enum ClientRequestEmitState {
|
||||
socket = 1,
|
||||
prefinish = 2,
|
||||
finish = 3,
|
||||
response = 4,
|
||||
}
|
||||
// These are declared as plain objects instead of `const enum` to prevent the
|
||||
// TypeScript enum reverse-mapping pattern (e.g. `Enum[Enum["x"] = 0] = "x"`)
|
||||
// from triggering setters on `Object.prototype` during module initialization.
|
||||
// See: https://github.com/oven-sh/bun/issues/24336
|
||||
export const ClientRequestEmitState = {
|
||||
socket: 1,
|
||||
prefinish: 2,
|
||||
finish: 3,
|
||||
response: 4,
|
||||
} as const;
|
||||
export type ClientRequestEmitState = (typeof ClientRequestEmitState)[keyof typeof ClientRequestEmitState];
|
||||
|
||||
export const enum NodeHTTPResponseAbortEvent {
|
||||
none = 0,
|
||||
abort = 1,
|
||||
timeout = 2,
|
||||
}
|
||||
export const enum NodeHTTPIncomingRequestType {
|
||||
FetchRequest,
|
||||
FetchResponse,
|
||||
NodeHTTPResponse,
|
||||
}
|
||||
export const enum NodeHTTPBodyReadState {
|
||||
none,
|
||||
pending = 1 << 1,
|
||||
done = 1 << 2,
|
||||
hasBufferedDataDuringPause = 1 << 3,
|
||||
}
|
||||
export const NodeHTTPResponseAbortEvent = {
|
||||
none: 0,
|
||||
abort: 1,
|
||||
timeout: 2,
|
||||
} as const;
|
||||
export type NodeHTTPResponseAbortEvent = (typeof NodeHTTPResponseAbortEvent)[keyof typeof NodeHTTPResponseAbortEvent];
|
||||
|
||||
export const NodeHTTPIncomingRequestType = {
|
||||
FetchRequest: 0,
|
||||
FetchResponse: 1,
|
||||
NodeHTTPResponse: 2,
|
||||
} as const;
|
||||
export type NodeHTTPIncomingRequestType =
|
||||
(typeof NodeHTTPIncomingRequestType)[keyof typeof NodeHTTPIncomingRequestType];
|
||||
|
||||
export const NodeHTTPBodyReadState = {
|
||||
none: 0,
|
||||
pending: 1 << 1,
|
||||
done: 1 << 2,
|
||||
hasBufferedDataDuringPause: 1 << 3,
|
||||
} as const;
|
||||
export type NodeHTTPBodyReadState = (typeof NodeHTTPBodyReadState)[keyof typeof NodeHTTPBodyReadState];
|
||||
|
||||
// Must be kept in sync with NodeHTTPResponse.Flags
|
||||
export const enum NodeHTTPResponseFlags {
|
||||
socket_closed = 1 << 0,
|
||||
request_has_completed = 1 << 1,
|
||||
export const NodeHTTPResponseFlags = {
|
||||
socket_closed: 1 << 0,
|
||||
request_has_completed: 1 << 1,
|
||||
closed_or_completed: (1 << 0) | (1 << 1),
|
||||
} as const;
|
||||
export type NodeHTTPResponseFlags = (typeof NodeHTTPResponseFlags)[keyof typeof NodeHTTPResponseFlags];
|
||||
|
||||
closed_or_completed = socket_closed | request_has_completed,
|
||||
}
|
||||
|
||||
export const enum NodeHTTPHeaderState {
|
||||
none,
|
||||
assigned,
|
||||
sent,
|
||||
}
|
||||
export const NodeHTTPHeaderState = {
|
||||
none: 0,
|
||||
assigned: 1,
|
||||
sent: 2,
|
||||
} as const;
|
||||
export type NodeHTTPHeaderState = (typeof NodeHTTPHeaderState)[keyof typeof NodeHTTPHeaderState];
|
||||
|
||||
function emitErrorNextTickIfErrorListenerNT(self, err, cb) {
|
||||
process.nextTick(emitErrorNextTickIfErrorListener, self, err, cb);
|
||||
|
||||
77
test/regression/issue/24336.test.ts
Normal file
77
test/regression/issue/24336.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/24336
|
||||
// require('http') should not trigger Object.prototype setters during module loading.
|
||||
// Node.js produces no output for both CJS and ESM, and Bun should match that behavior.
|
||||
test("require('http') does not trigger Object.prototype[0] setter", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
Object.defineProperty(Object.prototype, '0', {
|
||||
set() { console.log('SETTER_TRIGGERED'); }
|
||||
});
|
||||
require('http');
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toBe("");
|
||||
expect(stderr).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("require('url') does not trigger Object.prototype[0] setter", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
Object.defineProperty(Object.prototype, '0', {
|
||||
set() { console.log('SETTER_TRIGGERED'); }
|
||||
});
|
||||
require('url');
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toBe("");
|
||||
expect(stderr).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("require('util') does not trigger Object.prototype[0] setter", async () => {
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [
|
||||
bunExe(),
|
||||
"-e",
|
||||
`
|
||||
Object.defineProperty(Object.prototype, '0', {
|
||||
set() { console.log('SETTER_TRIGGERED'); }
|
||||
});
|
||||
require('util');
|
||||
`,
|
||||
],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout).toBe("");
|
||||
expect(stderr).toBe("");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user