From cb3108a9b4079ffd23fdb028889892dd5bc087f2 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 11:05:25 -0800 Subject: [PATCH] Plug-ins: a failed store write keeps the stored grant A module that lists no permissions now removes the grant from grants.json only after it has replaced the stored module. When the write fails, the old module stays stored and keeps the grant it needs at the next launch. --- crates/engine/src/cmd/plugin.rs | 6 +++++- crates/engine/src/tests/plugins.rs | 26 ++++++++++++++++++++++++++ docs/plugins.md | 3 ++- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/plugin.rs b/crates/engine/src/cmd/plugin.rs index c8bcc8964..75dd5d497 100644 --- a/crates/engine/src/cmd/plugin.rs +++ b/crates/engine/src/cmd/plugin.rs @@ -386,10 +386,14 @@ fn install(s: &mut Session, p: &Value) -> Result { } else { // This module takes the id over, so nothing the user approved for the id applies any more. registry::revoke(&m.id); - s.store_grant(&m.id, None); } // Installed plug-ins stay installed after a restart (#2541). let stored = s.store_plugin(plugin.id(), &bytes); + // When the write fails, the old stored module stays in the folder and needs its grant at the + // next launch. + if stored && !m.asks_permissions() { + s.store_grant(&m.id, None); + } // Installing a disabled plug-in again (an updated version) turns it on for the next launch too, // also when it is disabled by its stored file alone (it stopped a launch while loading). When // the new version could not replace the stored file, the old one stays disabled. diff --git a/crates/engine/src/tests/plugins.rs b/crates/engine/src/tests/plugins.rs index 5425b9d31..d11f13bee 100644 --- a/crates/engine/src/tests/plugins.rs +++ b/crates/engine/src/tests/plugins.rs @@ -735,6 +735,32 @@ fn a_module_without_permissions_ends_the_grant_kept_in_the_store() { run(&mut fourth, "plugin.remove", json!({"id": other})); } +/// A module listing no permissions that can't be written to the plug-in store leaves the stored +/// module in place, so `grants.json` keeps the grant that module needs at the next launch. +#[test] +fn a_failed_store_write_keeps_the_stored_grant() { + let folder = crate::tests_links::Folder::new("plugin-store-failed-write"); + let id = "test.engine.permissions.storefail"; + let empty = json!({"objects": []}); + let approved = asking(id, r#"["doc.read"]"#, &empty); + let plain = asking(id, "null", &empty); + let grants = folder.0.join("grants.json"); + let mut s = session(); + s.attach_plugin_store(folder.0.clone()); + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&approved), "grant": ["doc.read"]})); + let before = std::fs::read(&grants).unwrap(); + let file = s.stored_plugin_file(id).unwrap(); + std::fs::remove_file(&file).unwrap(); + std::fs::create_dir(&file).unwrap(); + + run(&mut s, "plugin.install", json!({"dataBase64": vectorcraft_format::base64_encode(&plain)})); + assert_eq!(std::fs::read(&grants).unwrap(), before, "the grant stays with the module left in the store"); + assert!(vectorcraft_plugins::registry::granted(id).is_none(), "the process runs the module that lists nothing"); + + std::fs::remove_dir(&file).unwrap(); + run(&mut s, "plugin.remove", json!({"id": id})); +} + /// A plug-in folder that leaves a module listing no permissions under an id forgets the process's /// grant for it, unless a later file in the folder leaves a granted module there. A grant kept in /// the plug-in store stays, with the stored module it covers. diff --git a/docs/plugins.md b/docs/plugins.md index e35d6d648..04a18c9f4 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -139,7 +139,8 @@ capability model proposed for PhotoCraft plug-ins grant stay as they are; once it is, the grant records the new hash. A grant whose hash is not 64 lower-case hexadecimal digits covers nothing. `plugin.remove` forgets the grant, and so does a successful `plugin.install` of a module that lists no permissions under that id, so the earlier bytes need a new approval. A refused install leaves - the grant as it is. A stored module that lists no permissions has any grant for its id forgotten when the `Plug-ins` + the grant as it is. When such an install can't write the module to the `Plug-ins` folder, the module already stored + there stays and `grants.json` keeps its grant. A stored module that lists no permissions has any grant for its id forgotten when the `Plug-ins` folder is attached. The Additional Plug-ins Folder and `plugin.reload` do not change the `Plug-ins` folder: when they leave a module that lists no permissions under an id, the process forgets its own grant for that id, and a grant kept in `grants.json` stays with the stored module it covers. An entry of `grants.json` that this version can't use (a