diff --git a/packages/code-runtime/code-runtime-worker/src/output-json.ts b/packages/code-runtime/code-runtime-worker/src/output-json.ts index cc668e5d99..06d56292bf 100644 --- a/packages/code-runtime/code-runtime-worker/src/output-json.ts +++ b/packages/code-runtime/code-runtime-worker/src/output-json.ts @@ -12,6 +12,7 @@ const intrinsicReflectApply = Reflect.apply as ( const intrinsicArrayIsArray = Array.isArray const IntrinsicBuffer = Buffer const intrinsicBufferByteLength = Reflect.get(Buffer, 'byteLength') as IntrinsicCallable +const intrinsicObjectCreate = Object.create const intrinsicObjectDefineProperty = Object.defineProperty const intrinsicObjectKeys = Object.keys const intrinsicString = String @@ -19,6 +20,22 @@ const intrinsicStringCharCodeAt = Reflect.get(String.prototype, 'charCodeAt') as const intrinsicStringCodePointAt = Reflect.get(String.prototype, 'codePointAt') as IntrinsicCallable const intrinsicStringSlice = Reflect.get(String.prototype, 'slice') as IntrinsicCallable +/** Build a data descriptor that cannot inherit model-defined accessor fields. */ +function dataDescriptor(value: unknown): PropertyDescriptor { + const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor + descriptor.value = value + return descriptor +} + +/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */ +function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void { + const descriptor = dataDescriptor(value) + descriptor.enumerable = true + descriptor.configurable = true + descriptor.writable = true + intrinsicObjectDefineProperty(target, key, descriptor) +} + /** UTF-8 byte length through the module-captured Node intrinsic. */ function byteLength(text: string): number { return intrinsicReflectApply(intrinsicBufferByteLength, IntrinsicBuffer, [text, 'utf8']) as number @@ -26,12 +43,7 @@ function byteLength(text: string): number { /** Append without consulting a model-mutated `Array.prototype`. */ function append(target: T[], value: T): void { - intrinsicObjectDefineProperty(target, target.length, { - value, - enumerable: true, - configurable: true, - writable: true, - }) + defineEnumerableDataProperty(target, target.length, value) } /** Pop without consulting a model-mutated `Array.prototype`. */ @@ -39,7 +51,7 @@ function takeLast(target: T[]): T | undefined { if (target.length === 0) return undefined const index = target.length - 1 const value = target[index] - intrinsicObjectDefineProperty(target, 'length', { value: index }) + intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index)) return value } diff --git a/packages/code-runtime/code-runtime-worker/src/worker-json.ts b/packages/code-runtime/code-runtime-worker/src/worker-json.ts index ac61d4eaab..b91005bb68 100644 --- a/packages/code-runtime/code-runtime-worker/src/worker-json.ts +++ b/packages/code-runtime/code-runtime-worker/src/worker-json.ts @@ -14,28 +14,42 @@ const intrinsicReflectApply = Reflect.get(Reflect, 'apply') as ( const IntrinsicError = Error const IntrinsicSet = Set const intrinsicArrayIsArray = Array.isArray +const intrinsicArrayPrototype = Array.prototype const intrinsicNumberIsFinite = Number.isFinite const intrinsicNumberIsSafeInteger = Number.isSafeInteger +const intrinsicObjectCreate = Object.create const intrinsicObjectDefineProperty = Object.defineProperty const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf const intrinsicObjectHasOwn = Object.hasOwn const intrinsicObjectIs = Object.is const intrinsicObjectKeys = Object.keys -const intrinsicObjectPropertyIsEnumerable = Reflect.get(Object.prototype, 'propertyIsEnumerable') as IntrinsicCallable +const intrinsicObjectPrototype = Object.prototype +const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, 'propertyIsEnumerable') as IntrinsicCallable const intrinsicReflectOwnKeys = Reflect.ownKeys const intrinsicSetAdd = Reflect.get(Set.prototype, 'add') as IntrinsicCallable const intrinsicSetDelete = Reflect.get(Set.prototype, 'delete') as IntrinsicCallable const intrinsicSetHas = Reflect.get(Set.prototype, 'has') as IntrinsicCallable +/** Build a data descriptor that cannot inherit model-defined accessor fields. */ +function dataDescriptor(value: unknown): PropertyDescriptor { + const descriptor = intrinsicObjectCreate(null) as PropertyDescriptor + descriptor.value = value + return descriptor +} + +/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */ +function defineEnumerableDataProperty(target: object, key: PropertyKey, value: unknown): void { + const descriptor = dataDescriptor(value) + descriptor.enumerable = true + descriptor.configurable = true + descriptor.writable = true + intrinsicObjectDefineProperty(target, key, descriptor) +} + /** Append without consulting a model-mutated `Array.prototype`. */ function append(target: T[], value: T): void { - intrinsicObjectDefineProperty(target, target.length, { - value, - enumerable: true, - configurable: true, - writable: true, - }) + defineEnumerableDataProperty(target, target.length, value) } /** Pop without consulting a model-mutated `Array.prototype`. */ @@ -43,7 +57,7 @@ function takeLast(target: T[]): T | undefined { if (target.length === 0) return undefined const index = target.length - 1 const value = target[index] - intrinsicObjectDefineProperty(target, 'length', { value: index }) + intrinsicObjectDefineProperty(target, 'length', dataDescriptor(index)) return value } @@ -76,26 +90,28 @@ function hasIntrinsicConstructor(prototype: object, name: 'Array' | 'Object'): b } } -/** Whether a candidate is one realm's intrinsic `Object.prototype`. */ -function isIntrinsicObjectPrototype(value: object): boolean { +/** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */ +function isForeignIntrinsicObjectPrototype(value: object): boolean { return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, 'Object') } /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */ function hasPlainArrayPrototype(value: unknown[]): boolean { const prototype: unknown = intrinsicObjectGetPrototypeOf(value) + if (prototype === intrinsicArrayPrototype) return true if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, 'Array')) return false const objectPrototype: unknown = intrinsicObjectGetPrototypeOf(prototype) return typeof objectPrototype === 'object' && objectPrototype !== null - && isIntrinsicObjectPrototype(objectPrototype) + && isForeignIntrinsicObjectPrototype(objectPrototype) } /** Whether an object is a plain or null-prototype record from any JavaScript realm. */ function hasPlainObjectPrototype(value: object): boolean { const prototype: unknown = intrinsicObjectGetPrototypeOf(value) return prototype === null - || typeof prototype === 'object' && isIntrinsicObjectPrototype(prototype) + || prototype === intrinsicObjectPrototype + || typeof prototype === 'object' && isForeignIntrinsicObjectPrototype(prototype) } /** Return every JSON-visible object key, or reject own data JSON would discard. */ @@ -135,19 +151,9 @@ export function snapshotCodeJsonValue(value: unknown): CodeJsonValue | undefined if (destination.kind === 'root') { root = item } else if (destination.kind === 'array') { - intrinsicObjectDefineProperty(destination.target, destination.index, { - value: item, - enumerable: true, - configurable: true, - writable: true, - }) + defineEnumerableDataProperty(destination.target, destination.index, item) } else { - intrinsicObjectDefineProperty(destination.target, destination.key, { - value: item, - enumerable: true, - configurable: true, - writable: true, - }) + defineEnumerableDataProperty(destination.target, destination.key, item) } } @@ -361,12 +367,7 @@ export function decodeWorkerJson(input: unknown): CodeJsonValue | undefined { const key = parent.keys[parent.index] /* v8 ignore next -- object frames are built from validated keys and their exact length. */ if (key === undefined) return false - intrinsicObjectDefineProperty(parent.target, key, { - value, - enumerable: true, - configurable: true, - writable: true, - }) + defineEnumerableDataProperty(parent.target, key, value) } parent.index += 1 return true diff --git a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts index 715dbb4121..ff75dc65b7 100644 --- a/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/runtime.spec.ts @@ -677,6 +677,8 @@ describe('WorkerCodeRuntime — hostile programs (real workers)', () => { stringPrototype.charCodeAt = stringPrototype.codePointAt = stringPrototype.slice = () => { throw new Error('mutated string method') }; Buffer.byteLength = () => 0; Function.prototype.toString = () => 'mutated'; + objectPrototype.get = () => undefined; + objectPrototype.constructor = arrayPrototype.constructor = null; globalThis.Array = globalThis.Buffer = globalThis.Function = globalThis.Number = globalThis.Object = globalThis.Reflect = globalThis.Set = globalThis.String = undefined; const echoed = await tools.echo({ request: ['€', 1] }); return { echoed, completion: { ok: true, amount: 42 } };