diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 1e352fc538bb..94375c3aee1e 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "d0b16ad0ce2bac652f0f63cdcc642a7d3ba69631"; +export const WEBKIT_VERSION = "autobuild-preview-pr-797-7d2692be"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/codegen/bindgen-emit.ts b/src/codegen/bindgen-emit.ts index 04b0a8e50987..4a19ee39102d 100644 --- a/src/codegen/bindgen-emit.ts +++ b/src/codegen/bindgen-emit.ts @@ -431,7 +431,7 @@ ${namespace.functions ` object->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, ${asciiLiteral(fn.name)}), ${fn.length}, ${hostFunctionName(fn)}, JSC::ImplementationVisibility::Public, JSC::NoIntrinsic, 0);`, ) .join("\n")} - return object; + return Bun::namespaceObjectOf(vm, owner, object); } } `; @@ -951,6 +951,7 @@ export function cppSource(file: SchemaFile): string { headers.delete(`Generated${file.origin.namespace}.h`); const accepted = file.types.filter(type => type instanceof DictionaryType && type.accepted) as DictionaryType[]; if (accepted.length > 0) headers.add("Bindgen/DictionaryMemberReader.h"); + if (file.namespaces.length > 0) headers.add("BunClientData.h"); const systemHeaders = ["JavaScriptCore/JSFunction.h"]; if (accepted.some(type => !type.allowArray)) systemHeaders.unshift("JavaScriptCore/ArrayConstructor.h"); if (file.namespaces.length > 0) systemHeaders.push("JavaScriptCore/ObjectConstructor.h"); diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 6c110a51cf74..8ff1c3c2b7e3 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -55,6 +55,7 @@ using namespace JSC; macro(close) \ macro(cmd) \ macro(code) \ + macro(consoleAsyncIterator) \ macro(createCommonJSModule) \ macro(createFIFO) \ macro(createInternalModuleById) \ diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index 3f84f23d0e73..ebe23570ea27 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -1,6 +1,10 @@ $overriddenName = "[Symbol.asyncIterator]"; export function asyncIterator(this: Console) { - var stream = Bun.stdin.stream(); + const stdin = Bun.stdin as typeof Bun.stdin & { $consoleAsyncIterator: (() => AsyncGenerator) | undefined }; + const existing = $getByIdDirectPrivate(stdin, "consoleAsyncIterator"); + if (existing) return existing(); + + var stream = stdin.stream(); var decoder = new TextDecoder("utf-8", { fatal: false }); var indexOf = Bun.indexOfLine; @@ -110,8 +114,7 @@ export function asyncIterator(this: Console) { } } - const symbol = globalThis.Symbol.asyncIterator; - this[symbol] = ConsoleAsyncIterator; + $putByIdDirectPrivate(stdin, "consoleAsyncIterator", ConsoleAsyncIterator); return ConsoleAsyncIterator(); } @@ -119,14 +122,15 @@ interface ConsoleWriter extends Bun.FileSink { flush(wait?: boolean): number | Promise; } -export function write(this: Console & { $writer: ConsoleWriter | undefined }, input) { +export function write(this: Console, input) { if (!$isObject(this)) throw $ERR_INVALID_THIS("Console"); - var writer = $getByIdDirectPrivate(this, "writer"); + const stdout = Bun.stdout as typeof Bun.stdout & { $writer: ConsoleWriter | undefined }; + var writer = $getByIdDirectPrivate(stdout, "writer"); if (!writer) { var length = $toLength(input?.length ?? 0); - writer = Bun.stdout.writer({ highWaterMark: length > 65536 ? length : 65536 }); - $putByIdDirectPrivate(this, "writer", writer); + writer = stdout.writer({ highWaterMark: length > 65536 ? length : 65536 }); + $putByIdDirectPrivate(stdout, "writer", writer); } var wrote = writer.write(input) as number; diff --git a/src/js/bun/ffi.ts b/src/js/bun/ffi.ts index bfb98abf9889..19c568e92582 100644 --- a/src/js/bun/ffi.ts +++ b/src/js/bun/ffi.ts @@ -75,9 +75,10 @@ const nativeDLOpen = ffi.dlopen; const nativeCallback = ffi.callback; const closeCallback = ffi.closeCallback; const nativeCFunction = ffi.cfunction; -delete ffi.callback; -delete ffi.closeCallback; -delete ffi.cfunction; +const { deleteBuiltinProperty } = require("internal/shared"); +deleteBuiltinProperty(ffi, "callback"); +deleteBuiltinProperty(ffi, "closeCallback"); +deleteBuiltinProperty(ffi, "cfunction"); class JSCallback { declare readonly ptr: number | null; diff --git a/src/js/internal/assert/assertion_error.ts b/src/js/internal/assert/assertion_error.ts index 3a407804f7ab..b083ff85467e 100644 --- a/src/js/internal/assert/assertion_error.ts +++ b/src/js/internal/assert/assertion_error.ts @@ -4,6 +4,7 @@ const { SafeSet } = require("internal/primordials"); const { inspect } = require("internal/util/inspect"); const colors = require("internal/util/colors"); const { validateObject } = require("internal/validators"); +const { setStackTraceLimitOverride } = require("internal/shared"); const { printMyersDiff, printSimpleMyersDiff } = require("internal/assert/myers_diff") as typeof Internal; const ErrorCaptureStackTrace = Error.captureStackTrace; @@ -274,103 +275,103 @@ class AssertionError extends Error { } = options; let { actual, expected } = options; - // NOTE: stack trace is always writable. - const limit = Error.stackTraceLimit; - Error.stackTraceLimit = 0; - - if (message != null) { - if (kMethodsWithCustomMessageDiff.has(operator)) { - super(createErrDiff(actual, expected, operator, message, diff)); + // No frames for this error, which captures its own below, or for one made while the message is rendered. + const previousOverride = setStackTraceLimitOverride(0); + try { + if (message != null) { + if (kMethodsWithCustomMessageDiff.has(operator)) { + super(createErrDiff(actual, expected, operator, message, diff)); + } else { + super(String(message)); + } } else { - super(String(message)); - } - } else { - // Reset colors on each call to make sure we handle dynamically set environment - // variables correct. - colors.refresh(); - // Prevent the error stack from being visible by duplicating the error - // in a very close way to the original in case both sides are actually - // instances of Error. - if ( - typeof actual === "object" && - actual !== null && - typeof expected === "object" && - expected !== null && - "stack" in actual && - actual instanceof Error && - "stack" in expected && - expected instanceof Error - ) { - actual = copyError(actual); - expected = copyError(expected); - } - - if (kMethodsWithCustomMessageDiff.has(operator)) { - super(createErrDiff(actual, expected, operator, message, diff)); - } else if (operator === "notDeepStrictEqual" || operator === "notStrictEqual") { - // In case the objects are equal but the operator requires unequal, show - // the first object and say A equals B - let base = kReadableOperator[operator]; - const res = StringPrototypeSplit.$call(inspectValue(actual), "\n"); - - // In case "actual" is an object or a function, it should not be - // reference equal. + // Reset colors on each call to make sure we handle dynamically set environment + // variables correct. + colors.refresh(); + // Prevent the error stack from being visible by duplicating the error + // in a very close way to the original in case both sides are actually + // instances of Error. if ( - operator === "notStrictEqual" && - ((typeof actual === "object" && actual !== null) || typeof actual === "function") + typeof actual === "object" && + actual !== null && + typeof expected === "object" && + expected !== null && + "stack" in actual && + actual instanceof Error && + "stack" in expected && + expected instanceof Error ) { - base = kReadableOperator.notStrictEqualObject; + actual = copyError(actual); + expected = copyError(expected); } - // Only remove lines in case it makes sense to collapse those. - const resLength = res.length; - if (resLength > 50 && diff !== "full") { - res[46] = `${colors.blue}...${colors.white}`; - while (res.length > 47) { - ArrayPrototypePop.$call(res); + if (kMethodsWithCustomMessageDiff.has(operator)) { + super(createErrDiff(actual, expected, operator, message, diff)); + } else if (operator === "notDeepStrictEqual" || operator === "notStrictEqual") { + // In case the objects are equal but the operator requires unequal, show + // the first object and say A equals B + let base = kReadableOperator[operator]; + const res = StringPrototypeSplit.$call(inspectValue(actual), "\n"); + + // In case "actual" is an object or a function, it should not be + // reference equal. + if ( + operator === "notStrictEqual" && + ((typeof actual === "object" && actual !== null) || typeof actual === "function") + ) { + base = kReadableOperator.notStrictEqualObject; } - } - // Only print a single input. - if (res.length === 1) { - super(`${base}${res[0].length > 5 ? "\n\n" : " "}${res[0]}`); - } else { - super(`${base}\n\n${ArrayPrototypeJoin.$call(res, "\n")}\n`); - } - } else { - let res = inspectValue(actual); - let other = inspectValue(expected); - const knownOperator = kReadableOperator[operator]; - if (operator === "notDeepEqual" && res === other) { - res = `${knownOperator}\n\n${res}`; - if (res.length > 1024 && diff !== "full") { - res = `${StringPrototypeSlice.$call(res, 0, 1021)}...`; - } - super(res); - } else { - if (res.length > kMaxLongStringLength && diff !== "full") { - res = `${StringPrototypeSlice.$call(res, 0, 509)}...`; + // Only remove lines in case it makes sense to collapse those. + const resLength = res.length; + if (resLength > 50 && diff !== "full") { + res[46] = `${colors.blue}...${colors.white}`; + while (res.length > 47) { + ArrayPrototypePop.$call(res); + } } - if (other.length > kMaxLongStringLength && diff !== "full") { - other = `${StringPrototypeSlice.$call(other, 0, 509)}...`; + + // Only print a single input. + if (res.length === 1) { + super(`${base}${res[0].length > 5 ? "\n\n" : " "}${res[0]}`); + } else { + super(`${base}\n\n${ArrayPrototypeJoin.$call(res, "\n")}\n`); } - if (operator === "deepEqual") { - res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`; + } else { + let res = inspectValue(actual); + let other = inspectValue(expected); + const knownOperator = kReadableOperator[operator]; + if (operator === "notDeepEqual" && res === other) { + res = `${knownOperator}\n\n${res}`; + if (res.length > 1024 && diff !== "full") { + res = `${StringPrototypeSlice.$call(res, 0, 1021)}...`; + } + super(res); } else { - const newOp = kReadableOperator[`${operator}Unequal`]; - if (newOp) { - res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`; + if (res.length > kMaxLongStringLength && diff !== "full") { + res = `${StringPrototypeSlice.$call(res, 0, 509)}...`; + } + if (other.length > kMaxLongStringLength && diff !== "full") { + other = `${StringPrototypeSlice.$call(other, 0, 509)}...`; + } + if (operator === "deepEqual") { + res = `${knownOperator}\n\n${res}\n\nshould loosely deep-equal\n\n`; } else { - other = ` ${operator} ${other}`; + const newOp = kReadableOperator[`${operator}Unequal`]; + if (newOp) { + res = `${newOp}\n\n${res}\n\nshould not loosely deep-equal\n\n`; + } else { + other = ` ${operator} ${other}`; + } } + super(`${res}${other}`); } - super(`${res}${other}`); } } + } finally { + setStackTraceLimitOverride(previousOverride); } - Error.stackTraceLimit = limit; - this.generatedMessage = !message; ObjectDefineProperty(this, "name", { __proto__: null, diff --git a/src/js/internal/cluster/child.ts b/src/js/internal/cluster/child.ts index fa695c3a7fdc..b271bb04982f 100644 --- a/src/js/internal/cluster/child.ts +++ b/src/js/internal/cluster/child.ts @@ -3,7 +3,7 @@ import type { ClusterWorker } from "internal/cluster/Worker"; const EventEmitter = require("node:events"); const Worker = require("internal/cluster/Worker"); const path = require("node:path"); -const { kClusterOwner: owner_symbol, kInternalSendOptions } = require("internal/shared"); +const { kClusterOwner: owner_symbol, kInternalSendOptions, defineBuiltinProperty } = require("internal/shared"); const onInternalMessage = $bindgen("node_cluster_binding.bind.ts", "onInternalMessageChild"); const closeRawHandle = $bindgen("node_cluster_binding.bind.ts", "clusterCloseHandle"); @@ -74,7 +74,13 @@ cluster._setupWorker = function () { // before calling, check if the channel is refd. if it isn't, then unref it after calling process.once(); $bindgen("node_cluster_binding.bind.ts", "channelIgnoreOneDisconnectEventListener")(); process.once("disconnect", () => { - (process as { channel: unknown }).channel = null; + defineBuiltinProperty(process, "channel", { + __proto__: null, + value: null, + writable: true, + enumerable: true, + configurable: true, + }); worker.emit("disconnect"); if (!worker.exitedAfterDisconnect) { diff --git a/src/js/internal/process/pre_execution.ts b/src/js/internal/process/pre_execution.ts index 604dc195728c..bd34fe3124c5 100644 --- a/src/js/internal/process/pre_execution.ts +++ b/src/js/internal/process/pre_execution.ts @@ -4,6 +4,8 @@ // file, worker, or `-e` eval — is evaluated. Must stay cheap: one execArgv // scan, bail early when nothing applies. +const { defineBuiltinProperty, setStackTraceLimitOverride } = require("internal/shared"); + let writeSync: typeof import("node:fs").writeSync; let envTracePrintMessage = false; let envTracePrintJsStack = false; @@ -126,10 +128,13 @@ function printEnvTrace(kind: EnvOpKind, key: string | null): void { // The capture burns 3 frames on trace machinery (printEnvTrace, the // proxy trap, and the Error line); widen the limit so the user still // sees `Error.stackTraceLimit` real frames. - const limit = Error.stackTraceLimit; - Error.stackTraceLimit = limit + 3; - const stack = new Error().stack!.split("\n"); - Error.stackTraceLimit = limit; + const previousOverride = setStackTraceLimitOverride(Error.stackTraceLimit + 3); + let stack: string[]; + try { + stack = new Error().stack!.split("\n"); + } finally { + setStackTraceLimitOverride(previousOverride); + } // stack[0] = "Error", [1] = printEnvTrace, [2] = the proxy trap. let corrected = false; for (let i = 3; i < stack.length; i++) { @@ -196,7 +201,7 @@ function installEnvTracing(): void { return ReflectOwnKeys(target); }, }); - Object.defineProperty(process, "env", { + defineBuiltinProperty(process, "env", { value: proxy, writable: true, enumerable: true, @@ -250,7 +255,7 @@ function installExitTracing(): void { writeStderr(out); return realExit.$call(process, code); } - process.exit = exit as typeof process.exit; + defineBuiltinProperty(process, "exit", { __proto__: null, value: exit }); } { @@ -297,7 +302,7 @@ function installExitTracing(): void { if (stackTraceLimit !== null) { const limit = Number(stackTraceLimit); - if (Number.isFinite(limit)) Error.stackTraceLimit = limit; + if (Number.isFinite(limit)) defineBuiltinProperty(Error, "stackTraceLimit", { __proto__: null, value: limit }); } // CLI-driven tracing runs in worker VMs too (workers inherit the parent's diff --git a/src/js/internal/quic/quic.ts b/src/js/internal/quic/quic.ts index b349306d081c..6ea72da2c7c0 100644 --- a/src/js/internal/quic/quic.ts +++ b/src/js/internal/quic/quic.ts @@ -36,7 +36,8 @@ const { isSharedArrayBuffer, isKeyObject, } = require("node:util/types"); -const { SocketAddress, BlockList } = require("node:net"); +const SocketAddress = $rust("node_net_binding.rs", "SocketAddress"); +const BlockList = $rust("node_net_binding.rs", "BlockList"); // The native binding hands certificates over as DER bytes; expose them as // X509Certificate objects like Node does. @@ -147,8 +148,9 @@ function ERR_QUIC_VERSION_NEGOTIATION_ERROR() { const kClientHttp = Symbol("kClientHttp"); const kNoteClientHttp = Symbol("kNoteClientHttp"); +const { defineBuiltinProperty } = require("internal/shared"); const kSocketAddressHandle = Symbol("kSocketAddressHandle"); -Object.defineProperty(SocketAddress.prototype, kSocketAddressHandle, { +defineBuiltinProperty(SocketAddress.prototype, kSocketAddressHandle, { __proto__: null, get() { return this; @@ -156,7 +158,7 @@ Object.defineProperty(SocketAddress.prototype, kSocketAddressHandle, { configurable: true, }); const kBlockListHandle = Symbol("kBlockListHandle"); -Object.defineProperty(BlockList.prototype, kBlockListHandle, { +defineBuiltinProperty(BlockList.prototype, kBlockListHandle, { __proto__: null, get() { return this; @@ -168,7 +170,7 @@ function InternalSocketAddress(handle): void { } const kBlobHandle = Symbol("kBlobHandle"); -Object.defineProperty(Blob.prototype, kBlobHandle, { +defineBuiltinProperty(Blob.prototype, kBlobHandle, { __proto__: null, get() { return this; diff --git a/src/js/internal/repl/node-errors.js b/src/js/internal/repl/node-errors.js index a9b3d5246185..e63f1304e2f0 100644 --- a/src/js/internal/repl/node-errors.js +++ b/src/js/internal/repl/node-errors.js @@ -74,14 +74,7 @@ for (const fn of [ }); } -function isErrorStackTraceLimitWritable() { - const desc = Object.getOwnPropertyDescriptor(Error, "stackTraceLimit"); - if (desc === undefined) return Object.isExtensible(Error); - return Object.prototype.hasOwnProperty.$call(desc, "writable") ? desc.writable : desc.set !== undefined; -} - export default { - isErrorStackTraceLimitWritable, AbortError, codes: { ERR_INVALID_ARG_TYPE, diff --git a/src/js/internal/shared.ts b/src/js/internal/shared.ts index d8b86b61066e..4f46e5b41a86 100644 --- a/src/js/internal/shared.ts +++ b/src/js/internal/shared.ts @@ -180,33 +180,27 @@ function guardCallback(callback) { } const nodeModulesRE = /[\\/]node_modules[\\/]/; -const ErrorCaptureStackTrace = Error.captureStackTrace; -function returnStackFrames(_err: unknown, frames: unknown[]) { - return frames; -} +const captureCallSites = $newCppFunction("FormatStackTraceForJS.cpp", "jsFunctionCaptureCallSites", 2) as ( + frameLimit: number, + startFunction: Function, +) => NodeJS.CallSite[]; +const setStackTraceLimitOverride = $newCppFunction( + "FormatStackTraceForJS.cpp", + "jsFunctionSetStackTraceLimitOverride", + 1, +) as (limit: number | undefined) => number | undefined; + +// Object.defineProperty(), `delete` and Object.setPrototypeOf() that also work on what BUN_JSC_immutableGlobals covers. +const defineBuiltinProperty = $newCppFunction("BunClientData.cpp", "jsFunctionDefineBuiltinProperty", 3); +const deleteBuiltinProperty = $newCppFunction("BunClientData.cpp", "jsFunctionDeleteBuiltinProperty", 2); +const setBuiltinPrototype = $newCppFunction("BunClientData.cpp", "jsFunctionSetBuiltinPrototype", 2); +// Makes immutable, if BUN_JSC_immutableGlobals is on, an object a built-in module creates for a native class. +const didCreateBuiltin = $newCppFunction("BunClientData.cpp", "jsFunctionDidCreateBuiltin", 1); // Port of node's IsInsideNodeModules: first real user frame inside node_modules? -// Guarded so a tampered Error.* never escapes to callers like url.parse. // https://github.com/nodejs/node/blob/main/src/node_util.cc -function isInsideNodeModules(frameLimit: number): boolean { - let prevLimit: unknown, prevPrepare: unknown; - let frames: { getFileName(): string | null }[] | undefined; - try { - prevLimit = Error.stackTraceLimit; - prevPrepare = Error.prepareStackTrace; - Error.stackTraceLimit = frameLimit; - Error.prepareStackTrace = returnStackFrames; - const target: { stack?: unknown } = {}; - ErrorCaptureStackTrace(target, isInsideNodeModules); - frames = target.stack as typeof frames; - } catch { - } finally { - try { - Error.stackTraceLimit = prevLimit as typeof Error.stackTraceLimit; - Error.prepareStackTrace = prevPrepare as typeof Error.prepareStackTrace; - } catch {} - } - if (!$isJSArray(frames)) return false; +function isInsideNodeModules(frameLimit: number = 10): boolean { + const frames = captureCallSites(frameLimit, isInsideNodeModules); try { for (const frame of frames) { const filename = frame.getFileName(); @@ -435,6 +429,12 @@ export default { once, getLazy, guardCallback, + captureCallSites, + setStackTraceLimitOverride, + defineBuiltinProperty, + deleteBuiltinProperty, + setBuiltinPrototype, + didCreateBuiltin, isInsideNodeModules, resistStopPropagation, diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 110f79ffa1a5..bbd3b84993ea 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -435,7 +435,10 @@ crypto_exports.DiffieHellman = DiffieHellman; crypto_exports.diffieHellman = diffieHellman; -ECDH.prototype.setPublicKey = deprecate(ECDH.prototype.setPublicKey, "ecdh.setPublicKey() is deprecated.", "DEP0031"); +require("internal/shared").defineBuiltinProperty(ECDH.prototype, "setPublicKey", { + __proto__: null, + value: deprecate(ECDH.prototype.setPublicKey, "ecdh.setPublicKey() is deprecated.", "DEP0031"), +}); crypto_exports.ECDH = ECDH; crypto_exports.createECDH = function createECDH(curve) { return new ECDH(curve); diff --git a/src/js/node/events.ts b/src/js/node/events.ts index 49921b283fd1..2023a6aa902f 100644 --- a/src/js/node/events.ts +++ b/src/js/node/events.ts @@ -33,7 +33,7 @@ const { validateString, } = require("internal/validators"); const { addAbortListener } = require("internal/abort_listener"); -const { resistStopPropagation } = require("internal/shared"); +const { resistStopPropagation, setBuiltinPrototype } = require("internal/shared"); const types = require("node:util/types"); let inspect: ((value: unknown, opts?: object) => string) | undefined; @@ -1047,7 +1047,7 @@ try { processPrototype !== Object.prototype && !(processPrototype instanceof EventEmitter) ) { - Object.setPrototypeOf(processPrototype, EventEmitter.prototype); + setBuiltinPrototype(processPrototype, EventEmitter.prototype); } } catch { // If the prototype is not relinkable, process simply keeps its native diff --git a/src/js/node/inspector.ts b/src/js/node/inspector.ts index 7f66cbc43848..ee9af069d419 100644 --- a/src/js/node/inspector.ts +++ b/src/js/node/inspector.ts @@ -36,6 +36,7 @@ const waitForNodeInspectorConnection = $newCppFunction( ); const postNodeInspectorControl = $newCppFunction("BunDebugger.cpp", "jsFunction_postNodeInspectorControl", 1); const closeNodeInspector = $newCppFunction("BunDebugger.cpp", "jsFunction_closeNodeInspector", 0); +const setDebugPort = $newCppFunction("BunProcess.cpp", "jsFunctionSetDebugPort", 1); let activeInspectorUrl: string | undefined; @@ -98,7 +99,7 @@ function open(port?: number, host?: string, wait?: boolean) { // Node writes the resolved port back so process.debugPort reflects it after // open(0) picks an ephemeral port. try { - process.debugPort = Number(new URL(resolvedUrl).port); + setDebugPort(Number(new URL(resolvedUrl).port)); } catch {} process.stderr.write(`Debugger listening on ${resolvedUrl}\n`); diff --git a/src/js/node/perf_hooks.ts b/src/js/node/perf_hooks.ts index 60b54f817404..ea8bca1e98c4 100644 --- a/src/js/node/perf_hooks.ts +++ b/src/js/node/perf_hooks.ts @@ -6,6 +6,7 @@ const { hasObserver, PerformanceNodeEntry, kEmptyObject, + defineBuiltinProperty, } = require("internal/shared"); const { validateFunction, validateObject } = require("internal/validators"); @@ -276,7 +277,8 @@ const nodeTiming = createPerformanceNodeTiming(); // timerify/eventLoopUtilization/nodeTiming go on Performance.prototype, // non-enumerable, per lib/internal/perf/performance.js. if (Performance) { - Object.defineProperties(Performance.prototype, { + const additions = { + __proto__: null, nodeTiming: { __proto__: null, configurable: true, @@ -298,7 +300,9 @@ if (Performance) { writable: true, value: eventLoopUtilization, }, - }); + }; + const prototype = Performance.prototype; + for (const name in additions) defineBuiltinProperty(prototype, name, additions[name]); } export default { diff --git a/src/js/node/repl.js b/src/js/node/repl.js index 727882cba818..a7c194387c1e 100644 --- a/src/js/node/repl.js +++ b/src/js/node/repl.js @@ -56,7 +56,6 @@ const { ArrayPrototypeShift, ArrayPrototypeSort, Boolean, - Error: MainContextError, FunctionPrototypeBind, FunctionPrototypeCall, JSONStringify, @@ -156,8 +155,8 @@ const { ERR_MISSING_ARGS, ERR_SCRIPT_EXECUTION_INTERRUPTED, }, - isErrorStackTraceLimitWritable, } = require("internal/repl/node-errors"); +const { setStackTraceLimitOverride } = require("internal/shared"); const { sendInspectorCommand } = require("internal/repl/node-shims"); const { getOptionValue } = require("internal/repl/node-shims"); const { validateFunction, validateObject } = require("internal/validators"); @@ -650,10 +649,9 @@ class REPLServer extends Interface { if (self.breakEvalOnSigint) { const interrupt = new Promise((resolve, reject) => { sigintListener = () => { - const tmp = MainContextError.stackTraceLimit; - if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = 0; + const previous = setStackTraceLimitOverride(0); const err = new ERR_SCRIPT_EXECUTION_INTERRUPTED(); - if (isErrorStackTraceLimitWritable()) MainContextError.stackTraceLimit = tmp; + setStackTraceLimitOverride(previous); reject(err); }; prioritizedSigintQueue.add(sigintListener); diff --git a/src/js/node/util.ts b/src/js/node/util.ts index f5dea4be6f87..b9397374c935 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -8,7 +8,7 @@ const { validateObject, validateInteger, } = require("internal/validators"); -const { resistStopPropagation, ErrnoException } = require("internal/shared"); +const { resistStopPropagation, ErrnoException, captureCallSites } = require("internal/shared"); const { MIMEType, MIMEParams } = require("internal/util/mime"); const { deprecate } = require("internal/util/deprecate"); @@ -547,7 +547,7 @@ interface CallSiteObject { column: number; } -function prepareCallSites(_err, callSites) { +function toCallSiteObjects(callSites) { const result: CallSiteObject[] = []; for (let i = 0; i < callSites.length; i++) { const callSite = callSites[i]; @@ -592,27 +592,7 @@ function getCallSites(frameCount = 10, options) { // Using kDefaultMaxCallStackSizeToCapture as reference validateInteger(frameCount, "frameCount", 1, 200); - // Capture with our own prepareStackTrace so a user-installed - // Error.prepareStackTrace is never invoked, and so Error.stackTraceLimit - // does not influence the number of frames returned. - const target: { stack?: CallSiteObject[] } = {}; - const savedPrepareStackTrace = Error.prepareStackTrace; - const savedStackTraceLimit = Error.stackTraceLimit; - try { - Error.prepareStackTrace = prepareCallSites; - // User code may have made stackTraceLimit non-writable; best-effort so the - // capture still runs and prepareStackTrace is always restored. - try { - Error.stackTraceLimit = frameCount; - } catch {} - Error.captureStackTrace(target, getCallSites); - return target.stack; - } finally { - Error.prepareStackTrace = savedPrepareStackTrace; - try { - Error.stackTraceLimit = savedStackTraceLimit; - } catch {} - } + return toCallSiteObjects(captureCallSites(frameCount, getCallSites)); } let lazySignals; diff --git a/src/js/node/worker_threads.ts b/src/js/node/worker_threads.ts index 12f2db6542d8..d1c43750a92d 100644 --- a/src/js/node/worker_threads.ts +++ b/src/js/node/worker_threads.ts @@ -12,7 +12,13 @@ type WebWorker = InstanceType & { const EventEmitter = require("node:events"); const { SafeMap } = require("internal/primordials"); -const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared"); +const { + throwNotImplemented, + warnNotImplementedOnce, + defineBuiltinProperty, + setBuiltinPrototype, + didCreateBuiltin, +} = require("internal/shared"); const { validateString, validateObject, @@ -291,7 +297,8 @@ function injectFakeEmitter(Class) { for (const [methodName, value] of emitterMethods) { Object.defineProperty(inherited, methodName, { value, writable: true, enumerable: false, configurable: true }); } - Object.setPrototypeOf(proto, inherited); + didCreateBuiltin(inherited); + setBuiltinPrototype(proto, inherited); } injectFakeEmitter(_MessagePort); @@ -302,7 +309,7 @@ const MessagePort = _MessagePort; // closedMessagePorts lets moveMessagePortToContext report ERR_CLOSED_MESSAGE_PORT. const closedMessagePorts = new WeakSet(); const nativeMessagePortClose = MessagePort.prototype.close; -Object.defineProperty(MessagePort.prototype, "close", { +defineBuiltinProperty(MessagePort.prototype, "close", { value: function close(cb) { closedMessagePorts.add(this); // node's mechanism is literally `this.once('close', cb)`, so cb interleaves @@ -320,7 +327,7 @@ Object.defineProperty(MessagePort.prototype, "close", { // node-style util.inspect output for MessagePort (shows whether the channel is // still active). Symbol-keyed so it does not appear in getOwnPropertyNames. const kInspectCustom = Symbol.for("nodejs.util.inspect.custom"); -Object.defineProperty(MessagePort.prototype, kInspectCustom, { +defineBuiltinProperty(MessagePort.prototype, kInspectCustom, { value: function (_depth, _options) { return `MessagePort [EventTarget] { active: ${_isMessagePortActive(this)}, refed: ${this.hasRef()} }`; }, @@ -355,7 +362,7 @@ function setupWorkerStdio(stdio) { // node routes console.log through process.stdout/stderr; Bun's global console // writes the fd directly, so rebind it to the port-backed streams. const { Console } = require("node:console"); - (globalThis as { console: unknown }).console = new Console(stdoutStream, stderrStream); + defineBuiltinProperty(globalThis, "console", { __proto__: null, value: new Console(stdoutStream, stderrStream) }); } // Emulation of Node's JSTransferable protocol (kTransfer/kTransferList/kDeserialize) for @@ -730,7 +737,7 @@ function applyWorkerProcessOverrides() { // node defaults debugPort to 9229 in workers (still settable). Per-object property: // the static accessor's setter writes a process-global shared across threads. try { - Object.defineProperty(proc, "debugPort", { value: 9229, writable: true, configurable: true, enumerable: true }); + defineBuiltinProperty(proc, "debugPort", { value: 9229, writable: true, configurable: true, enumerable: true }); } catch {} // process.umask(setMask) is unsupported in workers; the getter still works. const realUmask = proc.umask; @@ -738,7 +745,7 @@ function applyWorkerProcessOverrides() { if (mask === undefined) return realUmask.$call(proc); throw $ERR_WORKER_UNSUPPORTED_OPERATION("Setting process.umask() is not supported in workers"); } - proc.umask = umask; + defineBuiltinProperty(proc, "umask", { __proto__: null, value: umask }); // Disabled, throwing stubs (each carries `.disabled === true`, like node). const disabled = ["abort", "chdir"]; if (process.platform !== "win32") { @@ -755,12 +762,12 @@ function applyWorkerProcessOverrides() { throw $ERR_WORKER_UNSUPPORTED_OPERATION(`process.${name}() is not supported in workers`); }; stub.disabled = true; - Object.defineProperty(proc, name, { configurable: true, writable: true, enumerable: true, value: stub }); + defineBuiltinProperty(proc, name, { configurable: true, writable: true, enumerable: true, value: stub }); } // IPC accessors throw on access only in a worker that inherited an IPC channel. if (hasIpc) { for (const name of ["channel", "connected"]) { - Object.defineProperty(proc, name, { + defineBuiltinProperty(proc, name, { configurable: true, enumerable: false, get() { diff --git a/src/js/node/zlib.ts b/src/js/node/zlib.ts index 46e3cfe5deac..c730fa33788d 100644 --- a/src/js/node/zlib.ts +++ b/src/js/node/zlib.ts @@ -877,7 +877,7 @@ class ZstdDecompress extends Zstd { } // Legacy alias on the C++ wrapper object. -ObjectDefineProperty(NativeZlib.prototype, "jsref", { +require("internal/shared").defineBuiltinProperty(NativeZlib.prototype, "jsref", { __proto__: null, get() { return this[owner_symbol]; diff --git a/src/jsc/VM.rs b/src/jsc/VM.rs index ce53cdaeb33c..cea002068a78 100644 --- a/src/jsc/VM.rs +++ b/src/jsc/VM.rs @@ -35,6 +35,7 @@ unsafe extern "C" { safe fn JSC__VM__executionForbidden(vm: &VM) -> bool; safe fn JSC__VM__notifyNeedTermination(vm: &VM); safe fn JSC__VM__isEntered(vm: &VM) -> bool; + safe fn JSC__VM__allowLazyMaterializationOfImmutableProperties(vm: &VM, allow: bool); safe fn JSC__VM__terminationException(vm: &VM) -> JSValue; safe fn JSC__VM__throwError(vm: &VM, global_object: &JSGlobalObject, value: JSValue); safe fn JSC__VM__releaseWeakRefs(vm: &VM); @@ -137,6 +138,14 @@ impl VM { JSC__VM__isEntered(self) } + /// While the guard is alive, native code can put and delete the properties of what `BUN_JSC_immutableGlobals` covers. No JavaScript may run. + pub fn allow_lazy_materialization_of_immutable_properties( + &self, + ) -> AllowLazyMaterializationOfImmutableProperties<'_> { + JSC__VM__allowLazyMaterializationOfImmutableProperties(self, true); + AllowLazyMaterializationOfImmutableProperties { vm: self } + } + /// The VM's TerminationException cell (created on demand) — what a pending one reads as; inert /// until thrown. pub fn termination_exception(&self) -> JSValue { @@ -167,6 +176,16 @@ impl VM { } } +pub struct AllowLazyMaterializationOfImmutableProperties<'a> { + vm: &'a VM, +} + +impl Drop for AllowLazyMaterializationOfImmutableProperties<'_> { + fn drop(&mut self) { + JSC__VM__allowLazyMaterializationOfImmutableProperties(self.vm, false) + } +} + /// RAII JSLockHolder returned by [`VM::get_api_lock`]. Released on `Drop`. pub struct Lock<'a> { vm: &'a VM, diff --git a/src/jsc/bindings/BunClientData.cpp b/src/jsc/bindings/BunClientData.cpp index 15fc6f521a0c..f5dcf04bec8e 100644 --- a/src/jsc/bindings/BunClientData.cpp +++ b/src/jsc/bindings/BunClientData.cpp @@ -234,9 +234,130 @@ namespace Bun { JSC::Structure* createClassStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype, JSC::TypeInfo typeInfo, const JSC::ClassInfo* classInfo, JSC::IndexingType indexingType, unsigned inlineCapacity) { + if (prototype.isObject()) + globalObject->didCreateBuiltin(vm, asObject(prototype)); return JSC::Structure::create(vm, globalObject, prototype, typeInfo, classInfo, indexingType, inlineCapacity); } +JSC::JSValue namespaceObjectOf(JSC::VM& vm, JSC::JSObject* owner, JSC::JSValue namespaceObject) +{ + if (!owner->hasImmutableProperties() || !namespaceObject || !namespaceObject.isObject()) [[likely]] + return namespaceObject; + + // The objects it holds are part of it too: `Bun.FFI.read`, `process.config.variables`. + JSC::DeferGC deferGC(vm); + Vector objects { asObject(namespaceObject) }; + while (!objects.isEmpty()) { + auto* object = objects.takeLast(); + if (object->hasImmutableProperties()) + continue; + owner->globalObject()->makeObjectAndItsFunctionsImmutable(vm, object); + object->structure()->forEachProperty(vm, [&](const JSC::PropertyTableEntry& entry) { + if (JSC::JSValue value = object->getDirect(entry.offset()); value.isObject()) + objects.append(asObject(value)); + return true; + }); + } + return namespaceObject; +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionDefineBuiltinProperty, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* builtin = callFrame->argument(0).getObject(); + if (!builtin) + return throwVMTypeError(globalObject, scope, "Properties can only be defined on Objects."_s); + auto key = callFrame->argument(1).toPropertyKey(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + JSC::PropertyDescriptor descriptor; + JSC::toPropertyDescriptor(globalObject, callFrame->argument(2), descriptor); + RETURN_IF_EXCEPTION(scope, {}); + + bool isImmutable = builtin->hasImmutableProperties(); + if (isImmutable) { + // Building a lazy property may run JavaScript, which must not happen while the write is allowed. + JSC::PropertySlot slot(builtin, JSC::PropertySlot::InternalMethodType::GetOwnProperty); + builtin->methodTable()->getOwnPropertySlot(builtin, globalObject, key, slot); + RETURN_IF_EXCEPTION(scope, {}); + } + { + std::optional allowMaterialization; + if (isImmutable) + allowMaterialization.emplace(vm); + builtin->methodTable()->defineOwnProperty(builtin, globalObject, key, descriptor, true); + } + RETURN_IF_EXCEPTION(scope, {}); + if (isImmutable) { + for (JSC::JSValue added : { descriptor.value(), descriptor.isAccessorDescriptor() ? descriptor.getter() : JSC::JSValue(), descriptor.isAccessorDescriptor() ? descriptor.setter() : JSC::JSValue() }) { + if (added && added.isCallable()) + globalObject->makeObjectAndItsFunctionsImmutable(vm, asObject(added)); + } + } + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionDeleteBuiltinProperty, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* builtin = callFrame->argument(0).toObject(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + auto key = callFrame->argument(1).toPropertyKey(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + + bool isImmutable = builtin->hasImmutableProperties(); + if (isImmutable && !builtin->staticPropertiesReified()) { + // Building a lazy property may run JavaScript, which must not happen while the write is allowed. + builtin->reifyAllStaticProperties(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + } + bool didDelete; + { + std::optional allowMaterialization; + if (isImmutable) + allowMaterialization.emplace(vm); + didDelete = JSC::JSCell::deleteProperty(builtin, globalObject, key); + } + RETURN_IF_EXCEPTION(scope, {}); + if (!didDelete) + return throwVMTypeError(globalObject, scope, JSC::UnableToDeletePropertyError); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionSetBuiltinPrototype, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + auto* builtin = callFrame->argument(0).getObject(); + JSC::JSValue prototype = callFrame->argument(1); + if (!builtin || !(prototype.isObject() || prototype.isNull())) + return throwVMTypeError(globalObject, scope, "Object prototype may only be an Object or null"_s); + + std::optional allowMaterialization; + if (builtin->hasImmutableProperties()) + allowMaterialization.emplace(vm); + scope.release(); + builtin->setPrototype(vm, globalObject, prototype, true); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionDidCreateBuiltin, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + if (auto* builtin = callFrame->argument(0).getObject()) + globalObject->didCreateBuiltin(JSC::getVM(globalObject), builtin); + return JSC::JSValue::encode(JSC::jsUndefined()); +} + +// JSC::AllowLazyMaterializationOfImmutableProperties for Rust, which pairs the two calls in a guard. +extern "C" void JSC__VM__allowLazyMaterializationOfImmutableProperties(JSC::VM* vm, bool allow) +{ + vm->allowLazyMaterializationOfImmutablePropertiesCount += allow ? 1 : -1; +#if ASSERT_ENABLED + vm->disallowVMEntryCount += allow ? 1 : -1; +#endif +} + void reifyStaticPropertyTable(JSC::VM& vm, const JSC::ClassInfo* classInfo, std::span values, JSC::JSObject& object) { JSC::reifyStaticProperties(vm, classInfo, values, object); diff --git a/src/jsc/bindings/BunClientData.h b/src/jsc/bindings/BunClientData.h index 40f3a7db8aa7..fa4658ebdb60 100644 --- a/src/jsc/bindings/BunClientData.h +++ b/src/jsc/bindings/BunClientData.h @@ -141,6 +141,14 @@ namespace Bun { // once-per-class setup paths, so each wrapper class doesn't inline them. JSC::Structure* createClassStructure(JSC::VM&, JSC::JSGlobalObject*, JSC::JSValue prototype, JSC::TypeInfo, const JSC::ClassInfo*, JSC::IndexingType = JSC::NonArray, unsigned inlineCapacity = 0); void reifyStaticPropertyTable(JSC::VM&, const JSC::ClassInfo*, std::span, JSC::JSObject&); +// For an object of `owner`'s that is API (`Bun.dns`, `process.versions`), not state (`Bun.env`): immutable if `owner` is, with the objects it holds. +JSC::JSValue namespaceObjectOf(JSC::VM&, JSC::JSObject* owner, JSC::JSValue namespaceObject); +// For built-in modules, through internal/shared: Object.defineProperty(), `delete` and Object.setPrototypeOf() that work on an immutable built-in too. No JavaScript runs while the write is allowed. +JSC_DECLARE_HOST_FUNCTION(jsFunctionDefineBuiltinProperty); +JSC_DECLARE_HOST_FUNCTION(jsFunctionDeleteBuiltinProperty); +JSC_DECLARE_HOST_FUNCTION(jsFunctionSetBuiltinPrototype); +// JSGlobalObject::didCreateBuiltin() for a built-in module. +JSC_DECLARE_HOST_FUNCTION(jsFunctionDidCreateBuiltin); // `JSC::allocateCell` for a fieldless `JSNonFinalObject` subclass living in `vm.plainObjectSpace()` (prototype objects). void* allocatePlainObjectCell(JSC::VM&, size_t); // `JSC_TO_STRING_TAG_WITHOUT_TRANSITION()`, out of line. diff --git a/src/jsc/bindings/BunObject+exports.h b/src/jsc/bindings/BunObject+exports.h index c3036b513bd4..4636500a0bef 100644 --- a/src/jsc/bindings/BunObject+exports.h +++ b/src/jsc/bindings/BunObject+exports.h @@ -50,9 +50,15 @@ FOR_EACH_CALLBACK(DECLARE_ZIG_BUN_OBJECT_CALLBACK); FOR_EACH_GETTER(DECLARE_ZIG_BUN_OBJECT_GETTER); #undef DECLARE_ZIG_BUN_OBJECT_GETTER +// The getters above whose value goes through Bun::namespaceObjectOf(). +static constexpr bool isNamespaceObjectGetter(std::string_view name) { return name == "FFI" || name == "markdown"; } + // definition of the C++ wrapper to call the Rust function #define DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER(name) static JSC::JSValue BunObject_lazyPropCb_wrap_##name(JSC::VM &vm, JSC::JSObject *object) { \ - return JSC::JSValue::decode(BunObject_lazyPropCb_##name(object->globalObject(), object)); \ + JSC::JSValue value = JSC::JSValue::decode(BunObject_lazyPropCb_##name(object->globalObject(), object)); \ + if constexpr (isNamespaceObjectGetter(#name)) \ + return Bun::namespaceObjectOf(vm, object, value); \ + return value; \ } \ FOR_EACH_GETTER(DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER); diff --git a/src/jsc/bindings/BunObject.cpp b/src/jsc/bindings/BunObject.cpp index 802bb9ce1b9d..f82b8d88d2a1 100644 --- a/src/jsc/bindings/BunObject.cpp +++ b/src/jsc/bindings/BunObject.cpp @@ -398,9 +398,15 @@ static EncodedJSValue lazyBunObjectValue(JSGlobalObject* lexicalGlobalObject, En auto scope = DECLARE_THROW_SCOPE(vm); // A CustomValue getter's thisValue is the property's owner, not the receiver. JSObject* bunObject = JSValue::decode(thisValue).getObject(); - JSValue value = create(vm, defaultGlobalObject(bunObject->globalObject())); + auto* globalObject = defaultGlobalObject(bunObject->globalObject()); + JSValue value = create(vm, globalObject); RETURN_IF_EXCEPTION(scope, {}); - bunObject->putDirect(vm, propertyName, value, PropertyAttribute::DontDelete | 0); + { + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); + bunObject->putDirect(vm, propertyName, value, PropertyAttribute::DontDelete | 0); + } + if (value.isObject()) + globalObject->didCreateBuiltin(vm, asObject(value)); return JSValue::encode(value); } @@ -465,7 +471,7 @@ static JSValue constructDNSObject(VM& vm, JSObject* bunObject) JSC::PropertyAttribute::DontDelete | 0); dnsObject->putDirect(vm, JSC::Identifier::fromString(vm, "V4MAPPED"_s), jsNumber(AI_V4MAPPED), JSC::PropertyAttribute::DontDelete | 0); - return dnsObject; + return namespaceObjectOf(vm, bunObject, dnsObject); } JSC_DECLARE_HOST_FUNCTION(jsFunctionJSONLParse); @@ -668,7 +674,7 @@ static JSValue constructJSONLObject(VM& vm, JSObject* bunObject) JSC::PropertyAttribute::DontDelete | 0); jsonlObject->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsNontrivialString(vm, "JSONL"_s), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly); - return jsonlObject; + return namespaceObjectOf(vm, bunObject, jsonlObject); } static JSValue constructBunPeekObject(VM& vm, JSObject* bunObject) @@ -1130,7 +1136,10 @@ static JSC_DEFINE_CUSTOM_SETTER(setBunObjectMain, (JSC::JSGlobalObject * globalO { (void)thisEncoded; (void)propertyName; - return BunObject_setter_main(globalObject, encodedValue); + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + if (throwIfSetterOwnerIsImmutable(globalObject, scope, defaultGlobalObject(globalObject)->bunObject())) + return false; + RELEASE_AND_RETURN(scope, BunObject_setter_main(globalObject, encodedValue)); } // LazyProperty wrappers for stdin/stderr/stdout @@ -1176,7 +1185,7 @@ static JSValue constructModuleGraphObject(VM& vm, JSObject* bunObject) static JSValue constructSecretsObject(VM& vm, JSObject* bunObject) { auto* zigGlobalObject = uncheckedDowncast(bunObject->globalObject()); - return Bun::createSecretsObject(vm, zigGlobalObject); + return namespaceObjectOf(vm, bunObject, Bun::createSecretsObject(vm, zigGlobalObject)); } static JSValue constructWebViewObject(VM& vm, JSObject* bunObject) diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 1d0155876744..c17c6cfddcdd 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -318,7 +318,7 @@ static JSValue constructVersions(VM& vm, JSObject* processObject) #undef STRINGIFY #undef STRINGIFY_IMPL - return object; + return namespaceObjectOf(vm, processObject, object); } static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) @@ -332,7 +332,7 @@ static JSValue constructProcessReleaseObject(VM& vm, JSObject* processObject) putDirectNamed(vm, release, "headersUrl"_s, jsOwnedString(vm, String("https://nodejs.org/download/release/v" REPORTED_NODEJS_VERSION "/node-v" REPORTED_NODEJS_VERSION "-headers.tar.gz"_s))); RETURN_IF_EXCEPTION(scope, {}); - return release; + return namespaceObjectOf(vm, processObject, release); } static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* process, int exitCode) @@ -346,7 +346,10 @@ static void dispatchExitInternal(JSC::JSGlobalObject* globalObject, Process* pro if (vm.hasTerminationRequest() || vm.hasExceptionsAfterHandlingTraps()) return; - putDirectNamed(vm, process, "_exiting"_s, jsBoolean(true)); + { + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); + putDirectNamed(vm, process, "_exiting"_s, jsBoolean(true)); + } auto event = Identifier::fromString(vm, "exit"_s); if (!emitter.hasEventListeners(event)) { return; @@ -1870,7 +1873,7 @@ static JSValue constructFinalization(VM& vm, JSObject* processObject) auto* globalObject = defaultGlobalObject(processObject->globalObject()); JSC::MarkedArgumentBuffer args; args.append(processObject); - return callLazyProcessBuilder(vm, globalObject, processObjectInternalsCreateProcessFinalizationCodeGenerator, JSC::ArgList(args)); + return namespaceObjectOf(vm, processObject, callLazyProcessBuilder(vm, globalObject, processObjectInternalsCreateProcessFinalizationCodeGenerator, JSC::ArgList(args))); } static JSValue constructAllowedNodeEnvironmentFlags(VM& vm, JSObject* processObject) @@ -2395,6 +2398,8 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessExitCode, (JSC::JSGlobalObject * lexicalGloba return false; } auto throwScope = DECLARE_THROW_SCOPE(process->vm()); + if (throwIfSetterOwnerIsImmutable(lexicalGlobalObject, throwScope, process)) + return false; auto code = JSValue::decode(value); RELEASE_AND_RETURN(throwScope, setProcessExitCodeInner(lexicalGlobalObject, process, code)); @@ -2862,7 +2867,7 @@ static JSValue constructProcessReportObject(VM& vm, JSObject* processObject) putDirectNamed(vm, report, "excludeEnv"_s, JSC::jsString(vm, String("SIGUSR2"_s))); putDirectNamed(vm, report, "writeReport"_s, JSC::JSFunction::create(vm, globalObject, 1, String("writeReport"_s), Process_functionWriteReport, ImplementationVisibility::Public)); RETURN_IF_EXCEPTION(scope, {}); - return report; + return namespaceObjectOf(vm, processObject, report); } __attribute__((minsize)) static JSValue constructProcessConfigObject(VM& vm, JSObject* processObject) @@ -3001,7 +3006,7 @@ __attribute__((minsize)) static JSValue constructProcessConfigObject(VM& vm, JSO config->freeze(vm); RETURN_IF_EXCEPTION(scope, {}); - return config; + return namespaceObjectOf(vm, processObject, config); } static JSValue constructProcessHrtimeObject(VM& vm, JSObject* processObject) @@ -3269,10 +3274,13 @@ void Process::setArgv(JSGlobalObject* globalObject, JSValue value) JSC_DEFINE_CUSTOM_SETTER(setProcessArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName)) { + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); if (!process) { return true; } + if (throwIfSetterOwnerIsImmutable(globalObject, scope, process)) + return false; JSValue value = JSValue::decode(encodedValue); process->setArgv(globalObject, value); @@ -3319,10 +3327,13 @@ void Process::setExecArgv(JSGlobalObject* globalObject, JSValue value) JSC_DEFINE_CUSTOM_SETTER(setProcessExecArgv, (JSGlobalObject * globalObject, EncodedJSValue thisValue, EncodedJSValue encodedValue, PropertyName)) { + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); Process* process = getProcessObject(globalObject, JSValue::decode(thisValue)); if (!process) { return true; } + if (throwIfSetterOwnerIsImmutable(globalObject, scope, process)) + return false; JSValue value = JSValue::decode(encodedValue); process->setExecArgv(globalObject, value); @@ -4705,7 +4716,7 @@ static JSValue constructFeatures(VM& vm, JSObject* processObject) putDirectNamed(vm, object, "typescript"_s, jsString(vm, String("transform"_s))); RETURN_IF_EXCEPTION(scope, {}); - return object; + return namespaceObjectOf(vm, processObject, object); } static uint16_t debugPort = 9229; @@ -4715,11 +4726,10 @@ JSC_DEFINE_CUSTOM_GETTER(processDebugPort, (JSC::JSGlobalObject * globalObject, return JSC::JSValue::encode(jsNumber(debugPort)); } -JSC_DEFINE_CUSTOM_SETTER(setProcessDebugPort, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName)) +static bool setDebugPort(JSC::JSGlobalObject* globalObject, JSValue value) { auto& vm = JSC::getVM(globalObject); auto scope = DECLARE_THROW_SCOPE(vm); - JSValue value = JSValue::decode(encodedValue); double port = value.toNumber(globalObject); RETURN_IF_EXCEPTION(scope, {}); @@ -4737,6 +4747,24 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessDebugPort, (JSC::JSGlobalObject * globalObjec return true; } +JSC_DEFINE_CUSTOM_SETTER(setProcessDebugPort, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue encodedValue, JSC::PropertyName)) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (throwIfSetterOwnerIsImmutable(globalObject, scope, getProcessObject(globalObject, JSValue::decode(thisValue)))) + return false; + RELEASE_AND_RETURN(scope, setDebugPort(globalObject, JSValue::decode(encodedValue))); +} + +// For node:inspector, which reports the port it bound even if the process object is immutable. +JSC_DEFINE_HOST_FUNCTION(jsFunctionSetDebugPort, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + auto scope = DECLARE_THROW_SCOPE(JSC::getVM(globalObject)); + setDebugPort(globalObject, callFrame->argument(0)); + RETURN_IF_EXCEPTION(scope, {}); + return JSValue::encode(jsUndefined()); +} + JSC_DEFINE_CUSTOM_GETTER(processTitle, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) { auto& vm = JSC::getVM(globalObject); @@ -4777,6 +4805,8 @@ JSC_DEFINE_CUSTOM_SETTER(setProcessTitle, (JSC::JSGlobalObject * globalObject, J if (!thisObject || !jsString) { return false; } + if (throwIfSetterOwnerIsImmutable(globalObject, scope, getProcessObject(globalObject, thisObject))) + return false; WTF::String wtfStr = jsString->value(globalObject); RETURN_IF_EXCEPTION(scope, false); #if !OS(WINDOWS) diff --git a/src/jsc/bindings/BunProcess.h b/src/jsc/bindings/BunProcess.h index bec759f7f6a9..d2e95d276eb9 100644 --- a/src/jsc/bindings/BunProcess.h +++ b/src/jsc/bindings/BunProcess.h @@ -145,6 +145,7 @@ JSC_DECLARE_HOST_FUNCTION(Process_functionDlopen); // Routes its argument onto the uncaught-exception path. Used, via $newCppFunction, by the // node-style callback shims in src/js. JSC_DECLARE_HOST_FUNCTION(jsFunctionReportUncaughtException); +JSC_DECLARE_HOST_FUNCTION(jsFunctionSetDebugPort); // process.cwd() as a JSString, cached on the process object until it changes. JSC::JSValue getCachedCwd(JSC::JSGlobalObject*); diff --git a/src/jsc/bindings/ErrorCode.cpp b/src/jsc/bindings/ErrorCode.cpp index cd5f87c1d9a9..0bf4e34ceb47 100644 --- a/src/jsc/bindings/ErrorCode.cpp +++ b/src/jsc/bindings/ErrorCode.cpp @@ -194,6 +194,7 @@ static ErrorCodeCache* errorCache(Zig::GlobalObject* globalObject) static Structure* createErrorStructure(JSC::VM& vm, JSGlobalObject* globalObject, JSC::ErrorType type, WTF::ASCIILiteral name, WTF::ASCIILiteral code) { auto* prototype = createErrorPrototype(vm, globalObject, type, name, code); + globalObject->didCreateBuiltin(vm, prototype); return ErrorInstance::createStructure(vm, globalObject, prototype); } @@ -291,6 +292,14 @@ void throwTypeErrorOrOutOfMemory(JSC::JSGlobalObject* globalObject, JSC::ThrowSc JSC::throwTypeError(globalObject, scope, message); } +bool throwIfSetterOwnerIsImmutable(JSC::JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSC::JSObject* owner) +{ + if (!owner->hasImmutableProperties()) [[likely]] + return false; + JSC::throwTypeError(globalObject, scope, JSC::ReadonlyPropertyWriteError); + return true; +} + JSObject* createError(JSC::JSGlobalObject* globalObject, ErrorCode code, MessageBuilder& message) { auto built = message.tryToString(); diff --git a/src/jsc/bindings/ErrorCode.h b/src/jsc/bindings/ErrorCode.h index 84761a1c0f77..6715b52f6db6 100644 --- a/src/jsc/bindings/ErrorCode.h +++ b/src/jsc/bindings/ErrorCode.h @@ -55,6 +55,9 @@ class MessageBuilder final : public WTF::StringBuilder { // For a message built with `tryMakeString`: a null `message` did not fit, and is thrown as `RangeError: Out of memory`. void throwTypeErrorOrOutOfMemory(JSC::JSGlobalObject*, JSC::ThrowScope&, const WTF::String& message); +// For a native setter that keeps its value outside of `owner`'s properties, which JSC does not guard on every route. True if it threw. +bool throwIfSetterOwnerIsImmutable(JSC::JSGlobalObject*, JSC::ThrowScope&, JSC::JSObject* owner); + class ErrorCodeCache : public JSC::JSInternalFieldObjectImpl { public: using Base = JSInternalFieldObjectImpl; diff --git a/src/jsc/bindings/ExposeNodeModuleGlobals.cpp b/src/jsc/bindings/ExposeNodeModuleGlobals.cpp index a4c00ab0ba40..7080863152d8 100644 --- a/src/jsc/bindings/ExposeNodeModuleGlobals.cpp +++ b/src/jsc/bindings/ExposeNodeModuleGlobals.cpp @@ -85,6 +85,7 @@ extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__ExposeNodeModuleGlobals(Zig::Global #define EXPOSED_BUILTIN_ENTRY(id, field) { #id##_s, ExposeNodeModuleGlobalGetters::id }, static const Entry entries[] = { FOREACH_EXPOSED_BUILTIN_IMR(EXPOSED_BUILTIN_ENTRY) }; #undef EXPOSED_BUILTIN_ENTRY + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); for (auto& entry : entries) { globalObject->putDirectCustomAccessor( vm, @@ -144,6 +145,7 @@ extern "C" [[ZIG_EXPORT(check_slow)]] void Bun__REPL__setupGlobalRequire( requireFunction->putDirect(vm, vm.propertyNames->resolve, resolveFunction, 0); moduleObject->putDirect(vm, WebCore::clientData(vm)->builtinNames().requirePublicName(), requireFunction, 0); + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); globalObject->putDirect(vm, WebCore::builtinNames(vm).requirePublicName(), requireFunction, 0); Bun::putDirectNamed(vm, globalObject, "module"_s, moduleObject); Bun::putDirectNamed(vm, globalObject, "__filename"_s, filename); diff --git a/src/jsc/bindings/FormatStackTraceForJS.cpp b/src/jsc/bindings/FormatStackTraceForJS.cpp index 7a61fe847992..6fb6ab7b5224 100644 --- a/src/jsc/bindings/FormatStackTraceForJS.cpp +++ b/src/jsc/bindings/FormatStackTraceForJS.cpp @@ -482,7 +482,8 @@ static String computeErrorInfoWithoutPrepareStackTrace( RELEASE_AND_RETURN(scope, Bun::formatStackTrace(vm, globalObject, lexicalGlobalObject, name, message, line, column, sourceURL, stackTrace, errorInstance)); } -static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, Vector& stackFrames, OrdinalNumber& line, OrdinalNumber& column, String& sourceURL, JSObject* errorObject, JSObject* prepareStackTrace) +// The second argument of Error.prepareStackTrace. +static JSArray* createCallSitesArray(JSC::VM& vm, Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, Vector& stackFrames) { auto scope = DECLARE_THROW_SCOPE(vm); @@ -564,7 +565,14 @@ static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObj } } - JSArray* callSitesArray = JSC::constructArray(globalObject, globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), callSites); + RELEASE_AND_RETURN(scope, JSC::constructArray(globalObject, globalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous), callSites)); +} + +static JSValue computeErrorInfoWithPrepareStackTrace(JSC::VM& vm, Zig::GlobalObject* globalObject, JSC::JSGlobalObject* lexicalGlobalObject, Vector& stackFrames, JSObject* errorObject, JSObject* prepareStackTrace) +{ + auto scope = DECLARE_THROW_SCOPE(vm); + + JSArray* callSitesArray = createCallSitesArray(vm, globalObject, lexicalGlobalObject, stackFrames); RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN(scope, formatStackTraceToJSValue(vm, globalObject, lexicalGlobalObject, errorObject, callSitesArray, prepareStackTrace)); @@ -590,7 +598,7 @@ static JSValue computeErrorInfoToJSValueWithoutSkipping(JSC::VM& vm, VectorisInsideErrorPrepareStackTraceCallback = true; - auto result = computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, line, column, sourceURL, errorInstance, prepareStackTrace.getObject()); + auto result = computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, errorInstance, prepareStackTrace.getObject()); globalObject->isInsideErrorPrepareStackTraceCallback = false; RELEASE_AND_RETURN(scope, result); } @@ -601,7 +609,7 @@ static JSValue computeErrorInfoToJSValueWithoutSkipping(JSC::VM& vm, VectorisInsideErrorPrepareStackTraceCallback = true; - auto result = computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, line, column, sourceURL, errorInstance, prepareStackTrace.getObject()); + auto result = computeErrorInfoWithPrepareStackTrace(vm, globalObject, lexicalGlobalObject, stackTrace, errorInstance, prepareStackTrace.getObject()); globalObject->isInsideErrorPrepareStackTraceCallback = false; RELEASE_AND_RETURN(scope, result); } @@ -847,6 +855,8 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb } JSC::JSObject* errorObject = objectArg.asCell()->getObject(); + if (errorObject->hasImmutableProperties()) [[unlikely]] + return throwVMTypeError(lexicalGlobalObject, scope, JSC::ImmutablePropertyDefineError); JSC::JSValue caller = callFrame->argument(1); size_t stackTraceLimit = globalObject->stackTraceLimit().value_or(DEFAULT_ERROR_STACK_TRACE_LIMIT); @@ -897,6 +907,38 @@ JSC_DEFINE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace, (JSC::JSGlobalOb return JSC::JSValue::encode(JSC::jsUndefined()); } +JSC_DEFINE_HOST_FUNCTION(jsFunctionCaptureCallSites, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame)) +{ + auto* globalObject = defaultGlobalObject(lexicalGlobalObject); + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + + size_t frameLimit = callFrame->argument(0).toUInt32(lexicalGlobalObject); + RETURN_IF_EXCEPTION(scope, {}); + + WTF::Vector stackTrace; + JSCStackTrace::getFramesForCaller(vm, callFrame, globalObject, callFrame->argument(1), stackTrace, frameLimit); + RETURN_IF_EXCEPTION(scope, {}); + + JSC::MarkedArgumentBuffer protectedFrameCells; + protectFrameCells(protectedFrameCells, stackTrace); + if (protectedFrameCells.hasOverflowed()) [[unlikely]] { + throwOutOfMemoryError(lexicalGlobalObject, scope); + return {}; + } + + RELEASE_AND_RETURN(scope, JSValue::encode(createCallSitesArray(vm, globalObject, lexicalGlobalObject, stackTrace))); +} + +JSC_DEFINE_HOST_FUNCTION(jsFunctionSetStackTraceLimitOverride, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + std::optional previous = globalObject->stackTraceLimitOverride(); + JSValue limit = callFrame->argument(0); + bool isLimit = limit.isNumber() && !std::isnan(limit.asNumber()); + globalObject->setStackTraceLimitOverride(isLimit ? std::optional(clampToUnsigned(limit.asNumber())) : std::nullopt); + return JSValue::encode(previous ? jsNumber(*previous) : jsUndefined()); +} + } // namespace Bun namespace Zig { diff --git a/src/jsc/bindings/FormatStackTraceForJS.h b/src/jsc/bindings/FormatStackTraceForJS.h index feca960793c1..9d17b9971591 100644 --- a/src/jsc/bindings/FormatStackTraceForJS.h +++ b/src/jsc/bindings/FormatStackTraceForJS.h @@ -74,6 +74,10 @@ WTF::String formatStackTrace( JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncCaptureStackTrace); JSC_DECLARE_HOST_FUNCTION(errorConstructorFuncAppendStackTrace); JSC_DECLARE_HOST_FUNCTION(jsFunctionDefaultErrorPrepareStackTrace); +// (frameLimit, startFunction): call sites as Error.prepareStackTrace gets them, without using Error's properties. +JSC_DECLARE_HOST_FUNCTION(jsFunctionCaptureCallSites); +// (limit | undefined): JSGlobalObject::setStackTraceLimitOverride(), with a number read as Error.stackTraceLimit is. Returns the override there was, to be put back. +JSC_DECLARE_HOST_FUNCTION(jsFunctionSetStackTraceLimitOverride); // JSC Custom Accessors - error.stack getter/setter JSC_DECLARE_CUSTOM_GETTER(errorInstanceLazyStackCustomGetter); diff --git a/src/jsc/bindings/JSCommonJSModule.cpp b/src/jsc/bindings/JSCommonJSModule.cpp index aed824b8ae5a..a45a97f6505f 100644 --- a/src/jsc/bindings/JSCommonJSModule.cpp +++ b/src/jsc/bindings/JSCommonJSModule.cpp @@ -226,11 +226,14 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj // https://github.com/nodejs/node/blob/592c6907bfe1922f36240e9df076be1864c3d1bd/lib/internal/process/execution.js#L92 auto exports = moduleObject->exportsObject(); RETURN_IF_EXCEPTION(scope, {}); - globalObject->putDirect(vm, builtinNames(vm).exportsPublicName(), exports, 0); - globalObject->putDirect(vm, builtinNames(vm).requirePublicName(), requireFunction, 0); - Bun::putDirectNamed(vm, globalObject, "module"_s, moduleObject); - Bun::putDirectNamed(vm, globalObject, "__filename"_s, filename); - Bun::putDirectNamed(vm, globalObject, "__dirname"_s, dirname); + { + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); + globalObject->putDirect(vm, builtinNames(vm).exportsPublicName(), exports, 0); + globalObject->putDirect(vm, builtinNames(vm).requirePublicName(), requireFunction, 0); + Bun::putDirectNamed(vm, globalObject, "module"_s, moduleObject); + Bun::putDirectNamed(vm, globalObject, "__filename"_s, filename); + Bun::putDirectNamed(vm, globalObject, "__dirname"_s, dirname); + } // The 3-arg JSC::evaluate overload catches the exception into a // discarded NakedPtr (Completion.h), so a require() failure or any diff --git a/src/jsc/bindings/JSEnvironmentVariableMap.cpp b/src/jsc/bindings/JSEnvironmentVariableMap.cpp index 3f7172c3bb7a..47b6adbc0ace 100644 --- a/src/jsc/bindings/JSEnvironmentVariableMap.cpp +++ b/src/jsc/bindings/JSEnvironmentVariableMap.cpp @@ -963,6 +963,7 @@ RefPtr ensureSharedEnvStoreForWorker(Zig::GlobalObject* globalOb globalObject->m_processEnvObject.set(vm, globalObject, shared); auto envIdentifier = JSC::Identifier::fromString(vm, "env"_s); + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); // process.env may already be reified as an own property on the process object; // overwrite it so it resolves to the shared variant. diff --git a/src/jsc/bindings/JSMockFunction.cpp b/src/jsc/bindings/JSMockFunction.cpp index da1faeea8041..9b821dfb0399 100644 --- a/src/jsc/bindings/JSMockFunction.cpp +++ b/src/jsc/bindings/JSMockFunction.cpp @@ -1544,6 +1544,11 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb if (object->type() == JSC::JSType::GlobalProxyType) object = uncheckedDowncast(object)->target(); + if (object->hasImmutableProperties()) [[unlikely]] { + throwTypeError(globalObject, scope, "spyOn(target, prop) cannot replace a property of a target that has immutable properties"_s); + return {}; + } + // A WebAssembly GC object's Structure can't change, so the direct stores below would abort. if (object->type() == JSC::JSType::WebAssemblyGCObjectType) { throwTypeError(globalObject, scope, "spyOn(target, prop) does not support WebAssembly GC objects"_s); diff --git a/src/jsc/bindings/JSWrappingFunction.cpp b/src/jsc/bindings/JSWrappingFunction.cpp index eedd5538edb8..9242d7215806 100644 --- a/src/jsc/bindings/JSWrappingFunction.cpp +++ b/src/jsc/bindings/JSWrappingFunction.cpp @@ -62,6 +62,7 @@ extern "C" JSC::EncodedJSValue Bun__JSWrappingFunction__create( auto& vm = JSC::getVM(globalObject); JSC::JSValue wrappedFn = JSC::JSValue::decode(wrappedFnEncoded); auto function = JSWrappingFunction::create(vm, globalObject, symbolName, functionPointer, wrappedFn); + globalObject->didCreateBuiltin(vm, function); return JSC::JSValue::encode(function); } diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index 97978e72035a..49c3215a7e38 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -1120,6 +1120,8 @@ void NodeVMGlobalObject::finishCreation(JSC::VM& vm) if (parentGlobalObject->isAsyncContextTrackingEnabled()) setAsyncContextTrackingEnabled(true); } + + applyImmutableGlobalsOption(vm); } void NodeVMGlobalObject::destroy(JSCell* cell) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index 0f94b55fcfaa..575f0a3f926f 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -2548,6 +2548,7 @@ void GlobalObject::finishCreation(VM& vm) } }, { OBJECT_OFFSETOF(GlobalObject, m_JSDOMFileConstructor), [](const LazyProperty::Initializer& init) { JSObject* fileConstructor = Bun::createJSDOMFileConstructor(init.vm, init.owner); + init.owner->didCreateBuiltin(init.vm, fileConstructor); init.set(fileConstructor); } }, { OBJECT_OFFSETOF(GlobalObject, m_cryptoObject), [](const LazyProperty::Initializer& init) { @@ -2556,6 +2557,7 @@ void GlobalObject::finishCreation(VM& vm) // Node defines `subtle` on Crypto.prototype with a brand check, not on // the instance; the getter above enforces the brand. JSObject* prototype = crypto->getPrototypeDirect().getObject(); + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(init.vm); prototype->putDirectCustomAccessor( init.vm, Identifier::fromString(init.vm, "subtle"_s), @@ -2642,22 +2644,26 @@ void GlobalObject::finishCreation(VM& vm) init.set(toJS(init.owner, globalObject, globalObject->performance().get()).getObject()); } }, { OBJECT_OFFSETOF(GlobalObject, m_requireFunctionUnbound), [](const LazyProperty::Initializer& init) { + auto* prototype = RequireFunctionPrototype::create(init.owner); + init.owner->didCreateBuiltin(init.vm, prototype); init.set( JSFunction::create( init.vm, init.owner, commonJSRequireCodeGenerator(init.vm), init.owner->globalScope(), - JSFunction::createStructure(init.vm, init.owner, RequireFunctionPrototype::create(init.owner)))); + JSFunction::createStructure(init.vm, init.owner, prototype))); } }, { OBJECT_OFFSETOF(GlobalObject, m_requireResolveFunctionUnbound), [](const LazyProperty::Initializer& init) { + auto* prototype = RequireResolveFunctionPrototype::create(init.owner); + init.owner->didCreateBuiltin(init.vm, prototype); init.set( JSFunction::create( init.vm, init.owner, commonJSRequireResolveCodeGenerator(init.vm), init.owner->globalScope(), - JSFunction::createStructure(init.vm, init.owner, RequireResolveFunctionPrototype::create(init.owner)))); + JSFunction::createStructure(init.vm, init.owner, prototype))); } }, { OBJECT_OFFSETOF(GlobalObject, m_processBindingBuffer), [](const LazyProperty::Initializer& init) { init.set( @@ -2699,7 +2705,10 @@ void GlobalObject::finishCreation(VM& vm) static const LazyPropertyInit lazyFunctionInits[] = { { OBJECT_OFFSETOF(GlobalObject, m_errorConstructorPrepareStackTraceInternalValue), [](const LazyProperty::Initializer& init) { - init.set(JSFunction::create(init.vm, init.owner, 2, "ErrorPrepareStackTrace"_s, jsFunctionDefaultErrorPrepareStackTrace, ImplementationVisibility::Public)); + auto* function = JSFunction::create(init.vm, init.owner, 2, "ErrorPrepareStackTrace"_s, jsFunctionDefaultErrorPrepareStackTrace, ImplementationVisibility::Public); + init.set(function); + // A native getter of Error returns this function, so JSC does not find it among Error's properties. + init.owner->didCreateBuiltin(init.vm, function); } }, { OBJECT_OFFSETOF(GlobalObject, m_utilInspectStylizeNoColorFunction), [](const LazyProperty::Initializer& init) { init.set(JSC::JSFunction::create(init.vm, init.owner, utilInspectStylizeWithNoColorCodeGenerator(init.vm), init.owner)); @@ -2801,6 +2810,8 @@ void GlobalObject::finishCreation(VM& vm) addBuiltinGlobals(vm); + applyImmutableGlobalsOption(vm); + ASSERT(classInfo()); } @@ -2820,7 +2831,12 @@ JSC_DEFINE_CUSTOM_GETTER(getConsoleConstructor, (JSGlobalObject * globalObject, throwException(globalObject, scope, returnedException.get()); return {}; } - console->putDirect(vm, property, result, 0); + { + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); + console->putDirect(vm, property, result, 0); + } + if (result.isObject()) + globalObject->didCreateBuiltin(vm, asObject(result)); return JSValue::encode(result); } @@ -3182,7 +3198,10 @@ JSC_DEFINE_HOST_FUNCTION(functionJsGc, extern "C" [[ZIG_EXPORT(nothrow)]] void JSC__JSGlobalObject__addGc(JSC::JSGlobalObject* globalObject) { auto& vm = JSC::getVM(globalObject); - globalObject->putDirectNativeFunction(vm, globalObject, JSC::Identifier::fromString(vm, "gc"_s), 0, functionJsGc, ImplementationVisibility::Public, JSC::NoIntrinsic, PropertyAttribute::DontEnum | 0); + auto* function = JSC::JSFunction::create(vm, globalObject, 0, "gc"_s, functionJsGc, ImplementationVisibility::Public); + globalObject->didCreateBuiltin(vm, function); + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); + globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "gc"_s), function, PropertyAttribute::DontEnum | 0); } extern "C" [[ZIG_EXPORT(nothrow)]] double JSC__JSGlobalObject__jsDateNow(JSC::JSGlobalObject* globalObject) diff --git a/src/jsc/bindings/bindings.cpp b/src/jsc/bindings/bindings.cpp index 2266cc3cfd7d..630e2ab0e3c7 100644 --- a/src/jsc/bindings/bindings.cpp +++ b/src/jsc/bindings/bindings.cpp @@ -4176,8 +4176,10 @@ extern "C" [[ZIG_EXPORT(check_slow)]] void JSC__JSValue__putMayBeIndex(JSC::Enco JSC::Identifier identifier = Bun::toIdentifier(vm, *key); JSC::JSObject* object = JSC::JSValue::decode(target).asCell()->getObject(); - object->putDirectMayBeIndex(globalObject, JSC::PropertyName(identifier), JSC::JSValue::decode(value)); + bool didPut = object->putDirectMayBeIndex(globalObject, JSC::PropertyName(identifier), JSC::JSValue::decode(value)); RETURN_IF_EXCEPTION(scope, ); + if (!didPut) [[unlikely]] + throwTypeError(globalObject, scope, JSC::ReadonlyPropertyWriteError); } extern "C" bool JSC__JSValue__deleteProperty(JSC::EncodedJSValue target, JSC::JSGlobalObject* globalObject, const EncodedSlice* key) diff --git a/src/jsc/bindings/node/NodeTimers.cpp b/src/jsc/bindings/node/NodeTimers.cpp index 303f11942841..a98ae7555be9 100644 --- a/src/jsc/bindings/node/NodeTimers.cpp +++ b/src/jsc/bindings/node/NodeTimers.cpp @@ -278,6 +278,12 @@ JSValue createSetTimeoutFunction(VM& vm, JSObject* globalObject) return createTimerFunction(vm, globalObject, "setTimeout"_s, functionSetTimeout, setTimeoutPromisifyCustomGetter); } +extern "C" bool Bun__isSetTimeoutFunction(EncodedJSValue value) +{ + auto* function = dynamicDowncast(JSValue::decode(value)); + return function && function->isHostFunction() && function->nativeFunction() == functionSetTimeout; +} + JSValue createSetIntervalFunction(VM& vm, JSObject* globalObject) { return createTimerFunction(vm, globalObject, "setInterval"_s, functionSetInterval, setIntervalPromisifyCustomGetter); diff --git a/src/jsc/bindings/webcore/JSDOMGlobalObjectInlines.h b/src/jsc/bindings/webcore/JSDOMGlobalObjectInlines.h index 313b14aa2061..2eb49e4c367b 100644 --- a/src/jsc/bindings/webcore/JSDOMGlobalObjectInlines.h +++ b/src/jsc/bindings/webcore/JSDOMGlobalObjectInlines.h @@ -41,6 +41,7 @@ inline JSC::JSObject* getDOMConstructor(JSC::VM& vm, const Zig::GlobalObject& gl ASSERT(!globalObject.constructors().array()[static_cast(constructorID)].get()); Zig::GlobalObject& mutableGlobalObject = const_cast(globalObject); mutableGlobalObject.constructors().array()[static_cast(constructorID)].set(vm, &globalObject, constructor); + mutableGlobalObject.didCreateBuiltin(vm, constructor); return constructor; } diff --git a/src/jsc/modules/AbortControllerModuleModule.h b/src/jsc/modules/AbortControllerModuleModule.h index 0f97df6ddc82..750e0c94043a 100644 --- a/src/jsc/modules/AbortControllerModuleModule.h +++ b/src/jsc/modules/AbortControllerModuleModule.h @@ -37,6 +37,7 @@ inline void generateNativeModule_AbortControllerModule( exportValues.append(jsBoolean(true)); // https://github.com/mysticatea/abort-controller/blob/a935d38e09eb95d6b633a8c42fcceec9969e7b05/dist/abort-controller.js#L125 + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(vm); abortController->putDirect( vm, signalIdent, abortSignal, static_cast(PropertyAttribute::DontDelete)); diff --git a/src/jsc/modules/NodeModuleModule.cpp b/src/jsc/modules/NodeModuleModule.cpp index d70619b0f2b1..484dc132c1d7 100644 --- a/src/jsc/modules/NodeModuleModule.cpp +++ b/src/jsc/modules/NodeModuleModule.cpp @@ -613,6 +613,8 @@ JSC_DEFINE_CUSTOM_SETTER(setterRequireFunction, JSC::EncodedJSValue value, JSC::PropertyName propertyName)) { + // The property is Module.prototype's; the global object, which may be immutable, only stores its value. + JSC::AllowLazyMaterializationOfImmutableProperties allowMaterialization(globalObject->vm()); globalObject->putDirect(globalObject->vm(), WebCore::clientData(globalObject->vm()) ->builtinNames() diff --git a/src/runtime/test_runner/expect.rs b/src/runtime/test_runner/expect.rs index 9e7ca7a93d9f..803c0c868c09 100644 --- a/src/runtime/test_runner/expect.rs +++ b/src/runtime/test_runner/expect.rs @@ -1402,9 +1402,11 @@ impl Expect { ) }; + let allow_materialization = global_this.vm().allow_lazy_materialization_of_immutable_properties(); expect_proto.put_may_be_index(global_this, &matcher_name, wrapper_fn)?; expect_constructor.put_may_be_index(global_this, &matcher_name, wrapper_fn)?; expect_static_proto.put_may_be_index(global_this, &matcher_name, wrapper_fn)?; + drop(allow_materialization); } } diff --git a/src/runtime/test_runner/timers/FakeTimers.rs b/src/runtime/test_runner/timers/FakeTimers.rs index a030156e68dd..3d54bb647a82 100644 --- a/src/runtime/test_runner/timers/FakeTimers.rs +++ b/src/runtime/test_runner/timers/FakeTimers.rs @@ -11,6 +11,7 @@ use crate::timer::{ unsafe extern "C" { safe fn JSMock__setOverridenDateNow(global: &JSGlobalObject, value: f64); safe fn JSMock__getCurrentUnixTimeMs() -> f64; + safe fn Bun__isSetTimeoutFunction(value: JSValue) -> bool; } #[derive(Default)] @@ -323,11 +324,15 @@ fn set_fake_timer_marker(global: &JSGlobalObject, enabled: bool) -> JsResult<()> } // testing-library/react checks Object.hasOwnProperty.call(setTimeout, 'clock') // to detect if fake timers are enabled. + // Only Bun's own function: what a program put there may run JavaScript, which must not happen while the write is allowed. + let allow_materialization = Bun__isSetTimeoutFunction(set_timeout_fn) + .then(|| global.vm().allow_lazy_materialization_of_immutable_properties()); if enabled { bun_jsc::cpp::JSMock__defineFakeTimersMarker(global, set_timeout_fn)?; } else { set_timeout_fn.delete_property(global, "clock")?; } + drop(allow_materialization); Ok(()) } diff --git a/test/js/bun/jsc/immutable-globals.test.ts b/test/js/bun/jsc/immutable-globals.test.ts new file mode 100644 index 000000000000..941024e08f32 --- /dev/null +++ b/test/js/bun/jsc/immutable-globals.test.ts @@ -0,0 +1,946 @@ +import { describe, expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; + +// `*`, the only value, makes every built-in immutable. + +async function run(immutableGlobals: string | undefined, args: string[], cwd?: string, stdin?: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), ...args], + env: { ...bunEnv, BUN_JSC_immutableGlobals: immutableGlobals }, + cwd, + stdin: stdin === undefined ? "ignore" : Buffer.from(stdin), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + return { stdout, stderr, exitCode }; +} + +// Prints one line per expression, evaluated as strict code: the name of the error it threw, or "ok". +const attempts = (expressions: string[]) => ` + for (const expression of ${JSON.stringify(expressions)}) { + let outcome = "ok"; + try { + (0, eval)('"use strict";' + expression); + } catch (error) { + outcome = error.name; + } + console.log(outcome.padEnd(12) + expression); + } +`; + +describe.concurrent("BUN_JSC_immutableGlobals", () => { + test("`*` covers every built-in", async () => { + const { stdout, stderr, exitCode } = await run("*", [ + "-e", + attempts([ + // JavaScriptCore's + "Array.prototype.map = 1", + "Array.prototype.fresh = 1", + "delete Array.prototype.map", + "Array.prototype.map.call = 1", + "Object.defineProperty(Object.prototype, 'polluted', { value: 1 })", + "Object.getPrototypeOf(Uint8Array).prototype.map = 1", + "Object.getPrototypeOf([][Symbol.iterator]()).next = 1", + "Object.getPrototypeOf(async function () {}).fresh = 1", + "TypeError.prototype.name = 'x'", + "JSON.parse = 1", + "parseInt.fresh = 1", + "Intl.DateTimeFormat.prototype.format = 1", + // Bun's additions to them + "Error.captureStackTrace = 1", + "Error.captureStackTrace.fresh = 1", + "Error.stackTraceLimit = 1", + "Error.prepareStackTrace = () => {}", + "Error.prepareStackTrace.fresh = 1", + // Bun's + "Headers.prototype.get = 1", + "Headers.prototype.get.call = 1", + "Response.json = 1", + "Request.prototype.fresh = 1", + "Buffer.from = 1", + "Buffer.prototype.fresh = 1", + "Bun.serve = 1", + "Bun.serve.fresh = 1", + "Bun.fresh = 1", + "process.fresh = 1", + "process.exit = 1", + "fetch.fresh = 1", + // The global object itself: no global can be added, replaced or deleted + "globalThis.fresh = 1", + "globalThis.Array = 1", + "globalThis.JSON = {}", + "globalThis.fetch = 1", + "delete globalThis.Bun", + // A program's own objects can still shadow what they inherit from a built-in + "({}).toString = 1", + "(class extends Array {}).prototype.map = 1", + "new Error('x').name = 'y'", + "(function () {}).call = 1", + ]), + ]); + expect(stderr).toBe(""); + expect(stdout).toMatchInlineSnapshot(` + "TypeError Array.prototype.map = 1 + TypeError Array.prototype.fresh = 1 + TypeError delete Array.prototype.map + TypeError Array.prototype.map.call = 1 + TypeError Object.defineProperty(Object.prototype, 'polluted', { value: 1 }) + TypeError Object.getPrototypeOf(Uint8Array).prototype.map = 1 + TypeError Object.getPrototypeOf([][Symbol.iterator]()).next = 1 + TypeError Object.getPrototypeOf(async function () {}).fresh = 1 + TypeError TypeError.prototype.name = 'x' + TypeError JSON.parse = 1 + TypeError parseInt.fresh = 1 + TypeError Intl.DateTimeFormat.prototype.format = 1 + TypeError Error.captureStackTrace = 1 + TypeError Error.captureStackTrace.fresh = 1 + TypeError Error.stackTraceLimit = 1 + TypeError Error.prepareStackTrace = () => {} + TypeError Error.prepareStackTrace.fresh = 1 + TypeError Headers.prototype.get = 1 + TypeError Headers.prototype.get.call = 1 + TypeError Response.json = 1 + TypeError Request.prototype.fresh = 1 + TypeError Buffer.from = 1 + TypeError Buffer.prototype.fresh = 1 + TypeError Bun.serve = 1 + TypeError Bun.serve.fresh = 1 + TypeError Bun.fresh = 1 + TypeError process.fresh = 1 + TypeError process.exit = 1 + TypeError fetch.fresh = 1 + TypeError globalThis.fresh = 1 + TypeError globalThis.Array = 1 + TypeError globalThis.JSON = {} + TypeError globalThis.fetch = 1 + TypeError delete globalThis.Bun + ok ({}).toString = 1 + ok (class extends Array {}).prototype.map = 1 + ok new Error('x').name = 'y' + ok (function () {}).call = 1 + " + `); + expect(exitCode).toBe(0); + }); + + test.each(["Array", "-Array", "*,Array", "*,-Array", ",", ",*", "*,"])( + "any other value is an error: %s", + async value => { + const cmd = [bunExe(), "-e", "console.log('ran')"]; + await using proc = Bun.spawn({ + // The engine aborts, and CI counts a core file as a failure. + cmd: isWindows ? cmd : ["sh", "-c", 'ulimit -c 0; exec "$0" "$@"', ...cmd], + // Nor may it upload a crash report: the runner would pin it on the next test that fails. + env: { ...bunEnv, BUN_JSC_immutableGlobals: value, BUN_CRASH_REPORT_URL: "", BUN_ENABLE_CRASH_REPORTING: "0" }, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toContain("ERROR: the only value of immutableGlobals is '*': " + value); + expect(stdout).toBe(""); + expect(exitCode).not.toBe(0); + }, + ); + + test("without the option nothing is immutable", async () => { + const { stdout, exitCode } = await run(undefined, [ + "-e", + attempts([ + "Array.prototype.fresh = 1", + "Array.prototype.map.fresh = 1", + "Headers.prototype.fresh = 1", + "Bun.fresh = 1", + ]), + ]); + expect(stdout).toMatchInlineSnapshot(` + "ok Array.prototype.fresh = 1 + ok Array.prototype.map.fresh = 1 + ok Headers.prototype.fresh = 1 + ok Bun.fresh = 1 + " + `); + expect(exitCode).toBe(0); + }); + + test("a script cannot declare a global", async () => { + using dir = tempDir("immutable-globals-global-object", { + "index.mjs": + `import { runInThisContext } from "node:vm";` + + attempts([ + "var declared", + "function declared() {}", + "let declared", + "class declared {}", + "{ let inBlock }", + ]).replace(`(0, eval)('"use strict";' + expression)`, `runInThisContext(expression)`), + }); + const { stdout, stderr, exitCode } = await run("*", ["index.mjs"], String(dir)); + expect(stderr).toBe(""); + expect(stdout).toMatchInlineSnapshot(` + "TypeError var declared + TypeError function declared() {} + SyntaxError let declared + SyntaxError class declared {} + ok { let inBlock } + " + `); + expect(exitCode).toBe(0); + }); + + test("what Bun adds to the global object after it is created is there", async () => { + expect( + await run("*", [ + "--expose-gc", + "-e", + `console.log(typeof require("fs").readFileSync, typeof module, typeof fs.readFileSync, typeof gc, Object.isExtensible(gc))`, + ]), + ).toEqual({ + stdout: "function object function function false\n", + stderr: "", + exitCode: 0, + }); + }); + + test("`*` reaches every object and function reachable from a standard global", async () => { + const { stdout, stderr, exitCode } = await run("*", [ + "-e", + ` + const names = ["Object", "Function", "Array", "Number", "Boolean", "String", "Symbol", "BigInt", "Date", "Promise", "RegExp", + "Error", "AggregateError", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "SuppressedError", + "ArrayBuffer", "SharedArrayBuffer", "DataView", "Int8Array", "Uint8Array", "Uint8ClampedArray", "Int16Array", "Uint16Array", + "Int32Array", "Uint32Array", "Float16Array", "Float32Array", "Float64Array", "BigInt64Array", "BigUint64Array", + "Map", "Set", "WeakMap", "WeakSet", "WeakRef", "FinalizationRegistry", "Proxy", "Iterator", "DisposableStack", "AsyncDisposableStack", + "ShadowRealm", "Atomics", "JSON", "Math", "Reflect", "Intl", "Temporal", "WebAssembly", + "eval", "isFinite", "isNaN", "parseFloat", "parseInt", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape", "unescape"]; + const values = [function* () {}, async function () {}, async function* () {}, Object.getPrototypeOf((function* () {})()), + Object.getPrototypeOf((async function* () {})()), [][Symbol.iterator](), new Map().entries(), new Set().values(), ""[Symbol.iterator](), + "a".matchAll(/a/g), Iterator.from({ next() {} }), [].values().map(x => x), new Intl.Segmenter().segment("a"), + new Intl.Segmenter().segment("a")[Symbol.iterator](), Temporal.Now.instant(), new WebAssembly.Memory({ initial: 1 })]; + const seen = new Set(); + const queue = []; + const visit = (value, path) => { + if ((typeof value !== "object" && typeof value !== "function") || value === null || seen.has(value)) return; + seen.add(value); + queue.push([value, path]); + }; + for (const name of names) visit(globalThis[name], name); + values.forEach((value, index) => visit(Object.getPrototypeOf(value), "value " + index + ".__proto__")); + const extensible = []; + while (queue.length) { + const [object, path] = queue.pop(); + if (Object.isExtensible(object)) extensible.push(path); + visit(Object.getPrototypeOf(object), path + ".__proto__"); + for (const key of Reflect.ownKeys(object)) { + const descriptor = Reflect.getOwnPropertyDescriptor(object, key); + visit(descriptor.value, path + "." + String(key)); + visit(descriptor.get, "get " + path + "." + String(key)); + visit(descriptor.set, "set " + path + "." + String(key)); + } + } + console.log(JSON.stringify({ enough: seen.size > 1000, extensible })); + `, + ]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ enough: true, extensible: [] }); + expect(exitCode).toBe(0); + }); + + describe("what Bun creates on first use", () => { + // Prints which of these are still extensible. Most have no global name, and do not exist when the option is applied. + const fixture = /* js */ ` + import crypto from "node:crypto"; + import fs from "node:fs"; + import { File as FileOfNodeBuffer } from "node:buffer"; + import { createRequire } from "node:module"; + import net from "node:net"; + import "node:perf_hooks"; + import { MessagePort } from "node:worker_threads"; + const prototypeOf = Object.getPrototypeOf; + const require = createRequire(import.meta.url); + let nodeError; + try { fs.readFileSync(1.5); } catch (error) { nodeError = error; } + let element, sinkController; + await new HTMLRewriter().on("p", { element(e) { element = e; } }).transform(new Response("

")).text(); + const direct = new ReadableStream({ type: "direct", pull(c) { sinkController = c; c.close(); } }); + await Bun.write(process.argv[2], new Response(direct)); + const timeout = setTimeout(() => {}, 0), immediate = setImmediate(() => {}); + clearTimeout(timeout), clearImmediate(immediate); + const subprocess = Bun.spawn([process.execPath, "--version"], { stdout: "ignore" }); + await subprocess.exited; + const objects = { + "Headers iterator": prototypeOf(new Headers().entries()), + "Headers iterator next()": prototypeOf(new Headers().entries()).next, + "FormData iterator": prototypeOf(new FormData().entries()), + "URLSearchParams iterator": prototypeOf(new URLSearchParams().entries()), + "ReadableStream async iterator": prototypeOf(new ReadableStream().values()), + "HTMLRewriter element": prototypeOf(element), + "setTimeout() result": prototypeOf(timeout), + "setImmediate() result": prototypeOf(immediate), + "Bun.CookieMap iterator": prototypeOf(new Bun.CookieMap().entries()), + "sink controller": prototypeOf(sinkController), + "sink controller write()": prototypeOf(sinkController).write, + "Bun.file().writer()": prototypeOf(Bun.file(process.argv[2]).writer()), + "Bun.file().writer() constructor": prototypeOf(Bun.file(process.argv[2]).writer()).constructor, + "Bun.spawn() result": prototypeOf(subprocess), + "Bun.spawn() result kill()": prototypeOf(subprocess).kill, + "fs.statSync() result": prototypeOf(fs.statSync(process.argv[2])), + "fs.statfsSync() result": prototypeOf(fs.statfsSync(process.argv[2])), + "require": prototypeOf(require), + "require.resolve": prototypeOf(require.resolve), + "ERR_INVALID_ARG_TYPE": prototypeOf(nodeError), + "PublicKeyObject's parent": prototypeOf(prototypeOf(crypto.generateKeyPairSync("ed25519").publicKey)), + "node:buffer's File": FileOfNodeBuffer, + "node:buffer's File.prototype": FileOfNodeBuffer.prototype, + // Built-in modules add to these as they load. + "process's prototype": prototypeOf(process), + "crypto.ECDH.prototype": crypto.ECDH.prototype, + "crypto.ECDH.prototype.setPublicKey()": crypto.ECDH.prototype.setPublicKey, + "Performance.prototype.timerify()": Performance.prototype.timerify, + "MessagePort.prototype.close()": MessagePort.prototype.close, + "MessagePort.prototype's parent": prototypeOf(MessagePort.prototype), + "MessagePort.prototype's parent's on()": prototypeOf(MessagePort.prototype).on, + "net.SocketAddress.prototype": net.SocketAddress.prototype, + "net.BlockList.prototype": net.BlockList.prototype, + "Bun.password": Bun.password, + "Bun.password.hash()": Bun.password.hash, + "Bun.dns": Bun.dns, + "Bun.semver": Bun.semver, + "Bun.TOML": Bun.TOML, + "Bun.FFI": Bun.FFI, + "Bun.FFI.read": Bun.FFI.read, + "Bun.FFI.read.u8()": Bun.FFI.read.u8, + "process.versions": process.versions, + "process.report": process.report, + "process.config.variables": process.config.variables, + // These hold state, so they stay mutable. + "Bun.env": Bun.env, + "Bun.argv": Bun.argv, + "Bun.stdout": Bun.stdout, + "process.argv": process.argv, + }; + const stillExtensible = Object.keys(objects).filter(name => Object.isExtensible(objects[name])); + if (stillExtensible.length === Object.keys(objects).length) console.log("immutable: 0"); + else console.log(JSON.stringify(stillExtensible, null, 2)); + `; + test("is immutable", async () => { + using dir = tempDir("immutable-globals", { "fixture.mjs": fixture, "file.txt": "" }); + const { stdout, stderr, exitCode } = await run("*", ["fixture.mjs", "file.txt"], String(dir)); + expect({ stillExtensible: JSON.parse(stdout), stderr, exitCode }).toEqual({ + stillExtensible: ["Bun.env", "Bun.argv", "Bun.stdout", "process.argv"], + stderr: "", + exitCode: 0, + }); + }); + + test("is not without the option", async () => { + using dir = tempDir("immutable-globals", { "fixture.mjs": fixture, "file.txt": "" }); + const { stdout, stderr, exitCode } = await run(undefined, ["fixture.mjs", "file.txt"], String(dir)); + expect({ immutable: stdout.includes("immutable: 0\n"), stderr, exitCode }).toEqual({ + immutable: true, + stderr: "", + exitCode: 0, + }); + }); + + test("covers what belongs to a global nothing has read, and a constructor only its prototype leads to", async () => { + // The script reads no global of Bun's but Response and Request, so the others have not been created when it reaches their objects. + const script = ` + const prototypeOf = Object.getPrototypeOf; + const objects = { + "Headers iterator": prototypeOf(new Response("").headers.entries()), + "ReadableStream async iterator": prototypeOf(new Response("x").body.values()), + "AbortSignal.prototype": prototypeOf(new Request("http://localhost").signal), + "AbortSignal": prototypeOf(new Request("http://localhost").signal).constructor, + "AbortSignal.timeout": prototypeOf(new Request("http://localhost").signal).constructor.timeout, + }; + console.log(JSON.stringify(Object.keys(objects).filter(name => Object.isExtensible(objects[name])))); + `; + expect(await run("*", ["-e", script])).toEqual({ stdout: "[]\n", stderr: "", exitCode: 0 }); + }); + + test("crypto.subtle is there", async () => { + const script = `console.log(typeof crypto.subtle.digest, "subtle" in Crypto.prototype);`; + expect(await run("*", ["-e", script])).toEqual({ stdout: "function true\n", stderr: "", exitCode: 0 }); + }); + + test("covers a class that is first used internally", async () => { + // fetch() needs Response before anything reads globalThis.Response. + const script = ` + const response = await fetch("data:text/plain,hello"); + console.log(Object.isExtensible(Object.getPrototypeOf(response))); + `; + expect(await run("*", ["-e", script])).toEqual({ stdout: "false\n", stderr: "", exitCode: 0 }); + }); + + test("expect.extend() works", async () => { + using dir = tempDir("immutable-globals", { + "extend.test.js": ` + import { expect, test } from "bun:test"; + expect.extend({ toBeSeven: received => ({ pass: received === 7, message: () => "not seven" }) }); + test("custom matcher", () => { + expect(7).toBeSeven(); + expect(8).not.toBeSeven(); + expect({ n: 7 }).toEqual({ n: expect.toBeSeven() }); + expect(Object.isExtensible(expect.toBeSeven)).toBe(false); + }); + test("a name that is an index", () => { + expect(() => expect.extend({ 0: () => ({ pass: true }) })).toThrow(TypeError); + }); + `, + }); + const { stderr, exitCode } = await run("*", ["test", "extend.test.js"], String(dir)); + expect(stderr).toContain("2 pass"); + expect(exitCode).toBe(0); + }); + + test("Bun.$, Bun.sql, Bun.postgres and Bun.SQL are created once", async () => { + const script = ` + for (const name of ["$", "sql", "postgres", "SQL"]) console.log(name, Bun[name] === Bun[name], Object.isExtensible(Bun[name])); + try { + await Bun.$\`exit 3\`.quiet(); + } catch (error) { + console.log(error instanceof Bun.$.ShellError); + } + `; + expect(await run("*", ["-e", script])).toEqual({ + stdout: "$ true false\nsql true false\npostgres true false\nSQL true false\ntrue\n", + stderr: "", + exitCode: 0, + }); + }); + + test("jest.useFakeTimers() marks setTimeout", async () => { + using dir = tempDir("immutable-globals", { + "timers.test.js": ` + import { expect, jest, test } from "bun:test"; + test("marker", () => { + jest.useFakeTimers(); + expect(Object.hasOwn(setTimeout, "clock")).toBe(true); + jest.useRealTimers(); + expect(Object.hasOwn(setTimeout, "clock")).toBe(false); + expect(() => { "use strict"; setTimeout.clock = true; }).toThrow(TypeError); + }); + `, + }); + const { stderr, exitCode } = await run("*", ["test", "timers.test.js"], String(dir)); + expect(stderr).toContain("1 pass"); + expect(exitCode).toBe(0); + }); + + test("without the option, jest.useFakeTimers() calls the trap of a Proxy in setTimeout's place", async () => { + using dir = tempDir("immutable-globals", { + "timers.test.js": ` + import { expect, jest, test } from "bun:test"; + test("a Proxy", () => { + const trapped = []; + globalThis.setTimeout = new Proxy(function () {}, { + deleteProperty(target, key) { trapped.push(key); return true; }, + }); + jest.useFakeTimers(); + jest.useRealTimers(); + expect(trapped).toEqual(["clock"]); + }); + `, + }); + const { stderr, exitCode } = await run(undefined, ["test", "timers.test.js"], String(dir)); + expect(stderr).toContain("1 pass"); + expect(exitCode).toBe(0); + }); + + test("built-in modules add to native classes as they load", async () => { + const script = ` + import crypto from "node:crypto"; + import EventEmitter from "node:events"; + import { performance } from "node:perf_hooks"; + import { MessageChannel } from "node:worker_threads"; + import zlib from "node:zlib"; + import "bun:ffi"; + let calls = 0; + process.on("custom", () => calls++); + process.emit("custom"); + console.log(process instanceof EventEmitter, calls, typeof new MessageChannel().port1.on); + console.log(typeof performance.eventLoopUtilization, "callback" in Bun.FFI); + console.log(await new Promise(resolve => zlib.gzip("hello", (error, data) => resolve(zlib.gunzipSync(data).toString())))); + process.on("warning", warning => console.log(warning.code)); + crypto.createECDH("prime256v1").setPublicKey(crypto.createECDH("prime256v1").generateKeys()); + `; + const { stdout, exitCode } = await run("*", ["-e", script]); + expect({ stdout, exitCode }).toEqual({ + stdout: "true 1 function\nfunction false\nhello\nDEP0031\n", + exitCode: 0, + }); + }); + + // Counts the calls and records each one whose write to a built-in went through. + const attempt = ` + "use strict"; + const wrote = []; + let calls = 0; + function attempt(where) { + calls++; + try { + Array.prototype.fresh = 1; + wrote.push(where); + } catch {} + } + `; + + test("every built-in module loads, and a listener that is called meanwhile cannot write", async () => { + using dir = tempDir("immutable-globals-modules", { + "index.cjs": ` + ${attempt} + const failed = []; + let loading; + for (const event of ["newListener", "removeListener"]) process.on(event, name => attempt(loading + ": " + event + " " + String(name))); + for (loading of [...require("node:module").builtinModules, "node:quic", "bun:ffi", "bun:jsc", "bun:sqlite"]) { + try { + require(loading); + } catch (error) { + failed.push(loading + ": " + error.message); + } + } + console.log(JSON.stringify({ failed, called: calls > 0, wrote })); + `, + }); + const { stdout, exitCode } = await run("*", ["index.cjs"], String(dir)); + expect(JSON.parse(stdout)).toEqual({ failed: [], called: true, wrote: [] }); + expect(exitCode).toBe(0); + }); + + test("node:quic finishes its own class whatever a program puts in node:net's exports", async () => { + using dir = tempDir("immutable-globals-planted", { + "index.cjs": ` + const before = Reflect.ownKeys(Array.prototype).length; + require("node:net").SocketAddress = { prototype: Array.prototype }; + require("node:quic"); + console.log(Reflect.ownKeys(Array.prototype).length - before); + `, + }); + const { stdout, exitCode } = await run("*", ["index.cjs"], String(dir)); + expect(stdout).toBe("0\n"); + expect(exitCode).toBe(0); + }); + + test("Module.prototype.require can be replaced, as a module's exports can", async () => { + using dir = tempDir("immutable-globals-require", { + "index.cjs": ` + "use strict"; + const Module = require("node:module"), original = Module.prototype.require, ids = []; + Module.prototype.require = function (id) { ids.push(id); return original.call(this, id); }; + require("node:os"); + console.log(JSON.stringify(ids)); + `, + }); + expect(await run("*", ["index.cjs"], String(dir))).toEqual({ stdout: '["node:os"]\n', stderr: "", exitCode: 0 }); + }); + + test.each([ + ["bun:ffi", `Object.freeze(Bun.FFI);`, "Unable to delete property."], + [ + "node:perf_hooks", + `Object.preventExtensions(Performance.prototype);`, + "Attempting to define property on object that is not extensible.", + ], + ])("without the option, %s throws for a class it cannot finish", async (module, plant, message) => { + const script = ` + ${plant} + try { + require(${JSON.stringify(module)}); + } catch (error) { + console.log(error.message); + } + `; + const { stdout, exitCode } = await run(undefined, ["-e", script]); + expect({ stdout, exitCode }).toEqual({ stdout: message + "\n", exitCode: 0 }); + }); + }); + + test("applies to node:vm contexts, workers and ShadowRealms", async () => { + const { stdout, stderr, exitCode } = await run("*", [ + "-e", + ` + import vm from "node:vm"; + import { Worker as NodeWorker } from "node:worker_threads"; + const attempt = '"use strict"; try { Array.prototype.map.call = 1; "ok"; } catch (error) { error.name; }'; + console.log("vm", vm.runInNewContext(attempt), vm.runInContext(attempt, vm.createContext({}))); + console.log("vm global", vm.runInNewContext('"use strict"; globalThis.fresh = 1; var declared = 2; fresh + declared')); + console.log("ShadowRealm", new ShadowRealm().evaluate(attempt)); + const source = '"use strict"; const results = []; for (const f of [() => { Array.prototype.map = 1; }, () => { Headers.prototype.get = 1; }, () => { process.fresh = 1; }, () => { globalThis.fresh = 1; }]) { try { f(); results.push("ok"); } catch (error) { results.push(error.name); } }'; + const worker = new Worker("data:text/javascript," + encodeURIComponent(source + "postMessage(results.join());")); + const { promise, resolve, reject } = Promise.withResolvers(); + worker.onerror = event => reject(new Error(event.message)); + worker.onmessage = event => resolve(event.data); + console.log("worker", await promise); + worker.terminate(); + const nodeWorker = new NodeWorker(source + 'require("node:worker_threads").parentPort.postMessage(results.join());', { eval: true }); + console.log("node:worker_threads", await new Promise((resolve, reject) => nodeWorker.on("message", resolve).on("error", reject))); + `, + ]); + expect(stderr).toBe(""); + expect(stdout).toMatchInlineSnapshot(` + "vm TypeError TypeError + vm global 3 + ShadowRealm TypeError + worker TypeError,TypeError,TypeError,TypeError + node:worker_threads TypeError,TypeError,TypeError,TypeError + " + `); + expect(exitCode).toBe(0); + }); + + test("node:vm contexts can be created", async () => { + using dir = tempDir("immutable-globals-vm", { + "index.mjs": ` + import vm from "node:vm"; + console.log(vm.runInContext("typeof globalThis", vm.createContext(vm.constants.DONT_CONTEXTIFY))); + console.log(vm.runInContext("typeof globalThis", vm.createContext({}))); + console.log(vm.runInNewContext("typeof globalThis")); + `, + }); + expect(await run("*", ["index.mjs"], String(dir))).toEqual({ + stdout: "object\nobject\nobject\n", + stderr: "", + exitCode: 0, + }); + }); + + test("Bun's additions to console work when console is immutable", async () => { + const source = ` + console.write("written\\n"); + console.write("twice\\n"); + const { Console } = console; + console.log(Console === console.Console, new Console(process.stdout) instanceof console.Console); + for await (const line of console) { + console.log("first loop", line); + break; + } + for await (const line of console) console.log("second loop", line); + `; + const immutable = await run("*", ["-e", source], undefined, "a\nb\nc\n"); + expect(immutable).toEqual({ + stdout: "written\ntwice\ntrue true\nfirst loop a\nsecond loop b\nsecond loop c\n", + stderr: "", + exitCode: 0, + }); + expect(await run(undefined, ["-e", source], undefined, "a\nb\nc\n")).toEqual(immutable); + expect( + await run("*", ["-e", attempts(["console.log = 1", "console.write.fresh = 1", "console.Console.fresh = 1"])]), + ).toEqual({ + stdout: + "TypeError console.log = 1\nTypeError console.write.fresh = 1\nTypeError console.Console.fresh = 1\n", + stderr: "", + exitCode: 0, + }); + }); + + test("Error.captureStackTrace() throws for an object with immutable properties", async () => { + const { stdout, stderr, exitCode } = await run("*", [ + "-e", + attempts([ + "Error.captureStackTrace(Map.prototype)", + "Error.captureStackTrace(Map)", + "Error.captureStackTrace(Error.prototype)", + "Error.captureStackTrace({})", + "Error.captureStackTrace(new Error('x'))", + ]) + `console.log("stack" in Map.prototype, "stack" in Map, Object.hasOwn(Error.prototype, "stack"));`, + ]); + expect(stderr).toBe(""); + expect(stdout).toMatchInlineSnapshot(` + "TypeError Error.captureStackTrace(Map.prototype) + TypeError Error.captureStackTrace(Map) + TypeError Error.captureStackTrace(Error.prototype) + ok Error.captureStackTrace({}) + ok Error.captureStackTrace(new Error('x')) + false false false + " + `); + expect(exitCode).toBe(0); + }); + + test("setters that keep their value outside the object refuse every route when their owner is immutable", async () => { + const source = ` + (function () { + "use strict"; + const read = () => [Bun.main, process.argv, process.execArgv, process.title, process.debugPort, process.exitCode]; + const before = read(); + const routes = { + "owner.key = value": (owner, key, value) => { owner[key] = value; }, + "inheritor.key = value": (owner, key, value) => { Object.create(owner)[key] = value; }, + "proxy.key = value": (owner, key, value) => { new Proxy(owner, {})[key] = value; }, + "Reflect.set(inheritor, key, value, owner)": (owner, key, value) => { Reflect.set(Object.create(owner), key, value, owner); }, + "setter.call(owner, value)": (owner, key, value) => { Object.getOwnPropertyDescriptor(owner, key).set.call(owner, value); }, + "setter.call({}, value)": (owner, key, value) => { Object.getOwnPropertyDescriptor(owner, key).set.call({}, value); }, + }; + for (const route in routes) { + const outcomes = []; + for (const [owner, key, value] of [[Bun, "main", "/other.js"], [process, "argv", []], [process, "execArgv", []], [process, "title", "other"], [process, "debugPort", 1234], [process, "exitCode", 0]]) { + try { + routes[route](owner, key, value); + outcomes.push("ok"); + } catch (error) { + outcomes.push(error.name); + } + } + console.log(route + ": " + outcomes.join(" ")); + } + console.log("unchanged: " + read().map((value, index) => value === before[index]).join(" ")); + })(); + `; + const immutable = await run("*", ["-e", source]); + expect(immutable.stderr).toBe(""); + expect(immutable.stdout).toMatchInlineSnapshot(` + "owner.key = value: TypeError TypeError TypeError TypeError TypeError TypeError + inheritor.key = value: TypeError TypeError TypeError TypeError TypeError ok + proxy.key = value: TypeError TypeError TypeError TypeError TypeError ok + Reflect.set(inheritor, key, value, owner): TypeError TypeError TypeError TypeError TypeError TypeError + setter.call(owner, value): TypeError TypeError TypeError TypeError TypeError TypeError + setter.call({}, value): TypeError TypeError TypeError TypeError TypeError ok + unchanged: true true true true true true + " + `); + expect(immutable.exitCode).toBe(0); + + const mutable = await run(undefined, ["-e", source]); + expect(mutable.stderr).toBe(""); + expect(mutable.stdout).toMatchInlineSnapshot(` + "owner.key = value: ok ok ok ok ok ok + inheritor.key = value: ok ok ok ok ok ok + proxy.key = value: ok ok ok ok ok ok + Reflect.set(inheritor, key, value, owner): ok ok ok ok ok ok + setter.call(owner, value): ok ok ok ok ok ok + setter.call({}, value): ok ok ok ok ok ok + unchanged: false false false false false false + " + `); + expect(mutable.exitCode).toBe(0); + }); + + test("spyOn() throws for a target with immutable properties", async () => { + using dir = tempDir("immutable-globals-spyon", { + "spy.test.ts": ` + import { expect, spyOn, test } from "bun:test"; + test("spyOn", () => { + const map = Array.prototype.map; + expect(() => spyOn(Array.prototype, "map")).toThrow(TypeError); + expect(() => spyOn(Array.prototype, "fresh")).toThrow(TypeError); + expect(() => spyOn(Math, "max")).toThrow("immutable properties"); + expect(Array.prototype.map).toBe(map); + expect("fresh" in Array.prototype).toBe(false); + const list = [1, 2]; + const spy = spyOn(list, "map"); + list.map(x => x); + expect(spy).toHaveBeenCalledTimes(1); + }); + `, + }); + const { stderr, exitCode } = await run("*", ["test", "spy.test.ts"], String(dir)); + expect(stderr).toContain("1 pass"); + expect(stderr).toContain("0 fail"); + expect(exitCode).toBe(0); + }); + + // Each of these needs a stack trace limit or a stack format of its own for a moment. + const usesOfTheStack = ` + const util = require("node:util"); + const assert = require("node:assert"); + function outer() { return util.getCallSites(2); } + const sites = outer(); + console.log("getCallSites(2)", sites.length, sites[0].functionName, typeof sites[0].lineNumber, sites[0].scriptName === __filename); + console.log("getCallSites(1)", util.getCallSites(1).length); + process.on("warning", warning => console.log("warning", warning.code)); + require("node:url").parse("http://example.com/"); + let error; + try { + (function failing() { assert.strictEqual(1, 2); })(); + } catch (e) { + error = e; + } + console.log("AssertionError", error.name, error.code, error.stack.split("\\n").find(line => line.startsWith(" at ")).includes("failing")); + console.log("new AssertionError", new assert.AssertionError({ message: "m" }).stack.split("\\n")[0]); + try { + assert.deepStrictEqual({ get a() { return new Error("made while the message is rendered"); } }, { a: 2 }); + } catch (e) { + console.log(e.message.split("\\n").filter(line => line.includes("Getter")).join()); + } + console.log("afterwards", String(new Error("x").stack).includes(" at ")); + `; + + test("built-in modules that look at the stack work when Error is immutable", async () => { + using dir = tempDir("immutable-globals-stack", { "index.cjs": usesOfTheStack }); + const immutable = await run("*", ["index.cjs"], String(dir)); + const mutable = await run(undefined, ["index.cjs"], String(dir)); + expect(immutable.stdout).toMatchInlineSnapshot(` + "getCallSites(2) 2 outer number true + getCallSites(1) 1 + AssertionError AssertionError ERR_ASSERTION true + new AssertionError AssertionError [ERR_ASSERTION]: m + + a: [Getter] [Error: made while the message is rendered] + afterwards true + warning DEP0169 + " + `); + expect(mutable.stdout).toBe(immutable.stdout); + expect(immutable.exitCode).toBe(0); + }); + + test("built-in modules do not assign Error.stackTraceLimit or Error.prepareStackTrace", async () => { + using dir = tempDir("immutable-globals-writes", { + "index.cjs": ` + const writes = []; + for (const key of ["stackTraceLimit", "prepareStackTrace"]) { + let value = Error[key]; + Object.defineProperty(Error, key, { configurable: true, get: () => value, set(v) { writes.push(key); value = v; } }); + } + ${usesOfTheStack} + console.log("writes", JSON.stringify(writes)); + `, + }); + const { stdout, exitCode } = await run(undefined, ["index.cjs"], String(dir)); + expect(stdout).toContain("new AssertionError"); + expect(stdout).toContain("writes []\n"); + expect(exitCode).toBe(0); + }); + + test.each([ + [ + "the exports of abort-controller, and process._exiting", + ` + const controller = require("abort-controller"); + console.log(controller.default === AbortController, controller.AbortController === AbortController, controller.AbortSignal === AbortSignal); + process.on("exit", () => console.log(process._exiting)); + `, + "true true true\ntrue\n", + ], + [ + "process.env and Bun.env of a thread that shares them", + ` + const { Worker, SHARE_ENV, isMainThread } = require("node:worker_threads"); + if (isMainThread) { + void process.env.A, Bun.env.A; + const worker = new Worker(__filename, { env: SHARE_ENV }); + process.env.FROM_MAIN = "1"; + worker.on("exit", () => console.log(process.env.FROM_WORKER, Bun.env.FROM_WORKER)); + } else { + console.log(process.env.FROM_MAIN); + process.env.FROM_WORKER = "2"; + } + `, + "1\n2 2\n", + ], + [ + "process.channel of a cluster worker that disconnects", + ` + const cluster = require("node:cluster"); + if (cluster.isPrimary) { + const worker = cluster.fork(); + worker.on("online", () => worker.disconnect()); + worker.on("exit", code => console.log(code)); + } else { + process.on("disconnect", () => console.log(process.channel)); + } + `, + "null\n0\n", + ], + ])("what Bun itself writes on a built-in is written: %s", async (written, source, expected) => { + using dir = tempDir("immutable-globals-own-writes", { "index.cjs": source }); + const { stdout, exitCode } = await run("*", ["index.cjs"], String(dir)); + expect(stdout).toBe(expected); + expect(exitCode).toBe(0); + }); + + test("inspector.open(0) reports the port it bound in process.debugPort", async () => { + const { stdout, exitCode } = await run("*", [ + "-e", + ` + import inspector from "node:inspector"; + inspector.open(0); + console.log(process.debugPort === Number(new URL(inspector.url()).port)); + inspector.close(); + `, + ]); + expect(stdout).toBe("true\n"); + expect(exitCode).toBe(0); + }); + + test("--stack-trace-limit and --trace-exit work", async () => { + using dir = tempDir("immutable-globals-flags", { + "index.cjs": ` + function a() { return b(); } + function b() { return c(); } + function c() { return new Error().stack; } + console.log(Error.stackTraceLimit, a().split("\\n").filter(line => line.includes(" at ")).length); + (function leave() { process.exit(3); })(); + `, + }); + const { stdout, stderr, exitCode } = await run( + "*", + ["--stack-trace-limit=2", "--trace-exit", "index.cjs"], + String(dir), + ); + expect(stdout).toBe("2 2\n"); + expect(stderr).toContain("WARNING: Exited the environment with code 3"); + expect(exitCode).toBe(3); + }); + + test("--trace-env-js-stack works when Error is immutable", async () => { + using dir = tempDir("immutable-globals-trace-env", { + "index.cjs": `function reader() { return process.env.TRACED; }\nreader();`, + }); + const immutable = await run("*", ["--trace-env", "--trace-env-js-stack", "index.cjs"], String(dir)); + const mutable = await run(undefined, ["--trace-env", "--trace-env-js-stack", "index.cjs"], String(dir)); + const trace = (stderr: string) => + stderr.slice(stderr.indexOf('[--trace-env] get "TRACED"')).split("\n").slice(0, 2); + expect(trace(immutable.stderr)).toEqual(['[--trace-env] get "TRACED"', expect.stringContaining("at reader (")]); + expect(trace(mutable.stderr)).toEqual(trace(immutable.stderr)); + expect(immutable.exitCode).toBe(0); + }); + + test("--trace-env-js-stack takes a fractional Error.stackTraceLimit, and leaves the limit in force if it throws", async () => { + using dir = tempDir("immutable-globals-trace-env", { + "index.cjs": ` + function deep(n, f) { return n ? deep(n - 1, f) : f(); } + Error.stackTraceLimit = 4.5; + deep(20, () => process.env.TRACED); + Error.prepareStackTrace = (error, callSites) => callSites; + try { process.env.TRACED; } catch {} + Error.prepareStackTrace = undefined; + Error.stackTraceLimit = 1; + console.log(deep(20, () => new Error().stack).split("\\n").length - 1); + `, + }); + const { stdout, stderr, exitCode } = await run(undefined, ["--trace-env-js-stack", "index.cjs"], String(dir)); + expect(stderr.split("\n").filter(line => line.includes("at deep (")).length).toBe(4); + expect(stdout).toBe("1\n"); + expect(exitCode).toBe(0); + }); + + test("node:repl reports an await that SIGINT interrupts when Error is immutable", async () => { + const source = ` + const { PassThrough } = require("node:stream"); + const input = new PassThrough(), output = new PassThrough(); + let text = ""; + output.on("data", chunk => { + text += chunk; + if (text.includes("interrupted") && text.endsWith("> ")) { + console.log(JSON.stringify(text)); + process.exit(0); + } + }); + const repl = require("node:repl").start({ input, output, terminal: false, breakEvalOnSigint: true, useGlobal: false }); + repl.context.interrupt = () => repl.emit("SIGINT"); + input.write("await new Promise(() => { setImmediate(interrupt); })\\n"); + `; + using dir = tempDir("immutable-globals-repl", { "index.cjs": source }); + const [immutable, mutable] = await Promise.all([ + run("*", ["index.cjs"], String(dir)), + run(undefined, ["index.cjs"], String(dir)), + ]); + expect(immutable.stdout).toMatchInlineSnapshot(` + ""> Uncaught Error: Script execution was interrupted by \`SIGINT\`\\n> " + " + `); + expect(mutable.stdout).toBe(immutable.stdout); + expect(immutable.exitCode).toBe(0); + }); +});