LibWeb: Fix race condition between read_all_bytes and stream population

There might be a race between read_all_bytes and stream population.
If document load reads stream before it is populated, the stream will
be empty and might lead to hang in SessionHistoryTraversalQueue which
is expecting a promise to be resolved on document load.

This race can occur when stream population and document source are set
very close to each other. For example, when a newly generated blob is
set as the source of an iframe.
- navigation/multiple-navigable-cross-document-navigation.html has been
modified to trigger this race.
This commit is contained in:
Prajjwal 2025-07-29 01:10:42 +05:30 committed by Alexander Kalenik
parent 50a79c6af8
commit 1f5ffe04c8
Notes: github-actions[bot] 2025-11-26 11:28:23 +00:00
3 changed files with 22 additions and 16 deletions

View file

@ -145,24 +145,24 @@ WebIDL::ExceptionOr<Infrastructure::BodyWithType> extract_body(JS::Realm& realm,
// 12. If action is non-null, then run these steps in parallel:
if (action) {
Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, stream, action = move(action)] {
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// AD-HOC: There is a race condition between document population(Ex: load_html_document->fully_read->read_all_bytes->readable_stream_default_reader_read)
// and stream population. So currently we run stream population synchronously.
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Run action.
auto bytes = action();
// 1. Run action.
auto bytes = action();
// Whenever one or more bytes are available and stream is not errored, enqueue the result of creating a
// Uint8Array from the available bytes into stream.
if (!bytes.is_empty() && !stream->is_errored()) {
auto array_buffer = JS::ArrayBuffer::create(stream->realm(), move(bytes));
auto chunk = JS::Uint8Array::create(stream->realm(), array_buffer->byte_length(), *array_buffer);
// Whenever one or more bytes are available and stream is not errored, enqueue the result of creating a
// Uint8Array from the available bytes into stream.
if (!bytes.is_empty() && !stream->is_errored()) {
auto array_buffer = JS::ArrayBuffer::create(stream->realm(), move(bytes));
auto chunk = JS::Uint8Array::create(stream->realm(), array_buffer->byte_length(), *array_buffer);
stream->enqueue(chunk).release_value_but_fixme_should_propagate_errors();
}
stream->enqueue(chunk).release_value_but_fixme_should_propagate_errors();
}
// When running action is done, close stream.
stream->close();
}));
// When running action is done, close stream.
stream->close();
}
// 13. Let body be a body whose stream is stream, source is source, and length is length.

View file

@ -2,10 +2,12 @@
<script src="../include.js"></script>
<iframe id="a"></iframe>
<iframe id="b"></iframe>
<iframe id="c"></iframe>
<iframe id="d"></iframe>
<script>
asyncTest(done => {
let doneA = false, doneB = false;
function check() {if (doneA && doneB) done();}
let doneA = false, doneB = false, doneC = false, doneD = false;
function check() {if (doneA && doneB && doneC && doneD) done();}
function makeContent(id, n) {
let html = `<h3>${id} ${n}</h3>`;
if (n % 3 === 0) html += `<iframe id="nest1+${id}" srcdoc="${id} ${n}"></iframe>`;
@ -27,5 +29,7 @@
}
run(document.getElementById('a'), 'a', 0, 101, () => {doneA = true; check();});
run(document.getElementById('b'), 'b', 0, 101, () => {doneB = true; check();});
run(document.getElementById('c'), 'c', 0, 101, () => {doneC = true; check();});
run(document.getElementById('d'), 'd', 0, 101, () => {doneD = true; check();});
});
</script>