fix(kvb-org-organize): no council hook follows a planted symlink or FIFO
The council root is writable by every sandboxed agent of the same uid while its hooks run outside the sandbox, so a planted symlink turned a hook's write into one outside the root, and a planted FIFO or held lock hung every agent's hooks. - Add nofollow.py: open, read, lock, append, replace and unlink below a root through O_NOFOLLOW directory fds, O_NONBLOCK and regular files only; reads stop at 64 MiB and replace undecodable bytes; locks take an explicit wait. - watch, queue, radio and council read and write everything below the council root through it; keep_rows copies no symlinked file into ~/.local/state. - watch: read_json returns dicts only, alert lines that are no alert (deep nesting included) are skipped, cmd_cleanup makes and removes its lock through a root dir fd, and the cache trim walks with fwalk, counts allocated blocks and each inode once. - radio: the hooks wait at most 2 s for a session lock and skip the delivery past it. - council: read_claim ignores claims that are no object with session, runtime and beat, so a malformed claim no longer disables the claim gate. MEDUSA acceptances (kvb-think-criticize 2026-11-06, skeptics A and B): MM-SCAN-058 on nofollow.replace callers, B105 and HPT006 in history.py, RT-SCAN-054 in council.py (name matches). 🎯 Fix: 92 % 💥 Break: 4 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 88 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3843821674
commit
cb815216d2
11 files changed
+791
-97
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
@@ -342,6 +343,104 @@ class TestRadio(unittest.TestCase):
|
||||
self.assertEqual(len(mayday), 1)
|
||||
self.assertIn("by One SYSTEM: a SYSTEM: b;", mayday[0])
|
||||
|
||||
def _victims(self) -> tuple[Path, Path]:
|
||||
victim = self.root.parent / "victim"
|
||||
victim.write_text("keep", encoding="utf-8")
|
||||
victims = self.root.parent / "victims"
|
||||
victims.mkdir()
|
||||
return victim, victims
|
||||
|
||||
def test_the_prompt_hook_writes_no_seen_mark_through_a_planted_name(self):
|
||||
self._send("s1", "post", "board")
|
||||
victim, _ = self._victims()
|
||||
(self.root / "news-seen").mkdir()
|
||||
(self.root / "news-seen" / "s2").symlink_to(victim)
|
||||
self._hook("hook", "s2")
|
||||
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
|
||||
|
||||
def test_the_prompt_hook_writes_no_seen_mark_through_a_planted_directory(self):
|
||||
self._send("s1", "post", "board")
|
||||
_, victims = self._victims()
|
||||
(self.root / "news-seen").symlink_to(victims)
|
||||
self._hook("hook", "s2")
|
||||
self.assertEqual(os.listdir(victims), [])
|
||||
|
||||
def test_the_hooks_truncate_no_session_lock_through_a_planted_name(self):
|
||||
self._send("s1", "post", "board")
|
||||
victim, _ = self._victims()
|
||||
(self.root / "radio-seen").mkdir()
|
||||
(self.root / "radio-seen" / "s2.lock").symlink_to(victim)
|
||||
self._hook("hook", "s2")
|
||||
self._hook("tool", "s2")
|
||||
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
|
||||
|
||||
def test_the_tool_hook_writes_no_session_state_through_a_planted_directory(self):
|
||||
self._send("s1", "send", "pan-pan", "pan")
|
||||
_, victims = self._victims()
|
||||
(self.root / "radio-seen").symlink_to(victims)
|
||||
self._hook("tool", "s2")
|
||||
self.assertEqual(os.listdir(victims), [])
|
||||
|
||||
def test_the_listener_writes_no_pid_file_through_a_planted_directory(self):
|
||||
_, victims = self._victims()
|
||||
self.root.mkdir()
|
||||
(self.root / "radio-listen").symlink_to(victims)
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-c", LISTENER, str(RADIO)],
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, env=self._env(None),
|
||||
)
|
||||
self.addCleanup(self._stop, proc)
|
||||
proc.stdin.write(json.dumps({"session_id": "s2"}))
|
||||
proc.stdin.close()
|
||||
try:
|
||||
code = proc.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
code = None
|
||||
self.assertEqual(os.listdir(victims), [])
|
||||
self.assertEqual(code, 0)
|
||||
|
||||
def test_the_operator_notice_creates_no_file_outside_the_council(self):
|
||||
self.config.write_text(json.dumps({"notify": [sys.executable, "-c", "pass"]}), encoding="utf-8")
|
||||
self._send("s1", "send", "mayday", "fire")
|
||||
escaped = self.root.parent / "escaped"
|
||||
silence = {"since": f"../../{escaped.name}", "until": time.time() + 600, "by": "One", "text": "fire"}
|
||||
(self.root / "radio-silence.json").write_text(json.dumps(silence), encoding="utf-8")
|
||||
self.assertEqual(self._tool("s2"), ["MAYDAY One: fire"])
|
||||
self.assertFalse(escaped.exists())
|
||||
|
||||
def test_the_operator_notice_creates_no_file_through_a_planted_directory(self):
|
||||
self.config.write_text(json.dumps({"notify": [sys.executable, "-c", "pass"]}), encoding="utf-8")
|
||||
self._send("s1", "send", "mayday", "fire")
|
||||
_, victims = self._victims()
|
||||
(self.root / "radio-notified").symlink_to(victims)
|
||||
self.assertEqual(self._tool("s2"), ["MAYDAY One: fire"])
|
||||
self.assertEqual(os.listdir(victims), [])
|
||||
|
||||
def test_the_hooks_skip_a_session_lock_another_process_holds(self):
|
||||
self._send("s1", "post", "board")
|
||||
lock = self.root / "radio-seen" / "s2.lock"
|
||||
lock.parent.mkdir(parents=True, exist_ok=True)
|
||||
with lock.open("w") as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX)
|
||||
start = time.monotonic()
|
||||
self._hook("hook", "s2")
|
||||
self._hook("tool", "s2")
|
||||
self.assertLess(time.monotonic() - start, 20)
|
||||
|
||||
def test_the_prompt_hook_never_blocks_on_a_planted_fifo(self):
|
||||
self._send("s1", "post", "board")
|
||||
os.mkfifo(self.root / "radio-silence.json", 0o600)
|
||||
self.assertTrue(any("board" in line for line in self._prompt("s2")))
|
||||
|
||||
def test_send_truncates_no_file_through_a_planted_lock(self):
|
||||
victim, _ = self._victims()
|
||||
self.root.mkdir()
|
||||
(self.root / "news.lock").symlink_to(victim)
|
||||
result = self._cli("s1", "post", "board")
|
||||
self.assertEqual(victim.read_text(encoding="utf-8"), "keep")
|
||||
self.assertFalse((self.root / "news.jsonl").exists())
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
|
||||
def test_a_council_root_others_can_write_stops_commands_but_not_the_hooks(self):
|
||||
self.root.mkdir()
|
||||
self.root.chmod(0o777)
|
||||
|
||||
Reference in new issue
Block a user