Skip to content

Commit 11bd19e

Browse files
committed
fix(config-audit): guard scrub rename after temp write
Signed-off-by: sallyom <[email protected]>
1 parent b1cb190 commit 11bd19e

2 files changed

Lines changed: 78 additions & 0 deletions

File tree

src/config/io.audit.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,4 +671,63 @@ describe("config io audit helpers", () => {
671671
expect(after).toContain("xoxb-real-bot-token");
672672
expect(fs.existsSync(`${auditPath}.scrub.tmp`)).toBe(false);
673673
});
674+
675+
it("aborts without overwriting when the audit log is appended to after temp write", async () => {
676+
const home = await suiteRootTracker.make("scrub-race-after-temp-write");
677+
const auditPath = path.join(home, ".openclaw", "logs", "config-audit.jsonl");
678+
fs.mkdirSync(path.dirname(auditPath), { recursive: true, mode: 0o700 });
679+
const unredacted = {
680+
ts: "2026-05-02T00:03:48.471Z",
681+
argv: [
682+
"node",
683+
"openclaw.mjs",
684+
"config",
685+
"set",
686+
"channels.slack.botToken",
687+
"xoxb-real-bot-token-1234567890abcdef0123456789abcdef",
688+
],
689+
execArgv: [],
690+
};
691+
const appended = {
692+
ts: "2026-05-02T00:04:00.000Z",
693+
argv: ["node", "openclaw.mjs", "config", "set", "theme", "dark"],
694+
execArgv: [],
695+
};
696+
const original = `${JSON.stringify(unredacted)}\n`;
697+
const appendedLine = `${JSON.stringify(appended)}\n`;
698+
fs.writeFileSync(auditPath, original, { encoding: "utf-8", mode: 0o600 });
699+
let renameCalled = false;
700+
701+
const raceFs = {
702+
promises: {
703+
readFile: fsPromises.readFile,
704+
stat: fsPromises.stat,
705+
writeFile: async (
706+
p: string,
707+
data: string,
708+
options?: { encoding?: BufferEncoding; mode?: number },
709+
) => {
710+
await fsPromises.writeFile(p, data, options);
711+
await fsPromises.appendFile(auditPath, appendedLine, "utf-8");
712+
},
713+
rename: async () => {
714+
renameCalled = true;
715+
},
716+
unlink: fsPromises.unlink,
717+
},
718+
};
719+
const result = await scrubConfigAuditLog({
720+
fs: raceFs,
721+
env: {} as NodeJS.ProcessEnv,
722+
homedir: () => home,
723+
});
724+
725+
expect(result.aborted).toBe(true);
726+
expect(result.rewritten).toBeGreaterThan(0);
727+
expect(renameCalled).toBe(false);
728+
const after = fs.readFileSync(auditPath, "utf-8");
729+
expect(after).toBe(`${original}${appendedLine}`);
730+
expect(after).toContain("xoxb-real-bot-token");
731+
expect(fs.existsSync(`${auditPath}.scrub.tmp`)).toBe(false);
732+
});
674733
});

src/config/io.audit.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,25 @@ export async function scrubConfigAuditLog(params: {
575575
encoding: "utf-8",
576576
mode: 0o600,
577577
});
578+
let finalPreRenameSize: number;
579+
try {
580+
finalPreRenameSize = (await params.fs.promises.stat(auditPath)).size;
581+
} catch {
582+
try {
583+
await params.fs.promises.unlink(tmpPath);
584+
} catch {
585+
// best-effort cleanup; the stat failure is handled as a safe abort
586+
}
587+
return { scanned, rewritten, skipped, aborted: true };
588+
}
589+
if (finalPreRenameSize !== originalByteLength) {
590+
try {
591+
await params.fs.promises.unlink(tmpPath);
592+
} catch {
593+
// best-effort cleanup; the append detection is the actionable state
594+
}
595+
return { scanned, rewritten, skipped, aborted: true };
596+
}
578597
await params.fs.promises.rename(tmpPath, auditPath);
579598
} catch (err) {
580599
try {

0 commit comments

Comments
 (0)