mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 06:03:58 +00:00
fix(sessions): bound trajectory snapshot and pointer reads (#101450)
* fix(sessions): bound trajectory snapshot and pointer reads * test(sessions): align oversized snapshot assertion with bounded reader error message * fix(sessions): cap follow-mode trajectory deltas ClawSweeper review noted that the trajectory snapshot read was bounded but the follow-mode delta read (Buffer.alloc(fileState.size - offset)) was not. A runaway trajectory append could still OOM the tail loop. Reject deltas above TRAJECTORY_RUNTIME_FILE_MAX_BYTES and add regression coverage for the oversized-delta path. * test(sessions): exercise trajectory limits through command path --------- Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
resolveTrajectoryFilePath,
|
||||
resolveTrajectoryPointerFilePath,
|
||||
TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
|
||||
} from "../trajectory/paths.js";
|
||||
import type { TrajectoryEvent } from "../trajectory/types.js";
|
||||
import { sessionsTailCommand, setSessionsTailFollowIntervalMsForTests } from "./sessions-tail.js";
|
||||
@@ -492,4 +493,36 @@ describe("sessionsTailCommand", () => {
|
||||
expect(output).toContain("trajectory-dir ok");
|
||||
expect(output).not.toContain("No sessions found");
|
||||
});
|
||||
|
||||
it("rejects oversized trajectory snapshots", async () => {
|
||||
const runtime = makeRuntime();
|
||||
fs.writeFileSync(trajectoryPath, "");
|
||||
fs.truncateSync(trajectoryPath, TRAJECTORY_RUNTIME_FILE_MAX_BYTES + 1);
|
||||
|
||||
await expect(sessionsTailCommand({ store: storePath, sessionKey }, runtime)).rejects.toThrow(
|
||||
/File exceeds 52428800 bytes/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects oversized follow-mode trajectory deltas", async () => {
|
||||
const runtime = makeRuntime();
|
||||
writeJsonl(trajectoryPath, [
|
||||
makeEvent({ type: "session.started", ts: "2026-05-18T12:04:17.000Z" }),
|
||||
]);
|
||||
|
||||
const run = sessionsTailCommand({ store: storePath, sessionKey, follow: true }, runtime);
|
||||
try {
|
||||
await waitForRuntimeOutput(runtime, "session.started");
|
||||
const initialSize = fs.statSync(trajectoryPath).size;
|
||||
fs.truncateSync(trajectoryPath, initialSize + TRAJECTORY_RUNTIME_FILE_MAX_BYTES + 1);
|
||||
await vi.waitFor(() => {
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Trajectory delta exceeds 52428800 bytes"),
|
||||
);
|
||||
});
|
||||
} finally {
|
||||
process.emit("SIGTERM", "SIGTERM");
|
||||
await run;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,11 +15,13 @@ import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import { resolveStoredSessionKeyForAgentStore } from "../gateway/session-store-key.js";
|
||||
import { formatErrorMessage } from "../infra/errors.js";
|
||||
import { parseStrictNonNegativeInteger } from "../infra/parse-finite-number.js";
|
||||
import { readRegularFileSync } from "../infra/regular-file.js";
|
||||
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import {
|
||||
resolveTrajectoryFilePath,
|
||||
resolveTrajectoryPointerFilePath,
|
||||
TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
|
||||
} from "../trajectory/paths.js";
|
||||
import {
|
||||
isRegularNonSymlinkFile,
|
||||
@@ -279,8 +281,13 @@ function formatProgressLine(event: TrajectoryEvent): string {
|
||||
|
||||
function readTrajectorySnapshot(filePath: string): TrajectorySnapshot {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
const text = fs.readFileSync(filePath, "utf8");
|
||||
// Use the runtime trajectory limit so tail accepts any file the runtime
|
||||
// would have written and rejects anything larger.
|
||||
const { buffer, stat } = readRegularFileSync({
|
||||
filePath,
|
||||
maxBytes: TRAJECTORY_RUNTIME_FILE_MAX_BYTES,
|
||||
});
|
||||
const text = buffer.toString("utf8");
|
||||
return {
|
||||
events: parseTrajectoryEventLines(text.split(/\r?\n/u)),
|
||||
fileState: fileStateFromStat(stat),
|
||||
@@ -361,7 +368,11 @@ async function readTrajectoryPointerSessionId(sessionFile: string): Promise<stri
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(pointerPath, "utf8")) as unknown;
|
||||
const { buffer } = readRegularFileSync({
|
||||
filePath: pointerPath,
|
||||
maxBytes: 64 * 1024,
|
||||
});
|
||||
const parsed = JSON.parse(buffer.toString("utf8")) as unknown;
|
||||
if (!isRecord(parsed) || typeof parsed.sessionId !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
@@ -501,7 +512,13 @@ function readNewFollowEvents(state: FollowState): TrajectoryEvent[] {
|
||||
|
||||
const fd = fs.openSync(state.selection.trajectoryPath, "r");
|
||||
try {
|
||||
const buffer = Buffer.alloc(fileState.size - state.offset);
|
||||
const deltaBytes = fileState.size - state.offset;
|
||||
if (deltaBytes > TRAJECTORY_RUNTIME_FILE_MAX_BYTES) {
|
||||
throw new Error(
|
||||
`Trajectory delta exceeds ${TRAJECTORY_RUNTIME_FILE_MAX_BYTES} bytes: ${deltaBytes}`,
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.alloc(deltaBytes);
|
||||
fs.readSync(fd, buffer, 0, buffer.length, state.offset);
|
||||
state.offset = fileState.size;
|
||||
state.fileState = fileState;
|
||||
|
||||
Reference in New Issue
Block a user