LibWeb: Don't construct stylesheet when modifying CSSStyleSheet rules
Some checks are pending
CI / Lagom (arm64, Sanitizer_CI, false, macos-15, macOS, Clang) (push) Waiting to run
CI / Lagom (x86_64, Fuzzers_CI, false, ubuntu-24.04, Linux, Clang) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, false, ubuntu-24.04, Linux, GNU) (push) Waiting to run
CI / Lagom (x86_64, Sanitizer_CI, true, ubuntu-24.04, Linux, Clang) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (arm64, macos-15, macOS, macOS-universal2) (push) Waiting to run
Package the js repl as a binary artifact / build-and-package (x86_64, ubuntu-24.04, Linux, Linux-x86_64) (push) Waiting to run
Run test262 and test-wasm / run_and_update_results (push) Waiting to run
Lint Code / lint (push) Waiting to run
Label PRs with merge conflicts / auto-labeler (push) Waiting to run
Push notes / build (push) Waiting to run

Previously, `CSSStyleSheet.replace()` and `CSSStyleSheet.replaceSync()`
parsed the given CSS text into a temporary stylesheet object, from
which a list of rules was extracted. Doing this had the unintended
side-effect that a fetch request would be started if the given CSS text
referenced any external resources. This fetch request would cause a
crash, since the temporary stylesheet object didn't set the constructed
flag, or constructor document. We now parse the given CSS text as a
list of rules without constructing a temporary stylesheet.
This commit is contained in:
Tim Ledbetter 2025-04-15 18:01:54 +01:00 committed by Sam Atkins
commit ca200142e9
Notes: github-actions[bot] 2025-04-16 21:04:15 +00:00
5 changed files with 95 additions and 6 deletions

View file

@ -1,7 +1,7 @@
/*
* Copyright (c) 2019-2022, Andreas Kling <andreas@ladybird.org>
* Copyright (c) 2022-2024, Sam Atkins <sam@ladybird.org>
* Copyright (c) 2024, Tim Ledbetter <timledbetter@gmail.com>
* Copyright (c) 2024-2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -207,8 +207,7 @@ GC::Ref<WebIDL::Promise> CSSStyleSheet::replace(String text)
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
// 1. Let rules be the result of running parse a stylesheets contents from text.
auto parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text);
auto& rules = parsed_stylesheet->rules();
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
// 2. If rules contains one or more @import rules, remove those rules from rules.
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm.heap());
@ -240,8 +239,7 @@ WebIDL::ExceptionOr<void> CSSStyleSheet::replace_sync(StringView text)
return WebIDL::NotAllowedError::create(realm(), "Can't call replaceSync() on non-modifiable stylesheets"_string);
// 2. Let rules be the result of running parse a stylesheets contents from text.
auto parsed_stylesheet = parse_css_stylesheet(make_parsing_params(), text);
auto& rules = parsed_stylesheet->rules();
auto rules = CSS::Parser::Parser::create(make_parsing_params(), text).parse_as_stylesheet_contents();
// 3. If rules contains one or more @import rules, remove those rules from rules.
GC::RootVector<GC::Ref<CSSRule>> rules_without_import(realm().heap());

View file

@ -118,6 +118,22 @@ Vector<Rule> Parser::parse_a_stylesheets_contents(TokenStream<T>& input)
return consume_a_stylesheets_contents(input);
}
GC::RootVector<GC::Ref<CSSRule>> Parser::parse_as_stylesheet_contents()
{
auto raw_rules = parse_a_stylesheets_contents(m_token_stream);
GC::RootVector<GC::Ref<CSSRule>> rules(realm().heap());
for (auto const& raw_rule : raw_rules) {
auto rule = convert_to_rule(raw_rule, Nested::No);
if (!rule) {
log_parse_error();
continue;
}
rules.append(*rule);
}
return rules;
}
// https://drafts.csswg.org/css-syntax/#parse-a-css-stylesheet
GC::Ref<CSS::CSSStyleSheet> Parser::parse_as_css_stylesheet(Optional<::URL::URL> location, Vector<NonnullRefPtr<MediaQuery>> media_query_list)
{

View file

@ -100,6 +100,7 @@ public:
Vector<Descriptor> parse_as_list_of_descriptors(AtRuleID);
CSSRule* parse_as_css_rule();
Optional<StyleProperty> parse_as_supports_condition();
GC::RootVector<GC::Ref<CSSRule>> parse_as_stylesheet_contents();
enum class SelectorParsingMode {
Standard,

View file

@ -0,0 +1,8 @@
Harness status: OK
Found 3 tests
3 Pass
Pass Constructing sheet with custom base URL ueses that URL for CSS rules
Pass Constructing sheet with relative URL adds to the constructor document's base URL
Pass Constructing sheet with invalid base URL throws a NotAllowedError

View file

@ -0,0 +1,66 @@
<!DOCTYPE html>
<title>CSSStyleSheet baseURL</title>
<link rel="author" title="Erik Nordin" href="mailto:enordin@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/cssom-1/#dom-cssstylesheetinit-baseurl">
<div id="target"></div>
<script src='../../resources/testharness.js'></script>
<script src='../../resources/testharnessreport.js'></script>
<script>
function currentLocation() {
const sections = location.href.split("/")
sections.pop();
return sections.join("/");
}
test(() => {
const span = document.createElement("span");
target.appendChild(span);
span.attachShadow({ mode: "open" })
const shadowDiv = document.createElement("div");
span.shadowRoot.appendChild(shadowDiv);
const fileName = "example.png"
const baseURL = `${location.origin}/custom/path/`;
const fullURL = `${baseURL}${fileName}`;
const sheet = new CSSStyleSheet({ baseURL });
span.shadowRoot.adoptedStyleSheets = [sheet];
sheet.replaceSync(`* { background-image: url("${fileName}"); }`);
const styleFromRelative = getComputedStyle(shadowDiv).backgroundImage;
sheet.replaceSync(`* { background-image: url("${fullURL}"); }`);
const styleFromFull = getComputedStyle(shadowDiv).backgroundImage;
assert_equals(styleFromRelative, styleFromFull);
}, "Constructing sheet with custom base URL ueses that URL for CSS rules");
test(() => {
const span = document.createElement("span");
target.appendChild(span);
span.attachShadow({ mode: "open" })
const shadowDiv = document.createElement("div");
span.shadowRoot.appendChild(shadowDiv);
const fileName = "example.png"
const baseURL = "custom/path/";
const fullURL = `${currentLocation()}/${baseURL}${fileName}`;
const sheet = new CSSStyleSheet({ baseURL });
span.shadowRoot.adoptedStyleSheets = [sheet];
sheet.replaceSync(`* { background-image: url("${fileName}"); }`);
const styleFromRelative = getComputedStyle(shadowDiv).backgroundImage;
sheet.replaceSync(`* { background-image: url("${fullURL}"); }`);
const styleFromFull = getComputedStyle(shadowDiv).backgroundImage;
assert_equals(styleFromRelative, styleFromFull);
}, "Constructing sheet with relative URL adds to the constructor document's base URL");
test(() => {
assert_throws_dom("NotAllowedError", () => { new CSSStyleSheet({ baseURL: "https://test:test/"}) });
}, "Constructing sheet with invalid base URL throws a NotAllowedError");
</script>