Files
codex-app-server/cloud/tests/test_chv_muse_p5_taint.py
T
host 25bcd355f8 feat(chv): 对齐 Muse P5 纵深——eBPF 污点传播 + 五档严格能力 + 对话外审批 + 默认策略
- taint.py:污点追踪器(登记/污点不可逆/子进程继承/审计)
- ebpf_taint.py:eBPF 程序源码 + 加载器(能力探测 + 降级);
  本机实测加载被 seccomp 挡(bpf() EPERM),报 blocked 不冒充
- capabilities.py:五档严格能力(一次/会话/任务/时限/永久,
  三元组绑定 + 精确匹配 + 撤销;permanent 默认不签发)
- policy.py:默认策略规则表 + 谨慎度三档(写操作与敏感路径
  永远不自动放行,规则变化审计)
- approval_channel.py:对话外审批通道(nonce + 15 分钟过期 +
  HMAC,答复一次有效,对话伪造无效)
- sentinel.py 接线:evaluate() 可选接 taint/policy/tier/scope,
  不传时行为与 P3 一致;自动放行步骤 + 档位签发回落单次;
  Capability 基类属性放宽为 Optional(None = 不限)
- 新测试 61 项;全量 cloud 套件 1958 项 OK(skipped=8)

真机待验证(未做,排队在 round-11 之后):见
docs/chv-muse-p5-gap.md §4
2026-11-06 22:25:55 +00:00

268 lines
12 KiB
Python

"""对齐 Muse P5-A 测试:污点追踪 + eBPF 加载层。
只用标准库与 unittest,绝不启动真实 VM、不联网、不加载真实
eBPF 程序、不含真实凭证与用户数据内容(测试值全是假值)。
覆盖:
- 污点追踪器:登记干净、读用户数据后变污点、子进程继承、
污点不可清除(无清除入口、只能随进程退出消失)、污点进程
出口不自动放行、未知 pid 不可验证也不自动放行;
- 审计:事件名 + pid + 原因,无用户数据内容、无凭证值,
调用方误把内容当 source 传也不落盘;
- eBPF 加载器:在无 bpftool/无 bpffs/无 CAP 时报 blocked +
原因、降级模式仍可用策略引擎(mode 恒为降级模式、绝不
报 loaded);源码常量含 cgroup egress 拦截、进程归因 map、
LSM hook 骨架。
"""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
try:
from cloud.chv.ebpf_taint import (
EBPF_CGROUP_EGRESS_C,
EBPF_LSM_TAINT_C,
FALLBACK_MODE,
EbpfTaintLoader,
)
from cloud.chv.taint import TaintTracker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.chv.ebpf_taint import (
EBPF_CGROUP_EGRESS_C,
EBPF_LSM_TAINT_C,
FALLBACK_MODE,
EbpfTaintLoader,
)
from cloud.chv.taint import TaintTracker
REPO_ROOT = Path(__file__).resolve().parents[2]
SECRET_LIKE = "sk-fake-secret-content-12345"
def _fake_status(cap_eff_hex: str) -> str:
"""写一份假 /proc/self/status(只含 CapEff 一行)。"""
tmp = tempfile.NamedTemporaryFile(
"w", delete=False, suffix="-status")
tmp.write("Name:\tfake\nCapEff:\t%s\n" % cap_eff_hex)
tmp.close()
return tmp.name
# -- (1) 污点追踪器 --------------------------------------------------------
class TaintTrackerTest(unittest.TestCase):
def setUp(self):
self.tracker = TaintTracker(clock=lambda: 1000.0)
def test_clean_process_is_auto_allow_eligible(self):
self.assertTrue(self.tracker.register(
100, cgroup="/sys/fs/cgroup/chv-100",
connector="gitea", use_case="clone"))
self.assertFalse(self.tracker.is_tainted(100))
self.assertTrue(self.tracker.auto_allow_eligible(100))
state = self.tracker.state(100)
self.assertEqual(state["connector"], "gitea")
self.assertEqual(state["use_case"], "clone")
def test_register_rejects_bad_or_duplicate_pid(self):
self.assertFalse(self.tracker.register(0))
self.assertFalse(self.tracker.register(-5))
self.assertTrue(self.tracker.register(101))
self.assertFalse(self.tracker.register(101))
self.assertTrue(self.tracker.is_registered(101))
self.assertFalse(self.tracker.is_registered(999))
def test_read_user_data_marks_tainted(self):
self.tracker.register(110, connector="gitea",
use_case="sync")
self.assertTrue(
self.tracker.mark_tainted(110, "gitea_repo"))
self.assertTrue(self.tracker.is_tainted(110))
self.assertFalse(self.tracker.auto_allow_eligible(110))
events = [r for r in self.tracker.audit()
if r["event"] == "sentinel_taint_marked"]
self.assertEqual(len(events), 1)
self.assertEqual(events[0]["pid"], 110)
self.assertEqual(events[0]["source"], "gitea_repo")
self.assertIn("read_user_data", events[0]["reason"])
def test_mark_tainted_unknown_pid_registers_as_tainted(self):
# 未登记进程上报读用户数据:先记为污点,不等补登记。
self.assertTrue(self.tracker.mark_tainted(111, "user_file"))
self.assertTrue(self.tracker.is_tainted(111))
self.assertFalse(self.tracker.auto_allow_eligible(111))
def test_child_inherits_taint_and_clean_parent_stays_clean(self):
self.tracker.register(120, connector="gitea",
use_case="clone")
self.tracker.register(121)
self.tracker.mark_tainted(120, "user_memory")
self.assertTrue(self.tracker.fork_child(120, 130))
self.assertTrue(self.tracker.is_tainted(130))
self.assertFalse(self.tracker.auto_allow_eligible(130))
# 干净父进程的孩子也干净。
self.assertTrue(self.tracker.fork_child(121, 131))
self.assertFalse(self.tracker.is_tainted(131))
self.assertTrue(self.tracker.auto_allow_eligible(131))
# 继承带上父的连接器/用例。
self.assertEqual(self.tracker.state(130)["connector"],
"gitea")
def test_fork_child_rejects_unknown_parent_and_bad_pid(self):
self.assertFalse(self.tracker.fork_child(999, 140))
self.assertFalse(self.tracker.is_registered(140))
self.tracker.register(141)
self.assertFalse(self.tracker.fork_child(141, 0))
self.assertFalse(self.tracker.fork_child(141, 141))
def test_taint_cannot_be_cleared_only_exits_with_process(self):
# 否定测试:没有任何清除入口;污点只能随进程退出消失。
self.tracker.register(150)
self.tracker.mark_tainted(150, "chat_history")
for name in ("clear_taint", "untaint", "remove_taint",
"set_clean", "reset"):
self.assertFalse(hasattr(self.tracker, name), name)
# 再上报一次也不会变干净;重复登记也不会洗掉污点。
self.tracker.mark_tainted(150, "user_file")
self.assertTrue(self.tracker.is_tainted(150))
self.assertFalse(self.tracker.register(150))
self.assertTrue(self.tracker.is_tainted(150))
self.assertTrue(self.tracker.process_exit(150))
self.assertFalse(self.tracker.is_registered(150))
self.assertFalse(self.tracker.is_tainted(150))
# 退出后是未知 pid:仍不可自动放行。
self.assertFalse(self.tracker.auto_allow_eligible(150))
self.assertFalse(self.tracker.process_exit(150))
def test_unknown_pid_is_not_auto_allow_eligible(self):
# 否定测试:不可验证进程失去自动放行(不是默认放行)。
self.assertFalse(self.tracker.is_tainted(999))
self.assertFalse(self.tracker.auto_allow_eligible(999))
self.assertIsNone(self.tracker.state(999))
def test_audit_never_contains_user_data_content(self):
# 否定测试:调用方误把内容当 source 传,原始串也不落盘;
# 审计里只有事件名 + pid + 原因 + 分类。
self.tracker.register(160)
self.tracker.mark_tainted(160, SECRET_LIKE)
blob = json.dumps(self.tracker.audit())
self.assertNotIn(SECRET_LIKE, blob)
self.assertNotIn("sk-fake", blob)
marked = [r for r in self.tracker.audit()
if r["event"] == "sentinel_taint_marked"][0]
self.assertEqual(marked["source"], "unknown")
for record in self.tracker.audit():
self.assertTrue(set(record) <= {
"event", "pid", "reason", "ts", "source"}, record)
# -- (2) eBPF 加载器 --------------------------------------------------------
class EbpfLoaderTest(unittest.TestCase):
def _loader(self, cap_hex="0000000000000000",
bpffs="/nonexistent-bpffs",
tools_missing=True):
status_path = _fake_status(cap_hex)
self.addCleanup(os.unlink, status_path)
which = (lambda name: None) if tools_missing else None
return EbpfTaintLoader(
bpffs_path=bpffs, status_path=status_path, which=which)
def test_blocked_without_bpftool_bpffs_and_caps(self):
# 本机同款处境:无工具链、无 bpffs、无能力位。
loader = self._loader()
result = loader.probe()
self.assertEqual(result["status"], "blocked")
self.assertEqual(result["mode"], FALLBACK_MODE)
self.assertIn("tool_missing:bpftool", result["blockers"])
self.assertIn("tool_missing:clang", result["blockers"])
self.assertIn("bpffs_missing:/nonexistent-bpffs",
result["blockers"])
self.assertIn("cap_bpf_and_cap_sys_admin_missing",
result["blockers"])
self.assertFalse(result["cap_bpf"])
self.assertFalse(result["cap_sys_admin"])
self.assertNotEqual(result["status"], "loaded")
def test_blocked_without_caps_even_when_tools_present(self):
# 否定测试:工具有了、能力位没有,仍 blocked,不试加载。
loader = self._loader(tools_missing=False)
loader._which = lambda name: "/usr/bin/" + name
result = loader.probe()
self.assertEqual(result["status"], "blocked")
self.assertIn("cap_bpf_and_cap_sys_admin_missing",
result["blockers"])
def test_cap_bits_decoded_from_status(self):
# CAP_BPF bit 39、CAP_SYS_ADMIN bit 21 的解码对照。
only_bpf = self._loader(cap_hex="%016x" % (1 << 39))
self.assertTrue(only_bpf.probe()["cap_bpf"])
self.assertFalse(only_bpf.probe()["cap_sys_admin"])
only_admin = self._loader(cap_hex="%016x" % (1 << 21))
self.assertFalse(only_admin.probe()["cap_bpf"])
self.assertTrue(only_admin.probe()["cap_sys_admin"])
both = self._loader(
cap_hex="%016x" % ((1 << 39) | (1 << 21)))
result = both.probe()
self.assertTrue(result["cap_bpf"])
self.assertTrue(result["cap_sys_admin"])
def test_try_load_degrades_never_claims_loaded(self):
# 否定测试:加载失败一律降级,绝不冒充已加载。
loader = self._loader()
result = loader.try_load()
self.assertEqual(result["status"], "blocked")
self.assertEqual(result["mode"], FALLBACK_MODE)
self.assertNotEqual(result["status"], "loaded")
status = loader.load_status()
self.assertEqual(status["status"], "blocked")
self.assertEqual(status["mode"], FALLBACK_MODE)
def test_degraded_mode_still_usable_via_policy_engine(self):
# 降级模式仍可用:策略引擎(污点追踪器)照常工作,
# 加载器状态明确写着 degraded/blocked 而不是 loaded。
loader = self._loader()
loader.probe()
self.assertEqual(loader.load_status()["mode"],
FALLBACK_MODE)
tracker = TaintTracker(clock=lambda: 1000.0)
tracker.register(200)
self.assertTrue(tracker.auto_allow_eligible(200))
tracker.mark_tainted(200, "gitea_repo")
self.assertFalse(tracker.auto_allow_eligible(200))
def test_load_status_probes_lazily(self):
loader = self._loader()
status = loader.load_status()
self.assertIn(status["status"], ("blocked", "degraded"))
self.assertEqual(status["mode"], FALLBACK_MODE)
# -- (3) eBPF 程序源码常量 --------------------------------------------------
class EbpfSourceTest(unittest.TestCase):
def test_cgroup_program_has_egress_hook_and_taint_map(self):
self.assertIn("cgroup_skb/egress", EBPF_CGROUP_EGRESS_C)
self.assertIn("taint_map", EBPF_CGROUP_EGRESS_C)
self.assertIn("BPF_MAP_TYPE_HASH", EBPF_CGROUP_EGRESS_C)
self.assertIn("BPF_MAP_TYPE_RINGBUF", EBPF_CGROUP_EGRESS_C)
self.assertIn("bpf_get_current_pid_tgid",
EBPF_CGROUP_EGRESS_C)
self.assertIn("GPL", EBPF_CGROUP_EGRESS_C)
def test_lsm_program_has_hook_and_shared_map(self):
self.assertIn("lsm/file_open", EBPF_LSM_TAINT_C)
self.assertIn("taint_map", EBPF_LSM_TAINT_C)
self.assertIn("GPL", EBPF_LSM_TAINT_C)
def test_sources_do_not_claim_to_be_loaded(self):
# 否定测试:源码注释写的是作用与内核要求,不是“已拦截”。
for src in (EBPF_CGROUP_EGRESS_C, EBPF_LSM_TAINT_C):
self.assertNotIn("loaded", src.lower())
self.assertNotIn("已拦截", src)
if __name__ == "__main__":
unittest.main()