Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions agents/hermes/seed-dashboard-config.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,26 @@ def _rename_no_replace_at(src_fd: int, name: str, dst_fd: int) -> None:
import ctypes

libc = ctypes.CDLL(None, use_errno=True)
renameat2 = getattr(libc, "renameat2", None)
if renameat2 is None:
raise OSError(errno.ENOSYS, "renameat2 is unavailable")
renameat2.argtypes = [
if sys.platform == "darwin":
rename_no_replace = getattr(libc, "renameatx_np", None)
rename_flag = 0x00000004 # RENAME_EXCL from Darwin sys/stdio.h.
unavailable_message = "renameatx_np is unavailable"
else:
rename_no_replace = getattr(libc, "renameat2", None)
rename_flag = 1 # RENAME_NOREPLACE from Linux stdio.h.
unavailable_message = "renameat2 is unavailable"
if rename_no_replace is None:
raise OSError(errno.ENOSYS, unavailable_message)
rename_no_replace.argtypes = [
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_uint,
]
renameat2.restype = ctypes.c_int
rename_no_replace.restype = ctypes.c_int
encoded = os.fsencode(name)
if renameat2(src_fd, encoded, dst_fd, encoded, 1) != 0:
if rename_no_replace(src_fd, encoded, dst_fd, encoded, rename_flag) != 0:
error_number = ctypes.get_errno()
raise OSError(error_number, os.strerror(error_number))

Expand Down
50 changes: 50 additions & 0 deletions test/hermes-dashboard-profile-migration-security.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,56 @@ describe("Hermes dashboard profile migration security", () => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

it("moves between anchored directories without replacing a peer", () => {
const sourceDir = path.join(tmpDir, "source");
const destinationDir = path.join(tmpDir, "destination");
fs.mkdirSync(sourceDir);
fs.mkdirSync(destinationDir);
fs.writeFileSync(path.join(sourceDir, "state"), "legacy\n");

const harness = `
import errno
import importlib.util
import os
import sys

spec = importlib.util.spec_from_file_location("seed_dashboard_config", sys.argv[1])
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

source_fd = module._open_directory_no_follow(sys.argv[2])
destination_fd = module._open_directory_no_follow(sys.argv[3])
try:
module._rename_no_replace_at(source_fd, "state", destination_fd)
source_state_fd = os.open(
"state", os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600, dir_fd=source_fd
)
try:
os.write(source_state_fd, b"retry\\n")
finally:
os.close(source_state_fd)
try:
module._rename_no_replace_at(source_fd, "state", destination_fd)
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
else:
raise AssertionError("no-clobber rename replaced the destination")
finally:
os.close(destination_fd)
os.close(source_fd)
`;
const res = spawnSync("python3", ["-c", harness, SCRIPT_PATH, sourceDir, destinationDir], {
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"],
timeout: 10_000,
});

expect(res.status, res.stderr).toBe(0);
expect(fs.readFileSync(path.join(sourceDir, "state"), "utf-8")).toBe("retry\n");
expect(fs.readFileSync(path.join(destinationDir, "state"), "utf-8")).toBe("legacy\n");
});

it("does not replace a destination recreated before the whole-profile move (#7200)", () => {
const hermesHome = path.join(tmpDir, ".hermes");
const legacyHome = path.join(hermesHome, "dashboard-home");
Expand Down
Loading