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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
336 changes: 336 additions & 0 deletions src/js/internal/freeze_intrinsics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
// Adapted from SES/Caja - Copyright (C) 2011 Google Inc.
// Copyright (C) 2018 Agoric
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// SPDX-License-Identifier: Apache-2.0
//
// Port of Node.js lib/internal/freeze_intrinsics.js. Runs from
// internal/process/pre_execution before any user code, so the bare global
// lookups below observe pristine intrinsics.
Comment thread
robobun marked this conversation as resolved.
Outdated

const ObjectDefineProperty = Object.defineProperty;
const ObjectFreeze = Object.freeze;
const ObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
const ObjectGetOwnPropertyNames = Object.getOwnPropertyNames;
const ObjectGetOwnPropertySymbols = Object.getOwnPropertySymbols;
const ObjectGetPrototypeOf = Object.getPrototypeOf;
const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty;
const ReflectOwnKeys = Reflect.ownKeys;
const SymbolIterator = Symbol.iterator;
const SymbolMatchAll = Symbol.matchAll;
const TypedArray = ObjectGetPrototypeOf(Uint8Array);

export default function freezeIntrinsics(): void {
const intrinsicPrototypes: unknown[] = [
// 20 Fundamental Objects
Object.prototype, // 20.1
Function.prototype, // 20.2
Boolean.prototype, // 20.3
Symbol.prototype, // 20.4

Error.prototype, // 20.5
AggregateError.prototype,
EvalError.prototype,
RangeError.prototype,
ReferenceError.prototype,
SyntaxError.prototype,
TypeError.prototype,
URIError.prototype,
Comment thread
robobun marked this conversation as resolved.

// 21 Numbers and Dates
Number.prototype, // 21.1
BigInt.prototype, // 21.2
Date.prototype, // 21.4

// 22 Text Processing
String.prototype, // 22.1
ObjectGetPrototypeOf(String.prototype[SymbolIterator]()), // 22.1.5 StringIteratorPrototype
RegExp.prototype, // 22.2
ObjectGetPrototypeOf(new RegExp("e")[SymbolMatchAll]("")), // 22.2.7 RegExpStringIteratorPrototype

// 23 Indexed Collections
Array.prototype, // 23.1
ObjectGetPrototypeOf(Array.prototype[SymbolIterator]()), // 23.1.5 ArrayIteratorPrototype
TypedArray.prototype, // 23.2
Int8Array.prototype,
Uint8Array.prototype,
Uint8ClampedArray.prototype,
Int16Array.prototype,
Uint16Array.prototype,
Int32Array.prototype,
Uint32Array.prototype,
Float32Array.prototype,
Float64Array.prototype,
BigInt64Array.prototype,
BigUint64Array.prototype,

Check warning on line 76 in src/js/internal/freeze_intrinsics.ts

View check run for this annotation

Claude / Claude Code Review

--frozen-intrinsics leaves Float16Array unfrozen

The typed-array lists omit `Float16Array`, which Bun/JSC exposes as a global (unlike Node.js, the port source) — so under `--frozen-intrinsics`, `Float16Array` and `Float16Array.prototype` are never frozen and `Float16Array.prototype.map = evil` succeeds while the identical assignment on `Float32Array.prototype` throws. Add `Float16Array.prototype` / `Float16Array` alongside `Float32Array` in both the `intrinsicPrototypes` list here and the `intrinsics` list at ~line 156 (optionally guarded like
Comment thread
claude[bot] marked this conversation as resolved.

// 24 Keyed Collections
Map.prototype, // 24.1
ObjectGetPrototypeOf(new Map()[SymbolIterator]()), // 24.1.5 MapIteratorPrototype
Set.prototype, // 24.2
ObjectGetPrototypeOf(new Set()[SymbolIterator]()), // 24.2.5 SetIteratorPrototype
WeakMap.prototype, // 24.3
WeakSet.prototype, // 24.4

// 25 Structured Data
ArrayBuffer.prototype, // 25.1
DataView.prototype, // 25.3

// 26 Managing Memory
WeakRef.prototype, // 26.1
FinalizationRegistry.prototype, // 26.2

// 27 Control Abstraction Objects
ObjectGetPrototypeOf(ObjectGetPrototypeOf(Array.prototype[SymbolIterator]())), // 27.1.2 IteratorPrototype
ObjectGetPrototypeOf(ObjectGetPrototypeOf(ObjectGetPrototypeOf((async function* () {})()))), // 27.1.3 AsyncIteratorPrototype
Promise.prototype, // 27.2

// Other APIs / Web Compatibility
(console as { Console?: { prototype: object } }).Console?.prototype,
];

const intrinsics: unknown[] = [
// 10.2.4.1 ThrowTypeError
ObjectGetOwnPropertyDescriptor(Function.prototype, "caller")?.get,

// 19 The Global Object
// 19.2 Function Properties of the Global Object
Comment thread
robobun marked this conversation as resolved.
eval,
isFinite,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,

// 20 Fundamental Objects
Object,
Function,
Boolean,
Symbol,
Comment thread
robobun marked this conversation as resolved.
Error,
Comment thread
robobun marked this conversation as resolved.
AggregateError,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,

// 21 Numbers and Dates
Number,
BigInt,
Math,
Date,

// 22 Text Processing
String,
ObjectGetPrototypeOf(String.prototype[SymbolIterator]()),
RegExp,
ObjectGetPrototypeOf(new RegExp("e")[SymbolMatchAll]("")),

// 23 Indexed Collections
Array,
ObjectGetPrototypeOf(Array.prototype[SymbolIterator]()),
TypedArray,
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array,

// 24 Keyed Collections
Map,
ObjectGetPrototypeOf(new Map()[SymbolIterator]()),
Set,
ObjectGetPrototypeOf(new Set()[SymbolIterator]()),
WeakMap,
WeakSet,

// 25 Structured Data
ArrayBuffer,
DataView,
Atomics,
JSON,

// 26 Managing Memory
WeakRef,
FinalizationRegistry,

// 27 Control Abstraction Objects
ObjectGetPrototypeOf(ObjectGetPrototypeOf(Array.prototype[SymbolIterator]())), // IteratorPrototype
ObjectGetPrototypeOf(ObjectGetPrototypeOf(ObjectGetPrototypeOf((async function* () {})()))), // AsyncIteratorPrototype
Promise,
Comment thread
robobun marked this conversation as resolved.
ObjectGetPrototypeOf(function* () {}), // GeneratorFunction
ObjectGetPrototypeOf(async function* () {}), // AsyncGeneratorFunction
ObjectGetPrototypeOf(async function () {}), // AsyncFunction

// 28 Reflection
Reflect,
Proxy,

// B.2.1
escape,
unescape,

// Other APIs / Web Compatibility
clearImmediate,
clearInterval,
clearTimeout,
setImmediate,
setInterval,
setTimeout,
console,
];

if (typeof SharedArrayBuffer !== "undefined") {
intrinsicPrototypes.push(SharedArrayBuffer.prototype);
intrinsics.push(SharedArrayBuffer);
}
if (typeof WebAssembly !== "undefined") {
intrinsicPrototypes.push(
WebAssembly.Module.prototype,
WebAssembly.Instance.prototype,
WebAssembly.Table.prototype,
WebAssembly.Memory.prototype,
WebAssembly.CompileError.prototype,
WebAssembly.LinkError.prototype,
WebAssembly.RuntimeError.prototype,
);
intrinsics.push(WebAssembly);
}
if (typeof Intl !== "undefined") {
intrinsicPrototypes.push(
Intl.Collator.prototype,
Intl.DateTimeFormat.prototype,
Intl.ListFormat.prototype,
Intl.NumberFormat.prototype,
Intl.PluralRules.prototype,
Intl.RelativeTimeFormat.prototype,
);
intrinsics.push(Intl);
}

for (let i = 0; i < intrinsicPrototypes.length; i++) enableDerivedOverrides(intrinsicPrototypes[i]);

const frozenSet = new WeakSet<object>();
// Node.js's global `console` exposes `_stdout`/`_stderr` behind getters, so
// its deep-freeze stops at the accessor functions. Bun's are own data
// properties, which would pull the live stream instances (and through them
// every stream prototype) into the freeze set. Seed them as already-visited
// so traversal stops at the stream boundary like Node.js.
Comment thread
robobun marked this conversation as resolved.
Outdated
const consoleObj = console as { _stdout?: object; _stderr?: object };
if (consoleObj._stdout) frozenSet.add(consoleObj._stdout);

Check failure on line 242 in src/js/internal/freeze_intrinsics.ts

View workflow job for this annotation

GitHub Actions / Lint JavaScript

bun(no-duplicate-conditional-property-access)

`consoleObj._stdout` is read in the `if` condition and again in the body. Read it into a local first (e.g. `const { _stdout } = consoleObj`) so the property is only accessed once.
if (consoleObj._stderr) frozenSet.add(consoleObj._stderr);

Check failure on line 243 in src/js/internal/freeze_intrinsics.ts

View workflow job for this annotation

GitHub Actions / Lint JavaScript

bun(no-duplicate-conditional-property-access)

`consoleObj._stderr` is read in the `if` condition and again in the body. Read it into a local first (e.g. `const { _stderr } = consoleObj`) so the property is only accessed once.
for (let i = 0; i < intrinsics.length; i++) deepFreeze(intrinsics[i]);

// 19.1 Value Properties of the Global Object
ObjectDefineProperty(globalThis, "globalThis", {
__proto__: null,
configurable: false,
writable: false,
value: globalThis,
} as PropertyDescriptor);

function deepFreeze(root: unknown): void {
const freezingSet = new Set<object>();

function enqueue(val: unknown): void {
if (Object(val) !== val) return;
if (frozenSet.has(val as object) || freezingSet.has(val as object)) return;
freezingSet.add(val as object);
}

function doFreeze(obj: object): void {
ObjectFreeze(obj);
const proto = ObjectGetPrototypeOf(obj);
const descs = ObjectGetOwnPropertyDescriptors(obj);
enqueue(proto);
const keys = ReflectOwnKeys(descs);
for (let i = 0; i < keys.length; i++) {
const desc = descs[keys[i] as string];
if (ObjectPrototypeHasOwnProperty.$call(desc, "value")) {
enqueue(desc.value);
} else {
enqueue(desc.get);
enqueue(desc.set);
}
}
}

enqueue(root);
// New values added before forEach() has finished will be visited.
freezingSet.forEach(doFreeze);
freezingSet.forEach(frozenSet.add, frozenSet);
Comment thread
robobun marked this conversation as resolved.
Outdated
}

// ES5 specified that simple assignment to a non-existent own property must
// fail if it would override an inherited non-writable data property. Replace
// each configurable own data property on the listed prototypes with an
// accessor that preserves that assignment-to-derived-object behaviour after
// freezing.
Comment thread
robobun marked this conversation as resolved.
Outdated
function enableDerivedOverride(obj: object, prop: PropertyKey, desc: PropertyDescriptor): void {
if (!ObjectPrototypeHasOwnProperty.$call(desc, "value") || !desc.configurable) return;
const value = desc.value;

function getter(this: unknown) {
return value;
}
(getter as { value?: unknown }).value = value;
Comment thread
claude[bot] marked this conversation as resolved.

function setter(this: unknown, newValue: unknown) {
if (obj === this) {
throw new TypeError(`Cannot assign to read only property '${String(prop)}' of object '${obj}'`);
}
if (ObjectPrototypeHasOwnProperty.$call(this, prop)) {
(this as Record<PropertyKey, unknown>)[prop as string] = newValue;
} else {
ObjectDefineProperty(this as object, prop, {
__proto__: null,
value: newValue,
writable: true,
enumerable: true,
configurable: true,
} as PropertyDescriptor);
}
}

ObjectDefineProperty(obj, prop, {
__proto__: null,
get: getter,
set: setter,
enumerable: desc.enumerable,
configurable: desc.configurable,
} as PropertyDescriptor);
}

function enableDerivedOverrides(obj: unknown): void {
if (!obj) return;
const descs = ObjectGetOwnPropertyDescriptors(obj);
if (!descs) return;
const names = ObjectGetOwnPropertyNames(obj);
for (let i = 0; i < names.length; i++) enableDerivedOverride(obj as object, names[i], descs[names[i]]);
const syms = ObjectGetOwnPropertySymbols(obj);
for (let i = 0; i < syms.length; i++)
enableDerivedOverride(obj as object, syms[i], descs[syms[i] as unknown as string]);
}
}
11 changes: 11 additions & 0 deletions src/js/internal/process/pre_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@
let traceEnv = false;
let traceEnvJsStack = false;
let traceExit = false;
let frozenIntrinsics = false;

for (let i = 0; i < execArgv.length; i++) {
const arg = execArgv[i];
Expand Down Expand Up @@ -292,6 +293,8 @@
// keys its native-stack assertions on that string appearing.
} else if (arg === "--trace-exit") {
traceExit = true;
} else if (arg === "--frozen-intrinsics") {
frozenIntrinsics = true;
}
}

Expand Down Expand Up @@ -333,6 +336,14 @@
envTracePrintJsStack = traceEnvJsStack;
installEnvTracing();
}
// Last: nothing after this may assign to an intrinsic prototype property.
if (frozenIntrinsics) {
process.emitWarning(
"Frozen intristics is an experimental feature and might change at any time",
"ExperimentalWarning",

Check warning on line 343 in src/js/internal/process/pre_execution.ts

View check run for this annotation

Claude / Claude Code Review

Typo in --frozen-intrinsics ExperimentalWarning: 'intristics' should be 'intrinsics'

Typo: `"Frozen intristics"` → `"Frozen intrinsics"`. Node.js's `emitExperimentalWarning('Frozen intrinsics')` produces the correctly-spelled message, and the PR description claims to emit "the same ExperimentalWarning as Node.js". The test only asserts `.toContain("ExperimentalWarning")` and `.toContain("experimental feature")`, so the typo passes CI unnoticed.
Comment thread
robobun marked this conversation as resolved.
Outdated
);
require("internal/freeze_intrinsics")();
}
}

export default {};
2 changes: 1 addition & 1 deletion src/js/node/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ var defaultMaxListeners = 10;

// EventEmitter must be a standard function because some old code will do weird tricks like `EventEmitter.$apply(this)`.
function EventEmitter(opts) {
if (this._events === undefined || this._events === this.__proto__._events) {
if (this._events === undefined || this._events === Object.getPrototypeOf(this)._events) {
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
this._events = Object.create(null);
this._eventsCount = 0;
this[kShapeMode] = false;
Expand Down
4 changes: 3 additions & 1 deletion src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2304,7 +2304,9 @@ impl VirtualMachine {
// execArgv. (The JS side re-reads `process.execArgv`, so an explicit
// empty execArgv under a traced parent stays a no-op there.)
fn is_bootstrap_flag(arg: &[u8]) -> bool {
arg.starts_with(b"--trace-") || arg.starts_with(b"--stack-trace-limit")
arg.starts_with(b"--trace-")
|| arg.starts_with(b"--stack-trace-limit")
|| arg == b"--frozen-intrinsics"
Comment thread
robobun marked this conversation as resolved.
Outdated
}
let needs_pre_execution = bun_core::argv().into_iter().any(is_bootstrap_flag)
|| self
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ const errors: ErrorCodeMapping = [
["ERR_PARSE_ARGS_INVALID_OPTION_VALUE", TypeError],
["ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL", TypeError],
["ERR_PARSE_ARGS_UNKNOWN_OPTION", TypeError],
["ERR_PROTO_ACCESS", Error],
["ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2", Error, "PostgresError"],
["ERR_POSTGRES_CONNECTION_CLOSED", Error, "PostgresError"],
["ERR_POSTGRES_CONNECTION_TIMEOUT", Error, "PostgresError"],
Expand Down
Loading
Loading