fwh-pyrun 3.6 KB

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