ladybird/Libraries/LibWeb/CSS/StyleValues/ImageSetStyleValue.cpp
Aliaksandr Kalenik 35b582048e LibWeb: Avoid stale CSS image style GC pointers
ImageStyleValue stored GC-managed request, stylesheet, and animation
timer references as strong GC::Ptr fields even though image style values
are refcounted objects. When such a style value outlives the GC object
that normally visits it, those fields can keep stale pointers after GC
collects the referents. On Steam this allowed a stale image resource
request to be read as unrelated image data, making carousel SVG arrows
render at the wrong size.

Store these back references as GC::Weak instead. Reachable style values
still use live requests, stylesheets, and timers normally, but detached
values observe null after the GC collects the referent and can reload or
skip the now-dead association instead of dereferencing reused GC memory.
Keep a local timer handle while installing the timeout callback so setup
does not rely on the weak member.

With the image style values no longer hiding strong GC edges, remove the
obsolete IGNORE_GC annotation from CSSStyleSheet's pending image list.
2026-05-25 11:06:23 +02:00

215 lines
7 KiB
C++

/*
* Copyright (c) 2026-present, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/CSS/CSSStyleSheet.h>
#include <LibWeb/CSS/Resolution.h>
#include <LibWeb/CSS/StyleValues/CalculatedStyleValue.h>
#include <LibWeb/CSS/StyleValues/ImageSetStyleValue.h>
#include <LibWeb/CSS/StyleValues/ResolutionStyleValue.h>
#include <LibWeb/DOM/AbstractElement.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/HTML/SupportedImageTypes.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/DisplayListRecordingContext.h>
namespace Web::CSS {
ValueComparingNonnullRefPtr<ImageSetStyleValue const> ImageSetStyleValue::create(Vector<Option> options)
{
return adopt_ref(*new (nothrow) ImageSetStyleValue(move(options)));
}
ImageSetStyleValue::ImageSetStyleValue(Vector<Option> options)
: AbstractImageStyleValue(Type::ImageSet)
, m_options(move(options))
{
}
AbstractImageStyleValue const* ImageSetStyleValue::select_image(double device_pixels_per_css_pixel) const
{
ImageSetStyleValue::Option const* best_below_or_equal = nullptr;
Optional<double> best_below_or_equal_resolution;
ImageSetStyleValue::Option const* best_above = nullptr;
Optional<double> best_above_resolution;
for (auto const& option : m_options) {
if (option.type.has_value() && !HTML::is_supported_image_type(*option.type))
continue;
auto resolution = Resolution::from_style_value(option.resolution).to_dots_per_pixel();
if (resolution >= device_pixels_per_css_pixel) {
if (!best_above_resolution.has_value() || resolution < *best_above_resolution) {
best_above = &option;
best_above_resolution = resolution;
}
continue;
}
if (!best_below_or_equal_resolution.has_value() || resolution > *best_below_or_equal_resolution) {
best_below_or_equal = &option;
best_below_or_equal_resolution = resolution;
}
}
if (best_above)
return best_above->image.ptr();
if (best_below_or_equal)
return best_below_or_equal->image.ptr();
return nullptr;
}
void ImageSetStyleValue::visit_edges(JS::Cell::Visitor& visitor) const
{
Base::visit_edges(visitor);
visitor.visit(m_style_sheet);
for (auto const& option : m_options)
option.image->visit_edges(visitor);
}
void ImageSetStyleValue::serialize(StringBuilder& builder, SerializationMode mode) const
{
builder.append("image-set("sv);
for (size_t i = 0; i < m_options.size(); ++i) {
if (i > 0)
builder.append(", "sv);
auto const& option = m_options[i];
option.image->serialize(builder, mode);
builder.append(' ');
option.resolution->serialize(builder, mode);
if (option.type.has_value()) {
builder.append(" type(\""sv);
builder.append_escaped_for_json(*option.type);
builder.append("\")"sv);
}
}
builder.append(')');
}
bool ImageSetStyleValue::equals(StyleValue const& other) const
{
if (type() != other.type())
return false;
auto const& other_image_set = other.as_image_set();
if (m_options.size() != other_image_set.m_options.size())
return false;
for (size_t i = 0; i < m_options.size(); ++i) {
auto const& option = m_options[i];
auto const& other_option = other_image_set.m_options[i];
if (!option.image->equals(*other_option.image))
return false;
if (!option.resolution->equals(*other_option.resolution))
return false;
if (option.type != other_option.type)
return false;
}
return true;
}
bool ImageSetStyleValue::is_computationally_independent() const
{
for (auto const& option : m_options) {
if (!option.image->is_computationally_independent())
return false;
if (!option.resolution->is_computationally_independent())
return false;
}
return true;
}
void ImageSetStyleValue::load_any_resources(DOM::Document& document)
{
auto dpr = document.page().client().device_pixels_per_css_pixel();
if (auto const* image = select_image(dpr); image && image != m_selected_image) {
const_cast<AbstractImageStyleValue&>(*image).set_style_sheet(m_style_sheet.ptr());
m_selected_image = image;
}
if (m_selected_image)
const_cast<AbstractImageStyleValue&>(*m_selected_image).load_any_resources(document);
}
Optional<CSSPixels> ImageSetStyleValue::natural_width() const
{
if (m_selected_image)
return m_selected_image->natural_width();
return {};
}
Optional<CSSPixels> ImageSetStyleValue::natural_height() const
{
if (m_selected_image)
return m_selected_image->natural_height();
return {};
}
Optional<CSSPixelFraction> ImageSetStyleValue::natural_aspect_ratio() const
{
if (m_selected_image)
return m_selected_image->natural_aspect_ratio();
return {};
}
void ImageSetStyleValue::resolve_for_size(Layout::NodeWithStyle const& layout_node, CSSPixelSize size) const
{
if (m_selected_image)
m_selected_image->resolve_for_size(layout_node, size);
}
bool ImageSetStyleValue::is_paintable() const
{
if (m_selected_image)
return m_selected_image->is_paintable();
return false;
}
void ImageSetStyleValue::paint(DisplayListRecordingContext& context, DevicePixelRect const& dest_rect, ImageRendering image_rendering) const
{
if (m_selected_image)
m_selected_image->paint(context, dest_rect, image_rendering);
}
Optional<Gfx::Color> ImageSetStyleValue::color_if_single_pixel_bitmap() const
{
if (m_selected_image)
return m_selected_image->color_if_single_pixel_bitmap();
return {};
}
void ImageSetStyleValue::set_style_sheet(GC::Ptr<CSSStyleSheet> style_sheet)
{
Base::set_style_sheet(style_sheet);
m_style_sheet = style_sheet;
// Propagate the style sheet to candidate images whose type() filter does not exclude them. This ensures the
// candidate images register themselves as pending image resources on the style sheet, so their fetches start when
// the style sheet is associated with the document, properly delaying the document's load event.
for (auto const& option : m_options) {
if (option.type.has_value() && !HTML::is_supported_image_type(*option.type))
continue;
const_cast<AbstractImageStyleValue&>(*option.image).set_style_sheet(style_sheet);
}
}
ValueComparingNonnullRefPtr<StyleValue const> ImageSetStyleValue::absolutized(ComputationContext const& context) const
{
Vector<Option> options;
options.ensure_capacity(m_options.size());
for (auto const& option : m_options) {
auto image = option.image->absolutized(context);
VERIFY(image->is_abstract_image());
options.unchecked_append({
.image = image->as_abstract_image(),
.resolution = option.resolution->absolutized(context),
.type = option.type,
});
}
return ImageSetStyleValue::create(move(options));
}
}