fix(sandbox): handle missing stat paths across locales (#105263)

* fix(sandbox): stabilize stat errors across locales

* fix(sandbox): normalize remote stat locale

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
NianJiu
2026-07-16 16:11:33 +08:00
committed by GitHub
parent e481ed4229
commit 175761d203
4 changed files with 78 additions and 4 deletions

View File

@@ -22,7 +22,7 @@ export function buildStatPlan(
): SandboxFsCommandPlan {
return {
checks: [{ target, options: { action: "stat files" } }],
script: 'set -eu\ncd -- "$1"\nstat -c "%F|%s|%y" -- "$2"',
script: 'set -eu\ncd -- "$1"\nLC_ALL=C stat -c "%F|%s|%y" -- "$2"',
args: [anchoredTarget.canonicalParentPath, anchoredTarget.basename],
allowFailure: true,
};

View File

@@ -223,6 +223,77 @@ describe("sandbox fs bridge anchored ops", () => {
});
});
it("runs stat under the C locale so missing-file errors return null", async () => {
await withTempDir("openclaw-fs-bridge-stat-missing-", async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
mockedExecDockerRaw.mockImplementation(async (args) => {
const script = getDockerScript(args);
if (script.includes('readlink -f -- "$cursor"')) {
return dockerExecResult(`${getDockerArg(args, 1)}\n`);
}
if (script.includes('stat -c "%F|%s|%y"')) {
const stderr = script.includes('LC_ALL=C stat -c "%F|%s|%y"')
? "stat: cannot stat 'note.txt': No such file or directory\n"
: "stat: der Aufruf von statx für 'note.txt' ist nicht möglich: Datei oder Verzeichnis nicht gefunden\n";
return {
stdout: Buffer.alloc(0),
stderr: Buffer.from(stderr),
code: 1,
};
}
return dockerExecResult("");
});
const bridge = createSandboxFsBridge({
sandbox: createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
}),
});
await expect(bridge.stat({ filePath: "note.txt" })).resolves.toBeNull();
const statCall = requireDockerCall(
findCallByScriptFragment('stat -c "%F|%s|%y" -- "$2"'),
"stat",
);
expect(getDockerScript(statCall[0])).toContain('LC_ALL=C stat -c "%F|%s|%y" -- "$2"');
});
});
it("keeps non-missing stat failures as errors", async () => {
await withTempDir("openclaw-fs-bridge-stat-error-", async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace");
await fs.mkdir(workspaceDir, { recursive: true });
mockedExecDockerRaw.mockImplementation(async (args) => {
const script = getDockerScript(args);
if (script.includes('readlink -f -- "$cursor"')) {
return dockerExecResult(`${getDockerArg(args, 1)}\n`);
}
if (script.includes('stat -c "%F|%s|%y"')) {
return {
stdout: Buffer.alloc(0),
stderr: Buffer.from("stat: cannot stat 'note.txt': Permission denied\n"),
code: 1,
};
}
return dockerExecResult("");
});
const bridge = createSandboxFsBridge({
sandbox: createSandbox({
workspaceDir,
agentWorkspaceDir: workspaceDir,
}),
});
await expect(bridge.stat({ filePath: "note.txt" })).rejects.toThrow("Permission denied");
});
});
it("saturates unsafe stat size output", async () => {
await withTempDir("openclaw-fs-bridge-stat-parse-", async (stateDir) => {
const workspaceDir = path.join(stateDir, "workspace");

View File

@@ -188,7 +188,7 @@ describe("remote sandbox fs bridge", () => {
},
);
it("saturates unsafe stat size output without returning NaN", async () => {
it("normalizes stat output locale and saturates unsafe sizes", async () => {
// Remote stat output is untrusted shell text; unsafe numeric fields should
// clamp to deterministic values instead of leaking NaN into callers.
await withTempDir("openclaw-remote-fs-bridge-stat-", async (stateDir) => {
@@ -216,8 +216,11 @@ describe("remote sandbox fs bridge", () => {
};
}
if (command.script.includes('stat -c "%F|%s|%y"')) {
const kind = command.script.includes('LC_ALL=C stat -c "%F|%s|%y"')
? "regular file"
: "reguläre Datei";
return {
stdout: Buffer.from("regular file|9007199254740992|8640000000001\n"),
stdout: Buffer.from(`${kind}|9007199254740992|8640000000001\n`),
stderr: Buffer.alloc(0),
code: 0,
};

View File

@@ -255,7 +255,7 @@ class RemoteShellSandboxFsBridge implements SandboxFsBridge {
signal: params.signal,
});
const result = await this.runRemoteScript({
script: 'set -eu\nstat -c "%F|%s|%y" -- "$1"',
script: 'set -eu\nLC_ALL=C stat -c "%F|%s|%y" -- "$1"',
args: [canonical],
signal: params.signal,
});