From 165d62b15ff2249834ef1ab5bfcc6d760e53599c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 30 Apr 2026 15:27:41 +0100 Subject: [PATCH] fix(infra): tolerate concurrent tmp dir repair --- CHANGELOG.md | 1 + src/infra/tmp-openclaw-dir.test.ts | 76 ++++++++++++++++++++++++++++++ src/infra/tmp-openclaw-dir.ts | 18 ++++++- 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cac973784c..822b554ae72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Telegram: use durable message edits for streaming previews instead of native draft state, so generated replies no longer flicker through draft-to-message transitions that look like duplicates. (#75073) Thanks @obviyus. - Telegram: echo preflighted DM voice-note transcripts back to the originating chat, including Telegram DM topic thread metadata, instead of only echoing later media-understanding transcripts. Fixes #75084. Thanks @M-Lietz. - Web search: describe `web_search` as using the configured provider instead of hard-coding Brave when DuckDuckGo or another provider is active. Fixes #75088. Thanks @sun-rongyang. +- Infra/tmp: tolerate concurrent temp-dir permission repairs by rechecking directories that another process already tightened, so parallel ACP subprocess startup no longer throws `Unsafe fallback OpenClaw temp dir`. Fixes #66867. Thanks @Kane808-AI and @jarvisz8. ## 2026.4.29 diff --git a/src/infra/tmp-openclaw-dir.test.ts b/src/infra/tmp-openclaw-dir.test.ts index 690d6dd13fc..4dc27511520 100644 --- a/src/infra/tmp-openclaw-dir.test.ts +++ b/src/infra/tmp-openclaw-dir.test.ts @@ -394,6 +394,82 @@ describe("resolvePreferredOpenClawTmpDir", () => { expect(warn).toHaveBeenCalledWith(expect.stringContaining("tightened permissions on temp dir")); }); + it("uses /tmp/openclaw when another process tightened permissions before repair", () => { + const chmodSync = vi.fn(); + const warn = vi.fn(); + const tmpdir = vi.fn(() => "/var/fallback"); + const states = [0o40777, 0o40700, 0o40700]; + const lstatSync = vi.fn>((target: string) => { + if (target === POSIX_OPENCLAW_TMP_DIR) { + return makeDirStat({ mode: states.shift() ?? 0o40700 }); + } + return secureDirStat(); + }); + + const resolved = resolvePreferredOpenClawTmpDir({ + accessSync: vi.fn(), + lstatSync, + chmodSync, + mkdirSync: vi.fn(), + getuid: vi.fn(() => 501), + tmpdir, + warn, + }); + + expect(resolved).toBe(POSIX_OPENCLAW_TMP_DIR); + expect(chmodSync).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + expect(tmpdir).not.toHaveBeenCalled(); + }); + + it("uses fallback when another process tightened fallback permissions before repair", () => { + const fallbackPath = fallbackTmp(); + const chmodSync = vi.fn(); + const warn = vi.fn(); + const states = [0o40777, 0o40700, 0o40700]; + + const resolved = resolveWithReadOnlyTmpFallback({ + fallbackPath, + fallbackLstatSync: vi.fn(() => makeDirStat({ mode: states.shift() ?? 0o40700 })), + chmodSync, + warn, + }); + + expect(resolved).toBe(fallbackPath); + expect(chmodSync).not.toHaveBeenCalled(); + expect(warn).not.toHaveBeenCalled(); + }); + + it("uses /tmp/openclaw when chmod loses a concurrent repair race", () => { + const chmodSync = vi.fn((target: string, mode: number) => { + if (target === POSIX_OPENCLAW_TMP_DIR && mode === 0o700) { + throw nodeErrorWithCode("EPERM"); + } + }); + const warn = vi.fn(); + const states = [0o40777, 0o40777, 0o40700]; + const lstatSync = vi.fn>((target: string) => { + if (target === POSIX_OPENCLAW_TMP_DIR) { + return makeDirStat({ mode: states.shift() ?? 0o40700 }); + } + return secureDirStat(); + }); + + const resolved = resolvePreferredOpenClawTmpDir({ + accessSync: vi.fn(), + lstatSync, + chmodSync, + mkdirSync: vi.fn(), + getuid: vi.fn(() => 501), + tmpdir: vi.fn(() => "/var/fallback"), + warn, + }); + + expect(resolved).toBe(POSIX_OPENCLAW_TMP_DIR); + expect(chmodSync).toHaveBeenCalledWith(POSIX_OPENCLAW_TMP_DIR, 0o700); + expect(warn).not.toHaveBeenCalled(); + }); + it("throws when the fallback directory cannot be created", () => { expect(() => resolvePreferredOpenClawTmpDir({ diff --git a/src/infra/tmp-openclaw-dir.ts b/src/infra/tmp-openclaw-dir.ts index 1c95aea0ae0..afd93a85d1c 100644 --- a/src/infra/tmp-openclaw-dir.ts +++ b/src/infra/tmp-openclaw-dir.ts @@ -106,10 +106,24 @@ export function resolvePreferredOpenClawTmpDir( if (uid !== undefined && typeof st.uid === "number" && st.uid !== uid) { return false; } - if (typeof st.mode !== "number" || (st.mode & 0o022) === 0) { + if (typeof st.mode !== "number") { return false; } - chmodSync(candidatePath, 0o700); + if ((st.mode & 0o022) === 0) { + return resolveDirState(candidatePath) === "available"; + } + try { + chmodSync(candidatePath, 0o700); + } catch (chmodErr) { + if ( + isNodeErrorWithCode(chmodErr, "EPERM") || + isNodeErrorWithCode(chmodErr, "EACCES") || + isNodeErrorWithCode(chmodErr, "ENOENT") + ) { + return resolveDirState(candidatePath) === "available"; + } + throw chmodErr; + } warn(`[openclaw] tightened permissions on temp dir: ${candidatePath}`); return resolveDirState(candidatePath) === "available"; } catch {