LibWeb: Make global prototype chains immutable

Per https://webidl.spec.whatwg.org/#interface-prototype-object any
global platform object should reject prototype changes (besides
from ShadowRealmGlobalScope), and interface prototype objects on
global prototype chains must be immutable.

We already handled parts of this on the globals themselves, but not
the full chain.

Also align some spec comments to the latest WebIDL spec.
This commit is contained in:
Shannon Booth 2026-03-31 23:30:42 +02:00 committed by Shannon Booth
parent ce4861156b
commit 379461e047
6 changed files with 93 additions and 2 deletions

View file

@ -37,6 +37,16 @@ WorkerGlobalScope::WorkerGlobalScope(JS::Realm& realm, GC::Ref<Web::Page> page)
WorkerGlobalScope::~WorkerGlobalScope() = default;
// https://webidl.spec.whatwg.org/#platform-object-setprototypeof
JS::ThrowCompletionOr<bool> WorkerGlobalScope::internal_set_prototype_of(JS::Object* prototype)
{
// 1. If Os associated realms is global prototype chain mutable is true, return ? OrdinarySetPrototypeOf(O, V).
// NB: This is never the case for WorkerGlobalScope.
// 2. Return ? SetImmutablePrototype(O, V).
return set_immutable_prototype(prototype);
}
void WorkerGlobalScope::initialize_web_interfaces_impl()
{
auto& realm = this->realm();

View file

@ -72,6 +72,7 @@ public:
GC::Ref<WorkerGlobalScope const> self() const { return *this; }
virtual Optional<URL::Origin> extract_an_origin() const override { return window_or_worker_global_scope_extract_an_origin(); }
virtual JS::ThrowCompletionOr<bool> internal_set_prototype_of(JS::Object* prototype) override;
GC::Ref<WorkerLocation> location() const;
GC::Ref<WorkerNavigator> navigator() const;

View file

@ -3731,7 +3731,10 @@ JS::ThrowCompletionOr<bool> @named_properties_class@::internal_delete(JS::Proper
// https://webidl.spec.whatwg.org/#named-properties-object-setprototypeof
JS::ThrowCompletionOr<bool> @named_properties_class@::internal_set_prototype_of(JS::Object* prototype)
{
// 1. Return ? SetImmutablePrototype(O, V).
// 1. If Os associated realms is global prototype chain mutable is true, return ? OrdinarySetPrototypeOf(O, V).
// NB: This is only ever true for ShadowRealms.
// 2. Return ? SetImmutablePrototype(O, V).
return set_immutable_prototype(prototype);
}
@ -3751,6 +3754,17 @@ void @named_properties_class@::visit_edges(Visitor& visitor)
)~~~");
}
// https://webidl.spec.whatwg.org/#interface-prototype-object
static bool interface_prototype_has_immutable_prototype(IDL::Interface const& interface)
{
// 9. Otherwise, if interface is declared with the [Global] extended attribute, or interface is in the set of
// inherited interfaces of an interface that is declared with the [Global] extended attribute, then:
// NB: This currently assumes only Workers and Window can be globals.
return interface.extended_attributes.contains("Global")
|| interface.name == "WorkerGlobalScope"sv
|| interface.name == "EventTarget"sv;
}
enum class GenerateUnforgeables {
No,
Yes,
@ -5943,6 +5957,7 @@ void generate_prototype_header(IDL::Interface const& interface, StringBuilder& b
SourceGenerator generator { builder };
generator.set("prototype_class", interface.prototype_class);
auto has_immutable_prototype = interface_prototype_has_immutable_prototype(interface);
generator.append(R"~~~(
#pragma once
@ -5960,9 +5975,15 @@ public:
explicit @prototype_class@(JS::Realm&);
virtual void initialize(JS::Realm&) override;
virtual ~@prototype_class@() override;
private:
)~~~");
if (has_immutable_prototype) {
generator.append(R"~~~(
private:
virtual JS::ThrowCompletionOr<bool> internal_set_prototype_of(JS::Object* prototype) override;
)~~~");
}
// Generate an empty prototype object for global interfaces.
auto is_global_interface = interface.extended_attributes.contains("Global");
if (is_global_interface) {
@ -5988,6 +6009,7 @@ void generate_prototype_implementation(IDL::Interface const& interface, StringBu
generator.set("parent_name", interface.parent_name);
generator.set("prototype_class", interface.prototype_class);
generator.set("prototype_base_class", interface.prototype_base_class);
auto has_immutable_prototype = interface_prototype_has_immutable_prototype(interface);
generator.append(R"~~~(
#include <AK/Function.h>
@ -6101,6 +6123,17 @@ GC_DEFINE_ALLOCATOR(@prototype_class@);
}
)~~~");
if (has_immutable_prototype) {
generator.append(R"~~~(
// https://webidl.spec.whatwg.org/#es-interface-prototype-object
JS::ThrowCompletionOr<bool> @prototype_class@::internal_set_prototype_of(JS::Object* prototype)
{
return set_immutable_prototype(prototype);
}
)~~~");
}
// Generate a mostly empty prototype object for global interfaces.
auto is_global_interface = interface.extended_attributes.contains("Global");
if (is_global_interface) {

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass Setting to a different prototype
Pass Setting to the same prototype

View file

@ -0,0 +1,15 @@
<!doctype html>
<meta charset=utf-8>
<title>Immutability of the global prototype chain</title>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<div id=log></div>
<script src="../../webidl/ecmascript-binding/global-immutable-prototype.any.js"></script>

View file

@ -0,0 +1,25 @@
// META: global=window,worker
// META: title=Immutability of the global prototype chain
const objects = [];
setup(() => {
for (let object = self; object; object = Object.getPrototypeOf(object)) {
objects.push(object);
}
});
test(() => {
for (const object of objects) {
assert_throws_js(TypeError, () => {
Object.setPrototypeOf(object, {});
});
}
}, "Setting to a different prototype");
test(() => {
for (const object of objects) {
const expected = Object.getPrototypeOf(object);
Object.setPrototypeOf(object, expected);
assert_equals(Object.getPrototypeOf(object), expected);
}
}, "Setting to the same prototype");