From 46a7a56bbd4f8fc3383db1e16c558f5562e783ac Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Sat, 29 Aug 2026 02:29:33 +0000 Subject: [PATCH 1/2] fix(service): support systemd 219 install cleanup Replace the Linux cleanup probe's unsupported --value switch with explicit LoadState key parsing while preserving fail-closed manager handling.\n\nRefs #2866. --- src/service.ts | 36 +++++++++++++------ .../systemd-install-cleanup-hardening.test.ts | 21 ++++++++++- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/service.ts b/src/service.ts index c07c036c4e..bd5f282fdf 100644 --- a/src/service.ts +++ b/src/service.ts @@ -2676,6 +2676,25 @@ 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 match = /^LoadState=(.*)$/m.exec(output.replace(/\r/g, "")); + 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 }; @@ -2745,17 +2764,12 @@ function platformServiceInstallCleanupOps(backend: ServiceBackend): ServiceInsta } if (process.platform === "linux") { return { - status: () => { - // `list-unit-files ` 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 ` 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}`); }, }; } diff --git a/tests/systemd-install-cleanup-hardening.test.ts b/tests/systemd-install-cleanup-hardening.test.ts index a3c5d288e2..ca0a52e6d7 100644 --- a/tests/systemd-install-cleanup-hardening.test.ts +++ b/tests/systemd-install-cleanup-hardening.test.ts @@ -1,13 +1,32 @@ 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 helper = source.slice( + source.indexOf("export function systemdInstallCleanupStatus"), + source.indexOf("function platformOps"), + ); + + 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"]) { + expect(() => systemdInstallCleanupStatus({ show: () => output })) + .toThrow("systemd service status could not be verified"); + } + }); }); From cb8a2e7a02d5b39e2859d05f7ab25e5c67525685 Mon Sep 17 00:00:00 2001 From: Ingwannu Date: Sat, 29 Aug 2026 02:40:47 +0000 Subject: [PATCH 2/2] fix(service): reject malformed systemd show output --- src/service.ts | 6 +++++- .../systemd-install-cleanup-hardening.test.ts | 19 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/service.ts b/src/service.ts index bd5f282fdf..331705b428 100644 --- a/src/service.ts +++ b/src/service.ts @@ -2689,7 +2689,11 @@ export function systemdInstallCleanupStatus( deps: { show?: () => string } = {}, ): string | null { const output = (deps.show ?? (() => sh(`systemctl --user show ${TASK} -p LoadState`)))(); - const match = /^LoadState=(.*)$/m.exec(output.replace(/\r/g, "")); + 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; diff --git a/tests/systemd-install-cleanup-hardening.test.ts b/tests/systemd-install-cleanup-hardening.test.ts index ca0a52e6d7..98df18f16d 100644 --- a/tests/systemd-install-cleanup-hardening.test.ts +++ b/tests/systemd-install-cleanup-hardening.test.ts @@ -14,17 +14,26 @@ describe("systemd install cleanup status hardening", () => { }); test("uses the key/value output supported by systemd 219", () => { - const helper = source.slice( - source.indexOf("export function systemdInstallCleanupStatus"), - source.indexOf("function platformOps"), - ); + 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"]) { + 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"); }