Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 29 additions & 11 deletions src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2676,6 +2676,29 @@ type ServiceInstallCleanupOps = {
stop: () => void;
};

/**
* Query whether systemd already knows the user service before install cleanup.
*
* Keep the property name in the output instead of using `systemctl --value`:
* CentOS 7's systemd 219 supports `-p LoadState` but not the later `--value`
* switch (#2866). Requiring the explicit `LoadState=` key also preserves the
* fail-closed boundary — empty, truncated, or unrelated output must not be
* mistaken for a confirmed absent unit.
*/
export function systemdInstallCleanupStatus(
deps: { show?: () => string } = {},
): string | null {
const output = (deps.show ?? (() => sh(`systemctl --user show ${TASK} -p LoadState`)))();
const lines = output.replace(/\r/g, "").split("\n");
if (lines[lines.length - 1] === "") lines.pop();
const match = lines.length === 1
? /^LoadState=(.+)$/.exec(lines[0] ?? "")
: null;
const loadState = match?.[1]?.trim().toLowerCase() ?? "";
if (!loadState) throw new Error("systemd service status could not be verified.");
return loadState === "not-found" ? null : loadState;
}

function platformOps(backend: ServiceBackend = "scheduler"): ServiceOps | null {
if (process.platform === "darwin")
return { install: installLaunchd, start: startLaunchd, stop: stopLaunchd, status: statusLaunchd, uninstall: uninstallLaunchd };
Expand Down Expand Up @@ -2745,17 +2768,12 @@ function platformServiceInstallCleanupOps(backend: ServiceBackend): ServiceInsta
}
if (process.platform === "linux") {
return {
status: () => {
// `list-unit-files <name>` exits non-zero when the unit has never been
// installed, which made a clean first install look like an unknown manager
// failure. `show LoadState` gives us the tri-state we actually need: a
// healthy user manager returns `not-found` for a missing unit, while an
// unreachable/permission-denied manager still makes `sh()` throw and the
// caller therefore fails closed.
const loadState = sh(`systemctl --user show ${TASK} --property=LoadState --value`).trim().toLowerCase();
if (!loadState) throw new Error("systemd service status could not be verified.");
return loadState === "not-found" ? null : loadState;
},
// `list-unit-files <name>` exits non-zero when the unit has never been
// installed, which made a clean first install look like an unknown manager
// failure. `show LoadState` gives us the tri-state we actually need; the helper
// also keeps this compatible with systemd 219 without weakening fail-closed
// manager errors (#2866).
status: systemdInstallCleanupStatus,
stop: () => { sh(`systemctl --user stop ${TASK}`); },
};
}
Expand Down
30 changes: 29 additions & 1 deletion tests/systemd-install-cleanup-hardening.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,41 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { systemdInstallCleanupStatus } from "../src/service";

const source = readFileSync(join(import.meta.dir, "../src/service.ts"), "utf8");

describe("systemd install cleanup status hardening", () => {
test("only treats literal not-found as confirmed unit absence", () => {
expect(source).toContain('if (!loadState) throw new Error("systemd service status could not be verified.");');
expect(systemdInstallCleanupStatus({ show: () => "LoadState=not-found\n" })).toBeNull();
expect(systemdInstallCleanupStatus({ show: () => "LoadState=loaded\n" })).toBe("loaded");
expect(source).toContain('return loadState === "not-found" ? null : loadState;');
expect(source).not.toContain('return !loadState || loadState === "not-found" ? null : loadState;');
});

test("uses the key/value output supported by systemd 219", () => {
const start = source.indexOf("export function systemdInstallCleanupStatus");
const end = source.indexOf("function platformOps", start);
expect(start).toBeGreaterThanOrEqual(0);
expect(end).toBeGreaterThan(start);
const helper = source.slice(start, end);

expect(helper).toContain("systemctl --user show ${TASK} -p LoadState");
expect(helper).not.toContain("--value");
});

test("fails closed on missing, empty, or legacy bare-value output", () => {
for (const output of [
"",
"LoadState=\n",
"not-found\n",
"ActiveState=inactive\n",
"ActiveState=inactive\nLoadState=loaded\n",
"LoadState=loaded\nunexpected\n",
"LoadState=loaded\n\n",
]) {
expect(() => systemdInstallCleanupStatus({ show: () => output }))
.toThrow("systemd service status could not be verified");
}
});
});
Loading