mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(sandbox): harden EXDEV rename fallback
This commit is contained in:
@@ -78,6 +78,7 @@ Docs: https://docs.openclaw.ai
|
||||
- MS Teams: download inline DM images via Graph API and preserve channel reply threading in proactive fallback. (#52212, #55198) Thanks @Ted-developer and @hyojin.
|
||||
- MS Teams: replace the deprecated Teams SDK HttpPlugin stub with `httpServerAdapter` so recurring gateway deprecation warnings stop firing and the Express 5 compatibility workaround stays on the supported SDK path. (#60939) Thanks @coolramukaka-sys.
|
||||
- Control UI/chat: add a per-session thinking-level picker in the chat header and mobile chat settings, and keep the browser bundle on UI-local thinking/session-key helpers so Safari no longer crashes on Node-only imports before rendering chat controls.
|
||||
- Sandbox/SSH: reject hardlinked files during cross-device rename fallback so EXDEV file copies preserve the same pinned file-boundary checks as direct reads.
|
||||
- Control UI: keep Stop visible during tool-only execution, preserve pending-send busy state, and clear stale ClawHub search results as soon as the query changes. (#54528, #59800, #60267) Thanks @chziyue and @frankekn.
|
||||
- Control UI/avatar: honor `ui.assistant.avatar` when serving `/avatar/:agentId` so Appearance UI avatar paths stop falling back to initials placeholders. (#60778) Thanks @hannasdev.
|
||||
- Control UI/cron: highlight the Cron refresh button while refresh is in flight so the page's loading state stays visible even when prior data remains on screen. (#60394) Thanks @coder-zhuzm.
|
||||
|
||||
@@ -18,6 +18,14 @@ function runMutation(args: string[], input?: string) {
|
||||
});
|
||||
}
|
||||
|
||||
function runMutationWithSource(source: string, args: string[], input?: string) {
|
||||
return spawnSync("python3", ["-c", source, ...args], {
|
||||
input,
|
||||
encoding: "utf8",
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function runWritePlan(args: string[], input?: string, env?: NodeJS.ProcessEnv) {
|
||||
const plan = buildPinnedWritePlan({
|
||||
check: {
|
||||
@@ -52,6 +60,11 @@ const hasAbsolutePythonCandidate = SANDBOX_PINNED_MUTATION_PYTHON_CANDIDATES.som
|
||||
existsSync(candidate),
|
||||
);
|
||||
|
||||
const FORCED_EXDEV_MUTATION_PYTHON = SANDBOX_PINNED_MUTATION_PYTHON.replace(
|
||||
" os.rename(src_basename, dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
||||
" raise OSError(errno.EXDEV, 'forced EXDEV for test')\n os.rename(src_basename, dst_basename, src_dir_fd=src_parent_fd, dst_dir_fd=dst_parent_fd)",
|
||||
);
|
||||
|
||||
describe("sandbox pinned mutation helper", () => {
|
||||
it("writes through a pinned directory fd", async () => {
|
||||
await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
|
||||
@@ -249,4 +262,38 @@ describe("sandbox pinned mutation helper", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.runIf(process.platform !== "win32")(
|
||||
"rejects hardlinked files during rename EXDEV fallback",
|
||||
async () => {
|
||||
await withTempDir({ prefix: "openclaw-mutation-helper-" }, async (root) => {
|
||||
const sourceRoot = path.join(root, "source");
|
||||
const destRoot = path.join(root, "dest");
|
||||
const outsideRoot = path.join(root, "outside");
|
||||
await fs.mkdir(sourceRoot, { recursive: true });
|
||||
await fs.mkdir(destRoot, { recursive: true });
|
||||
await fs.mkdir(outsideRoot, { recursive: true });
|
||||
await fs.writeFile(path.join(outsideRoot, "secret.txt"), "classified", "utf8");
|
||||
await fs.link(path.join(outsideRoot, "secret.txt"), path.join(sourceRoot, "linked.txt"));
|
||||
|
||||
const result = runMutationWithSource(FORCED_EXDEV_MUTATION_PYTHON, [
|
||||
"rename",
|
||||
sourceRoot,
|
||||
"",
|
||||
"linked.txt",
|
||||
destRoot,
|
||||
"",
|
||||
"copied.txt",
|
||||
"1",
|
||||
]);
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toMatch(/hardlinked file/i);
|
||||
await expect(fs.stat(path.join(destRoot, "copied.txt"))).rejects.toThrow();
|
||||
await expect(fs.readFile(path.join(outsideRoot, "secret.txt"), "utf8")).resolves.toBe(
|
||||
"classified",
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -183,6 +183,11 @@ export const SANDBOX_PINNED_MUTATION_PYTHON = [
|
||||
" temp_fd = None",
|
||||
" temp_name = None",
|
||||
" try:",
|
||||
" src_file_stat = os.fstat(src_fd)",
|
||||
" if not stat.S_ISREG(src_file_stat.st_mode):",
|
||||
" raise OSError(errno.EPERM, 'only regular files are allowed', src_basename)",
|
||||
" if src_file_stat.st_nlink > 1:",
|
||||
" raise OSError(errno.EPERM, 'hardlinked file is not allowed', src_basename)",
|
||||
" temp_name, temp_fd = create_temp_file(dst_parent_fd, dst_basename)",
|
||||
" while True:",
|
||||
" chunk = os.read(src_fd, 65536)",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// language=python
|
||||
export const SANDBOX_PINNED_FS_MUTATION_PYTHON = String.raw`import os
|
||||
export const SANDBOX_PINNED_FS_MUTATION_PYTHON = String.raw`import errno
|
||||
import os
|
||||
import secrets
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -156,6 +158,15 @@ def run_rename(args):
|
||||
try:
|
||||
from_parent_fd = walk_parent(from_root_fd, from_relative_parent, False)
|
||||
to_parent_fd = walk_parent(to_root_fd, to_relative_parent, True)
|
||||
src_fd = os.open(from_basename, os.O_RDONLY, dir_fd=from_parent_fd)
|
||||
try:
|
||||
src_stat = os.fstat(src_fd)
|
||||
if not stat.S_ISREG(src_stat.st_mode):
|
||||
raise OSError(errno.EPERM, "only regular files are allowed", from_basename)
|
||||
if src_stat.st_nlink > 1:
|
||||
raise OSError(errno.EPERM, "hardlinked file is not allowed", from_basename)
|
||||
finally:
|
||||
os.close(src_fd)
|
||||
run_command(
|
||||
[
|
||||
"mv",
|
||||
|
||||
Reference in New Issue
Block a user