libservo: Make Servo (and servoshell) more resilient against extreme sizes (#39204)

Make several changes which should address panics and inconsistent
behavior around attempts to set extreme sizes:

1. Limit the minimum size of the `RenderingContext` to 1 pixel by 1
   pixel. This should address problems where users of the API try to
   directly set the size to a zero or negative dimension. In addition,
   improve the documentation around `WebView::resize` to mention this.
2. Clamp values sent in the `WebViewDelegate::request_resize_to` method
   to be at least 1x1. This prevents Servo from sending nonsense values
   to embedders. Improve documentation in this method.
3. In servoshell:
    - More consistently clamp inner and outer window size values.
    - Clamp all resize values to the available screen size, so that
      large screen sizes aren't processed directly.

Testing: This change fixes an existing WPT and adds two new API tests.
Fixes: #36763.
Fixes: #36841.
Fixes: #39141.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-09 05:51:30 -07:00 committed by GitHub
parent 406eab4ec2
commit ebfb5b1abb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 153 additions and 43 deletions

View file

@ -132,6 +132,7 @@ use crate::proxies::ConstellationProxy;
use crate::responders::ServoErrorChannel;
pub use crate::servo_delegate::{ServoDelegate, ServoError};
use crate::webrender_api::FrameReadyParams;
use crate::webview::MINIMUM_WEBVIEW_SIZE;
pub use crate::webview::{WebView, WebViewBuilder};
pub use crate::webview_delegate::{
AllowOrDenyRequest, AuthenticationRequest, ColorPicker, FormControl, NavigationRequest,
@ -693,7 +694,9 @@ impl Servo {
},
EmbedderMsg::ResizeTo(webview_id, size) => {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview.delegate().request_resize_to(webview, size);
webview
.delegate()
.request_resize_to(webview, size.max(MINIMUM_WEBVIEW_SIZE));
}
},
EmbedderMsg::ShowSimpleDialog(webview_id, prompt_definition) => {

View file

@ -56,7 +56,7 @@ pub(crate) fn run_test(
}
pub struct ServoTest {
servo: Servo,
pub servo: Rc<Servo>,
#[allow(dead_code)]
pub rendering_context: Rc<dyn RenderingContext>,
}
@ -101,7 +101,7 @@ impl ServoTest {
let builder = ServoBuilder::new(rendering_context.clone())
.event_loop_waker(Box::new(EventLoopWakerImpl(user_event_triggered)));
let builder = customize(builder);
let servo = builder.build();
let servo = Rc::new(builder.build());
Self {
servo,
rendering_context,

View file

@ -11,16 +11,19 @@
mod common;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use anyhow::ensure;
use common::{ServoTest, WebViewDelegateImpl, evaluate_javascript, run_api_tests};
use euclid::Point2D;
use dpi::PhysicalSize;
use euclid::{Point2D, Size2D};
use servo::{
Cursor, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus, MouseLeftViewportEvent,
MouseMoveEvent, Theme, WebViewBuilder,
MouseMoveEvent, Servo, Theme, WebView, WebViewBuilder, WebViewDelegate,
};
use url::Url;
use webrender_api::units::DeviceIntSize;
fn test_create_webview(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
let delegate = Rc::new(WebViewDelegateImpl::default());
@ -199,6 +202,94 @@ fn test_cursor_change(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
Ok(())
}
/// A test that ensure that negative resize requests do not get passed to the embedder.
fn test_negative_resize_to_request(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
struct WebViewResizeTestDelegate {
servo: Rc<Servo>,
popup: RefCell<Option<WebView>>,
resize_request: Cell<Option<DeviceIntSize>>,
}
impl WebViewDelegate for WebViewResizeTestDelegate {
fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
let webview = WebViewBuilder::new_auxiliary(&self.servo)
.delegate(parent_webview.delegate())
.build();
self.popup.borrow_mut().replace(webview.clone());
Some(webview)
}
fn request_resize_to(&self, _: WebView, requested_outer_size: DeviceIntSize) {
self.resize_request.set(Some(requested_outer_size));
}
}
let delegate = Rc::new(WebViewResizeTestDelegate {
servo: servo_test.servo.clone(),
popup: None.into(),
resize_request: None.into(),
});
let webview = WebViewBuilder::new(servo_test.servo())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html><script>\
let popup = window.open('about:blank');\
popup.resizeTo(-100, -100);\
</script></body>",
)
.unwrap(),
)
.build();
let load_webview = webview.clone();
let _ = servo_test.spin(move || Ok(load_webview.load_status() != LoadStatus::Complete));
let popup = delegate
.popup
.borrow()
.clone()
.expect("Should have created popup");
let load_webview = popup.clone();
let _ = servo_test.spin(move || Ok(load_webview.load_status() != LoadStatus::Complete));
// Resize requests should be floored to 1.
ensure!(delegate.resize_request.get() == Some(DeviceIntSize::new(1, 1)));
// Ensure that the popup WebView is released before the end of the test.
*delegate.popup.borrow_mut() = None;
Ok(())
}
/// This test verifies that trying to set the WebView size to a negative value does
/// not crash Servo.
fn test_resize_webview_zero(servo_test: &ServoTest) -> Result<(), anyhow::Error> {
let delegate = Rc::new(WebViewDelegateImpl::default());
let webview = WebViewBuilder::new(servo_test.servo())
.delegate(delegate.clone())
.url(
Url::parse(
"data:text/html,<!DOCTYPE html><style> html { cursor: crosshair; margin: 0}</style><body>hello</body>",
)
.unwrap(),
)
.build();
webview.focus();
webview.show(true);
webview.move_resize(Size2D::new(-100.0, -100.0).into());
webview.resize(PhysicalSize::new(0, 0));
let load_webview = webview.clone();
let _ = servo_test.spin(move || Ok(load_webview.load_status() != LoadStatus::Complete));
Ok(())
}
fn main() {
run_api_tests!(
test_create_webview,
@ -206,6 +297,8 @@ fn main() {
test_evaluate_javascript_basic,
test_evaluate_javascript_panic,
test_theme_change,
test_negative_resize_to_request,
test_resize_webview_zero,
// This test needs to be last, as it tests creating and dropping
// a WebView right before shutdown.
test_create_webview_and_immediately_drop_webview_before_shutdown

View file

@ -27,6 +27,8 @@ use crate::javascript_evaluator::JavaScriptEvaluator;
use crate::webview_delegate::{DefaultWebViewDelegate, WebViewDelegate};
use crate::{ConstellationProxy, Servo, WebRenderDebugOption};
pub(crate) const MINIMUM_WEBVIEW_SIZE: Size2D<i32, DevicePixel> = Size2D::new(1, 1);
/// A handle to a Servo webview. If you clone this handle, it does not create a new webview,
/// but instead creates a new handle to the webview. Once the last handle is dropped, Servo
/// considers that the webview has closed and will clean up all associated resources related
@ -343,6 +345,9 @@ impl WebView {
return;
}
let rect =
DeviceRect::from_origin_and_size(rect.min, rect.size().max(Size2D::new(1.0, 1.0)));
self.inner_mut().rect = rect;
self.inner()
.compositor
@ -350,7 +355,15 @@ impl WebView {
.move_resize_webview(self.id(), rect);
}
/// Request that the given [`WebView`]'s rendering area be resized. Note that the
/// minimum size for a WebView is 1 pixel by 1 pixel so any requested size will be
/// clamped by that value.
pub fn resize(&self, new_size: PhysicalSize<u32>) {
let new_size = PhysicalSize {
width: new_size.width.max(MINIMUM_WEBVIEW_SIZE.width as u32),
height: new_size.height.max(MINIMUM_WEBVIEW_SIZE.height as u32),
};
self.inner()
.compositor
.borrow_mut()

View file

@ -474,7 +474,11 @@ pub trait WebViewDelegate {
fn request_unload(&self, _webview: WebView, _unload_request: AllowOrDenyRequest) {}
/// Move the window to a point.
fn request_move_to(&self, _webview: WebView, _: DeviceIntPoint) {}
/// Try to resize the window that contains this [`WebView`] to the provided outer size.
/// Try to resize the window that contains this [`WebView`] to the provided outer
/// size. These resize requests can come from page content. Servo will ensure that the
/// values are greater than zero, but it is up to the embedder to limit the maximum
/// size. For instance, a reasonable limitation might be that the final size is no
/// larger than the screen size.
fn request_resize_to(&self, _webview: WebView, _requested_outer_size: DeviceIntSize) {}
/// Whether or not to allow script to open a new `WebView`. If not handled by the
/// embedder, these requests are automatically denied.