mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 10:41:20 +00:00
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:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user