forked from unom/punktfunk
fix(plugins/windows): the runner can read its own bundle, so it actually starts
Enabling the plugin runner did everything right and then nothing happened.
The task went Ready with LastTaskResult 1 within a second of every start, and
the console kept showing it enabled-but-not-running with nothing to explain
why. Task Scheduler discards the action's output, so the reason never
surfaced anywhere:
error: EPERM reading "C:\Program Files\punktfunk\scripting\runner-cli.js"
This file already knows why. RUNNER_UNIT_DIRS carries the note that bun opens
the files it loads asking for FILE_WRITE_ATTRIBUTES on top of read, so a plain
(RX) grant makes them die with EPERM — found on glass when the plugin and
script directories were fixed. The runner's own entry script was missed. It
lives in {app}\scripting, which carries only Users:(RX), and LocalService
reaches that through Authenticated Users, so bun could never open the one file
it was started to run. The runner has not been able to start since it moved
off SYSTEM, which is a principal that has full control everywhere and so never
met this.
So {app}\scripting now gets the same (OI)(CI)(RX,WA) the unit dirs get, at
enable, revoked at disable like every other grant here. WA moves timestamps
and the read-only bit and cannot touch content, so the three-way split the
module maintains still holds: code read-only, secrets read-only, only
plugin-state writable. A plugin still cannot rewrite the runner.
Measured on glass on .173, same box, same task, only the ACE differing:
without it Ready/lastResult=1 and no runner process; with it Running, a live
runner, and GET /store/runtime answering running:true — which is exactly what
the console reads. Also checked what bun does with the access: nothing. It
opens the entry for write and never writes, and the directory is byte-identical
afterwards.
Windows-only code, so it is rustfmt-clean and type-reviewed but NOT
compile-verified here — the Windows CI job is the gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -569,6 +569,43 @@ fn grant_runner_secret_reads() {
|
||||
);
|
||||
}
|
||||
}
|
||||
// The runner's OWN bundle, in the install dir rather than under the config dir. Same reason as
|
||||
// RUNNER_UNIT_DIRS: bun opens the file it is asked to run requesting FILE_WRITE_ATTRIBUTES on
|
||||
// top of read, and {app}\scripting only carries Users:(RX), which LocalService reaches through
|
||||
// Authenticated Users. So `bun runner-cli.js` died with
|
||||
// error: EPERM reading "C:\Program Files\punktfunk\scripting\runner-cli.js"
|
||||
// the task exited 1 within a second of every start, and the console showed the runner as
|
||||
// enabled-but-not-running with nothing to explain it. The unit dirs were given (RX,WA) when
|
||||
// that behaviour was first found on-glass; the entry script itself was missed, so the runner
|
||||
// could never start at all. Verified on glass: without this the task is Ready/lastResult=1,
|
||||
// with it the task is Running and `GET /store/runtime` reports running:true.
|
||||
// WA touches timestamps and the read-only bit, never content, so "code is read-only — a plugin
|
||||
// cannot rewrite itself" still holds for the runner's own bundle.
|
||||
if let Some(dir) = runner_bundle_dir() {
|
||||
let ok = Command::new(icacls_path())
|
||||
.arg(&dir)
|
||||
.args(["/grant:r", &format!("{LOCAL_SERVICE_SID}:(OI)(CI)(RX,WA)")])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|s| s.success());
|
||||
if !ok {
|
||||
eprintln!(
|
||||
"warning: could not grant LocalService read on {} - the plugin runner will not \
|
||||
start (bun exits EPERM on its own entry script)",
|
||||
dir.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `{app}\scripting` — where the installer lays down `runner-cli.js` + `scripting-run.cmd`
|
||||
/// (packaging/windows/punktfunk-host.iss), resolved from the running exe like
|
||||
/// [`runner_command`] does. `None` when the exe path cannot be resolved; callers treat that as
|
||||
/// "nothing to grant" rather than failing the whole enable.
|
||||
#[cfg(target_os = "windows")]
|
||||
fn runner_bundle_dir() -> Option<std::path::PathBuf> {
|
||||
Some(std::env::current_exe().ok()?.parent()?.join("scripting"))
|
||||
}
|
||||
|
||||
/// Best-effort removal of the LocalService read grants when the runner is switched off — the
|
||||
@@ -606,6 +643,17 @@ fn revoke_runner_secret_reads() {
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
}
|
||||
// …and the bundle dir in the install tree, which is not under `cfg` so the loop above misses
|
||||
// it. Removing the explicit ACE leaves the inherited Users:(RX) from Program Files, so it
|
||||
// reverts to plain read-only rather than losing access altogether.
|
||||
if let Some(dir) = runner_bundle_dir().filter(|d| d.exists()) {
|
||||
let _ = Command::new(icacls_path())
|
||||
.arg(&dir)
|
||||
.args(["/remove:g", LOCAL_SERVICE_SID])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve icacls by full System32 path rather than PATH — same planted-binary reasoning as
|
||||
|
||||
Reference in New Issue
Block a user