LibWeb: Use enum for serialization and reimplement interface exposure

Our currently implementation of structured serialization has a design
flaw, where if the serialized/transferred type was not used in the
destination realm, it would not be seen as exposed and thus we would
not re-create the type on the other side.

This is very common, for example, transferring a MessagePort to a just
inserted iframe, or the just inserted iframe transferring a MessagePort
to it's parent. This is what Google reCAPTCHA does.

This flaw occurred due to relying on lazily populated HashMaps of
constructors, namespaces and interfaces. This commit changes it so that
per-type "is exposed" implementations are generated.

Since it no longer relies on interface name strings, this commit
changes serializable types to indicate their type with an enum,
in line with how transferrable types indicate their type.

This makes Google reCAPTCHA work on https://www.google.com/recaptcha/api2/demo
It currently doesn't work on non-Google origins due to a separate
same-origin policy bug.
This commit is contained in:
Luke Wilde 2025-07-14 17:15:09 +01:00 committed by Tim Flynn
commit d08d6b08d3
Notes: github-actions[bot] 2025-07-15 13:21:14 +00:00
25 changed files with 356 additions and 130 deletions

View file

@ -76,7 +76,11 @@ static ErrorOr<void> generate_intrinsic_definitions(StringView output_path, Inte
generator.append(R"~~~(
#include <LibGC/DeferGC.h>
#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/Intrinsics.h>)~~~");
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/DedicatedWorkerGlobalScope.h>
#include <LibWeb/HTML/SharedWorkerGlobalScope.h>
#include <LibWeb/HTML/ShadowRealmGlobalScope.h>)~~~");
for (auto& interface : interface_sets.intrinsics) {
auto gen = generator.fork();
@ -134,11 +138,77 @@ void Intrinsics::create_web_namespace<@namespace_class@>(JS::Realm& realm)
)~~~");
};
auto add_interface = [](SourceGenerator& gen, StringView name, StringView prototype_class, StringView constructor_class, Optional<LegacyConstructor> const& legacy_constructor, StringView named_properties_class) {
auto add_interface = [](SourceGenerator& gen, InterfaceSets const& interface_sets, StringView name, StringView prototype_class, StringView constructor_class, Optional<LegacyConstructor> const& legacy_constructor, StringView named_properties_class) {
gen.set("interface_name", name);
gen.set("prototype_class", prototype_class);
gen.set("constructor_class", constructor_class);
// https://webidl.spec.whatwg.org/#dfn-exposed
// An interface, callback interface, namespace, or member construct is exposed in a given realm realm if the
// following steps return true:
// FIXME: Make this compatible with the non-interface types.
gen.append(R"~~~(
template<>
bool Intrinsics::is_interface_exposed<@prototype_class@>(JS::Realm& realm) const
{
[[maybe_unused]] auto& global_object = realm.global_object();
)~~~");
// 1. If constructs exposure set is not *, and realm.[[GlobalObject]] does not implement an interface that is in constructs exposure set, then return false.
auto window_exposed_iterator = interface_sets.window_exposed.find_if([&name](IDL::Interface const& interface) {
return interface.name == name;
});
if (window_exposed_iterator != interface_sets.window_exposed.end()) {
gen.append(R"~~~(
if (is<HTML::Window>(global_object))
return true;
)~~~");
}
auto dedicated_worker_exposed_iterator = interface_sets.dedicated_worker_exposed.find_if([&name](IDL::Interface const& interface) {
return interface.name == name;
});
if (dedicated_worker_exposed_iterator != interface_sets.dedicated_worker_exposed.end()) {
gen.append(R"~~~(
if (is<HTML::DedicatedWorkerGlobalScope>(global_object))
return true;
)~~~");
}
auto shared_worker_exposed_iterator = interface_sets.shared_worker_exposed.find_if([&name](IDL::Interface const& interface) {
return interface.name == name;
});
if (shared_worker_exposed_iterator != interface_sets.shared_worker_exposed.end()) {
gen.append(R"~~~(
if (is<HTML::SharedWorkerGlobalScope>(global_object))
return true;
)~~~");
}
auto shadow_realm_exposed_iterator = interface_sets.shadow_realm_exposed.find_if([&name](IDL::Interface const& interface) {
return interface.name == name;
});
if (shadow_realm_exposed_iterator != interface_sets.shadow_realm_exposed.end()) {
gen.append(R"~~~(
if (is<HTML::ShadowRealmGlobalScope>(global_object))
return true;
)~~~");
}
// FIXME: 2. If realms settings object is not a secure context, and construct is conditionally exposed on
// [SecureContext], then return false.
// FIXME: 3. If realms settings objects cross-origin isolated capability is false, and construct is
// conditionally exposed on [CrossOriginIsolated], then return false.
gen.append(R"~~~(
return false;
}
)~~~");
gen.append(R"~~~(
template<>
void Intrinsics::create_web_prototype_and_constructor<@prototype_class@>(JS::Realm& realm)
@ -188,7 +258,7 @@ void Intrinsics::create_web_prototype_and_constructor<@prototype_class@>(JS::Rea
if (interface.is_namespace)
add_namespace(gen, interface.name, interface.namespace_class);
else
add_interface(gen, interface.namespaced_name, interface.prototype_class, interface.constructor_class, lookup_legacy_constructor(interface), named_properties_class);
add_interface(gen, interface_sets, interface.namespaced_name, interface.prototype_class, interface.constructor_class, lookup_legacy_constructor(interface), named_properties_class);
}
generator.append(R"~~~(