#!/usr/bin/env bash # ============================================================================= # fwh-pyrun —— 在 bwrap 沙箱中运行 Python3,供 fwh-toolbox-agent 安全调用 # ============================================================================= # 用法: # fwh-pyrun /tmp/fwh-toolbox-agent/<脚本>.py [args...] # # 安全模型(三层): # B(强制隔离,bwrap):只读挂载系统目录以支撑 Python 运行;仅沙箱目录 # /tmp/fwh-toolbox-agent 可写;--unshare-net 断网;--unshare-all 降权。 # 脚本在笼子里“看不见” /home、项目目录、凭据等,越界访问会直接失败。 # A(准入校验):脚本必须位于沙箱目录内、以 .py 结尾、真实存在;解析真实 # 路径以防 ../ 与软链逃逸。(agent.json 的 allowedCommands 已在更外层拦截) # C(资源限额,ulimit + timeout):限制 CPU / 内存 / 单文件大小 / 进程数, # 外层 timeout 兜底防死循环。 # # 退出码: # 2 入参非法 / 路径越界 / 脚本不存在 # 124 运行超时(timeout 触发) # 其它 透传 Python 退出码 # ============================================================================= set -euo pipefail SANDBOX_ROOT="/tmp/fwh-toolbox-agent" PY="/usr/bin/python3" # --- 依赖自检(部署脚本应已确保安装;此处兜底给出清晰报错) --- command -v bwrap >/dev/null 2>&1 || { echo "fwh-pyrun: 未找到 bwrap(bubblewrap),无法建立沙箱" >&2; exit 2; } [ -x "$PY" ] || { echo "fwh-pyrun: 未找到 python3($PY)" >&2; exit 2; } # --- A:入参与路径校验 --- script="${1:-}" [ -n "$script" ] || { echo "fwh-pyrun: 缺少脚本路径。用法: fwh-pyrun $SANDBOX_ROOT/<脚本>.py [args...]" >&2; exit 2; } shift || true # 解析真实绝对路径,防止 ../ 或软链逃逸 real_script="$(readlink -f -- "$script" 2>/dev/null)" || { echo "fwh-pyrun: 无法解析路径:$script" >&2; exit 2; } case "$real_script" in "$SANDBOX_ROOT"/*.py) : ;; # 合法:沙箱内 .py *) echo "fwh-pyrun: 拒绝——脚本必须位于 $SANDBOX_ROOT 内且以 .py 结尾(实际:$real_script)" >&2; exit 2 ;; esac [ -f "$real_script" ] || { echo "fwh-pyrun: 脚本不存在:$real_script" >&2; exit 2; } # 确保沙箱目录存在(幂等) mkdir -p "$SANDBOX_ROOT" # --- C:资源限额 --- ulimit -t 30 # CPU 时间上限 30 秒 ulimit -v 1048576 # 虚拟内存上限 1GB(KB) ulimit -f 262144 # 单文件写入上限 ~128MB(blocks,512B/block) ulimit -c 0 # 不产生 core dump # 注:不在此处收紧 ulimit -u(进程数按整个用户计,过紧会导致 timeout/bwrap # 自身 fork 失败);fork 炸弹由 bwrap 的 PID namespace 隔离 + CPU/内存限额抑制。 # --- B:bwrap 强制隔离 + 外层 timeout 兜底(总运行 ≤ 60s) --- # 只读挂载 Python 运行所需系统目录;仅沙箱目录可写;无网络;降权。 # 用数组构造参数,避免路径含特殊字符时被拆分。 bwrap_args=( --ro-bind /usr /usr --ro-bind /etc /etc --bind "$SANDBOX_ROOT" "$SANDBOX_ROOT" --proc /proc --dev /dev --unshare-all --unshare-net --die-with-parent --new-session --setenv HOME "$SANDBOX_ROOT" --setenv TMPDIR "$SANDBOX_ROOT" --setenv PYTHONDONTWRITEBYTECODE 1 --setenv PYTHONPATH "" --chdir "$SANDBOX_ROOT" ) # /lib /lib64 /bin /sbin 在多数发行版是指向 /usr 的符号链接;存在时才只读挂载 for d in /lib /lib64 /bin /sbin; do [ -e "$d" ] && bwrap_args+=( --ro-bind "$d" "$d" ) done exec timeout --kill-after=5s 60s \ bwrap "${bwrap_args[@]}" -- "$PY" -I -B "$real_script" "$@"