Skip to content

fix(sessions): keep session store live during rotation#71359

Merged
steipete merged 1 commit intomainfrom
fix/session-store-rotation-copy
Apr 25, 2026
Merged

fix(sessions): keep session store live during rotation#71359
steipete merged 1 commit intomainfrom
fix/session-store-rotation-copy

Conversation

@steipete
Copy link
Copy Markdown
Contributor

Summary

  • Replace session-store rotation rename() with copyFile() so the live sessions.json remains authoritative until the later atomic rewrite succeeds.
  • Keep .bak.* rotation backups for diagnostics/rollback without introducing {} placeholders or backup-resurrection heuristics.
  • Add regressions for the crash-before-final-save window and for legitimate empty stores with stale backups.

Why

A rename-based rotation removes sessions.json before writeTextAtomic() writes the new store. If the gateway crashes in that window, startup can treat the session store as empty and lose the channel/thread -> transcript-file mapping.

Copying the live file to .bak.* before the atomic rewrite avoids that state entirely:

  • crash before final write: old sessions.json remains authoritative
  • crash after final write: new sessions.json is authoritative
  • backup exists, but is never treated as source of truth over a valid live store

Fixes #68229.
Supersedes #71328.

Tests

  • pnpm test src/config/sessions/store.pruning.test.ts src/config/sessions/store.pruning.integration.test.ts src/config/sessions/sessions.test.ts
  • pnpm check:changed

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 25, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Symlink/TOCTOU risk when creating session store rotation backups via copyFile()
2 🔵 Low Verbose logging of filesystem error object may disclose internal paths
1. 🟡 Symlink/TOCTOU risk when creating session store rotation backups via copyFile()
Property Value
Severity Medium
CWE CWE-59
Location src/config/sessions/store-maintenance.ts:342-345

Description

The session store rotation logic creates backups using fs.promises.copyFile(storePath, backupPath) without validating that the source/destination paths are regular files and without protecting against symlink/hardlink races.

If an attacker can influence storePath or can write within the sessions directory (e.g., via a misconfigured writable state dir, shared host, or compromised local account), they may be able to:

  • Read unintended files: if storePath is (or is replaced with) a symlink to another file, copyFile() will copy the symlink target into a backup.
  • Overwrite unintended files: if an attacker can pre-create or race-create backupPath (predictable Date.now()-based name) as a symlink/hardlink, copyFile() may overwrite the link target.
  • Exploit TOCTOU: the code checks size with stat() and then performs the copy later; the file could be swapped between these operations.

Vulnerable code:

const backupPath = `${storePath}.bak.${Date.now()}`;
await fs.promises.copyFile(storePath, backupPath);

Recommendation

Harden backup creation against symlink/hardlink attacks and TOCTOU:

  • Ensure the source is a regular file (use lstat() and reject symlinks), and consider opening it once and copying from the file descriptor.
  • Create the destination atomically and exclusively, and avoid following symlinks.
  • Optionally, enforce that storePath resides within an expected trusted directory.

Example approach (POSIX-oriented):

import fs from "node:fs";
import path from "node:path";

const srcStat = await fs.promises.lstat(storePath);
if (!srcStat.isFile()) throw new Error("session store is not a regular file");

const backupPath = `${storePath}.bak.${Date.now()}`;// Open destination with O_EXCL so an attacker cannot precreate a symlink/hardlink.
const dstFd = await fs.promises.open(backupPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
try {// Copy from an already-opened source fd to avoid TOCTOU swaps.
  const srcFd = await fs.promises.open(storePath, fs.constants.O_RDONLY);
  try {
    await fs.promises.copyFile(srcFd.fd, dstFd.fd);
  } finally {
    await srcFd.close();
  }
} finally {
  await dstFd.close();
}

Also consider best-effort cleanup using unlink() only after verifying the target is within the expected directory and (if possible) is a regular file.

2. 🔵 Verbose logging of filesystem error object may disclose internal paths
Property Value
Severity Low
CWE CWE-532
Location src/config/sessions/store-maintenance.ts:349-352

Description

rotateSessionFile() logs the raw err object when backup creation fails.

  • Input: err comes from fs.promises.copyFile(storePath, backupPath) failures.
  • Many Node.js filesystem errors (NodeJS.ErrnoException) include enumerable fields such as path, dest, syscall, errno, and code.
  • The logging subsystem’s JSON console mode and file logger both serialize metadata objects (see formatConsoleLine() spreading ...opts.meta and file transport JSON.stringify({ ...logObj, time })).
  • As a result, logs can contain full filesystem paths and other operational details that may be sensitive in shared log environments.

Vulnerable code:

} catch (err) {
  log.warn("session store rotation backup failed", { err });
  return false;
}

Recommendation

Avoid logging the raw error object. Log a minimized/sanitized subset (e.g., code and a redacted message), and avoid including full paths.

Example fix:

} catch (err) {
  const e = err as NodeJS.ErrnoException;
  log.warn("session store rotation backup failed", {
    code: e.code,
    syscall: e.syscall,
    message: e.message, // consider redacting/normalizing if it may contain paths
    path: e.path ? path.basename(String(e.path)) : undefined,
  });
  return false;
}

If you need stack traces, gate them behind an explicit debug/verbose flag and ensure log redaction is applied before serialization.


Analyzed PR: #71359 at commit 15de2b7

Last updated on: 2026-04-25T02:30:16Z

@cursor
Copy link
Copy Markdown

cursor Bot commented Apr 25, 2026

PR Summary

Medium Risk
Changes session-store rotation semantics from rename to copy, which affects durability and crash behavior of session-to-transcript mappings; risk is moderate because it touches persistence but is narrowly scoped and covered by new tests.

Overview
Prevents a crash window during sessions.json rotation by switching rotation from rename() to copyFile(), so the live session store remains the source of truth until the later atomic rewrite completes.

Updates logging/error handling around rotation backup creation and adds unit tests covering the interrupted-rotation scenario and ensuring a legitimately empty live store is not replaced by stale .bak.* backups.

Reviewed by Cursor Bugbot for commit 15de2b7. Bugbot is set up for automated code reviews on this repo. Configure here.

@steipete steipete force-pushed the fix/session-store-rotation-copy branch from 2d2fdab to 122f130 Compare April 25, 2026 02:19
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 25, 2026

Greptile Summary

This PR fixes a crash-window bug in session store rotation by replacing fs.promises.rename with fs.promises.copyFile, so sessions.json remains on disk and authoritative at all times. The caller's subsequent atomic write then replaces the oversized live file, while the backup exists purely for diagnostics/rollback — never as a resurrection source. Two new regression tests are added to cover the interrupted-write and stale-backup scenarios.

Confidence Score: 5/5

Safe to merge — the change is minimal, well-tested, and correctly eliminates the crash window without introducing new failure modes.

The fix is a focused one-liner swap (rename → copyFile) with no semantic side effects. The return value is already discarded by the caller, so the change in behavior on copy failure (still returning false, still non-blocking) is consistent. Two clear regression tests cover both the interrupted-write and stale-backup scenarios. No security concerns.

No files require special attention.

Reviews (1): Last reviewed commit: "fix: keep session store live during rota..." | Re-trigger Greptile

@steipete steipete force-pushed the fix/session-store-rotation-copy branch from 122f130 to 15de2b7 Compare April 25, 2026 02:21
@steipete steipete merged commit 4da25d0 into main Apr 25, 2026
11 checks passed
@steipete steipete deleted the fix/session-store-rotation-copy branch April 25, 2026 02:21
@openclaw-barnacle openclaw-barnacle Bot added size: S maintainer Maintainer-authored PR labels Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Session file replaced on cache-ttl expiry — all conversation history lost

1 participant