Security: linear work for shortcut review, gradients, recovery names, include cycles, batch references, DOCX style ids, CAD layouts and long strokes

CodeRabbit findings #2, #16, #31, #103, #114, #124, #162, #179, #182, #183, #185:

- Shortcut import review: example names for each code in one pass over the rows.
- Gradients: a midpoint stop is mixed from its two stops; freeform recolor samples its
  increasing positions with one advancing search (`Gradient::sample_increasing_with`).
- Recovery: image entry names are looked up by address, and dead ones swept when the map
  doubles, not on every image.
- Script checker: include cycles from strongly connected components (iterative Tarjan), not
  a reachability row per file.
- Batch step references copy at most 256 MB of values for one step.
- DOCX: style ids over 256 bytes are ignored where defined and where used.
- DXF: layouts keep at most 4 MB of names; a hatch's loops count against the node limit.
- Freehand and Liquify: past 1000 samples a drag previews every n/1000 samples, strokes keep
  at most 100,000 samples, and the stroke kept is previewed in full before it is committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dylan M. Taylor
2026-10-24 17:19:59 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent bc56851904
commit 20bba9ebae
12 changed files with 322 additions and 65 deletions
+17 -4
View File
@@ -77,6 +77,10 @@ const MAX_BASED_ON: usize = 32;
const MAX_FONTS: usize = 4096;
/// The most bytes of a font name kept.
const MAX_NAME: usize = 256;
/// The most bytes of a style id: a longer one (Word's are under 256 characters) is ignored, both
/// where it is defined and where it is used, since every paragraph and run looks its style up by
/// it.
const MAX_STYLE_ID: usize = 256;
/// The most paragraphs placed type takes from one Word document.
const MAX_PARAGRAPHS: usize = 100_000;
/// The most runs of one character formatting placed type takes from one Word document.
@@ -484,7 +488,7 @@ impl Reader {
}
}
Some("style") if self.styles.len() < MAX_STYLES => {
let Some(id) = attr(n, "styleId") else { continue };
let Some(id) = attr(n, "styleId").filter(|id| id.len() <= MAX_STYLE_ID) else { continue };
let default = match attr(n, "type") {
Some("paragraph") => Some(&mut self.default_style),
Some("character") => Some(&mut self.default_char_style),
@@ -495,7 +499,7 @@ impl Reader {
}
let run = child(n, "rPr").map(|r| self.run_props(r)).unwrap_or_default();
let para = child(n, "pPr").map(para_props).unwrap_or_default();
let based_on = child(n, "basedOn").and_then(|b| attr(b, "val")).map(str::to_string);
let based_on = child(n, "basedOn").and_then(|b| attr(b, "val")).filter(|v| v.len() <= MAX_STYLE_ID).map(str::to_string);
self.styles.entry(id.to_string()).or_insert(Style { based_on, run, para });
}
_ => {}
@@ -637,7 +641,12 @@ impl Reader {
fn paragraph(&mut self, p: Node, depth: usize) {
self.flush();
let ppr = child(p, "pPr");
let style = ppr.and_then(|n| child(n, "pStyle")).and_then(|n| attr(n, "val")).map(str::to_string).or_else(|| self.default_style.clone());
let style = ppr
.and_then(|n| child(n, "pStyle"))
.and_then(|n| attr(n, "val"))
.filter(|v| v.len() <= MAX_STYLE_ID)
.map(str::to_string)
.or_else(|| self.default_style.clone());
let mut para = Para::default();
para.apply(&self.default_para);
let mut base = Chars::default();
@@ -688,7 +697,11 @@ impl Reader {
fn run(&mut self, r: Node) {
let rpr = child(r, "rPr");
let mut chars = self.base;
let style = rpr.and_then(|n| child(n, "rStyle")).and_then(|n| attr(n, "val")).or(self.default_char_style.as_deref());
let style = rpr
.and_then(|n| child(n, "rStyle"))
.and_then(|n| attr(n, "val"))
.filter(|v| v.len() <= MAX_STYLE_ID)
.or(self.default_char_style.as_deref());
for s in self.chain(style) {
chars.apply(&s.run, true);
}
+24 -6
View File
@@ -589,17 +589,35 @@ static WRITES: Mutex<()> = Mutex::new(());
type Named = (Weak<Vec<u8>>, String);
/// The entry names of image bytes already hashed ([`blob_name`]), so an image is hashed once,
/// not at every interval. Bytes behind a live `Weak` never change (`Arc::make_mut` copies them).
static NAMES: Mutex<Vec<Named>> = Mutex::new(Vec::new());
/// not at every interval, keyed by the bytes' address. Bytes behind a live `Weak` never change
/// (`Arc::make_mut` copies them).
static NAMES: Mutex<Names> = Mutex::new(Names { by_address: None, live_after_sweep: 0 });
struct Names {
by_address: Option<std::collections::HashMap<usize, Named>>,
/// How many entries were live at the last sweep of dead ones: the next sweep waits until there
/// are twice as many, so sweeping costs a constant per name, not a pass over all of them.
live_after_sweep: usize,
}
/// The name of the entry holding `bytes`: from their content (128 bits of hash and the length), so
/// identical images share one entry and one written before isn't written again.
fn blob_name(bytes: &Arc<Vec<u8>>) -> String {
let mut names = NAMES.lock().unwrap_or_else(|e| e.into_inner());
names.retain(|(w, _)| w.strong_count() > 0);
if let Some((_, name)) = names.iter().find(|(w, _)| std::ptr::eq(w.as_ptr(), Arc::as_ptr(bytes))) {
let mut guard = NAMES.lock().unwrap_or_else(|e| e.into_inner());
let Names { by_address, live_after_sweep } = &mut *guard;
let names = by_address.get_or_insert_with(Default::default);
let address = Arc::as_ptr(bytes) as usize;
// A dead entry at this address named other bytes that were freed there before.
if let Some((w, name)) = names.get(&address)
&& w.strong_count() > 0
&& std::ptr::eq(w.as_ptr(), Arc::as_ptr(bytes))
{
return name.clone();
}
if names.len() >= (*live_after_sweep * 2).max(64) {
names.retain(|_, (w, _)| w.strong_count() > 0);
*live_after_sweep = names.len();
}
let hash = |seed: u8| {
let mut h = std::collections::hash_map::DefaultHasher::new();
seed.hash(&mut h);
@@ -607,7 +625,7 @@ fn blob_name(bytes: &Arc<Vec<u8>>) -> String {
h.finish()
};
let name = format!("img-{:016x}{:016x}-{}{BLOB_EXT}", hash(1), hash(2), bytes.len());
names.push((Arc::downgrade(bytes), name.clone()));
names.insert(address, (Arc::downgrade(bytes), name.clone()));
name
}