diff --git a/src/agents/sandbox/remote-fs-bridge.test.ts b/src/agents/sandbox/remote-fs-bridge.test.ts index b608e886e66..f1340a3133e 100644 --- a/src/agents/sandbox/remote-fs-bridge.test.ts +++ b/src/agents/sandbox/remote-fs-bridge.test.ts @@ -232,6 +232,56 @@ describe("remote sandbox fs bridge", () => { }); }); }); + + it("does not reject malformed non-decimal hardlink counts", async () => { + await withTempDir("openclaw-remote-fs-bridge-hardlink-", async (stateDir) => { + const workspaceDir = path.join(stateDir, "workspace"); + await fs.mkdir(workspaceDir, { recursive: true }); + const runtime: RemoteShellSandboxHandle = { + remoteWorkspaceDir: workspaceDir, + remoteAgentWorkspaceDir: workspaceDir, + runRemoteShellScript: async (command) => { + if (command.script.includes('if [ -e "$1" ] || [ -L "$1" ]')) { + return { stdout: Buffer.from("1\n"), stderr: Buffer.alloc(0), code: 0 }; + } + if (command.script.includes('readlink -f -- "$cursor"')) { + return { + stdout: Buffer.from(`${workspaceDir}/note.txt\n`), + stderr: Buffer.alloc(0), + code: 0, + }; + } + if (command.script.includes('stat -c "%F|%h"')) { + return { + stdout: Buffer.from("regular file|0x2\n"), + stderr: Buffer.alloc(0), + code: 0, + }; + } + if (command.script.includes('stat -c "%F|%s|%y"')) { + return { + stdout: Buffer.from("regular file|12|2026-05-29 12:00:00.000000000 +0000\n"), + stderr: Buffer.alloc(0), + code: 0, + }; + } + throw new Error(`unexpected remote script: ${command.script}`); + }, + }; + const bridge = createRemoteShellSandboxFsBridge({ + sandbox: createSandbox({ + workspaceDir, + agentWorkspaceDir: workspaceDir, + }), + runtime, + }); + + await expect(bridge.stat({ filePath: "note.txt" })).resolves.toMatchObject({ + type: "file", + size: 12, + }); + }); + }); }); async function withTempDir(prefix: string, run: (stateDir: string) => Promise): Promise { diff --git a/src/agents/sandbox/remote-fs-bridge.ts b/src/agents/sandbox/remote-fs-bridge.ts index 2ab5e175e0c..cd4ee3ab214 100644 --- a/src/agents/sandbox/remote-fs-bridge.ts +++ b/src/agents/sandbox/remote-fs-bridge.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { parseStrictNonNegativeInteger } from "../../infra/parse-finite-number.js"; import { isPathInside } from "../../infra/path-guards.js"; import type { SandboxBackendCommandParams, @@ -24,6 +25,14 @@ type ResolvedRemotePath = SandboxResolvedPath & { source: RemoteMountSource; }; +function hasMultipleHardlinks(raw: string): boolean { + const linkCount = parseStrictNonNegativeInteger(raw); + if (linkCount !== undefined) { + return linkCount > 1; + } + return /^\d+$/.test(raw); +} + type MountInfo = { localRoot: string; containerRoot: string; @@ -513,7 +522,7 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge { return; } const [kind = "", linksRaw = "1"] = output.split("|"); - if (kind === "regular file" && Number(linksRaw) > 1) { + if (kind === "regular file" && hasMultipleHardlinks(linksRaw)) { throw new Error( `Hardlinked path is not allowed under sandbox mount root: ${params.containerPath}`, );