mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 01:32:00 +01:00
security(cms): bound inverse-curve and CLUT interpolation work
Two per-pixel/per-sample amplifiers driven by a crafted ICC profile: - table_inverse recomputed a sampled curve's flat-run edge indices by scanning the whole table on every call, and transform construction inverts each destination curve OUT_TABLE times. Compute the edges once per curve via a new Curve::inverse_table batch method. - eval_multilinear enumerated 2^inputs cube corners over every declared input, including single-grid-point (degenerate) dimensions that contribute nothing. Interpolate over the effective dimensions only, so a 15-channel CLUT of one-point dims no longer does 2^15 iterations per pixel. Findings: - acfbe3a3-ee58-4b7e-b72a-5a4877dd15c9 (CWE-407, repeated flat-run scans) - 9fcc72e7-46d6-4f75-9333-cb14a65eeafd (CWE-407, degenerate-dim corner blowup)
This commit is contained in:
+24
-3
@@ -172,11 +172,16 @@ impl Clut {
|
||||
}
|
||||
let o = self.outputs;
|
||||
out[..o].iter_mut().for_each(|v| *v = 0.0);
|
||||
for corner in 0..(1usize << n) {
|
||||
// Only dimensions with more than one grid point contribute a cube axis. Degenerate
|
||||
// dimensions have step 0 and ratio 0, so they always multiply the corner weight by 1
|
||||
// and never change the offset; enumerating 2^n corners over all declared inputs would
|
||||
// do 2^(degenerate) redundant iterations. Interpolate over the effective dims only.
|
||||
let eff: Vec<usize> = (0..n).filter(|&d| s[d] != 0).collect();
|
||||
for corner in 0..(1usize << eff.len()) {
|
||||
let mut w = 1.0f32;
|
||||
let mut off = base;
|
||||
for d in 0..n {
|
||||
if corner & (1 << d) != 0 {
|
||||
for (bit, &d) in eff.iter().enumerate() {
|
||||
if corner & (1 << bit) != 0 {
|
||||
w *= r[d];
|
||||
off += s[d];
|
||||
} else {
|
||||
@@ -245,4 +250,20 @@ mod tests {
|
||||
c.eval(&[0.5, 0.25], &mut o);
|
||||
assert!((o[0] - 0.75).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn degenerate_dimensions_do_not_change_the_result() {
|
||||
// Dimensions with a single grid point contribute nothing, but the old code still
|
||||
// enumerated 2^inputs corners over them. Interpolating over effective dims only must
|
||||
// give the same answer as a reference built without the degenerate axes.
|
||||
let f = |p: &[f32], o: &mut [f32]| o[0] = 0.25 * p[0] + 0.5 * p[2] + 0.125 * p[4];
|
||||
// Axes 1 and 3 have a single grid point (degenerate); axes 0, 2, 4 vary.
|
||||
let c = Clut::sample(vec![3, 1, 4, 1, 5], 1, f);
|
||||
let mut got = [0.0];
|
||||
for p in [[0.2f32, 0.0, 0.7, 0.0, 0.4], [1.0, 0.0, 0.0, 0.0, 1.0], [0.5, 0.0, 0.5, 0.0, 0.5]] {
|
||||
c.eval_multilinear(&p, &mut got);
|
||||
let want = 0.25 * p[0] + 0.5 * p[2] + 0.125 * p[4];
|
||||
assert!((got[0] - want).abs() < 1e-5, "got {} want {want}", got[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+47
-2
@@ -99,6 +99,20 @@ impl Curve {
|
||||
(0..n).map(|i| self.eval64(i as f64 / (n - 1) as f64) as f32).collect()
|
||||
}
|
||||
|
||||
/// Inverse of the curve at each `y` in `ys`. For sampled-table curves the flat-run edge
|
||||
/// indices are computed once for the whole batch instead of rescanning the table on every
|
||||
/// call, so inverting a long constant table many times stays linear.
|
||||
pub fn inverse_table(&self, ys: impl Iterator<Item = f64>) -> Vec<f32> {
|
||||
match self {
|
||||
Curve::Table(t) if t.len() >= 2 => {
|
||||
let increasing = t[t.len() - 1] >= t[0];
|
||||
let (edge_start, edge_end) = (edge_index(t, true), edge_index(t, false));
|
||||
ys.map(|y| table_inverse_with(t, y as f32, increasing, edge_start, edge_end)).collect()
|
||||
}
|
||||
_ => ys.map(|y| self.eval_inverse64(y) as f32).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` when the curve never decreases over `[0, 1]`.
|
||||
pub fn is_monotonic(&self) -> bool {
|
||||
let s = self.sample(1024);
|
||||
@@ -244,13 +258,24 @@ fn table_inverse(t: &[f32], y: f32) -> f32 {
|
||||
return y;
|
||||
}
|
||||
let increasing = t[n - 1] >= t[0];
|
||||
table_inverse_with(t, y, increasing, edge_index(t, true), edge_index(t, false))
|
||||
}
|
||||
|
||||
/// [`table_inverse`] with the flat-run edge indices supplied. Computing those edges scans the
|
||||
/// whole table, and transform construction inverts the same table thousands of times, so the
|
||||
/// edges are computed once and reused across a batch (see [`Curve::inverse_table`]).
|
||||
fn table_inverse_with(t: &[f32], y: f32, increasing: bool, edge_start: f32, edge_end: f32) -> f32 {
|
||||
let n = t.len();
|
||||
if n < 2 {
|
||||
return y;
|
||||
}
|
||||
let (lo_v, hi_v) = if increasing { (t[0], t[n - 1]) } else { (t[n - 1], t[0]) };
|
||||
if y <= lo_v {
|
||||
// Flat start: choose the last index still at the lowest value.
|
||||
return if increasing { edge_index(t, true) } else { edge_index(t, false) };
|
||||
return if increasing { edge_start } else { edge_end };
|
||||
}
|
||||
if y >= hi_v {
|
||||
return if increasing { edge_index(t, false) } else { edge_index(t, true) };
|
||||
return if increasing { edge_end } else { edge_start };
|
||||
}
|
||||
let (mut lo, mut hi) = (0usize, n - 1);
|
||||
while hi - lo > 1 {
|
||||
@@ -336,4 +361,24 @@ mod tests {
|
||||
let g = Curve::Gamma(2.2);
|
||||
assert!((g.eval64(-0.5) + 0.5f64.powf(2.2)).abs() < 1e-12);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inverse_table_matches_per_call_including_flat_runs() {
|
||||
// Batch inverse must equal calling eval_inverse64 per point. Include a table with flat
|
||||
// runs at both ends (the case that used to rescan the table on every call).
|
||||
let tables = [
|
||||
Curve::Table(vec![0.0, 0.0, 0.0, 0.25, 0.5, 1.0, 1.0, 1.0]),
|
||||
Curve::Table(vec![0.0, 0.2, 0.4, 0.6, 0.8, 1.0]),
|
||||
Curve::Table(vec![1.0, 0.5, 0.0]),
|
||||
Curve::Gamma(2.2),
|
||||
];
|
||||
let ys: Vec<f64> = (0..=40).map(|i| i as f64 / 40.0).collect();
|
||||
for c in &tables {
|
||||
let batch = c.inverse_table(ys.iter().copied());
|
||||
for (i, &y) in ys.iter().enumerate() {
|
||||
let one = c.eval_inverse64(y) as f32;
|
||||
assert!((batch[i] - one).abs() < 1e-6, "y={y}: batch {} vs per-call {one}", batch[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,12 +133,11 @@ impl Transform {
|
||||
let tables = cs
|
||||
.iter()
|
||||
.map(|c| {
|
||||
(0..OUT_TABLE)
|
||||
.map(|i| {
|
||||
let u = i as f64 / (OUT_TABLE - 1) as f64;
|
||||
c.eval_inverse64(u * u * u * u).clamp(0.0, 1.0) as f32
|
||||
})
|
||||
.collect()
|
||||
let ys = (0..OUT_TABLE).map(|i| {
|
||||
let u = i as f64 / (OUT_TABLE - 1) as f64;
|
||||
u * u * u * u
|
||||
});
|
||||
c.inverse_table(ys).into_iter().map(|v| v.clamp(0.0, 1.0)).collect()
|
||||
})
|
||||
.collect();
|
||||
post = Some(Post { tables });
|
||||
|
||||
Reference in New Issue
Block a user