diff --git a/src/cli/root-guard.test.ts b/src/cli/root-guard.test.ts index 424a2037045..aa539ca5951 100644 --- a/src/cli/root-guard.test.ts +++ b/src/cli/root-guard.test.ts @@ -5,19 +5,22 @@ describe("assertNotRoot", () => { const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => undefined as never); const stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true); - // Save and restore real getuid so we can replace it per test. + // Save and restore real getuid/geteuid so we can replace them per test. const realGetuid = process.getuid; + const realGeteuid = process.geteuid; beforeEach(() => { exitSpy.mockClear(); stderrSpy.mockClear(); process.getuid = realGetuid; + process.geteuid = realGeteuid; }); afterAll(() => { exitSpy.mockRestore(); stderrSpy.mockRestore(); process.getuid = realGetuid; + process.geteuid = realGeteuid; }); it("exits with code 1 when uid is 0 and no env override", () => { @@ -50,6 +53,27 @@ describe("assertNotRoot", () => { expect(exitSpy).not.toHaveBeenCalled(); }); + it("exits when real uid is non-zero but effective uid is 0 (setuid-root)", () => { + process.getuid = () => 1000; + process.geteuid = () => 0; + assertNotRoot({}); + expect(exitSpy).toHaveBeenCalledWith(1); + }); + + it("does not exit when real uid is non-zero and effective uid is non-zero", () => { + process.getuid = () => 1000; + process.geteuid = () => 1000; + assertNotRoot({}); + expect(exitSpy).not.toHaveBeenCalled(); + }); + + it("does not exit when euid is 0 but OPENCLAW_ALLOW_ROOT=1", () => { + process.getuid = () => 1000; + process.geteuid = () => 0; + assertNotRoot({ OPENCLAW_ALLOW_ROOT: "1" }); + expect(exitSpy).not.toHaveBeenCalled(); + }); + it("does not exit when getuid is undefined (Windows)", () => { process.getuid = undefined as unknown as typeof process.getuid; assertNotRoot({}); diff --git a/src/cli/root-guard.ts b/src/cli/root-guard.ts index db133b5663b..81ec7447a40 100644 --- a/src/cli/root-guard.ts +++ b/src/cli/root-guard.ts @@ -1,7 +1,7 @@ import process from "node:process"; /** - * Block CLI execution when running as root (uid 0) unless explicitly opted in. + * Block CLI execution when running as root (uid 0 or euid 0) unless explicitly opted in. * * Running as root causes: * - Separate state dir (/root/.openclaw/ vs /home//.openclaw/) @@ -12,7 +12,9 @@ export function assertNotRoot(env: NodeJS.ProcessEnv = process.env): void { if (typeof process.getuid !== "function") { return; } - if (process.getuid() !== 0) { + const uid = process.getuid(); + const euid = typeof process.geteuid === "function" ? process.geteuid() : uid; + if (uid !== 0 && euid !== 0) { return; } if (