From 63b6de336fe29614eeaab867c355d13f96846bef Mon Sep 17 00:00:00 2001 From: radialmonster Date: Sun, 25 Oct 2026 20:22:54 -0400 Subject: [PATCH 1/2] raw: CR3 preview box scan checks 32-bit lengths for overflow (#2569) On wasm32 a PRVW/THMB box or JPEG length near u32::MAX wrapped j + len and start + size, so both bounds checks passed and the slice panicked (abort in the web build). Use checked_add and bytes.get, and add a regression test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM --- crates/raw/src/preview.rs | 6 ++++-- crates/raw/src/preview/tests.rs | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/raw/src/preview.rs b/crates/raw/src/preview.rs index 2047174da..2403e5035 100644 --- a/crates/raw/src/preview.rs +++ b/crates/raw/src/preview.rs @@ -438,10 +438,12 @@ fn cr3_preview_boxes(bytes: &[u8]) -> Option<&[u8]> { let be32 = |at: usize| bytes.get(at..at + 4).map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]) as usize); let (Some(size), Some(len)) = (be32(start), be32(start + 20)) else { continue }; let j = start + 24; - if len < 4 || j + len > start + size.max(24) || j + len > bytes.len() { + // 32-bit box lengths can wrap `usize` on wasm32: check the sums, then slice with `get` + let (Some(end), Some(lim)) = (j.checked_add(len), start.checked_add(size.max(24))) else { continue }; + if len < 4 || end > lim { continue; } - let jpeg = &bytes[j..j + len]; + let Some(jpeg) = bytes.get(j..end) else { continue }; // One that doesn't fit the budget left is skipped; a smaller one later may still fit. if walked + jpeg.len() > CR3_MAX_WALK { continue; diff --git a/crates/raw/src/preview/tests.rs b/crates/raw/src/preview/tests.rs index 2ee098414..f526e868b 100644 --- a/crates/raw/src/preview/tests.rs +++ b/crates/raw/src/preview/tests.rs @@ -199,6 +199,20 @@ fn picks_largest_dct_jpeg() { } } +/// #2569: `PRVW` box and JPEG lengths near `u32::MAX` (which wrap `usize` on wasm32) find no preview, never panic. +#[test] +fn cr3_preview_box_huge_lengths_are_rejected() { + let j = fake_jpeg(300); + for (size, len) in [(u32::MAX, u32::MAX), (u32::MAX, u32::MAX - 23), (24 + j.len() as u32, u32::MAX - 3)] { + let mut cr3 = b"ftypcrx crx isom".to_vec(); + cr3.extend_from_slice(&size.to_be_bytes()); + cr3.extend_from_slice(b"PRVWT8"); + cr3.extend_from_slice(&len.to_be_bytes()); + cr3.extend_from_slice(&j); + assert_eq!(cr3_preview_boxes(&cr3), None, "size {size:#x}, len {len:#x}"); + } +} + /// A JPEG like [`fake_jpeg`] whose frame header says `w` x `h` with `nc` components. fn sized_jpeg(w: u16, h: u16, nc: u8, n: usize) -> Vec { let mut j = vec![0xff, 0xd8, 0xff, 0xc0, 0x00, 8 + 3 * nc, 8]; From 29f9c190468b4df0edb2bbdbc8db4f99a4111d2d Mon Sep 17 00:00:00 2001 From: radialmonster Date: Thu, 29 Oct 2026 21:48:04 -0400 Subject: [PATCH 2/2] test(raw): spell control bytes in CR3 preview literals as escapes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM --- crates/raw/src/preview/tests.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/raw/src/preview/tests.rs b/crates/raw/src/preview/tests.rs index f526e868b..56a726f78 100644 --- a/crates/raw/src/preview/tests.rs +++ b/crates/raw/src/preview/tests.rs @@ -204,9 +204,9 @@ fn picks_largest_dct_jpeg() { fn cr3_preview_box_huge_lengths_are_rejected() { let j = fake_jpeg(300); for (size, len) in [(u32::MAX, u32::MAX), (u32::MAX, u32::MAX - 23), (24 + j.len() as u32, u32::MAX - 3)] { - let mut cr3 = b"ftypcrx crx isom".to_vec(); + let mut cr3 = b"\x00\x00\x00\x18ftypcrx \x00\x00\x00\x01crx isom".to_vec(); cr3.extend_from_slice(&size.to_be_bytes()); - cr3.extend_from_slice(b"PRVWT8"); + cr3.extend_from_slice(b"PRVW\x00\x00\x00\x00\x00\x01\x06T\x048\x00\x01"); cr3.extend_from_slice(&len.to_be_bytes()); cr3.extend_from_slice(&j); assert_eq!(cr3_preview_boxes(&cr3), None, "size {size:#x}, len {len:#x}");