fix(gui): cap ipc_client frame reads at the service's 1 MiB frame cap

A torn stream could yield an arbitrary u32 length prefix, turning a
re-dial into a multi-GiB allocation attempt. Mirror the service reader:
len > MAX_FRAME is InvalidData, so the worker treats it as a dead
connection and re-dials instead of OOMing.
This commit is contained in:
2026-10-25 01:53:28 -07:00
parent 6c6729324e
commit e40f0d5052
+39 -1
View File
@@ -119,15 +119,29 @@ fn write_frame(stream: &mut UnixStream, body: &[u8]) -> io::Result<()> {
}
/// Read one `[u32 LE len][body]` frame from the stream.
fn read_frame(stream: &mut UnixStream) -> io::Result<Vec<u8>> {
///
/// Mirrors the service's reader: a length prefix above [`MAX_FRAME`] is a
/// protocol violation (`InvalidData`) rather than an allocation, so a torn
/// or hostile frame kills the connection (worker re-dials) instead of the
/// process.
fn read_frame(stream: &mut impl Read) -> io::Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
stream.read_exact(&mut len_buf)?;
let len = u32::from_le_bytes(len_buf) as usize;
if len > MAX_FRAME {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("frame {len} bytes exceeds {MAX_FRAME}"),
));
}
let mut buf = vec![0u8; len];
stream.read_exact(&mut buf)?;
Ok(buf)
}
/// 1 MiB cap on a single JSON frame, matching the service's own reader.
const MAX_FRAME: usize = 1 << 20;
/// The worker loop: dial, serve requests in order, redial forever.
fn worker(
socket: PathBuf,
@@ -196,3 +210,27 @@ fn worker(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_frame_accepts_a_valid_frame() {
let body = b"{\"ok\":true}";
let mut frame = Vec::new();
frame.extend_from_slice(&(body.len() as u32).to_le_bytes());
frame.extend_from_slice(body);
assert_eq!(read_frame(&mut &frame[..]).unwrap(), body);
}
#[test]
fn read_frame_rejects_an_oversized_len_prefix() {
// A torn stream could yield any u32; the cap keeps it from becoming
// an allocation.
let mut frame = ((MAX_FRAME as u32) + 1).to_le_bytes().to_vec();
frame.extend_from_slice(&[0u8; 8]);
let err = read_frame(&mut &frame[..]).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidData);
}
}