From a6bb958b1f67f3d2fb62e77ab5d360c0e72f1f5d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 11 Oct 2026 15:56:35 +0800 Subject: [PATCH] fix(streams): mark internal rejections handled without author catch hooks Use the native Promise handled flag for internal Streams promises instead of reading author-controlled catch methods. Cover locked reader release and writer abort with a poisoned Promise.prototype.catch getter. --- .../context_bootstrap/stream_adapter/utils.rs | 16 +++------ .../tests/streams/intrinsic_promises.rs | 34 +++++++++++++++++++ .../webidl_callback_source_boundary_tests.rs | 2 +- 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/stream_adapter/utils.rs b/moli-renderer-v8/src/context_bootstrap/stream_adapter/utils.rs index 08b77b7313..9d10c3feb2 100644 --- a/moli-renderer-v8/src/context_bootstrap/stream_adapter/utils.rs +++ b/moli-renderer-v8/src/context_bootstrap/stream_adapter/utils.rs @@ -269,22 +269,16 @@ pub(in crate::context_bootstrap) fn suppress_pending_read_unhandled_rejection<'s } pub(in crate::context_bootstrap) fn suppress_promise_unhandled_rejection<'s>( - scope: &mut v8::PinScope<'s, '_>, + _: &mut v8::PinScope<'s, '_>, promise: v8::Local<'s, v8::Value>, ) { let Ok(promise) = v8::Local::::try_from(promise) else { return; }; - let Some(noop) = v8::Function::builder(promise_return_undefined_callback).build(scope) else { - return; - }; - let Some(catch) = promise - .get(scope, v8str(scope, "catch").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { - return; - }; - let _ = catch.call(scope, promise.into(), &[noop.into()]); + // Streams algorithms mark their internal promises handled directly. An + // author can replace catch, its getter, or the Promise species constructor. + // Looking them up here would add observable script to the native algorithm. + promise.mark_as_handled(); } pub(in crate::context_bootstrap) fn value_buffer_source_bytes( diff --git a/moli-renderer-v8/src/script_vm/tests/streams/intrinsic_promises.rs b/moli-renderer-v8/src/script_vm/tests/streams/intrinsic_promises.rs index 6f1fd73078..d475c8cbf2 100644 --- a/moli-renderer-v8/src/script_vm/tests/streams/intrinsic_promises.rs +++ b/moli-renderer-v8/src/script_vm/tests/streams/intrinsic_promises.rs @@ -1,5 +1,39 @@ use super::*; +#[test] +fn stream_internal_rejection_handling_does_not_read_author_promise_catch() { + let mut vm = stream_test_vm(); + vm.eval(r#" + globalThis.__handlingResult='pending'; + (async()=>{ + let hooks=0; + const saved=Object.getOwnPropertyDescriptor(Promise.prototype,'catch'); + Object.defineProperty(Promise.prototype,'catch',{configurable:true,get(){hooks++;throw Error('author catch getter');}}); + try { + const reader=new ReadableStream().getReader(); + const pending=reader.read().then(()=>false,error=>error instanceof TypeError); + const readerClosed=reader.closed.then(()=>false,error=>error instanceof TypeError); + reader.releaseLock(); + const writer=new WritableStream().getWriter(); + const writerClosed=writer.closed.then(()=>false,error=>error==='stop'); + await writer.abort('stop'); + const writerReady=writer.ready.then(()=>false,error=>error==='stop'); + const results=await Promise.all([pending,readerClosed,writerClosed,writerReady]); + writer.releaseLock(); + return JSON.stringify({hooks,results}); + } finally {Object.defineProperty(Promise.prototype,'catch',saved);} + })().then(value=>globalThis.__handlingResult=value,error=>globalThis.__handlingResult='error:'+error.name+':'+error.message); + "#).unwrap(); + let mut result = String::new(); + for _ in 0..16 { + result = vm.eval("__handlingResult").unwrap(); + if result != "pending" { + break; + } + } + assert_eq!(result, r#"{"hooks":0,"results":[true,true,true,true]}"#); +} + #[test] fn writable_stream_writer_promises_survive_missing_or_poisoned_global_promise() { for (label, replacement) in [ diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index 45ac18c4cf..7c7dfe38a1 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -134,7 +134,7 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[ "context_bootstrap/stream_adapter/utils.rs", // Pending promise settlement and cancellation continuations use the // native Promise API; the remaining calls run stream algorithms. - 5, + 4, DirectCallOwner::BrowserAlgorithm, ), allowed(