| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 |
- #!/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" "$@"
|