Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
9e71b8cd33 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>
2026-02-19 11:06:47 +00:00
3 changed files with 128 additions and 37 deletions

View File

@@ -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;
}

View File

@@ -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);

View 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);
});