"""对齐 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()