fix(streams): avoid inherited setters in readable algorithm arrays

This commit is contained in:
ldm0
2026-10-07 14:21:22 +08:00
committed by Donough Liu
parent bc215b9fa8
commit fea8bba3d2
4 changed files with 91 additions and 7 deletions
@@ -618,15 +618,15 @@ fn initialize_readable_stream_object_fields<'s>(
let source_value = source
.map(|value| value.into())
.unwrap_or_else(|| v8::null(scope).into());
let algorithms = v8::Array::new(scope, 4);
let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_SOURCE_INDEX, source_value);
let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_PULL_INDEX, pull_algorithm);
let _ = algorithms.set_index(
let algorithms = v8::Array::new_with_elements(
scope,
READABLE_STREAM_ALGORITHM_CANCEL_INDEX,
cancel_algorithm,
&[
source_value,
pull_algorithm,
cancel_algorithm,
size_algorithm,
],
);
let _ = algorithms.set_index(scope, READABLE_STREAM_ALGORITHM_SIZE_INDEX, size_algorithm);
let controller = super::super::stream_objects::new_readable_stream_controller_object(
scope,
stream,
@@ -2163,6 +2163,7 @@ mod observer_receivers;
mod offline_audio_float;
mod post_parse;
mod queue_microtask;
mod readable_algorithm_arrays;
mod remote_playback_interface;
mod rendering_update;
mod script_terminal_completion;
@@ -0,0 +1,57 @@
(async () => {
const checks = [];
async function check(name, test) {
try { checks.push({name, passed: (await test()) === true}); }
catch (error) { checks.push({name, passed: false, error: String(error)}); }
}
let frame;
const owners = [['main', globalThis]];
if (typeof document !== 'undefined') {
frame = document.createElement('iframe');document.body.append(frame);
owners.push(['child', frame.contentWindow]);
}
try {
for (const [label, owner] of owners) for (const kind of ['default', 'bytes', 'response']) {
for (const prototype of ['Array', 'Object']) for (const index of [0, 1, 2, 3]) {
for (const poison of ['setter', 'throwing-setter', 'getter-only', 'readonly-data']) {
await check(`${label}/${kind}/${prototype}/${index}/${poison}`, async () => {
const target = owner[prototype].prototype;
const previous = Object.getOwnPropertyDescriptor(target, String(index));
const reason = {numericPoison: poison};
let calls = 0, starts = 0, pulls = 0, stream, canceled, cancelReason;
const descriptor = poison === 'readonly-data' ? {configurable: true, value: 42, writable: false}
: poison === 'getter-only' ? {configurable: true, get() {calls++; throw reason;}}
: {configurable: true, set() {calls++; if (poison === 'throwing-setter') throw reason;}};
Object.defineProperty(target, String(index), descriptor);
try {
if (kind === 'response') stream = new owner.Response(new owner.Uint8Array([65, 66]));
else {
const source = {
start(controller) {starts++; controller.enqueue(kind === 'bytes' ? new owner.Uint8Array([65]) : 'first');},
pull(controller) {pulls++; controller.enqueue(kind === 'bytes' ? new owner.Uint8Array([66]) : 'second');controller.close();}
};
if (kind === 'bytes') source.type = 'bytes';
stream = new owner.ReadableStream(source);
canceled = new owner.ReadableStream({cancel(value) {cancelReason = value;}});
}
} finally {
if (previous) Object.defineProperty(target, String(index), previous);
else delete target[String(index)];
}
if (calls !== 0) return false;
if (kind === 'response') return Object.getPrototypeOf(stream) === owner.Response.prototype && await stream.text() === 'AB';
if (Object.getPrototypeOf(stream) !== owner.ReadableStream.prototype) return false;
const reader = stream.getReader();
const first = await reader.read(), second = await reader.read(), done = await reader.read();
if (starts !== 1 || pulls !== 1 || first.done || second.done || !done.done) return false;
if (kind === 'bytes' ? first.value[0] !== 65 || second.value[0] !== 66 : first.value !== 'first' || second.value !== 'second') return false;
const cancelValue = {cancelValue: label};await canceled.cancel(cancelValue);
return cancelReason === cancelValue;
});
}
}
}
} finally {if (frame) frame.remove();}
globalThis.__readableAlgorithmResults = {checks, total: checks.length, passed: checks.filter(row => row.passed).length, complete: true};
return true;
})()
@@ -0,0 +1,26 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn readable_stream_construction_ignores_inherited_numeric_properties() {
let loader = static_http_loader(std::iter::empty::<String>());
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
"https://readable-algorithm-arrays.test/",
&loader,
);
vm.exec(include_str!("readable_algorithm_arrays.js"), None)
.unwrap();
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(globalThis.__readableAlgorithmResults?.complete===true)",
"true",
"readable-algorithm-array-prototype-fixture",
)
.await;
assert_eq!(vm.eval("__readableAlgorithmResults.total").unwrap(), "192");
assert_eq!(
vm.eval("JSON.stringify(__readableAlgorithmResults.checks.filter(row=>!row.passed))")
.unwrap(),
"[]"
);
}