From 98de85c64ea677bc1cb0e8e489b3d1369deb92c4 Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 22 Oct 2026 23:40:47 +0200 Subject: [PATCH] fix(raster): prevent overflow in strided row sampling (#8668) Keep scan coordinates and tile boundaries in i64 so valid reads at the final tile and large strides do not overflow. Bound each run by the remaining output and return the surface default beyond the i32 coordinate range. Add differential regressions across all sample depths and missing/present tiles. Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com> --- crates/raster/src/lib.rs | 55 +++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/crates/raster/src/lib.rs b/crates/raster/src/lib.rs index f0d9b63b78..49806e521c 100644 --- a/crates/raster/src/lib.rs +++ b/crates/raster/src/lib.rs @@ -476,30 +476,37 @@ impl Surface { /// Channel `c` at `(x0 + i·step, y)` for `i in 0..out.len()`, looking each tile up once per run /// (much faster than per-sample [`Surface::sample_channel`] for strided scans). + /// Steps below 1 read consecutive pixels; positions beyond `i32::MAX` read the default pixel. pub fn sample_row_strided(&self, y: i32, x0: i32, step: i32, c: usize, out: &mut [f32]) { - let step = step.max(1); + let step = i64::from(step.max(1)); let dp = read_sample(&self.default_pixel, self.format.sample, c); let n = self.format.channels(); let ty = y.div_euclid(TILE_SIZE); let ly = y.rem_euclid(TILE_SIZE) as usize; let mut i = 0; + let mut x = i64::from(x0); while i < out.len() { - let x = x0 + i as i32 * step; - let tx = x.div_euclid(TILE_SIZE); + if x > i64::from(i32::MAX) { + out[i..].fill(dp); + break; + } + let tx = x.div_euclid(i64::from(TILE_SIZE)); // Samples that fall in this tile. - let tile_end = (tx + 1) * TILE_SIZE; + let tile_end = (tx + 1) * i64::from(TILE_SIZE); let count = (((tile_end - x) + step - 1) / step).max(1) as usize; - let end = (i + count).min(out.len()); - match self.tiles.get(&TileCoord { tx, ty }) { + let count = count.min(out.len() - i); + let end = i + count; + match self.tiles.get(&TileCoord { tx: tx as i32, ty }) { None => out[i..end].fill(dp), Some(t) => { for (k, o) in out[i..end].iter_mut().enumerate() { - let lx = (x + k as i32 * step - tx * TILE_SIZE) as usize; + let lx = (x + k as i64 * step - tx * i64::from(TILE_SIZE)) as usize; *o = read_sample(&t.data, self.format.sample, (ly * TILE_SIZE as usize + lx) * n + c); } } } i = end; + x += count as i64 * step; } } @@ -1894,6 +1901,40 @@ mod tests { } } + #[test] + fn strided_row_sampling_handles_extreme_coordinates_and_steps() { + for sample in SampleType::ALL { + let mut s = Surface::with_default(PixelFormat::new(ColorMode::Grayscale, sample, false), &[0.25]); + for (x, value) in [(i32::MIN, 0.1), (-1, 0.3), (0, 0.4), (i32::MAX - 1, 0.6), (i32::MAX, 0.9)] { + s.write_pixel(x, 0, &[value]); + } + for y in [0, 256] { + for (x0, step, len) in [(i32::MAX, 1, 1), (i32::MIN, i32::MAX, 3), (0, i32::MAX, 2), (i32::MAX - 1, 1, 2), (i32::MAX, i32::MAX, 0)] { + let mut out = vec![0.0; len]; + s.sample_row_strided(y, x0, step, 0, &mut out); + for (i, &got) in out.iter().enumerate() { + let x = i32::try_from(i64::from(x0) + i as i64 * i64::from(step)).unwrap(); + assert_eq!(got, s.sample_channel(x, y, 0), "{sample:?} y={y} x0={x0} step={step} i={i}"); + } + } + } + } + } + + #[test] + fn strided_row_sampling_uses_default_past_coordinate_range() { + for sample in SampleType::ALL { + let mut s = Surface::with_default(PixelFormat::new(ColorMode::Grayscale, sample, false), &[0.25]); + s.write_pixel(i32::MAX, 0, &[0.75]); + s.write_pixel(i32::MIN, 0, &[1.0]); + for step in [1, i32::MAX] { + let mut out = [0.0; 4]; + s.sample_row_strided(0, i32::MAX, step, 0, &mut out); + assert_eq!(out, [s.sample_channel(i32::MAX, 0, 0), s.default_pixel()[0], s.default_pixel()[0], s.default_pixel()[0]]); + } + } + } + #[test] fn strided_row_sampling_matches_sample_channel() { let mut s = Surface::with_default(PixelFormat::GRAY8, &[0.25]);