tc39 / ecma262

Status, process, and documents for ECMA-262
https://tc39.es/ecma262/
Other
14.98k stars 1.28k forks source link

TypedArray exotic objects need [[PreventExtensions]] #3385

Open anba opened 1 month ago

anba commented 1 month ago

TypedArray exotic objects need a [[PreventExtensions]] internal method to handle resizable TypedArrays:

js> var ab = new ArrayBuffer(0, {maxByteLength: 10})
js> var ta = new Int8Array(ab)
js> typeof Object.getOwnPropertyDescriptor(ta, 0)
"undefined"
js> void Object.preventExtensions(ta)
js> Object.isExtensible(ta)
false
js> ab.resize(10)
js> Object.getOwnPropertyDescriptor(ta, 0)
({value:0, writable:true, enumerable:true, configurable:true})

CC @syg

syg commented 4 weeks ago

Good catch. I agree with your suggested solution, that TAs need a [[PreventExtensions]] override that returns false, similar to how you can't freeze TAs today.

bakkot commented 4 weeks ago

Only those backed by resizable buffers, or all TAs?

If the former, do the become non-extensible when the backing buffer is detached?

syg commented 4 weeks ago

Only those backed by resizable buffers, or all TAs?

It's technically only needed for those backed by resizable buffers, since those backed by fixed-length TAs never get new properties in practice even when Object.isExtensible() returns true.

If the former, do the become non-extensible when the backing buffer is detached?

I don't think so, by analogy with TAs backed by fixed-length buffers returning true for Object.isExtensible() when their backing store is detached. The extensibility invariant only cares about properties being added, not deleted, right?

syg commented 4 weeks ago

Thinking through this some more, I think the most "feels correct" solution is that the [[PreventExtensions]] override needs to change the value of a TypedArray's [[ArrayLength]] from ~auto~ to the length at the time of calling [[PreventExtensions]], if it's ~auto~. If it's not ~auto~, there's no way to for new properties to manifest, so there's nothing to do.

I think this is implementable without performance penalty, but that's an open question.

bakkot commented 4 weeks ago

I don't think so, by analogy with TAs backed by fixed-length buffers returning true for Object.isExtensible() when their backing store is detached.

Sorry, I meant for those which had Object.preventExtensions called on them. Like:

let ta = new Uint8Array(new ArrayBuffer(8, { maxByteLength: 16 }))
ta.buffer.transfer();
Object.preventExtensions(ta);
Object.isExtensible(ta); // ??
syg commented 4 weeks ago

Sorry, I meant for those which had Object.preventExtensions called on them. Like:

Going by my current thinking in https://github.com/tc39/ecma262/issues/3385#issuecomment-2289957136, I think your example should return false, and things like Reflect.setPrototypeOf stops working on that ta.

syg commented 4 weeks ago

I think this is implementable without performance penalty, but that's an open question.

Ah crap, I'm wrong about the performance penalty here. This is doable for TAs backed by growable SABs, but resizable ABs pose a problem:

ab = new ArrayBuffer(2, { maxByteLength: 4 });
ta = new Int8Array(ab, 0, 2);
ta.length; // 2
Object.seal(ta);
ab.resize(1);
ta.length; // 0 (because it's now out of bounds);
ab.resize(4);
// If this wasn't sealed, the following should be 2 again.
// But for sealed TAs, once you go OOB you must never go back.
// Tracking this state is expensive.
ta.length;

Edit: It's still doable, but the implementation cost of "OOBs are one-way for sealed resizable AB-backed TAs" probably doesn't pay for the use case.