From 777df012e3f9e30d2684f24fb1f932766afdb770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Wed, 13 May 2026 00:29:26 -0400 Subject: [PATCH] Validate Servo hardware frame surfaces --- crates/ely_app/src/shell/web_surface_frame.rs | 45 +++++++++++++- crates/ely_app/src/shell/web_surface_tests.rs | 58 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/ely_app/src/shell/web_surface_frame.rs b/crates/ely_app/src/shell/web_surface_frame.rs index f1b489c..97d11b8 100644 --- a/crates/ely_app/src/shell/web_surface_frame.rs +++ b/crates/ely_app/src/shell/web_surface_frame.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use ahash::AHasher; #[cfg(target_os = "macos")] -use core_video::pixel_buffer::CVPixelBuffer; +use core_video::pixel_buffer::{CVPixelBuffer, kCVPixelFormatType_32BGRA}; use gpui::RenderImage; use image::{ImageBuffer, Rgba}; use thiserror::Error; @@ -109,6 +109,11 @@ impl WebSurfaceFrame { return Err(WebSurfaceError::MissingRenderablePayload); } + #[cfg(target_os = "macos")] + if let Some(pixel_buffer) = parts.pixel_buffer.as_ref() { + validate_hardware_pixel_buffer(pixel_buffer, parts.width, parts.height)?; + } + let image = if parts.rgba_bytes.is_empty() { None } else { @@ -249,6 +254,44 @@ pub(super) enum WebSurfaceError { InvalidFrameBuffer { width: u32, height: u32 }, #[error("servo live frame did not include a software image or hardware IOSurface")] MissingRenderablePayload, + #[cfg(target_os = "macos")] + #[error( + "servo hardware surface size {actual_width}x{actual_height} did not match frame report {expected_width}x{expected_height}" + )] + HardwareSurfaceSizeMismatch { + expected_width: u32, + expected_height: u32, + actual_width: usize, + actual_height: usize, + }, + #[cfg(target_os = "macos")] + #[error("servo hardware surface pixel format 0x{actual:x} is unsupported; expected 32BGRA")] + UnsupportedHardwareSurfaceFormat { actual: u32 }, +} + +#[cfg(target_os = "macos")] +fn validate_hardware_pixel_buffer( + pixel_buffer: &CVPixelBuffer, + expected_width: u32, + expected_height: u32, +) -> Result<(), WebSurfaceError> { + let actual_width = pixel_buffer.get_width(); + let actual_height = pixel_buffer.get_height(); + if actual_width != expected_width as usize || actual_height != expected_height as usize { + return Err(WebSurfaceError::HardwareSurfaceSizeMismatch { + expected_width, + expected_height, + actual_width, + actual_height, + }); + } + + let actual_format = pixel_buffer.get_pixel_format(); + if actual_format != kCVPixelFormatType_32BGRA { + return Err(WebSurfaceError::UnsupportedHardwareSurfaceFormat { actual: actual_format }); + } + + Ok(()) } /// Swap byte 0 and byte 2 of every 4-byte pixel, converting Servo's diff --git a/crates/ely_app/src/shell/web_surface_tests.rs b/crates/ely_app/src/shell/web_surface_tests.rs index 2198d2f..b11b66a 100644 --- a/crates/ely_app/src/shell/web_surface_tests.rs +++ b/crates/ely_app/src/shell/web_surface_tests.rs @@ -432,6 +432,64 @@ fn hardware_live_frame_with_pixel_buffer_skips_software_image() -> Result<(), St Ok(()) } +#[cfg(target_os = "macos")] +#[test] +fn hardware_live_frame_rejects_mismatched_surface_size() -> Result<(), String> { + use core_video::pixel_buffer::{CVPixelBuffer, kCVPixelFormatType_32BGRA}; + + use crate::services::servo_live::ServoLiveFrame; + use crate::shell::web_surface_frame::WebSurfaceFrame; + use crate::shell::web_surface_geometry::WebSurfaceScrollOffset; + + let pixel_buffer = CVPixelBuffer::new(kCVPixelFormatType_32BGRA, 2, 1, None) + .map_err(|status| format!("CVPixelBufferCreate returned status {status}"))?; + let live = ServoLiveFrame::for_test_with_pixel_buffer(1, 1, pixel_buffer); + let result = WebSurfaceFrame::from_live_frame( + "https://example.com/".to_string(), + WebSurfaceScrollOffset::default(), + 100, + live, + ); + + let error = match result { + Ok(_) => return Err("mismatched hardware surface reached Ready state".to_string()), + Err(error) => error, + }; + assert_eq!(error.to_string(), "servo hardware surface size 2x1 did not match frame report 1x1",); + Ok(()) +} + +#[cfg(target_os = "macos")] +#[test] +fn hardware_live_frame_rejects_unsupported_surface_format() -> Result<(), String> { + use core_video::pixel_buffer::{CVPixelBuffer, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange}; + + use crate::services::servo_live::ServoLiveFrame; + use crate::shell::web_surface_frame::WebSurfaceFrame; + use crate::shell::web_surface_geometry::WebSurfaceScrollOffset; + + let pixel_buffer = + CVPixelBuffer::new(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, 2, 2, None) + .map_err(|status| format!("CVPixelBufferCreate returned status {status}"))?; + let live = ServoLiveFrame::for_test_with_pixel_buffer(2, 2, pixel_buffer); + let result = WebSurfaceFrame::from_live_frame( + "https://example.com/".to_string(), + WebSurfaceScrollOffset::default(), + 100, + live, + ); + + let error = match result { + Ok(_) => return Err("unsupported hardware surface format reached Ready state".to_string()), + Err(error) => error, + }; + assert_eq!( + error.to_string(), + "servo hardware surface pixel format 0x34323066 is unsupported; expected 32BGRA", + ); + Ok(()) +} + fn web_bounds() -> Bounds { Bounds::new(point(px(0.0), px(0.0)), size(px(640.0), px(480.0))) }