fix(daemon): skip machine-scope fallback on permission-denied bus errors (#62337)

* fix(daemon): skip machine-scope fallback on permission-denied bus errors; fall back to --user when sudo machine scope fails

When systemctl --user fails with "Failed to connect to bus: Permission
denied", the machine-scope fallback is now skipped. A Permission denied
error means the bus socket exists but the process cannot connect to it,
so --machine user@ would hit the same wall.

Additionally, the sudo path in execSystemctlUser now tries machine scope
first but falls through to a direct --user attempt if it fails, instead
of returning the error immediately.

Fixes #61959

* fix(daemon): guard against double machine-scope call when sudo path already tried it

When SUDO_USER is set and machine scope fails with a non-permission-denied
bus error, execution falls through to the direct --user attempt. If that
also fails with a bus-unavailable message, shouldFallbackToMachineUserScope
returns true and machine scope is tried a second time -- a redundant exec
that was never reachable before this PR opened the fallthrough path.

Add machineScopeAlreadyTried flag and include it in the bottom-fallback
guard condition so the second call is skipped when machine scope was
already attempted in the sudo branch.

Add regression test asserting exactly 2 execFile calls in this scenario.

* fix: keep sudo systemctl scoped

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Aftab
2026-04-08 05:52:31 +05:30
committed by GitHub
parent 381d229699
commit 700efe6d16
3 changed files with 44 additions and 2 deletions

View File

@@ -169,6 +169,40 @@ describe("systemd availability", () => {
await expect(isSystemdUserServiceAvailable({ USER: "debian" })).resolves.toBe(true);
});
it("does not fall back to machine scope when --user fails with permission denied", async () => {
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
expect(args).toEqual(["--user", "status"]);
cb(
createExecFileError("Failed to connect to bus: Permission denied", {
stderr: "Failed to connect to bus: Permission denied",
code: 1,
}),
"",
"",
);
});
// Only one call should be made: no machine-scope fallback for permission denied errors.
await expect(isSystemdUserServiceAvailable({ USER: "debian" })).resolves.toBe(false);
expect(execFileMock).toHaveBeenCalledTimes(1);
});
it("does not fall back to direct --user when machine scope fails under sudo", async () => {
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
assertMachineUserSystemctlArgs(args, "ai", "status");
cb(
createExecFileError("Failed to connect to bus: No such file or directory", {
stderr: "Failed to connect to bus: No such file or directory",
code: 1,
}),
"",
"",
);
});
await expect(isSystemdUserServiceAvailable({ SUDO_USER: "ai" })).resolves.toBe(false);
expect(execFileMock).toHaveBeenCalledTimes(1);
});
});
describe("isSystemdServiceEnabled", () => {

View File

@@ -351,7 +351,13 @@ function resolveSystemctlMachineUserScopeArgs(user: string): string[] {
}
function shouldFallbackToMachineUserScope(detail: string): boolean {
return isSystemdUserBusUnavailableDetail(detail);
if (!isSystemdUserBusUnavailableDetail(detail)) {
return false;
}
// "Permission denied" means the bus socket exists but this process cannot connect to it.
// The machine-scope approach targets the same bus infrastructure and will also fail,
// so do not trigger the fallback in this case.
return !detail.toLowerCase().includes("permission denied");
}
async function execSystemctlUser(
@@ -361,10 +367,11 @@ async function execSystemctlUser(
const machineUser = resolveSystemctlMachineScopeUser(env);
const sudoUser = env.SUDO_USER?.trim();
// Under sudo, prefer the invoking non-root user's scope directly.
// Under sudo, prefer the invoking non-root user's scope directly via machine scope.
if (sudoUser && sudoUser !== "root" && machineUser) {
const machineScopeArgs = resolveSystemctlMachineUserScopeArgs(machineUser);
if (machineScopeArgs.length > 0) {
// Do not fall through to bare --user: under sudo that can target root's user manager.
return await execSystemctl([...machineScopeArgs, ...args]);
}
}