fix(code-execution): lock down remote kernel/RPC dirs, keep RPC token out of argv
On shared remote backends the execute_code channel created kernel and sandbox dirs under shared temp at the process umask (775 group-writable under umask 002), wrote request/result files group-readable, and carried HERMES_RPC_TOKEN on remote command lines where co-tenant users read argv via ps for the whole run. A co-tenant could read tool arguments and results, and on group-writable dirs forge RPC requests dispatched under the user's approval context. - All remote dirs are created owner-only (umask 077 + chmod 700, checked fail-closed) and every Hermes file write is mode 600. - The token travels in a sourced env file inside a subshell so the vars never enter the backend's session-snapshot dump, and ships via stdin on pipe-capable backends so it never enters argv at all. - The RPC poll loop rejects non-int seq requests before dispatch instead of replaying them every cycle. - tool_result_storage gets the same owner-only treatment for archived tool output. (cherry picked from commit aef21731d7fb8a4e0a6ada4ff9889264df4a8893)
This commit is contained in:
@@ -83,7 +83,10 @@ def main():
|
||||
payload, _ = run_cell(request, execution_count)
|
||||
res_name = name.replace("cell_req_", "cell_res_")
|
||||
tmp = os.path.join(CELLS, res_name + ".tmp")
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
# Cell results carry the executed code's output: owner-only, even if
|
||||
# the process umask is permissive.
|
||||
fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, ensure_ascii=False)
|
||||
os.replace(tmp, os.path.join(CELLS, res_name))
|
||||
if payload["status"] == "exit":
|
||||
@@ -204,23 +207,51 @@ atexit.register(shutdown_all_remote_kernels)
|
||||
def _spawn_remote_kernel(env, env_type: str, owner: str, task_env_id: str,
|
||||
sandbox_tools: frozenset, *, idle_exit: int) -> Optional[RemoteKernel]:
|
||||
"""Start a detached kernel runner on the remote. None on failure (dir removed)."""
|
||||
from hermes_time import get_timezone_name
|
||||
from tools.code_execution_rpc import _private_dirs_cmd
|
||||
from tools.code_execution_tool import (
|
||||
MAX_STDOUT_BYTES, _ship_file_to_remote, _env_temp_dir, generate_hermes_tools_module,
|
||||
MAX_STDOUT_BYTES, _ship_file_to_remote, _env_temp_dir,
|
||||
_ship_env_file_and_launch_prefix, generate_hermes_tools_module,
|
||||
)
|
||||
kernel_dir = f"{_env_temp_dir(env)}/hermes_rkernel_{uuid.uuid4().hex[:12]}"
|
||||
q_dir = shlex.quote(kernel_dir)
|
||||
kernel = None
|
||||
try:
|
||||
_sh(env, f"mkdir -p {q_dir}/cells {q_dir}/rpc")
|
||||
# Private dirs: the kernel dir lives under a shared temp dir and carries
|
||||
# the RPC token (in req files), tool results, and cell code/output.
|
||||
# Fail closed on setup failure rather than ship secrets into a dir that
|
||||
# stayed permissive.
|
||||
setup = env.execute(
|
||||
_private_dirs_cmd([f"{kernel_dir}/cells", f"{kernel_dir}/rpc"],
|
||||
[kernel_dir, f"{kernel_dir}/cells", f"{kernel_dir}/rpc"]),
|
||||
cwd="/", timeout=15)
|
||||
if not isinstance(setup, dict) or setup.get("returncode", 1) != 0:
|
||||
raise RuntimeError(
|
||||
f"remote kernel dir setup failed: {(setup or {}).get('output', setup)!r}")
|
||||
rpc_token = secrets.token_urlsafe(32)
|
||||
_ship_file_to_remote(env, f"{kernel_dir}/kernel_runner.py", REMOTE_KERNEL_RUNNER_SOURCE.format(
|
||||
cell_source=RUNNER_CELL_SOURCE, capture_limit=MAX_STDOUT_BYTES, idle_exit=idle_exit))
|
||||
_ship_file_to_remote(env, f"{kernel_dir}/hermes_tools.py",
|
||||
generate_hermes_tools_module(list(sandbox_tools), transport="file"))
|
||||
env_prefix = (f"HERMES_KERNEL_DIR={q_dir} HERMES_RPC_DIR={shlex.quote(kernel_dir + '/rpc')} "
|
||||
f"HERMES_RPC_TOKEN={shlex.quote(rpc_token)} PYTHONDONTWRITEBYTECODE=1 PYTHONPATH={q_dir}")
|
||||
started = _sh(env, f"cd {q_dir} && nohup env {env_prefix} python3 kernel_runner.py "
|
||||
f"> {q_dir}/runner.log 2>&1 & echo PID:$!", timeout=20)
|
||||
env_map = {"HERMES_KERNEL_DIR": kernel_dir,
|
||||
"HERMES_RPC_DIR": f"{kernel_dir}/rpc",
|
||||
"HERMES_RPC_TOKEN": rpc_token,
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PYTHONPATH": kernel_dir}
|
||||
tz = get_timezone_name() # routed profile's timezone, matching the per-call path
|
||||
if tz:
|
||||
env_map["TZ"] = tz
|
||||
launch_prefix = _ship_env_file_and_launch_prefix(
|
||||
env, kernel_dir, "kernel.env", env_map)
|
||||
# kernel.env is removed after sourcing: the runner's env keeps the
|
||||
# values, so the token file need not sit at rest for the kernel's
|
||||
# lifetime. runner.log is pre-created 600 so the launch redirect never
|
||||
# lands at the remote's default umask. The inner `&` stays inside the
|
||||
# subshell where `$!` resolves to the runner pid.
|
||||
started = _sh(env, f"{launch_prefix} rm -f ./kernel.env && "
|
||||
f"touch runner.log && chmod 600 runner.log && "
|
||||
f"{{ nohup python3 kernel_runner.py > runner.log 2>&1 & "
|
||||
f'echo "PID:$!"; }} )', timeout=20)
|
||||
pid = next((line.strip()[4:].strip() for line in started.splitlines()
|
||||
if line.strip().startswith("PID:")), "")
|
||||
if not pid.isdigit():
|
||||
|
||||
Reference in New Issue
Block a user