From e40f0d5052c95568c676e96123955c4c78ce0c5c Mon Sep 17 00:00:00 2001 From: "K. Isom" Date: Sun, 25 Oct 2026 01:53:28 -0700 Subject: [PATCH] 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. --- tines-gui/src/ipc_client.rs | 40 ++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tines-gui/src/ipc_client.rs b/tines-gui/src/ipc_client.rs index 4075b4a2..ee519404 100644 --- a/tines-gui/src/ipc_client.rs +++ b/tines-gui/src/ipc_client.rs @@ -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> { +/// +/// 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> { 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); + } +}