From 9b883f7d4ed05722fcf882189f91fbfae8813ad5 Mon Sep 17 00:00:00 2001 From: Shannon Booth Date: Sun, 20 Apr 2025 16:49:34 +1200 Subject: [PATCH] LibJS+LibWeb: Set [[CanBlock]] false to Agent for window agent similar-origin window agents have the [[CanBlock]] flag set to false. Achieve this by hooking up JS's concept with an agent to HTML::Agent. For now, this is only hooked up to the similar-origin window agent case but should be extended to the other agent types in the future. --- Libraries/LibJS/Forward.h | 1 + Libraries/LibJS/Runtime/Agent.cpp | 17 +++++++++++++---- Libraries/LibJS/Runtime/Agent.h | 18 +++++++++++++++++- Libraries/LibJS/Runtime/AtomicsObject.cpp | 2 +- Libraries/LibJS/Runtime/Completion.cpp | 5 +++-- Libraries/LibJS/Runtime/VM.cpp | 8 ++++---- Libraries/LibJS/Runtime/VM.h | 16 ++++++---------- Libraries/LibWeb/Bindings/MainThreadVM.cpp | 13 ++++--------- Libraries/LibWeb/Bindings/MainThreadVM.h | 8 -------- Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp | 2 +- Libraries/LibWeb/HTML/Scripting/Agent.cpp | 14 +++++++++++++- Libraries/LibWeb/HTML/Scripting/Agent.h | 8 +++++++- Tests/LibJS/test262-runner.cpp | 6 ++++-- .../requires-failure.https.any.txt | 6 ++++++ .../requires-failure.https.any.html | 15 +++++++++++++++ .../requires-failure.https.any.js | 11 +++++++++++ 16 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.js diff --git a/Libraries/LibJS/Forward.h b/Libraries/LibJS/Forward.h index c1bb0fa7f6b..ae0dd77b743 100644 --- a/Libraries/LibJS/Forward.h +++ b/Libraries/LibJS/Forward.h @@ -154,6 +154,7 @@ namespace JS { class ASTNode; class Accessor; +class Agent; struct AsyncGeneratorRequest; class BigInt; class BoundFunction; diff --git a/Libraries/LibJS/Runtime/Agent.cpp b/Libraries/LibJS/Runtime/Agent.cpp index 76877b86ff5..05fedb687af 100644 --- a/Libraries/LibJS/Runtime/Agent.cpp +++ b/Libraries/LibJS/Runtime/Agent.cpp @@ -1,19 +1,28 @@ /* * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2025, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #include +#include namespace JS { +Agent::~Agent() = default; + // 9.7.2 AgentCanSuspend ( ), https://tc39.es/ecma262/#sec-agentcansuspend -bool agent_can_suspend() +bool agent_can_suspend(VM const& vm) { - // FIXME: 1. Let AR be the Agent Record of the surrounding agent. - // FIXME: 2. Return AR.[[CanBlock]]. - return true; + // 1. Let AR be the Agent Record of the surrounding agent. + auto const* agent = vm.agent(); + + // 2. Return AR.[[CanBlock]]. + // NOTE: We default to true if no agent has been provided (standalone LibJS with no embedder). + if (!agent) + return true; + return agent->can_block(); } } diff --git a/Libraries/LibJS/Runtime/Agent.h b/Libraries/LibJS/Runtime/Agent.h index 8837625309a..f63324cd308 100644 --- a/Libraries/LibJS/Runtime/Agent.h +++ b/Libraries/LibJS/Runtime/Agent.h @@ -1,13 +1,29 @@ /* * Copyright (c) 2023, Tim Flynn + * Copyright (c) 2025, Shannon Booth * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include +#include +#include + namespace JS { -bool agent_can_suspend(); +// https://tc39.es/ecma262/#sec-agents +class Agent { +public: + virtual ~Agent(); + + // [[CanBlock]] + virtual bool can_block() const = 0; + + virtual void spin_event_loop_until(GC::Root> goal_condition) = 0; +}; + +bool agent_can_suspend(VM const&); } diff --git a/Libraries/LibJS/Runtime/AtomicsObject.cpp b/Libraries/LibJS/Runtime/AtomicsObject.cpp index f2e8c2ef072..e3f3032b77f 100644 --- a/Libraries/LibJS/Runtime/AtomicsObject.cpp +++ b/Libraries/LibJS/Runtime/AtomicsObject.cpp @@ -186,7 +186,7 @@ static ThrowCompletionOr do_wait(VM& vm, WaitMode mode, TypedArrayBase& t timeout = max(timeout_number.as_double(), 0.0); // 10. If mode is sync and AgentCanSuspend() is false, throw a TypeError exception. - if (mode == WaitMode::Sync && !agent_can_suspend()) + if (mode == WaitMode::Sync && !agent_can_suspend(vm)) return vm.throw_completion(ErrorType::AgentCannotSuspend); // FIXME: Implement the remaining steps when we support SharedArrayBuffer. diff --git a/Libraries/LibJS/Runtime/Completion.cpp b/Libraries/LibJS/Runtime/Completion.cpp index f0104c617ad..9f9674eb5ed 100644 --- a/Libraries/LibJS/Runtime/Completion.cpp +++ b/Libraries/LibJS/Runtime/Completion.cpp @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -98,9 +99,9 @@ ThrowCompletionOr await(VM& vm, Value value) // FIXME: Since we don't support context suspension, we attempt to "wait" for the promise to resolve // by syncronously running all queued promise jobs. - if (auto* custom_data = vm.custom_data()) { + if (auto* agent = vm.agent()) { // Embedder case (i.e. LibWeb). Runs all promise jobs by performing a microtask checkpoint. - custom_data->spin_event_loop_until(GC::create_function(vm.heap(), [success] { + agent->spin_event_loop_until(GC::create_function(vm.heap(), [success] { return success.has_value(); })); } else { diff --git a/Libraries/LibJS/Runtime/VM.cpp b/Libraries/LibJS/Runtime/VM.cpp index 671265ecedb..da24a99306a 100644 --- a/Libraries/LibJS/Runtime/VM.cpp +++ b/Libraries/LibJS/Runtime/VM.cpp @@ -37,12 +37,12 @@ namespace JS { -ErrorOr> VM::create(OwnPtr custom_data) +ErrorOr> VM::create(OwnPtr agent) { ErrorMessages error_messages {}; error_messages[to_underlying(ErrorMessage::OutOfMemory)] = ErrorType::OutOfMemory.message(); - auto vm = adopt_ref(*new VM(move(custom_data), move(error_messages))); + auto vm = adopt_ref(*new VM(move(agent), move(error_messages))); WellKnownSymbols well_known_symbols { #define __JS_ENUMERATE(SymbolName, snake_name) \ @@ -63,12 +63,12 @@ static constexpr auto make_single_ascii_character_strings(IndexSequence()); -VM::VM(OwnPtr custom_data, ErrorMessages error_messages) +VM::VM(OwnPtr agent, ErrorMessages error_messages) : m_heap(this, [this](HashMap& roots) { gather_roots(roots); }) , m_error_messages(move(error_messages)) - , m_custom_data(move(custom_data)) + , m_agent(move(agent)) { m_bytecode_interpreter = make(*this); diff --git a/Libraries/LibJS/Runtime/VM.h b/Libraries/LibJS/Runtime/VM.h index f1309bdf4e6..fa547f39dd4 100644 --- a/Libraries/LibJS/Runtime/VM.h +++ b/Libraries/LibJS/Runtime/VM.h @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -46,13 +47,7 @@ enum class EvalMode { class VM : public RefCounted { public: - struct CustomData { - virtual ~CustomData() = default; - - virtual void spin_event_loop_until(GC::Root> goal_condition) = 0; - }; - - static ErrorOr> create(OwnPtr = {}); + static ErrorOr> create(OwnPtr = {}); ~VM(); GC::Heap& heap() { return m_heap; } @@ -245,7 +240,8 @@ public: Function on_promise_rejection_handled; Function on_unimplemented_property_access; - CustomData* custom_data() { return m_custom_data; } + Agent* agent() { return m_agent; } + Agent const* agent() const { return m_agent; } void save_execution_context_stack(); void clear_execution_context_stack(); @@ -294,7 +290,7 @@ private: #undef __JS_ENUMERATE }; - VM(OwnPtr, ErrorMessages); + VM(OwnPtr, ErrorMessages); void load_imported_module(ImportedModuleReferrer, ModuleRequest const&, GC::Ptr, ImportedModulePayload); ThrowCompletionOr link_and_eval_module(CyclicModule&); @@ -339,7 +335,7 @@ private: u32 m_execution_generation { 0 }; - OwnPtr m_custom_data; + OwnPtr m_agent; OwnPtr m_bytecode_interpreter; diff --git a/Libraries/LibWeb/Bindings/MainThreadVM.cpp b/Libraries/LibWeb/Bindings/MainThreadVM.cpp index 743a3fe5626..a449363f224 100644 --- a/Libraries/LibWeb/Bindings/MainThreadVM.cpp +++ b/Libraries/LibWeb/Bindings/MainThreadVM.cpp @@ -76,10 +76,10 @@ ErrorOr initialize_main_thread_vm(HTML::EventLoop::Type type) { VERIFY(!s_main_thread_vm); - s_main_thread_vm = TRY(JS::VM::create(make())); + s_main_thread_vm = TRY(JS::VM::create(make())); - auto& custom_data = as(*s_main_thread_vm->custom_data()); - custom_data.agent.event_loop = s_main_thread_vm->heap().allocate(type); + auto& agent = as(*s_main_thread_vm->agent()); + agent.event_loop = s_main_thread_vm->heap().allocate(type); s_main_thread_vm->on_unimplemented_property_access = [](auto const& object, auto const& property_key) { dbgln("FIXME: Unimplemented IDL interface: '{}.{}'", object.class_name(), property_key.to_string()); @@ -658,7 +658,7 @@ JS::VM& main_thread_vm() void queue_mutation_observer_microtask(DOM::Document const& document) { auto& vm = main_thread_vm(); - auto& surrounding_agent = as(*vm.custom_data()).agent; + auto& surrounding_agent = as(*vm.agent()); // 1. If the surrounding agent’s mutation observer microtask queued is true, then return. if (surrounding_agent.mutation_observer_microtask_queued) @@ -750,11 +750,6 @@ NonnullOwnPtr create_a_new_javascript_realm(JS::VM& vm, Fu return realm_execution_context; } -void WebEngineCustomData::spin_event_loop_until(GC::Root> goal_condition) -{ - Platform::EventLoopPlugin::the().spin_until(move(goal_condition)); -} - // https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions void invoke_custom_element_reactions(Vector>& element_queue) { diff --git a/Libraries/LibWeb/Bindings/MainThreadVM.h b/Libraries/LibWeb/Bindings/MainThreadVM.h index bc46a89fd66..3e7c9a5780e 100644 --- a/Libraries/LibWeb/Bindings/MainThreadVM.h +++ b/Libraries/LibWeb/Bindings/MainThreadVM.h @@ -17,14 +17,6 @@ namespace Web::Bindings { -struct WebEngineCustomData final : public JS::VM::CustomData { - virtual ~WebEngineCustomData() override = default; - - virtual void spin_event_loop_until(GC::Root> goal_condition) override; - - HTML::Agent agent; -}; - struct WebEngineCustomJobCallbackData final : public JS::JobCallback::CustomData { WebEngineCustomJobCallbackData(JS::Realm& incumbent_realm, OwnPtr active_script_context) : incumbent_realm(incumbent_realm) diff --git a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp index a22f5454de5..0306b963282 100644 --- a/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp +++ b/Libraries/LibWeb/HTML/EventLoop/EventLoop.cpp @@ -68,7 +68,7 @@ void EventLoop::schedule() EventLoop& main_thread_event_loop() { - return *static_cast(Bindings::main_thread_vm().custom_data())->agent.event_loop; + return *static_cast(Bindings::main_thread_vm().agent())->event_loop; } // https://html.spec.whatwg.org/multipage/webappapis.html#spin-the-event-loop diff --git a/Libraries/LibWeb/HTML/Scripting/Agent.cpp b/Libraries/LibWeb/HTML/Scripting/Agent.cpp index f499cd621e9..9a3752e4aee 100644 --- a/Libraries/LibWeb/HTML/Scripting/Agent.cpp +++ b/Libraries/LibWeb/HTML/Scripting/Agent.cpp @@ -7,15 +7,27 @@ #include #include #include +#include namespace Web::HTML { +bool Agent::can_block() const +{ + // similar-origin window agents can not block, see: https://html.spec.whatwg.org/multipage/webappapis.html#obtain-similar-origin-window-agent + return false; +} + +void Agent::spin_event_loop_until(GC::Root> goal_condition) +{ + Platform::EventLoopPlugin::the().spin_until(move(goal_condition)); +} + // https://html.spec.whatwg.org/multipage/webappapis.html#relevant-agent Agent& relevant_agent(JS::Object const& object) { // The relevant agent for a platform object platformObject is platformObject's relevant Realm's agent. // Spec Note: This pointer is not yet defined in the JavaScript specification; see tc39/ecma262#1357. - return static_cast(relevant_realm(object).vm().custom_data())->agent; + return *static_cast(relevant_realm(object).vm().agent()); } } diff --git a/Libraries/LibWeb/HTML/Scripting/Agent.h b/Libraries/LibWeb/HTML/Scripting/Agent.h index e15f757b6bb..ccc3273403b 100644 --- a/Libraries/LibWeb/HTML/Scripting/Agent.h +++ b/Libraries/LibWeb/HTML/Scripting/Agent.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -17,7 +18,7 @@ namespace Web::HTML { // https://html.spec.whatwg.org/multipage/webappapis.html#similar-origin-window-agent -struct Agent { +struct Agent : public JS::Agent { GC::Root event_loop; // FIXME: These should only be on similar-origin window agents, but we don't currently differentiate agent types. @@ -40,6 +41,11 @@ struct Agent { // A similar-origin window agent's current element queue is the element queue at the top of its custom element reactions stack. Vector>& current_element_queue() { return custom_element_reactions_stack.element_queue_stack.last(); } Vector> const& current_element_queue() const { return custom_element_reactions_stack.element_queue_stack.last(); } + + // [[CanBlock]] + virtual bool can_block() const override; + + virtual void spin_event_loop_until(GC::Root> goal_condition) override; }; Agent& relevant_agent(JS::Object const&); diff --git a/Tests/LibJS/test262-runner.cpp b/Tests/LibJS/test262-runner.cpp index aaf4c7b5b5e..a3f27621eef 100644 --- a/Tests/LibJS/test262-runner.cpp +++ b/Tests/LibJS/test262-runner.cpp @@ -373,8 +373,10 @@ static ErrorOr extract_metadata(StringView source) metadata.harness_files.append(async_include); metadata.is_async = true; } else if (flag == "CanBlockIsFalse"sv) { - if (JS::agent_can_suspend()) - metadata.skip_test = SkipTest::Yes; + // NOTE: This should only be skipped if AgentCanSuspend is set to true. This is currently always the case. + // Ideally we would check that, but we don't have the VM by this stage. So for now, we rely on that + // assumption. + metadata.skip_test = SkipTest::Yes; } } } else if (line.starts_with("includes:"sv)) { diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.txt b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.txt new file mode 100644 index 00000000000..9a6fdf7e5b2 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.txt @@ -0,0 +1,6 @@ +Harness status: OK + +Found 1 tests + +1 Pass +Pass [[CanBlock]] in a Window \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.html b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.html new file mode 100644 index 00000000000..2e1ae320d2e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.html @@ -0,0 +1,15 @@ + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.js b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.js new file mode 100644 index 00000000000..fddf85dbede --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/scripting/processing-model-2/integration-with-the-javascript-agent-formalism/requires-failure.https.any.js @@ -0,0 +1,11 @@ +// META: global=window,serviceworker + +test(() => { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + const sab = new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer; + const ta = new Int32Array(sab); + + assert_throws_js(TypeError, () => { + Atomics.wait(ta, 0, 0, 10); + }); +}, `[[CanBlock]] in a ${self.constructor.name}`);