AI 生成的 systemd 服务文件声明的沙箱属性不一定在运行时兑现,需用运行时形状 diff 验证实际进程行为。
我想要论证的立场非常明确:当 AI 模型在 Linux 服务器上编写或修改 systemd 服务时,仅在合并前审查生成的 diff 是不够的。单元文件是对运行时行为的一组承诺,而运行时形状 diff 是在进程启动后验证这些承诺的手段。
我使用 MonkeyCode 的免费模型访问来生成基线服务,并使用其免费服务器选项来运行变更前后的状态,因此下面的产物是可复现的,而非假设。披露:本文是 MonkeyCode 产品推广的一部分。
ProtectSystem=full、PrivateTmp=true、NoNewPrivileges=true、CapabilityBoundingSet= 等 Systemd 服务属性描述了 systemd 应该创建的环境。这些属性由服务管理器强制执行,但它们并不能捕获运行中的进程通过自身代码或通过其他已启用的服务所能触及的一切。AI 生成的单元可以声明严格的沙箱,但仍然可能绑定一个意外的本地主机端口、在私有命名空间中创建一个可写套接字,或与单元控制组之外的进程交互。静态审查看到的是声明;运行时形状 diff 看到的是 resulting process。
这一点很重要,因为 AI 生成的变更通常以针对已运行服务的小补丁形式到达。审查者可能批准一个看起来无害的三行编辑,而实际的运行时占用却以 diff 无法揭示的方式增长了。更有用的把关手段是在两个观察状态之间进行结构化比较:变更之前和变更之后。
以下脚本为一个运行中的 systemd 单元捕获最小的运行时指纹。它记录单元的声明属性、主进程的有效能力、NoNewPrivileges 是否处于活动状态、与该进程关联的监听套接字,以及对该进程可见的挂载点。
#!/usr/bin/env bash
set -euo pipefail
unit=${1:?unit name required}
main_pid=$(systemctl show -p MainPID --value "$unit")
if [ -z "$main_pid" ] || [ "$main_pid" -le 0 ]; then
jq -n --arg unit "$unit" '{unit: $unit, state: "not-running"}'
exit 1
fi
properties=$(systemctl show "$unit" -p CapabilityBoundingSet,NoNewPrivileges,ProtectSystem,PrivateTmp,RestrictAddressFamilies,SystemCallFilter)
cap_eff=$(awk '/^CapEff:/ {print $2}' "/proc/$main_pid/status")
no_new_privs=$(awk '/^NoNewPrivs:/ {print $2}' "/proc/$main_pid/status")
sockets=$(ss -lntup | grep "pid=$main_pid" || true)
relevant_mounts=$(grep -E ' /( |$)' "/proc/$main_pid/mounts" | awk '{print $2}')
jq -n \
--arg unit "$unit" \
--arg main_pid "$main_pid" \
--arg properties "$properties" \
--arg cap_eff "$cap_eff" \
--arg no_new_privs "$no_new_privs" \
--arg sockets "$sockets" \
--arg relevant_mounts "$relevant_mounts" \
'{unit: $unit, main_pid: $main_pid, unit_properties: $properties, runtime_cap_eff: $cap_eff, runtime_no_new_privs: $no_new_privs, listening_sockets: $sockets, relevant_mounts: $relevant_mounts}'
将其保存为 runtime-shape.sh,赋予可执行权限,并以单元名称作为唯一参数运行。输出是一个 JSON 对象,可以存储在 before.json 文件中,随后与 after.json 文件进行比较。
以下比较脚本读取两个形状文件,并标记任何变更的字段。它不断定变更是否安全;它强制由人工或策略门来解释每项变更。
#!/usr/bin/env bash
set -euo pipefail
before=${1:?before json required}
after=${2:?after json required}
jq -n --slurpfile before "$before" --slurpfile after "$after" '
($before[0] // {}) as $b |
($after[0] // {}) as $a |
{
unit: $a.unit,
cap_eff_changed: ($a.runtime_cap_eff != $b.runtime_cap_eff),
no_new_privs_changed: ($a.runtime_no_new_privs != $b.runtime_no_new_privs),
sockets_changed: ($a.listening_sockets != $b.listening_sockets),
mounts_changed: ($a.relevant_mounts != $b.relevant_mounts),
before: $b,
after: $a
}
'
当两个脚本都在 CI 作业或部署后钩子中运行时,布尔字段成为实际的审查门。一个将 mounts_changed 从 false 翻转为 true 的单行变更无法静默通过,即使单元文件 diff 看起来是可以接受的。
生成基线服务。使用 MonkeyCode 中的免费模型访问创建一个最小的 Python HTTP 服务,监听 127.0.0.1:8080 并以 nobody 用户运行。请求一个包含 ProtectSystem=full、PrivateTmp=true、NoNewPrivileges=true 和空的 CapabilityBoundingSet 的单元文件。
在免费服务器选项上运行基线。启动服务,等待监听套接字出现,并用 runtime-shape.sh shape-demo.service > before.json 捕获 before.json。
让模型添加一个功能。例如,请求一个定期清理任务,将操作日志写入 /var/tmp/shape-demo/。这就是那种经常扩大运行时范围的微小 AI 编辑。
在相同环境中运行变更后的服务。停止基线,启动修改后的单元,并捕获 after.json。
Diff 形状并拒绝未解释的漂移。运行 runtime-diff.sh before.json after.json 并检查每个变更的布尔字段。预期的变更可能是一个新的可写挂载,但它必须是故意的且可见的。
该过程将 AI 生成的补丁转化为可测试的运行时断言。生成的代码不被信任;观察到的进程状态与之前观察到的状态进行比较,任何未见的增量都会导致门失败。
此表使审查保持客观。脚本不会给 AI 模型留有余地;它们为审查工程师提供了一份具体的运行时增量列表,以供批准或拒绝。
此方法不是安全边界。它观察进程状态的子集,无法证明服务是安全的、无法证明数据不会通过允许的套接字被泄露、或无法证明代码不包含逻辑错误。它还需要 Linux 主机上的 systemd 以及足够检查目标进程的权限。在某些发行版上,ss 命令可能无法将套接字与进程关联(除非使用额外选项),而挂载过滤器仅包含匹配简单模式的最顶层路径。
不要将其用作沙箱、防火墙或代码审查的替代品。运行非 systemd 平台、Windows 服务、没有可见 PID 命名空间的容器、或合法创建许多短生命周期套接字的服务团队,会看到太多噪音或没有有用的信号。此工作流最适合小型、稳定的 systemd 服务,在这些服务中,AI 生成的变更应该产生狭窄且可解释的运行时增量。
如果你已经可以访问免费模型和免费服务器来进行此类实验,上述两个脚本就是整个门。困难的部分不是构建工具;而是当运行时形状尚未比较时,拒绝信任一个干净的单元文件 diff。