diff --git a/chv-guest/build.sh b/chv-guest/build.sh index a0ffe82..365b0e8 100755 --- a/chv-guest/build.sh +++ b/chv-guest/build.sh @@ -388,7 +388,20 @@ VC_ARGS=(-a "$OUT/disk.raw" # root 所有时 chv-browser 建不了 cdp-broker.sock,broker # bind 报 PermissionError 而 crash-loop。绝不用 0666/0777 # 那种放宽(与 round-11 authd socket 组权限 0660 同款口径)。 - --run-command 'printf "d /run/chv 2775 root chv-browser -\nd /run/chv/shots 0755 chv-browser chv-browser -\nd /run/chv/ask-queue 0750 chv-browser chv-browser -\nd /run/chv-browser 0755 chv-browser chv-browser -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser chv-cdp-broker || true') + # round-12 修复轮第二轮(项 1,P6):tmpfiles.d 再预建 worker + # 的运行时路径。chv-gitea-worker.service 用 ProtectSystem=strict + # + ReadWritePaths=/home/chv/workspace /run/chv-gitea + # /var/log/chv-gitea-worker.log;systemd 建 mount namespace 时 + # 要求每个列出路径事先存在,否则 226/NAMESPACE(R12 复测钉死: + # /run 是 tmpfs、开机不存在;log 文件要 worker 首次写审计才建, + # 而 worker 永远跑不到那一步)。ExecStartPre 救不了——命名空间 + # 搭建在任何 ExecStartPre 之前,226 发生在它运行之前;改 + # ReadWritePaths 为已存在目录(/var/log)会把写权限放大到整个 + # 目录,也不要。tmpfiles.d 由 systemd-tmpfiles-setup 在 sysinit + # 跑、早于 multi-user 的服务启动,且与 P4 的 /run/chv 同款写法, + # 不新增进镜像的文件(chv.conf 本就是 build.sh 内联生成), + # lockfile 无需登记。模式与 worker 审计 0600 同款口径。 + --run-command 'printf "d /run/chv 2775 root chv-browser -\nd /run/chv/shots 0755 chv-browser chv-browser -\nd /run/chv/ask-queue 0750 chv-browser chv-browser -\nd /run/chv-browser 0755 chv-browser chv-browser -\nd /run/chv-gitea 0700 root root -\nf /var/log/chv-gitea-worker.log 0600 root root -\n" > /etc/tmpfiles.d/chv.conf && systemctl enable chv-broker chv-browser chv-cdp-broker || true') # P3 交接(Muse P3 的 authd/sentinel 叶子模块):文件在仓库里存在时 # 拷入镜像(本分支已有,合入后自动生效);不存在时只警告、不失败—— # M4 既定 build.sh 不硬要求 cloud/,见上。缺叶子模块时 chv-authd diff --git a/chv-guest/chv-gitea-worker.service b/chv-guest/chv-gitea-worker.service index c697bb3..dc954fa 100644 --- a/chv-guest/chv-gitea-worker.service +++ b/chv-guest/chv-gitea-worker.service @@ -24,6 +24,13 @@ NoNewPrivileges=yes CapabilityBoundingSet= RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 SystemCallFilter=@system-service +# ReadWritePaths 下的路径必须在开机时已存在(ProtectSystem=strict +# 下 systemd 建 mount namespace 的硬要求,缺一个即 226/NAMESPACE): +# /run/chv-gitea 与 /var/log/chv-gitea-worker.log 由 +# /etc/tmpfiles.d/chv.conf 预建(build.sh 内联写,round-12 修复轮 +# 第二轮项 1);/home/chv/workspace 开机时已在(R12 复测的 226 +# 只报过前两处)。ExecStartPre 不能替代 tmpfiles.d——命名空间 +# 搭建在它之前,226 发生在它运行之前。 ReadWritePaths=/home/chv/workspace /run/chv-gitea /var/log/chv-gitea-worker.log # worker 崩溃不拖垮 agent:broker/薄 CLI 收到类型化失败即返回, # 不重试、不挂起;systemd 只负责拉起 worker 本身。 diff --git a/cloud/tests/test_chv_muse_p6.py b/cloud/tests/test_chv_muse_p6.py index 678e2b0..47144c0 100644 --- a/cloud/tests/test_chv_muse_p6.py +++ b/cloud/tests/test_chv_muse_p6.py @@ -17,6 +17,7 @@ import importlib.util import json import os +import re import shutil import socket import subprocess @@ -756,6 +757,78 @@ class ExecutorAuditHookTest(unittest.TestCase): safety_mod.DEFAULT_AUDIT_LOGGER = old +# -- (7b) worker 单元沙箱路径预建 ------------------------------------------- +class WorkerUnitTmpfilesTest(unittest.TestCase): + """round-12 修复轮第二轮(项 1,P6):ProtectSystem=strict 下 + ReadWritePaths 的每一项必须在开机时已存在,否则 systemd 建 + mount namespace 直接 226/NAMESPACE(R12 复测钉死:/run/chv-gitea + 与 /var/log/chv-gitea-worker.log 两处缺失,worker 从未起来)。 + + 预建机制只能是 tmpfiles.d:ExecStartPre 跑在命名空间搭建之后, + 救不了 226;改 ReadWritePaths 为已存在目录会放大写权限。 + tmpfiles.d 与 P4 的 /run/chv 同款写法、同一文件 + (/etc/tmpfiles.d/chv.conf,build.sh 内联生成,不新增进镜像 + 的文件,lockfile 无需登记)。 + """ + + def _readwrite_paths(self): + text = (REPO_ROOT / "chv-guest" + / "chv-gitea-worker.service").read_text() + for line in text.splitlines(): + if line.startswith("ReadWritePaths="): + return line.split("=", 1)[1].split() + self.fail("unit 文件里找不到 ReadWritePaths") + + def _tmpfiles_entries(self): + text = (REPO_ROOT / "chv-guest" / "build.sh").read_text() + # chv.conf 只由一处 printf 写(与 P4 共用同一文件)。 + self.assertEqual( + text.count("> /etc/tmpfiles.d/chv.conf"), 1, + "chv.conf 须只由 build.sh 里一处 printf 写") + match = re.search( + r"printf \"((?:[^\"\\]|\\.)*)\" > /etc/tmpfiles.d/chv.conf", + text) + self.assertIsNotNone(match, "build.sh 里找不到写 chv.conf 的行") + raw = match.group(1).replace("\\n", "\n") + entries = {} + for line in raw.splitlines(): + parts = line.split() + if len(parts) >= 3 and parts[0] in ("d", "f"): + entries[parts[1]] = (parts[0], parts[2]) + return entries + + def test_volatile_readwrite_paths_have_tmpfiles(self): + entries = self._tmpfiles_entries() + paths = self._readwrite_paths() + # /run 下的目录(tmpfs,开机不存在)须有 d 行; + # 文件路径(worker 首次写审计才建)须有 f 行。 + self.assertEqual(entries.get("/run/chv-gitea"), + ("d", "0700")) + self.assertEqual(entries.get("/var/log/chv-gitea-worker.log"), + ("f", "0600")) + for path in paths: + if path.startswith("/run/") or path.endswith(".log"): + self.assertIn(path, entries, + "%s 无 tmpfiles 预建,服务会 226" % path) + + def test_p4_tmpfiles_lines_kept(self): + # 同一文件里的 P4 行是回归哨兵:改 chv.conf 别把它们挤掉。 + entries = self._tmpfiles_entries() + for path, kind_mode in ( + ("/run/chv", ("d", "2775")), + ("/run/chv/shots", ("d", "0755")), + ("/run/chv/ask-queue", ("d", "0750")), + ("/run/chv-browser", ("d", "0755"))): + self.assertEqual(entries.get(path), kind_mode, path) + + def test_no_exec_start_pre_in_unit(self): + # ExecStartPre 跑在命名空间搭建之后,救不了 226—— + # 若有人加了它,说明误以为它能预建路径。 + text = (REPO_ROOT / "chv-guest" + / "chv-gitea-worker.service").read_text() + self.assertNotIn("ExecStartPre=", text) + + class BuildInfoContentsTest(unittest.TestCase): """round-12 修复轮(项 2,P6):worker 三件须进 BUILDINFO contents(无条件拷入,与 P2/P4 同款钉法)。"""