fix(cdp): bound IO stream buffers so downloads cannot leak (#387)

* fix(js): fire trusted input/change from setFileInputFiles

DOM.setFileInputFiles dispatched plain input/change events, so
event.isTrusted was false. Upload flows and anti-bot code that gate their
change handler on isTrusted ignored them, which silently broke the exact
case the feature targets. The Input domain already marks its synthesized
clicks/keys trusted via __obscura_markTrusted; do the same here.

Fixes #384.

* fix(cdp): bound IO stream buffers so downloads cannot leak

Fetch.takeResponseBodyAsStream moved a response body out of the page's
LRU-bounded cache into CdpContext.io_streams, a server-lifetime map freed only
by an explicit IO.close. A client that opens a stream and disconnects, errors
mid-download, or never closes it leaked the full buffer forever; no teardown
path (WS close, Browser.close, Target.closeTarget) touched io_streams. That
reintroduced the unbounded memory growth streaming was meant to avoid (#360).

Replace the raw map + counter with an IoStreamStore that caps the number of
open streams and their total bytes (OBSCURA_IO_STREAM_MAX_ENTRIES / _BYTES),
evicting the oldest first and always keeping the just-opened stream. Reading an
evicted handle fails cleanly, which is the right trade against an OOM. Read
chunking is unchanged.

Fixes #386.
This commit is contained in:
Andrey Mnatsakanov authored and GitHub committed 2026-07-11 16:42:57 +02:00
1 parent d4fd2884a2
commit 50e66320b0
3 files changed
+150 -22

No files matched your search

+7 -7
View File
@@ -45,11 +45,12 @@ pub struct CdpContext {
pub next_isolated_context_id: i64,
pub fetch_intercept: FetchInterceptState,
pub intercept_tx: Option<tokio::sync::mpsc::UnboundedSender<InterceptedRequest>>,
// Open IO streams for Fetch.takeResponseBodyAsStream: handle -> (bytes, cursor).
// Each holds a response body taken out of the page cache so a large download
// is streamed chunk-by-chunk via IO.read and freed on IO.close (issue #360).
pub io_streams: HashMap<String, (Vec<u8>, usize)>,
pub io_stream_counter: u64,
// Open IO streams for Fetch.takeResponseBodyAsStream. Each holds a response
// body taken out of the page cache so a large download is streamed
// chunk-by-chunk via IO.read and freed on IO.close (issue #360). The store
// caps how many bodies (and how many bytes) can be held at once, evicting
// the oldest, so an abandoned or disconnected stream cannot leak unbounded.
pub io_streams: crate::domains::io::IoStreamStore,
}
impl CdpContext {
@@ -146,8 +147,7 @@ impl CdpContext {
isolated_worlds: Vec::new(),
valid_context_ids,
next_isolated_context_id: 100,
io_streams: HashMap::new(),
io_stream_counter: 0,
io_streams: crate::domains::io::IoStreamStore::default(),
}
}