fix: harden sandbox writes and centralize atomic file writes

This commit is contained in:
Peter Steinberger
2026-03-02 16:44:46 +00:00
parent 14e4575af5
commit 18f8393b6c
12 changed files with 203 additions and 139 deletions

View File

@@ -14,23 +14,45 @@ export async function readJsonFile<T>(filePath: string): Promise<T | null> {
export async function writeJsonAtomic(
filePath: string,
value: unknown,
options?: { mode?: number },
options?: { mode?: number; trailingNewline?: boolean; ensureDirMode?: number },
) {
const text = JSON.stringify(value, null, 2);
await writeTextAtomic(filePath, text, {
mode: options?.mode,
ensureDirMode: options?.ensureDirMode,
appendTrailingNewline: options?.trailingNewline,
});
}
export async function writeTextAtomic(
filePath: string,
content: string,
options?: { mode?: number; ensureDirMode?: number; appendTrailingNewline?: boolean },
) {
const mode = options?.mode ?? 0o600;
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
const tmp = `${filePath}.${randomUUID()}.tmp`;
await fs.writeFile(tmp, JSON.stringify(value, null, 2), "utf8");
try {
await fs.chmod(tmp, mode);
} catch {
// best-effort; ignore on platforms without chmod
const payload =
options?.appendTrailingNewline && !content.endsWith("\n") ? `${content}\n` : content;
const mkdirOptions: { recursive: true; mode?: number } = { recursive: true };
if (typeof options?.ensureDirMode === "number") {
mkdirOptions.mode = options.ensureDirMode;
}
await fs.rename(tmp, filePath);
await fs.mkdir(path.dirname(filePath), mkdirOptions);
const tmp = `${filePath}.${randomUUID()}.tmp`;
try {
await fs.chmod(filePath, mode);
} catch {
// best-effort; ignore on platforms without chmod
await fs.writeFile(tmp, payload, "utf8");
try {
await fs.chmod(tmp, mode);
} catch {
// best-effort; ignore on platforms without chmod
}
await fs.rename(tmp, filePath);
try {
await fs.chmod(filePath, mode);
} catch {
// best-effort; ignore on platforms without chmod
}
} finally {
await fs.rm(tmp, { force: true }).catch(() => undefined);
}
}

View File

@@ -6,6 +6,7 @@ import type { loadConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { runCommandWithTimeout } from "../process/exec.js";
import { VERSION } from "../version.js";
import { writeJsonAtomic } from "./json-files.js";
import { resolveOpenClawPackageRoot } from "./openclaw-root.js";
import { normalizeUpdateChannel, DEFAULT_PACKAGE_CHANNEL } from "./update-channels.js";
import { compareSemverStrings, resolveNpmChannelTag, checkUpdateStatus } from "./update-check.js";
@@ -124,8 +125,7 @@ async function readState(statePath: string): Promise<UpdateCheckState> {
}
async function writeState(statePath: string, state: UpdateCheckState): Promise<void> {
await fs.mkdir(path.dirname(statePath), { recursive: true });
await fs.writeFile(statePath, JSON.stringify(state, null, 2), "utf-8");
await writeJsonAtomic(statePath, state);
}
function sameUpdateAvailable(a: UpdateAvailable | null, b: UpdateAvailable | null): boolean {