fix(engine): link watch and Link Info read within the asking caller's roots

This commit is contained in:
Dylan M. Taylor
2026-11-03 22:17:45 +07:00
committed by bflatastic
parent 40b9fa4717
commit 4d0ecb264c
3 changed files with 32 additions and 4 deletions
+4 -2
View File
@@ -1338,8 +1338,10 @@ fn placement_json(o: PlacementOptions) -> Value {
/// Link Info's fields of the linked file `l` (of object `id` of `st`) into `out`.
fn file_fields(st: &crate::DocState, id: NodeId, l: &LinkInfo, out: &mut Value) {
// Within what the caller that opened the document may read, whoever asks.
crate::file_access::confine(st.opened_by.as_ref(), || file_fields_here(st, id, l, out));
// A confined caller reads within its own roots; anyone else within what the caller that
// opened the document may read (never wider than the caller asking now).
let roots = crate::file_access::current().or_else(|| st.opened_by.clone());
crate::file_access::confine(roots.as_ref(), || file_fields_here(st, id, l, out));
}
fn file_fields_here(st: &crate::DocState, id: NodeId, l: &LinkInfo, out: &mut Value) {
+10 -2
View File
@@ -69,6 +69,9 @@ impl Session {
}
let Some(st) = self.active() else { return };
let (uid, files, doc_path) = (st.uid, crate::cmd::links::linked_groups(&st.doc), st.path.clone());
// A confined caller's roots, else what the caller that opened the document may read (the
// desktop frame is unconfined).
let opened_by = st.opened_by.clone();
if files.is_empty() {
return;
}
@@ -76,7 +79,11 @@ impl Session {
let out = slot.clone();
let scope = crate::file_access::ScopeHandle::capture();
let spawned = std::thread::Builder::new().name("link-watch".into()).spawn(move || {
let stamps = scope.run(|| files.into_iter().map(|(p, ids)| (p.clone(), ids, stamp(&p, doc_path.as_deref()))).collect());
let stamps = scope.run(|| {
crate::file_access::confine(crate::file_access::current().or(opened_by).as_ref(), || {
files.into_iter().map(|(p, ids)| (p.clone(), ids, stamp(&p, doc_path.as_deref()))).collect()
})
});
*out.lock().unwrap_or_else(PoisonError::into_inner) = Some((uid, stamps));
});
if spawned.is_ok() {
@@ -141,7 +148,8 @@ impl Session {
let mode = if update { "automatically" } else { self.prefs.update_links.as_str() };
match mode {
"automatically" => {
let r = self.execute("links.update", &json!({"ids": ids}))?;
let roots = crate::file_access::current().or_else(|| self.active().and_then(|d| d.opened_by.clone()));
let r = crate::file_access::confine(roots.as_ref(), || self.execute("links.update", &json!({"ids": ids})))?;
// Not readable yet (still being written): the next look tries again.
let unread: BTreeSet<u64> = r["missing"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default();
for (path, ids) in &changed {
+18
View File
@@ -278,6 +278,24 @@ fn link_info_has_the_file_and_placement_details() {
assert_eq!((i["status"].as_str(), i["linked"].as_bool(), i["fileName"].as_str()), (Some("embedded"), Some(false), None));
}
/// Link Info reads within the opener's roots for an unconfined caller, and never past a confined
/// caller's own roots.
#[test]
fn link_info_stays_within_the_asking_callers_roots() {
let dir = Folder::new("info-roots");
let other = Folder::new("info-other");
let pic = dir.file("photo.png");
write(&pic, &png(60, 30, RED));
let mut s = session();
let id = place(&mut s, &pic);
let roots = |p: &std::path::Path| crate::file_access::AutomationRoots::new(&[p], &[]).unwrap().unwrap();
s.active_mut().unwrap().opened_by = Some(roots(&dir.0));
assert_eq!(run(&mut s, "links.info", json!({"id": id.0}))["status"], "ok");
let narrow = roots(&other.0);
let i = crate::file_access::confine(Some(&narrow), || run(&mut s, "links.info", json!({"id": id.0})));
assert_ne!(i["status"], "ok", "{i}");
}
/// A 600×300 pt image at the centre of the artboard, its placement options set to `options`,
/// relinked to a 300×100 px file → the image's id and session.
fn relinked(name: &str, options: Value) -> (Session, NodeId, Folder) {