LibJS: Extend Array.prototype.shift() fast path to holey arrays

indexed_take_first() already memmoves elements down for both Packed and
Holey storage, but the caller at ArrayPrototype::shift() only entered
the fast path for Packed arrays. Holey arrays fell through to the
spec-literal per-element loop (has_property / get / set /
delete_property_or_throw), which is substantially slower.

Add a separate Holey predicate with the additional safety checks the
spec semantics require: default_prototype_chain_intact() (so
HasProperty on a hole doesn't escape to a poisoned prototype) and
extensible() (so set() on a hole slot doesn't create a new own
property on a non-extensible object). The existing Packed predicate
is left unchanged -- packed arrays don't need these checks because
every index in [0, size) is already an own data property.

Allows us to fail at Cloudflare Turnstile way much faster!
This commit is contained in:
Aliaksandr Kalenik
2026-04-23 18:45:03 +02:00
committed by Andreas Kling
parent ad7177eccb
commit bfbc3352b5
Notes: github-actions[bot] 2026-04-23 19:48:23 +00:00
2 changed files with 97 additions and 1 deletions

View File

@@ -47,3 +47,78 @@ test("throws if the array length is not writable", () => {
expect(1 in a).toBeFalse();
expect(a.length).toBe(2);
});
describe("holey arrays", () => {
test("shift on clean holey array with interior hole preserves hole", () => {
const a = [0, , 2];
const result = a.shift();
expect(result).toBe(0);
expect(a.length).toBe(2);
expect(0 in a).toBeFalse();
expect(a[0]).toBeUndefined();
expect(1 in a).toBeTrue();
expect(a[1]).toBe(2);
});
test("shift on clean holey array with leading hole returns undefined", () => {
const a = [, 1, 2];
const result = a.shift();
expect(result).toBeUndefined();
expect(a.length).toBe(2);
expect(a[0]).toBe(1);
expect(a[1]).toBe(2);
});
test("shift on clean holey array with trailing hole propagates it", () => {
const a = [0, 1, ,];
expect(a.length).toBe(3);
const result = a.shift();
expect(result).toBe(0);
expect(a.length).toBe(2);
expect(a[0]).toBe(1);
expect(1 in a).toBeFalse();
});
test("holey shift with prototype pollution follows spec", () => {
Array.prototype[1] = "polluted";
try {
const a = [0, , 2];
const result = a.shift();
expect(result).toBe(0);
// Spec: HasProperty(a, 1) is true via proto, Get returns "polluted",
// which is set as an own property at index 0.
expect(a[0]).toBe("polluted");
expect(0 in a).toBeTrue();
expect(a[1]).toBe(2);
expect(a.length).toBe(2);
} finally {
delete Array.prototype[1];
}
});
test("holey shift on non-extensible array throws TypeError", () => {
const a = [, 1];
Object.preventExtensions(a);
expect(() => a.shift()).toThrow(TypeError);
});
test("packed shift with prototype pollution still fast-paths correctly", () => {
Array.prototype[0] = "polluted";
try {
const a = [1, 2, 3];
expect(a.shift()).toBe(1);
expect(a).toEqual([2, 3]);
} finally {
delete Array.prototype[0];
}
});
test("packed shift on non-extensible array works (no new properties)", () => {
const a = [1, 2, 3];
Object.preventExtensions(a);
expect(a.shift()).toBe(1);
expect(a.length).toBe(2);
expect(a[0]).toBe(2);
expect(a[1]).toBe(3);
});
});