Skip to content

Commit 6478582

Browse files
fix(gateway): ignore stale sudo scope for root user services (#93693)
Co-authored-by: openclaw-clownfish[bot] <280122609+openclaw-clownfish[bot]@users.noreply.github.com>
1 parent 6e3ebac commit 6478582

3 files changed

Lines changed: 64 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Docs: https://docs.openclaw.ai
3838
- TUI: reload the active session after external `/new` or `/reset` session-change events so stale transcript and stream state clear promptly. Fixes #38966; carries forward #40472. Thanks @yizhanzjz and @wsyjh8.
3939
- Control UI: preserve Gateway Access tokens during same-normalized WebSocket URL edits and reload gateway-scoped tokens when switching endpoints. Fixes #41545; repairs #42001 with additional source PRs #41546, #41552, and #41718. Thanks @wsyjh8, @llagy0020, @llagy007, @pingfanfan, and @zheliu2.
4040
- Gateway CLI: tolerate a single transient clean WebSocket close before `hello-ok` so one-shot RPC calls reconnect instead of failing noisily, while repeated clean pre-hello closes still surface. Carries forward source PRs #54475 and #54774; #85253 covered adjacent connect assembly diagnostics. Thanks @ruanrrn.
41+
- Gateway/Linux: keep root-owned systemd user service lifecycle commands on root's user manager when a stale `SUDO_USER` remains in a root shell with root's user bus environment. Fixes #81410. Thanks @Ericksza and @ChuckClose-tech.
4142
- Release and test reliability: extend slow Gateway/full-suite watchdogs, split local full-suite shards when throttled, stabilize plugin auth marker fixtures, avoid brittle provider-ref error text, and keep QA Lab bootstrap selection assertions aligned with flow-only scenarios. (#92652)
4243
- macOS Peekaboo bridge: update the embedded Peekaboo package to 3.5.2 and route bundled-skill CLI commands through the OpenClaw app bridge so they inherit its Screen Recording and Accessibility grants.
4344
- Agent routing: route subagent RPC callbacks addressed to an agent-shaped `--to` target to the correct session key instead of falling back to the main session, so WeChat (and other channel) session-key callbacks reach the intended subagent session. (#90231) Thanks @zhangguiping-xydt.

src/daemon/systemd.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,26 @@ describe("systemd availability", () => {
326326
expect(execFileMock).toHaveBeenCalledTimes(1);
327327
});
328328

329+
it("keeps root user scope when stale SUDO_USER is paired with root bus environment", async () => {
330+
mockEffectiveUid(0);
331+
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
332+
assertUserSystemctlArgs(args, "status");
333+
cb(null, "", "");
334+
});
335+
336+
await expect(
337+
isSystemdUserServiceAvailable({
338+
HOME: "/root",
339+
USER: "root",
340+
LOGNAME: "root",
341+
SUDO_USER: "debian",
342+
XDG_RUNTIME_DIR: "/run/user/0",
343+
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/0/bus",
344+
}),
345+
).resolves.toBe(true);
346+
expect(execFileMock).toHaveBeenCalledTimes(1);
347+
});
348+
329349
it("does not let stale SUDO_USER override a sudo-u target user scope", async () => {
330350
mockEffectiveUid(1000);
331351
execFileMock.mockImplementationOnce((_cmd, args, _opts, cb) => {
@@ -2049,6 +2069,27 @@ describe("systemd service control", () => {
20492069
await assertRestartSuccess({ SUDO_USER: "debian" });
20502070
});
20512071

2072+
it("restarts root user services directly when stale SUDO_USER is paired with root bus environment", async () => {
2073+
mockEffectiveUid(0);
2074+
execFileMock
2075+
.mockImplementationOnce((_cmd, args, _opts, cb) => {
2076+
assertUserSystemctlArgs(args, "status");
2077+
cb(null, "", "");
2078+
})
2079+
.mockImplementationOnce((_cmd, args, _opts, cb) => {
2080+
assertUserSystemctlArgs(args, "restart", GATEWAY_SERVICE);
2081+
cb(null, "", "");
2082+
});
2083+
await assertRestartSuccess({
2084+
HOME: "/root",
2085+
USER: "root",
2086+
LOGNAME: "root",
2087+
SUDO_USER: "debian",
2088+
XDG_RUNTIME_DIR: "/run/user/0",
2089+
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/0/bus",
2090+
});
2091+
});
2092+
20522093
it("keeps direct --user scope when SUDO_USER is root", async () => {
20532094
execFileMock
20542095
.mockImplementationOnce((_cmd, args, _opts, cb) => {

src/daemon/systemd.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,17 @@ function isNonRootUser(user: string | null): user is string {
677677
return Boolean(user && user !== "root");
678678
}
679679

680+
function hasRootUserManagerEnvironment(env: GatewayServiceEnv): boolean {
681+
const home = env.HOME?.trim();
682+
const runtimeDir = env.XDG_RUNTIME_DIR?.trim();
683+
const dbusAddress = env.DBUS_SESSION_BUS_ADDRESS?.trim();
684+
return (
685+
home === "/root" &&
686+
runtimeDir === "/run/user/0" &&
687+
Boolean(dbusAddress?.includes("/run/user/0/bus"))
688+
);
689+
}
690+
680691
function resolveSystemctlUserScope(env: GatewayServiceEnv): {
681692
machineUser: string | null;
682693
preferMachineScope: boolean;
@@ -686,14 +697,17 @@ function resolveSystemctlUserScope(env: GatewayServiceEnv): {
686697
const effectiveUid = readSystemctlEffectiveUid();
687698
const effectiveUser = readSystemctlEffectiveUser();
688699
const isEffectiveRoot = effectiveUid === null ? effectiveUser === "root" : effectiveUid === 0;
689-
const isSudoToRoot = isEffectiveRoot && isNonRootUser(sudoUser);
690-
const machineUser = isSudoToRoot
691-
? sudoUser
692-
: isNonRootUser(envUser)
693-
? envUser
694-
: isNonRootUser(sudoUser)
695-
? sudoUser
696-
: effectiveUser || envUser || sudoUser || null;
700+
const hasRootUserManager = isEffectiveRoot && hasRootUserManagerEnvironment(env);
701+
const isSudoToRoot = isEffectiveRoot && !hasRootUserManager && isNonRootUser(sudoUser);
702+
const machineUser = hasRootUserManager
703+
? null
704+
: isSudoToRoot
705+
? sudoUser
706+
: isNonRootUser(envUser)
707+
? envUser
708+
: isNonRootUser(sudoUser)
709+
? sudoUser
710+
: effectiveUser || envUser || sudoUser || null;
697711
return {
698712
machineUser,
699713
preferMachineScope: isSudoToRoot,

0 commit comments

Comments
 (0)