From 958bb99b3260e3b7c16ec46ee4f40b3bb659ae63 Mon Sep 17 00:00:00 2001 From: NetWin Date: Mon, 19 Oct 2026 22:12:46 +0200 Subject: [PATCH] fix(maple): call the host's array methods past a script's own property Array methods ran the host's implementation as a.slice(), which an own property of the same name on the script's array shadowed, ending the run with an internal error. They now call it through Array.prototype. --- CONTRIBUTING.md | 1 + .../src/builtins/array/array-prototype.ts | 4 +- .../maple/src/builtins/array/array.spec.ts | 57 +++++++++++++++++++ .../maple/src/builtins/array/methods/at.ts | 2 +- .../src/builtins/array/methods/concat.ts | 2 +- .../src/builtins/array/methods/copy-within.ts | 4 +- .../src/builtins/array/methods/entries.ts | 2 +- .../maple/src/builtins/array/methods/fill.ts | 4 +- .../src/builtins/array/methods/includes.ts | 2 +- .../src/builtins/array/methods/index-of.ts | 2 +- .../maple/src/builtins/array/methods/keys.ts | 2 +- .../builtins/array/methods/last-index-of.ts | 4 +- .../maple/src/builtins/array/methods/pop.ts | 2 +- .../maple/src/builtins/array/methods/push.ts | 2 +- .../src/builtins/array/methods/reverse.ts | 3 +- .../maple/src/builtins/array/methods/shift.ts | 2 +- .../maple/src/builtins/array/methods/slice.ts | 3 +- .../src/builtins/array/methods/splice.ts | 7 ++- .../array/methods/to-locale-string.ts | 2 +- .../src/builtins/array/methods/to-reversed.ts | 2 +- .../src/builtins/array/methods/to-sorted.ts | 2 +- .../src/builtins/array/methods/to-spliced.ts | 12 +++- .../src/builtins/array/methods/unshift.ts | 2 +- .../src/builtins/array/methods/values.ts | 2 +- .../maple/src/builtins/array/methods/with.ts | 3 +- 25 files changed, 106 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2d28e96..0a9a2cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,7 @@ The declared exceptions are the guarded `performance.now()` behind `durationMs`, If the method takes a callback, invoke it through `context.call` so it runs under the budget. Never hand a script closure to a host implementation. 3. If the method needs no callback and cannot observe prototypes, it may delegate to the host implementation on a Maple-owned value (for example `Array.prototype.with`). + Call it through the prototype, as `Array.prototype.with.call(a, …)`, never as `a.with(…)`: a script can give its array an own property of that name. Wrap it in `guardHostErrors` when the spec requires a `RangeError`, `URIError` or `SyntaxError` for it, or when it can build a string or array longer than the host allows. 4. Return only sandbox values: primitives, Maple-created objects and arrays, or wrappers. Never return a native `Date`, `RegExp`, `Map`, `Set` or function. diff --git a/packages/maple/src/builtins/array/array-prototype.ts b/packages/maple/src/builtins/array/array-prototype.ts index 1d44c14..88ca227 100644 --- a/packages/maple/src/builtins/array/array-prototype.ts +++ b/packages/maple/src/builtins/array/array-prototype.ts @@ -40,7 +40,9 @@ import { ARRAY_METHOD_WITH } from './methods/with.ts'; /** * Callbacks always run through the interpreter, so they stay under the budget and the host never - * holds a script function. Methods that mutate refuse read-only input arrays. + * holds a script function. Methods that mutate refuse read-only input arrays. A method that runs + * the host's implementation calls it through `Array.prototype`, never as `a.slice()`, since a + * script can give its array an own property of that name. */ export const ARRAY_PROTOTYPE: Prototype> = new Prototype( 'Array', diff --git a/packages/maple/src/builtins/array/array.spec.ts b/packages/maple/src/builtins/array/array.spec.ts index 26a8391..a6bc66b 100644 --- a/packages/maple/src/builtins/array/array.spec.ts +++ b/packages/maple/src/builtins/array/array.spec.ts @@ -278,6 +278,63 @@ describe('array methods', () => { ).toEqual(['1-2', '1,2', '1,2', '1,2']); }); + it('call the host implementation even where the array has an own property of that name', () => { + expect(run('const a = [1, 2, 3]; a.at = 1; return [].at.call(a, -1);')).toBe(3); + expect(run('const a = [1]; a.concat = 1; return [].concat.call(a, [2], 3);')).toEqual([ + 1, 2, 3 + ]); + expect( + run('const a = [1, 2, 3]; a.copyWithin = 1; [].copyWithin.call(a, 0, 1); return [...a];') + ).toEqual([2, 3, 3]); + expect(run('const a = ["x"]; a.entries = 1; return [...[].entries.call(a)];')).toEqual([ + [0, 'x'] + ]); + expect(run('const a = [1, 2]; a.fill = 1; [].fill.call(a, 0); return [...a];')).toEqual([ + 0, 0 + ]); + expect(run('const a = [1, 2]; a.includes = 1; return [].includes.call(a, 2);')).toBe(true); + expect(run('const a = [1, 2]; a.indexOf = 1; return [].indexOf.call(a, 2);')).toBe(1); + expect(run('const a = ["x", "y"]; a.keys = 1; return [...[].keys.call(a)];')).toEqual([0, 1]); + expect( + run( + 'const a = [1, 2, 1]; a.lastIndexOf = 1; return [[].lastIndexOf.call(a, 1), [].lastIndexOf.call(a, 1, 1)];' + ) + ).toEqual([2, 0]); + expect(run('const a = [1, 2]; a.pop = 1; return [[].pop.call(a), [...a]];')).toEqual([ + 2, + [1] + ]); + expect(run('const a = [1]; a.push = 1; [].push.call(a, 2); return [...a];')).toEqual([1, 2]); + expect(run('const a = [1, 2]; a.reverse = 1; [].reverse.call(a); return [...a];')).toEqual([ + 2, 1 + ]); + expect(run('const a = [1, 2]; a.shift = 1; return [[].shift.call(a), [...a]];')).toEqual([ + 1, + [2] + ]); + expect(run('const a = [3, 1, 2]; a.slice = 1; return [].slice.call(a, 1);')).toEqual([1, 2]); + expect( + run( + 'const a = [1, 2, 3]; a.splice = 1; return [[].splice.call(a, 1), [].splice.call(a, 0, 1, 9), [...a]];' + ) + ).toEqual([[2, 3], [1], [9]]); + expect(run('const a = [1, 2]; a.map = 1; return [].toLocaleString.call(a);')).toBe('1,2'); + expect(run('const a = [1, 2]; a.toReversed = 1; return [].toReversed.call(a);')).toEqual([ + 2, 1 + ]); + expect(run('const a = [2, 1]; a.toSorted = 1; return [].toSorted.call(a);')).toEqual([1, 2]); + expect( + run( + 'const a = [1, 2, 3]; a.toSpliced = 1; return [[].toSpliced.call(a, 1), [].toSpliced.call(a, 0, 1, 9)];' + ) + ).toEqual([[1], [9, 2, 3]]); + expect(run('const a = [1]; a.unshift = 1; [].unshift.call(a, 0); return [...a];')).toEqual([ + 0, 1 + ]); + expect(run('const a = ["x"]; a.values = 1; return [...[].values.call(a)];')).toEqual(['x']); + expect(run('const a = [1, 2]; a.with = 1; return [].with.call(a, 0, 9);')).toEqual([9, 2]); + }); + it('mutate script arrays', () => { expect(run('const l = [1]; const n = l.push(2, 3); return [n, l];')).toEqual([3, [1, 2, 3]]); expect(run('const l = [1, 2]; return [l.pop(), l];')).toEqual([2, [1]]); diff --git a/packages/maple/src/builtins/array/methods/at.ts b/packages/maple/src/builtins/array/methods/at.ts index c7148ef..de0e567 100644 --- a/packages/maple/src/builtins/array/methods/at.ts +++ b/packages/maple/src/builtins/array/methods/at.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_AT: Method> = method( Array.prototype.at.length, - (a, [index]) => a.at(toNumber(index)) + (a, [index]) => Array.prototype.at.call(a, toNumber(index)) ); diff --git a/packages/maple/src/builtins/array/methods/concat.ts b/packages/maple/src/builtins/array/methods/concat.ts index 6fbd505..3f0852a 100644 --- a/packages/maple/src/builtins/array/methods/concat.ts +++ b/packages/maple/src/builtins/array/methods/concat.ts @@ -2,5 +2,5 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts' export const ARRAY_METHOD_CONCAT: Method> = method( Array.prototype.concat.length, - (a, items, { fail }) => guardHostErrors(fail, () => a.concat(...items)) + (a, items, { fail }) => guardHostErrors(fail, () => Array.prototype.concat.call(a, ...items)) ); diff --git a/packages/maple/src/builtins/array/methods/copy-within.ts b/packages/maple/src/builtins/array/methods/copy-within.ts index 76f827e..ead9184 100644 --- a/packages/maple/src/builtins/array/methods/copy-within.ts +++ b/packages/maple/src/builtins/array/methods/copy-within.ts @@ -9,6 +9,8 @@ export const ARRAY_METHOD_COPY_WITHIN: Method> = method( const to = toNumber(target); const from = toNumber(start); const until = optionalNumberArgument(end); - return guardIntegrityErrors(context.fail, a, () => a.copyWithin(to, from, until)); + return guardIntegrityErrors(context.fail, a, () => + Array.prototype.copyWithin.call(a, to, from, until) + ); } ); diff --git a/packages/maple/src/builtins/array/methods/entries.ts b/packages/maple/src/builtins/array/methods/entries.ts index 532410d..7b0f24b 100644 --- a/packages/maple/src/builtins/array/methods/entries.ts +++ b/packages/maple/src/builtins/array/methods/entries.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_ENTRIES: Method> = method( Array.prototype.entries.length, - (a) => new IteratorWrapper(a.entries()) + (a) => new IteratorWrapper(Array.prototype.entries.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/fill.ts b/packages/maple/src/builtins/array/methods/fill.ts index 6763801..7e8e098 100644 --- a/packages/maple/src/builtins/array/methods/fill.ts +++ b/packages/maple/src/builtins/array/methods/fill.ts @@ -7,6 +7,8 @@ export const ARRAY_METHOD_FILL: Method> = method( (a, [value, start, end], context) => { const from = optionalNumberArgument(start); const to = optionalNumberArgument(end); - return guardIntegrityErrors(context.fail, a, () => a.fill(value, from, to)); + return guardIntegrityErrors(context.fail, a, () => + Array.prototype.fill.call(a, value, from, to) + ); } ); diff --git a/packages/maple/src/builtins/array/methods/includes.ts b/packages/maple/src/builtins/array/methods/includes.ts index 56ed975..62c78f9 100644 --- a/packages/maple/src/builtins/array/methods/includes.ts +++ b/packages/maple/src/builtins/array/methods/includes.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_INCLUDES: Method> = method( Array.prototype.includes.length, - (a, [value, from]) => a.includes(value, optionalNumberArgument(from)) + (a, [value, from]) => Array.prototype.includes.call(a, value, optionalNumberArgument(from)) ); diff --git a/packages/maple/src/builtins/array/methods/index-of.ts b/packages/maple/src/builtins/array/methods/index-of.ts index aae6943..cdd5e23 100644 --- a/packages/maple/src/builtins/array/methods/index-of.ts +++ b/packages/maple/src/builtins/array/methods/index-of.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_INDEX_OF: Method> = method( Array.prototype.indexOf.length, - (a, [value, from]) => a.indexOf(value, optionalNumberArgument(from)) + (a, [value, from]) => Array.prototype.indexOf.call(a, value, optionalNumberArgument(from)) ); diff --git a/packages/maple/src/builtins/array/methods/keys.ts b/packages/maple/src/builtins/array/methods/keys.ts index 48b36b0..ec6105e 100644 --- a/packages/maple/src/builtins/array/methods/keys.ts +++ b/packages/maple/src/builtins/array/methods/keys.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_KEYS: Method> = method( Array.prototype.keys.length, - (a) => new IteratorWrapper(a.keys()) + (a) => new IteratorWrapper(Array.prototype.keys.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/last-index-of.ts b/packages/maple/src/builtins/array/methods/last-index-of.ts index 2140a6a..b804dd2 100644 --- a/packages/maple/src/builtins/array/methods/last-index-of.ts +++ b/packages/maple/src/builtins/array/methods/last-index-of.ts @@ -4,5 +4,7 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_LAST_INDEX_OF: Method> = method( Array.prototype.lastIndexOf.length, (a, args) => - args.length < 2 ? a.lastIndexOf(args[0]) : a.lastIndexOf(args[0], toNumber(args[1])) + args.length < 2 + ? Array.prototype.lastIndexOf.call(a, args[0]) + : Array.prototype.lastIndexOf.call(a, args[0], toNumber(args[1])) ); diff --git a/packages/maple/src/builtins/array/methods/pop.ts b/packages/maple/src/builtins/array/methods/pop.ts index 2b7c087..bd270a1 100644 --- a/packages/maple/src/builtins/array/methods/pop.ts +++ b/packages/maple/src/builtins/array/methods/pop.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_POP: Method> = method( Array.prototype.pop.length, - (a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.pop()) + (a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.pop.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/push.ts b/packages/maple/src/builtins/array/methods/push.ts index c8d3886..0c3029b 100644 --- a/packages/maple/src/builtins/array/methods/push.ts +++ b/packages/maple/src/builtins/array/methods/push.ts @@ -5,6 +5,6 @@ export const ARRAY_METHOD_PUSH: Method> = method( Array.prototype.push.length, (a, items, context) => guardHostErrors(context.fail, () => - guardIntegrityErrors(context.fail, a, () => a.push(...items)) + guardIntegrityErrors(context.fail, a, () => Array.prototype.push.call(a, ...items)) ) ); diff --git a/packages/maple/src/builtins/array/methods/reverse.ts b/packages/maple/src/builtins/array/methods/reverse.ts index 13272a3..a922235 100644 --- a/packages/maple/src/builtins/array/methods/reverse.ts +++ b/packages/maple/src/builtins/array/methods/reverse.ts @@ -5,6 +5,5 @@ export const ARRAY_METHOD_REVERSE: Method> = method( Array.prototype.reverse.length, (a, _args, context) => // The host's own reverse runs no script code and keeps holes where JavaScript puts them - // oxlint-disable-next-line unicorn/no-array-reverse - reversing in place is what this method does - guardIntegrityErrors(context.fail, a, () => a.reverse()) + guardIntegrityErrors(context.fail, a, () => Array.prototype.reverse.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/shift.ts b/packages/maple/src/builtins/array/methods/shift.ts index 62913a8..d8f5063 100644 --- a/packages/maple/src/builtins/array/methods/shift.ts +++ b/packages/maple/src/builtins/array/methods/shift.ts @@ -3,5 +3,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_SHIFT: Method> = method( Array.prototype.shift.length, - (a, _args, context) => guardIntegrityErrors(context.fail, a, () => a.shift()) + (a, _args, context) => guardIntegrityErrors(context.fail, a, () => Array.prototype.shift.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/slice.ts b/packages/maple/src/builtins/array/methods/slice.ts index feb0ff3..affc1b6 100644 --- a/packages/maple/src/builtins/array/methods/slice.ts +++ b/packages/maple/src/builtins/array/methods/slice.ts @@ -3,5 +3,6 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_SLICE: Method> = method( Array.prototype.slice.length, - (a, [start, end]) => a.slice(optionalNumberArgument(start), optionalNumberArgument(end)) + (a, [start, end]) => + Array.prototype.slice.call(a, optionalNumberArgument(start), optionalNumberArgument(end)) ); diff --git a/packages/maple/src/builtins/array/methods/splice.ts b/packages/maple/src/builtins/array/methods/splice.ts index bb01f4a..5a76e1c 100644 --- a/packages/maple/src/builtins/array/methods/splice.ts +++ b/packages/maple/src/builtins/array/methods/splice.ts @@ -2,12 +2,17 @@ import { guardIntegrityErrors } from '../../../values/guard-integrity-errors.ts' import { method, type Method } from '../../../values/native.ts'; import { splice } from '../utils/splice.ts'; +// `call` is typed by the last overload, which needs a delete count, so `splice(start)` gets its own +const spliceToEnd: (this: Array, start: number) => Array = Array.prototype.splice; + export const ARRAY_METHOD_SPLICE: Method> = method( Array.prototype.splice.length, (a, args, context) => { return splice(a, args, (start, count, items) => { return guardIntegrityErrors(context.fail, a, () => - count === undefined ? a.splice(start) : a.splice(start, count, ...items) + count === undefined + ? spliceToEnd.call(a, start) + : Array.prototype.splice.call(a, start, count, ...items) ); }); } diff --git a/packages/maple/src/builtins/array/methods/to-locale-string.ts b/packages/maple/src/builtins/array/methods/to-locale-string.ts index 8092a6d..f41da9e 100644 --- a/packages/maple/src/builtins/array/methods/to-locale-string.ts +++ b/packages/maple/src/builtins/array/methods/to-locale-string.ts @@ -8,7 +8,7 @@ export const ARRAY_METHOD_TO_LOCALE_STRING: Method> = method( (a, [locales, options], { fail }) => { const tags = localesArgument(locales); const intlOptions = intlOptionsArgument(options); - const mapped = a.map((element) => { + const mapped = Array.prototype.map.call(a, (element) => { if (element === null || element === undefined) { return ''; } diff --git a/packages/maple/src/builtins/array/methods/to-reversed.ts b/packages/maple/src/builtins/array/methods/to-reversed.ts index fa83e7b..c0681f1 100644 --- a/packages/maple/src/builtins/array/methods/to-reversed.ts +++ b/packages/maple/src/builtins/array/methods/to-reversed.ts @@ -2,5 +2,5 @@ import { method, type Method } from '../../../values/native.ts'; export const ARRAY_METHOD_TO_REVERSED: Method> = method( Array.prototype.toReversed.length, - (a) => a.toReversed() + (a) => Array.prototype.toReversed.call(a) ); diff --git a/packages/maple/src/builtins/array/methods/to-sorted.ts b/packages/maple/src/builtins/array/methods/to-sorted.ts index c0f85a9..f651d21 100644 --- a/packages/maple/src/builtins/array/methods/to-sorted.ts +++ b/packages/maple/src/builtins/array/methods/to-sorted.ts @@ -3,5 +3,5 @@ import { comparator } from '../utils/comparator.ts'; export const ARRAY_METHOD_TO_SORTED: Method> = method( Array.prototype.toSorted.length, - (a, [fn], context) => a.toSorted(comparator(fn, context)) + (a, [fn], context) => Array.prototype.toSorted.call(a, comparator(fn, context)) ); diff --git a/packages/maple/src/builtins/array/methods/to-spliced.ts b/packages/maple/src/builtins/array/methods/to-spliced.ts index 7f63459..e87658e 100644 --- a/packages/maple/src/builtins/array/methods/to-spliced.ts +++ b/packages/maple/src/builtins/array/methods/to-spliced.ts @@ -1,11 +1,21 @@ import { method, type Method } from '../../../values/native.ts'; import { splice } from '../utils/splice.ts'; +// `call` is typed by the last overload, which takes no items, so the one with items gets its own +const toSplicedWithItems: ( + this: ReadonlyArray, + start: number, + skipCount: number, + ...items: Array +) => Array = Array.prototype.toSpliced; + export const ARRAY_METHOD_TO_SPLICED: Method> = method( Array.prototype.toSpliced.length, (a, args) => { return splice(a, args, (start, count, items) => { - return count === undefined ? a.toSpliced(start) : a.toSpliced(start, count, ...items); + return count === undefined + ? Array.prototype.toSpliced.call(a, start) + : toSplicedWithItems.call(a, start, count, ...items); }); } ); diff --git a/packages/maple/src/builtins/array/methods/unshift.ts b/packages/maple/src/builtins/array/methods/unshift.ts index 2bdb6cc..81f5e5f 100644 --- a/packages/maple/src/builtins/array/methods/unshift.ts +++ b/packages/maple/src/builtins/array/methods/unshift.ts @@ -5,6 +5,6 @@ export const ARRAY_METHOD_UNSHIFT: Method> = method( Array.prototype.unshift.length, (a, items, context) => guardHostErrors(context.fail, () => - guardIntegrityErrors(context.fail, a, () => a.unshift(...items)) + guardIntegrityErrors(context.fail, a, () => Array.prototype.unshift.call(a, ...items)) ) ); diff --git a/packages/maple/src/builtins/array/methods/values.ts b/packages/maple/src/builtins/array/methods/values.ts index 670d718..1ee1c10 100644 --- a/packages/maple/src/builtins/array/methods/values.ts +++ b/packages/maple/src/builtins/array/methods/values.ts @@ -3,5 +3,5 @@ import { IteratorWrapper } from '../../../values/wrappers/iterator-wrapper.ts'; export const ARRAY_METHOD_VALUES: Method> = method( Array.prototype.values.length, - (a) => new IteratorWrapper(a.values()) + (a) => new IteratorWrapper(Array.prototype.values.call(a)) ); diff --git a/packages/maple/src/builtins/array/methods/with.ts b/packages/maple/src/builtins/array/methods/with.ts index 3ffc491..0bd5415 100644 --- a/packages/maple/src/builtins/array/methods/with.ts +++ b/packages/maple/src/builtins/array/methods/with.ts @@ -3,5 +3,6 @@ import { guardHostErrors, method, type Method } from '../../../values/native.ts' export const ARRAY_METHOD_WITH: Method> = method( Array.prototype.with.length, - (a, [index, value], { fail }) => guardHostErrors(fail, () => a.with(toNumber(index), value)) + (a, [index, value], { fail }) => + guardHostErrors(fail, () => Array.prototype.with.call(a, toNumber(index), value)) );