Audit: automation roots follow file.showPackage, the Place queue, headless file methods, clipboard files, placed-document workers and caches, folder searches and the recovery folder

This commit is contained in:
Dylan M. Taylor
2026-10-24 05:16:20 +07:00
committed by bflatastic
parent 8743f63f50
commit e95e8418ed
13 changed files with 118 additions and 34 deletions
+4
View File
@@ -979,6 +979,10 @@ fn may_write(path: &str) -> Result<()> {
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn read_file(path: &str) -> Result<Vec<u8>> {
may_read(path)?;
// A device or pipe may never end: only a regular file is read to its end.
if !std::fs::metadata(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?.is_file() {
return Err(EngineError::Other(format!("{path}: not a file")));
}
std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))
}
+9 -1
View File
@@ -259,6 +259,10 @@ pub(crate) fn start_in(
}
// The walkers follow no links, so a folder automation may read holds all they read.
crate::file_access::check_read(&folder.to_string_lossy())?;
// The check above reads a lossy string: under roots, a path it would alter names another folder.
if folder.to_str().is_none() && crate::file_access::current().is_some() {
return Err("automation path rejected: the folder's name isn't valid text".into());
}
let seconds = if limits.seconds.is_finite() { limits.seconds.clamp(0.0, 3600.0) } else { 60.0 };
let n = pool.reserve(threads.clamp(1, MAX_THREADS));
if n == 0 {
@@ -279,6 +283,7 @@ pub(crate) fn start_in(
walk: Mutex::new(Walk { root: Some(folder), alive: n, found: vec![0; items], ..Walk::default() }),
wake: Condvar::new(),
pool,
scope: crate::file_access::ScopeHandle::capture(),
});
let mut spawned = 0;
for _ in 0..n {
@@ -325,6 +330,8 @@ struct Shared {
/// Woken when folders are queued, a walker finishes a folder, or the search ends.
wake: Condvar,
pool: &'static Pool,
/// The automation roots in force when the search started, for the folders the walkers add.
scope: crate::file_access::ScopeHandle,
}
/// The state walkers change under the lock.
@@ -490,7 +497,8 @@ impl Shared {
}
queue.push((path, depth + 1));
}
let home = rules.home_below(&listed);
// The home folder is added by a rule, not picked: it needs the picked folder's authority.
let home = rules.home_below(&listed).filter(|h| !rules.is_refused(h) && self.scope.clone().run(|| crate::file_access::check_read(&h.to_string_lossy())).is_ok());
{
let mut w = self.lock();
w.files += files;
+17 -8
View File
@@ -159,7 +159,11 @@ pub(crate) fn read_document(p: &PlacedDocument, bytes: &[u8]) -> Option<(Documen
/// `p`'s file read again, while it is the file it was read from (how the document model gets it
/// when only the preview is stored, [`vectorcraft_doc::placed_document::set_file_reader`]).
pub(crate) fn read_file_again(p: &PlacedDocument) -> Option<Vec<u8>> {
let bytes = fileio::read_file(&p.link.path).ok()?;
// One byte over the limit tells a file that is too big, without reading it all.
let bytes = fileio::read_file_head(&p.link.path, MAX_FILE + 1).ok()?;
if bytes.len() > MAX_FILE {
return None;
}
if p.link.hash.as_ref().is_some_and(|h| *h != hash_bytes(&bytes)) {
return None;
}
@@ -186,7 +190,10 @@ pub(crate) struct Source {
pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bounding: bool, page_box: PageBox, cmd: &str) -> Result<Source> {
// The same document placed the same way is read once (placing it again is free).
let key = key_for(&hash_bytes(bytes), page, bounding, page_box);
if let Some(s) = lock_memo().iter().find(|s| s.key == key).cloned() {
// Resolving its links depends on where the file is and on what the caller may read, so a hit
// needs the same path, and nothing is shared with or from a confined (automation) caller.
let confined = crate::file_access::current().is_some();
if !confined && let Some((_, s)) = lock_memo().iter().find(|(at, s)| s.key == key && at.as_deref() == path).cloned() {
return Ok(s);
}
let a = document_art(name, bytes, path, page, bounding, page_box, cmd)?;
@@ -206,11 +213,13 @@ pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bo
// Drawing it needn't read it again.
vectorcraft_doc::placed_document::prime(&shown, blob.bytes.clone(), (a.doc, art, a.frame));
let s = Source { blob, key, page_box, frame: a.frame, warnings: a.warnings };
let mut m = lock_memo();
if m.len() >= MEMO_SIZE {
m.remove(0);
if !confined {
let mut m = lock_memo();
if m.len() >= MEMO_SIZE {
m.remove(0);
}
m.push((path.map(str::to_string), s.clone()));
}
m.push(s.clone());
Ok(s)
}
@@ -218,9 +227,9 @@ pub(crate) fn source(name: &str, bytes: &[u8], path: Option<&str>, page: u32, bo
const MEMO_SIZE: usize = 32;
/// Files read lately for placing, by key.
static MEMO: std::sync::Mutex<Vec<Source>> = std::sync::Mutex::new(Vec::new());
static MEMO: std::sync::Mutex<Vec<(Option<String>, Source)>> = std::sync::Mutex::new(Vec::new());
fn lock_memo() -> std::sync::MutexGuard<'static, Vec<Source>> {
fn lock_memo() -> std::sync::MutexGuard<'static, Vec<(Option<String>, Source)>> {
// Entries are only pushed and removed: a panic while held leaves it whole.
MEMO.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
+6 -1
View File
@@ -350,7 +350,12 @@ fn load_folder(folder: &str) -> Value {
return json!({"folder": folder, "error": e});
}
// A file in it may be a link to one elsewhere.
match registry::load_folder_where(std::path::Path::new(folder), |f| crate::file_access::check_read(&f.to_string_lossy())) {
match registry::load_folder_where(std::path::Path::new(folder), |f| match f.to_str() {
Some(f) => crate::file_access::check_read(f),
// A lossy name would check another file than the one loaded.
None if crate::file_access::current().is_none() => Ok(()),
None => Err(format!("automation path rejected: {} isn't valid text", f.display())),
}) {
Ok(r) => {
json!({"folder": folder, "loaded": r.loaded, "failed": r.failed.iter().map(|(f, e)| json!({"file": f, "error": e})).collect::<Vec<_>>()})
}
+7 -6
View File
@@ -472,13 +472,14 @@ impl Prefs {
/// The folders the app reads (fonts, plug-ins) or writes (Data Recovery copies, the Templates
/// folder it makes for its dialogs) on its own, with the access each needs.
fn folders_mut(&mut self) -> [(&mut String, crate::file_access::Access); 4] {
fn folders_mut(&mut self) -> [(&mut String, &'static [crate::file_access::Access]); 4] {
use crate::file_access::Access;
[
(&mut self.fonts_folder, Access::Read),
(&mut self.plugins_folder, Access::Read),
(&mut self.recovery_folder, Access::Write),
(&mut self.templates_folder, Access::Write),
(&mut self.fonts_folder, &[Access::Read][..]),
(&mut self.plugins_folder, &[Access::Read][..]),
// Data Recovery lists and restores what is in its folder too.
(&mut self.recovery_folder, &[Access::Write, Access::Read][..]),
(&mut self.templates_folder, &[Access::Write][..]),
]
}
@@ -494,7 +495,7 @@ impl Prefs {
if folder.is_empty() || folder == old.trim() {
continue;
}
if let Err(e) = roots.check(folder, access) {
if let Err(e) = access.iter().try_for_each(|a| roots.check(folder, *a)) {
refused.push(e);
*new = old.clone();
}