fix(sandbox): validate remote hardlink counts

This commit is contained in:
Peter Steinberger
2026-05-29 09:47:27 -04:00
parent 67697fa309
commit 8a4573917d
2 changed files with 60 additions and 1 deletions

View File

@@ -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<T>(prefix: string, run: (stateDir: string) => Promise<T>): Promise<T> {

View File

@@ -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}`,
);