From 012dcaa9963d170d5fac7793a05ff25d815a9794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 22 Sep 2026 08:41:45 +0200 Subject: [PATCH 1/3] Reject writable file commands after close or abort --- ...webscene_v8_runtime_file_system_access.inc | 6 +++- .../tests/native_file_system_access_tests.inc | 30 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc index 753a1fa8..bb92fc85 100644 --- a/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc +++ b/experiments/WebScene.NativeEngine.Probe/native/webscene_v8_runtime_file_system_access.inc @@ -1563,10 +1563,14 @@ } }; const queueWriterCommand = (stream, command) => { - if (!writerStates.has(stream)) { + const state = writerStates.get(stream); + if (!state) { )JS", R"JS( return Promise.reject(new TypeError('Illegal invocation')); } + if (!state.transaction) { + return Promise.reject(new TypeError('Writer is closed')); + } let writer; try { writer = WritableStream.prototype.getWriter.call(stream); } catch (error) { return Promise.reject(error); } diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc index 57e63925..68f89594 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc @@ -1684,10 +1684,18 @@ void test_native_file_system_access_picker_and_handle_contract() && write_state.find("\"closed\":true") != std::string::npos, "createWritable stream identity or close changed: " + write_state); require(evaluate(engine.get(), R"JS((async()=>{ - try { await __writeStream.write('late'); return 'unexpected'; } - catch (error) { return error.name; } - })())JS", "file-handle-write-after-close.js") == "\"TypeError\"", - "write after close did not reject TypeError"); + const errors = []; + for (const command of [ + () => __writeStream.write('late'), + () => __writeStream.seek(0), + () => __writeStream.truncate(0)]) { + try { await command(); errors.push('resolved'); } + catch (error) { errors.push(error.name); } + } + return errors.join(','); + })())JS", "file-handle-command-after-close.js") + == "\"TypeError,TypeError,TypeError\"", + "write/seek/truncate after close did not reject TypeError"); activate_file_picker(engine.get(), 50005U); execute(engine.get(), R"JS( @@ -1770,6 +1778,7 @@ void test_native_file_system_access_picker_and_handle_contract() execute(engine.get(), R"JS( globalThis.__abortedWrite='pending'; __saveHandle.createWritable().then(async stream => { + globalThis.__abortedStream = stream; await stream.abort('discard'); __abortedWrite='aborted'; }, error => { __abortedWrite=error.name; }); @@ -1795,6 +1804,19 @@ void test_native_file_system_access_picker_and_handle_contract() require(evaluate(engine.get(), "__abortedWrite", "file-handle-create-writable-abort-result.js") == "\"aborted\"", "stream abort did not preserve terminal state"); + require(evaluate(engine.get(), R"JS((async()=>{ + const errors = []; + for (const command of [ + () => __abortedStream.write('late'), + () => __abortedStream.seek(0), + () => __abortedStream.truncate(0)]) { + try { await command(); errors.push('resolved'); } + catch (error) { errors.push(error.name); } + } + return errors.join(','); + })())JS", "file-handle-command-after-abort.js") + == "\"TypeError,TypeError,TypeError\"", + "write/seek/truncate after abort did not reject TypeError"); execute(engine.get(), R"JS( globalThis.__writeFailure='pending'; From 53132fb2ee5da2b2fe1a3d422063188af84f928b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 22 Sep 2026 08:56:19 +0200 Subject: [PATCH 2/3] Report exact post-close writable command results --- .../tests/native_file_system_access_tests.inc | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc index 68f89594..89428bf3 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc @@ -1683,7 +1683,7 @@ void test_native_file_system_access_picker_and_handle_contract() && write_state.find("\"instance\":true") != std::string::npos && write_state.find("\"closed\":true") != std::string::npos, "createWritable stream identity or close changed: " + write_state); - require(evaluate(engine.get(), R"JS((async()=>{ + const auto closed_command_state = evaluate(engine.get(), R"JS((async()=>{ const errors = []; for (const command of [ () => __writeStream.write('late'), @@ -1693,9 +1693,10 @@ void test_native_file_system_access_picker_and_handle_contract() catch (error) { errors.push(error.name); } } return errors.join(','); - })())JS", "file-handle-command-after-close.js") - == "\"TypeError,TypeError,TypeError\"", - "write/seek/truncate after close did not reject TypeError"); + })())JS", "file-handle-command-after-close.js"); + require(closed_command_state == "\"TypeError,TypeError,TypeError\"", + "write/seek/truncate after close did not reject TypeError: " + + closed_command_state); activate_file_picker(engine.get(), 50005U); execute(engine.get(), R"JS( @@ -1804,7 +1805,7 @@ void test_native_file_system_access_picker_and_handle_contract() require(evaluate(engine.get(), "__abortedWrite", "file-handle-create-writable-abort-result.js") == "\"aborted\"", "stream abort did not preserve terminal state"); - require(evaluate(engine.get(), R"JS((async()=>{ + const auto aborted_command_state = evaluate(engine.get(), R"JS((async()=>{ const errors = []; for (const command of [ () => __abortedStream.write('late'), @@ -1814,9 +1815,10 @@ void test_native_file_system_access_picker_and_handle_contract() catch (error) { errors.push(error.name); } } return errors.join(','); - })())JS", "file-handle-command-after-abort.js") - == "\"TypeError,TypeError,TypeError\"", - "write/seek/truncate after abort did not reject TypeError"); + })())JS", "file-handle-command-after-abort.js"); + require(aborted_command_state == "\"TypeError,TypeError,TypeError\"", + "write/seek/truncate after abort did not reject TypeError: " + + aborted_command_state); execute(engine.get(), R"JS( globalThis.__writeFailure='pending'; From 01b8cd4a09bb61be80a0e87656aee498e8c984d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 22 Sep 2026 09:02:51 +0200 Subject: [PATCH 3/3] Await writable-stream terminal command results in native test --- .../tests/native_file_system_access_tests.inc | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc index 89428bf3..93308dc0 100644 --- a/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc +++ b/experiments/WebScene.NativeEngine.Probe/tests/native_file_system_access_tests.inc @@ -1683,7 +1683,7 @@ void test_native_file_system_access_picker_and_handle_contract() && write_state.find("\"instance\":true") != std::string::npos && write_state.find("\"closed\":true") != std::string::npos, "createWritable stream identity or close changed: " + write_state); - const auto closed_command_state = evaluate(engine.get(), R"JS((async()=>{ + execute(engine.get(), R"JS(globalThis.__closedCommandState = 'pending'; (async()=>{ const errors = []; for (const command of [ () => __writeStream.write('late'), @@ -1692,8 +1692,11 @@ void test_native_file_system_access_picker_and_handle_contract() try { await command(); errors.push('resolved'); } catch (error) { errors.push(error.name); } } - return errors.join(','); + globalThis.__closedCommandState = errors.join(','); })())JS", "file-handle-command-after-close.js"); + const auto closed_command_state = evaluate_until_equals(engine.get(), + "__closedCommandState", "file-handle-command-after-close-result.js", + "\"TypeError,TypeError,TypeError\""); require(closed_command_state == "\"TypeError,TypeError,TypeError\"", "write/seek/truncate after close did not reject TypeError: " + closed_command_state); @@ -1805,7 +1808,7 @@ void test_native_file_system_access_picker_and_handle_contract() require(evaluate(engine.get(), "__abortedWrite", "file-handle-create-writable-abort-result.js") == "\"aborted\"", "stream abort did not preserve terminal state"); - const auto aborted_command_state = evaluate(engine.get(), R"JS((async()=>{ + execute(engine.get(), R"JS(globalThis.__abortedCommandState = 'pending'; (async()=>{ const errors = []; for (const command of [ () => __abortedStream.write('late'), @@ -1814,8 +1817,11 @@ void test_native_file_system_access_picker_and_handle_contract() try { await command(); errors.push('resolved'); } catch (error) { errors.push(error.name); } } - return errors.join(','); + globalThis.__abortedCommandState = errors.join(','); })())JS", "file-handle-command-after-abort.js"); + const auto aborted_command_state = evaluate_until_equals(engine.get(), + "__abortedCommandState", "file-handle-command-after-abort-result.js", + "\"TypeError,TypeError,TypeError\""); require(aborted_command_state == "\"TypeError,TypeError,TypeError\"", "write/seek/truncate after abort did not reject TypeError: " + aborted_command_state);