From 0f6bba6742af3ac4b0cdd22b3410ec1a9abb2772 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza <62690310+FarhanAliRaza@users.noreply.github.com> Date: Wed, 22 Jul 2026 19:46:03 +0500 Subject: [PATCH 1/2] fix(core): bound malformed display-list allocations --- src/kernels.rs | 1 + src/lib.rs | 5 ++++- src/raster.rs | 40 ++++++++++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/kernels.rs b/src/kernels.rs index f9489ff3..673bd432 100644 --- a/src/kernels.rs +++ b/src/kernels.rs @@ -5926,6 +5926,7 @@ mod fuzz { } #[test] + #[cfg(any(not(target_family = "wasm"), target_feature = "atomics"))] fn fuzz_parallel_matches_serial() { // The public fns only fan out past PAR_THRESHOLD, so drive the impl // directly: hostile data must produce bitwise-identical results for diff --git a/src/lib.rs b/src/lib.rs index fa061108..8e6a95d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,9 @@ fn finite_ordered(lo: f64, hi: f64) -> bool { /// point's error sentinel instead; output buffers may then be partially /// written, exactly like the existing invalid-argument paths, and callers /// already treat the sentinel as "output undefined". `AssertUnwindSafe` is -/// sound because nothing observes the closure's captures after a panic. +/// sound because nothing observes the closure's captures after a panic. This +/// backstop only operates when the target supports panic unwinding; on +/// panic-abort targets such as wasm32, an internal panic aborts the instance. fn ffi_guard(sentinel: R, body: impl FnOnce() -> R) -> R { std::panic::catch_unwind(std::panic::AssertUnwindSafe(body)).unwrap_or(sentinel) } @@ -2840,6 +2842,7 @@ mod tests { use super::*; #[test] + #[cfg(panic = "unwind")] fn ffi_guard_maps_panic_to_sentinel() { // A panic anywhere behind the C ABI must become the entry point's // error sentinel, never an unwind across `extern "C"` (which would diff --git a/src/raster.rs b/src/raster.rs index 79d30d99..5da85840 100644 --- a/src/raster.rs +++ b/src/raster.rs @@ -1318,6 +1318,12 @@ struct Reader<'a> { } impl<'a> Reader<'a> { + /// Bound allocations driven by an untrusted element count to the number + /// of complete elements that the unread command buffer could contain. + fn bounded_capacity(&self, count: usize, item_size: usize) -> usize { + count.min(self.b.len().saturating_sub(self.i) / item_size) + } + fn u8(&mut self) -> Option { let v = *self.b.get(self.i)?; self.i += 1; @@ -1358,7 +1364,7 @@ impl<'a> Reader<'a> { ]) } fn pts(&mut self, n: usize) -> Option> { - let mut v = Vec::with_capacity(n); + let mut v = Vec::with_capacity(self.bounded_capacity(n, 8)); for _ in 0..n { v.push((self.f32()?, self.f32()?)); } @@ -1912,7 +1918,7 @@ fn rasterize_with_spans( let pts = r.pts(n)?; let (g0x, g0y, g1x, g1y) = (r.f32()?, r.f32()?, r.f32()?, r.f32()?); let ns = r.u32()? as usize; - let mut stops = Vec::with_capacity(ns); + let mut stops = Vec::with_capacity(r.bounded_capacity(ns, 8)); for _ in 0..ns { stops.push((r.f32()?, r.rgba()?)); } @@ -1930,7 +1936,7 @@ fn rasterize_with_spans( let c = r.rgba()?; let closed = r.u8()? != 0; let nd = r.u32()? as usize; - let mut dash = Vec::with_capacity(nd); + let mut dash = Vec::with_capacity(r.bounded_capacity(nd, 4)); for _ in 0..nd { dash.push(r.f32()?); } @@ -2375,7 +2381,7 @@ fn rasterize_with_spans( let width = r.f32()?; let color = r.rgba()?; let nd = r.u32()? as usize; - let mut dash = Vec::with_capacity(nd); + let mut dash = Vec::with_capacity(r.bounded_capacity(nd, 4)); for _ in 0..nd { dash.push(r.f32()?); } @@ -2660,9 +2666,31 @@ mod tests { #[test] fn malformed_buffer_is_rejected_not_panicked() { - let cmd = vec![OP_FILL_POLY, 9, 9, 9, 9]; // claims a huge point count let mut out = vec![0u8; 4 * 4 * 4]; - assert!(!rasterize_into(&cmd, 4, 4, &mut out)); + + let huge = [9, 9, 9, 9]; + let points = vec![OP_FILL_POLY, huge[0], huge[1], huge[2], huge[3]]; + + let mut gradient_stops = vec![OP_FILL_POLY_GRAD]; + gradient_stops.extend(u32le(0)); + gradient_stops.extend([0; 16]); + gradient_stops.extend(huge); + + let mut stroke_dashes = vec![OP_STROKE]; + stroke_dashes.extend(u32le(0)); + stroke_dashes.extend([0; 9]); // width, RGBA, and closed flag + stroke_dashes.extend(huge); + + let mut smooth_dashes = vec![OP_SMOOTH_STROKE]; + smooth_dashes.extend(u32le(2)); + smooth_dashes.extend([0; 64]); // x and y affine scales + smooth_dashes.extend([0; 32]); // two f64 x values and two f64 y values + smooth_dashes.extend([0; 8]); // width and RGBA + smooth_dashes.extend(huge); + + for cmd in [points, gradient_stops, stroke_dashes, smooth_dashes] { + assert!(!rasterize_into(&cmd, 4, 4, &mut out)); + } } fn one_point( From 4d77e8a411230126b44850cd010940ee736a1e3b Mon Sep 17 00:00:00 2001 From: Alek Date: Thu, 23 Jul 2026 12:58:53 -0700 Subject: [PATCH 2/2] fix(core): skip forced parallel test on wasm --- src/kernels.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kernels.rs b/src/kernels.rs index 673bd432..0fbfd0ca 100644 --- a/src/kernels.rs +++ b/src/kernels.rs @@ -5926,7 +5926,7 @@ mod fuzz { } #[test] - #[cfg(any(not(target_family = "wasm"), target_feature = "atomics"))] + #[cfg(not(target_family = "wasm"))] fn fuzz_parallel_matches_serial() { // The public fns only fan out past PAR_THRESHOLD, so drive the impl // directly: hostile data must produce bitwise-identical results for