fix(servo): bound live request frames

This commit is contained in:
2026-07-09 23:44:27 -04:00
parent 83badaea81
commit 58e9bd7d38
11 changed files with 244 additions and 17 deletions
@@ -11,6 +11,8 @@ mod live;
mod live_output;
#[path = "ely_servo_sidecar/live_protocol.rs"]
mod live_protocol;
#[path = "ely_servo_sidecar/live_request.rs"]
mod live_request;
#[path = "ely_servo_sidecar/live_session.rs"]
mod live_session;
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
@@ -1,7 +1,7 @@
use std::{
collections::{HashMap, VecDeque},
fs::{self, File, OpenOptions, TryLockError},
io::{self, BufRead},
io,
path::Path,
};
@@ -19,6 +19,7 @@ use super::{
LIVE_PROTOCOL_VERSION, LiveFrameReport, LiveOutcome, LiveRequest, LiveSidecarError,
MAX_LOADED_URL_BYTES, MAX_PERMISSION_CONSUMPTIONS_PER_RESPONSE, validated_frame_byte_count,
},
live_request::{MAX_REQUEST_LINE_BYTES, RequestLineRead, read_request_line},
live_session::{
LiveInput, LiveSession, apply_input, apply_layout, apply_permissions, bind_profile,
ensure_session,
@@ -57,17 +58,26 @@ pub(super) fn run(args: LiveArgs) -> Result<(), LiveSidecarError> {
let mut handshake_complete = false;
let mut pending_permission_consumptions = VecDeque::new();
let stdin = io::stdin();
let mut stdin = stdin.lock();
let mut stdout = io::stdout().lock();
let mut line = Vec::new();
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let request = serde_json::from_str::<LiveRequest>(&line);
loop {
let request = match read_request_line(&mut stdin, &mut line)? {
RequestLineRead::Eof => break,
RequestLineRead::Ready if line.iter().all(|byte| byte.is_ascii_whitespace()) => {
continue;
}
RequestLineRead::Ready => {
serde_json::from_slice::<LiveRequest>(&line).map_err(LiveSidecarError::from)
}
RequestLineRead::TooLarge => {
Err(LiveSidecarError::RequestLineTooLarge { limit: MAX_REQUEST_LINE_BYTES })
}
};
let should_shutdown =
request.as_ref().is_ok_and(|request| matches!(request, LiveRequest::Shutdown));
let outcome = request.map_err(LiveSidecarError::from).and_then(|request| {
let outcome = request.and_then(|request| {
handle_request(
&mut host,
&mut sessions,
@@ -297,6 +297,9 @@ pub(super) enum LiveSidecarError {
#[error("live protocol mismatch: expected {expected}, received {actual}")]
ProtocolVersionMismatch { expected: u32, actual: u32 },
#[error("live request line exceeds the {limit}-byte protocol limit")]
RequestLineTooLarge { limit: usize },
#[error("request URL exceeds the {limit}-byte live protocol limit")]
RequestUrlTooLong { limit: usize },
@@ -0,0 +1,31 @@
use std::io::{self, BufRead, Read};
pub(super) use ely_servo_host::MAX_REQUEST_LINE_BYTES;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum RequestLineRead {
Eof,
Ready,
TooLarge,
}
pub(super) fn read_request_line(
reader: &mut impl BufRead,
line: &mut Vec<u8>,
) -> io::Result<RequestLineRead> {
line.clear();
let bytes = {
let mut limited = Read::by_ref(reader).take((MAX_REQUEST_LINE_BYTES + 1) as u64);
limited.read_until(b'\n', line)?
};
if bytes == 0 {
return Ok(RequestLineRead::Eof);
}
if bytes <= MAX_REQUEST_LINE_BYTES {
return Ok(RequestLineRead::Ready);
}
if !line.ends_with(b"\n") {
reader.skip_until(b'\n')?;
}
Ok(RequestLineRead::TooLarge)
}
@@ -1,6 +1,10 @@
use std::collections::VecDeque;
use std::{collections::VecDeque, io::Cursor};
use super::take_permission_batch;
use crate::{
live_protocol::LiveRequest,
live_request::{MAX_REQUEST_LINE_BYTES, RequestLineRead, read_request_line},
};
#[test]
fn nine_permission_consumptions_are_sent_in_order_across_two_responses() {
@@ -10,6 +14,66 @@ fn nine_permission_consumptions_are_sent_in_order_across_two_responses() {
assert_eq!(take_permission_batch(&mut pending), vec![9]);
}
#[test]
fn request_line_accepts_the_exact_byte_limit() -> Result<(), Box<dyn std::error::Error>> {
let mut input = Cursor::new(padded_handshake_line(MAX_REQUEST_LINE_BYTES));
let mut line = Vec::new();
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::Ready);
assert_eq!(line.len(), MAX_REQUEST_LINE_BYTES);
assert!(matches!(serde_json::from_slice::<LiveRequest>(&line)?, LiveRequest::Handshake { .. }));
Ok(())
}
#[test]
fn request_line_rejects_limit_plus_one_and_preserves_the_next_frame()
-> Result<(), Box<dyn std::error::Error>> {
let mut wire = padded_handshake_line(MAX_REQUEST_LINE_BYTES + 1);
wire.extend_from_slice(br#"{"type":"shutdown"}"#);
wire.push(b'\n');
let mut input = Cursor::new(wire);
let mut line = Vec::new();
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::TooLarge);
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::Ready);
assert!(matches!(serde_json::from_slice::<LiveRequest>(&line)?, LiveRequest::Shutdown));
Ok(())
}
#[test]
fn request_line_drains_a_distant_delimiter_before_recovery()
-> Result<(), Box<dyn std::error::Error>> {
let mut wire = padded_handshake_line(MAX_REQUEST_LINE_BYTES + 4_096);
wire.extend_from_slice(br#"{"type":"shutdown"}"#);
wire.push(b'\n');
let mut input = Cursor::new(wire);
let mut line = Vec::new();
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::TooLarge);
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::Ready);
assert!(matches!(serde_json::from_slice::<LiveRequest>(&line)?, LiveRequest::Shutdown));
Ok(())
}
#[test]
fn request_line_accepts_a_bounded_eof_terminated_frame() -> Result<(), Box<dyn std::error::Error>> {
let mut input = Cursor::new(br#"{"type":"shutdown"}"#.to_vec());
let mut line = Vec::new();
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::Ready);
assert!(matches!(serde_json::from_slice::<LiveRequest>(&line)?, LiveRequest::Shutdown));
assert_eq!(read_request_line(&mut input, &mut line)?, RequestLineRead::Eof);
Ok(())
}
fn padded_handshake_line(bytes: usize) -> Vec<u8> {
let mut line = br#"{"type":"handshake","protocol_version":3}"#.to_vec();
assert!(bytes > line.len());
line.resize(bytes - 1, b' ');
line.push(b'\n');
line
}
#[cfg(all(feature = "hardware-render", target_os = "macos"))]
mod hardware {
use super::super::{HardwarePollAction, hardware_poll_action};
+2
View File
@@ -16,6 +16,8 @@ mod runtime_waker;
#[cfg(feature = "servo-engine")]
mod runtime_webview;
pub const MAX_REQUEST_LINE_BYTES: usize = 1024 * 1024;
pub use error::ServoHostError;
#[cfg(feature = "hardware-render")]
pub use hardware_rendering_context::HardwareOffscreenContext;