File size: 9,297 Bytes
9633e56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27cb6df
 
 
 
 
 
 
 
 
 
9633e56
 
27cb6df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9633e56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
faf819a
9633e56
 
 
 
 
 
 
 
 
 
27cb6df
9633e56
 
 
 
 
 
 
 
 
27cb6df
9633e56
 
 
 
27cb6df
9633e56
27cb6df
9633e56
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""Exercise content detection with descendants, supervisor startup and restart."""

from __future__ import annotations

import argparse
import json
import os
import secrets
import shutil
import subprocess
import tempfile
import time
from pathlib import Path
from typing import Any

from smoke_content_ai import assets, command, receipt_after, stop

DETECTIONS = Path("/var/lib/lumi-eggcracker/detections")
SERVICE = "lumi-eggcracker.service"


def launch(user: str, wrapper: Path, argv: list[str], output: Path) -> subprocess.Popen[bytes]:
    handle = output.open("wb")
    try:
        return subprocess.Popen(
            ["/usr/sbin/runuser", "-u", user, "--", "/usr/bin/python3", str(wrapper), *argv],
            stdout=handle,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )
    finally:
        handle.close()


def wait_service(*, timeout: float = 10) -> None:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if (
            subprocess.run(
                ["/usr/bin/systemctl", "is-active", "--quiet", SERVICE], check=False
            ).returncode
            == 0
        ):
            return
        time.sleep(0.02)
    raise RuntimeError("supervisor did not become active")


def process_identity(pid: int) -> tuple[str, int]:
    raw = (Path("/proc") / str(pid) / "stat").read_text(encoding="ascii")
    close = raw.rfind(")")
    if close < 1:
        raise RuntimeError("process stat is malformed")
    fields = raw[close + 2 :].split()
    return fields[0], int(fields[19])


def child_identities(path: Path) -> list[tuple[int, int]]:
    if not path.is_file():
        return []
    identities: list[tuple[int, int]] = []
    for item in path.read_text(encoding="ascii").split():
        fields = item.split(":", maxsplit=1)
        if len(fields) != 2 or not all(field.isdigit() for field in fields):
            raise RuntimeError("descendant identity marker is malformed")
        pid, start_time = (int(field) for field in fields)
        if pid < 2 or start_time < 1:
            raise RuntimeError("descendant identity marker is invalid")
        identities.append((pid, start_time))
    if len({pid for pid, _start_time in identities}) != len(identities):
        raise RuntimeError("descendant identity marker contains a duplicate PID")
    return identities


def identity_alive(identity: tuple[int, int]) -> bool:
    pid, expected_start_time = identity
    try:
        state, start_time = process_identity(pid)
    except (FileNotFoundError, OSError, UnicodeError, ValueError):
        return False
    return start_time == expected_start_time and state not in {"X", "Z"}


def wait_identities_terminated(identities: list[tuple[int, int]], *, timeout: float = 5) -> bool:
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        if not any(identity_alive(identity) for identity in identities):
            return True
        time.sleep(0.01)
    return not any(identity_alive(identity) for identity in identities)


def descendant_wrapper(pid_file: Path) -> str:
    return (
        "import os,sys,time\n"
        f"p=open({str(pid_file)!r},'w')\n"
        "def identity(pid):\n"
        " for _ in range(1000):\n"
        "  try:\n"
        "   raw=open(f'/proc/{pid}/stat').read()\n"
        "   close=raw.rfind(')')\n"
        "   return int(raw[close+2:].split()[19])\n"
        "  except FileNotFoundError: time.sleep(0.001)\n"
        " raise RuntimeError('descendant identity unavailable')\n"
        "for _ in range(32):\n"
        " c=os.fork()\n"
        " if c==0: time.sleep(30); os._exit(0)\n"
        " print(f'{c}:{identity(c)}',file=p,flush=True)\n"
        "p.close()\n"
        "os.execv(sys.argv[1],sys.argv[1:])\n"
    )


def main() -> int:
    if os.geteuid() != 0:
        raise SystemExit("content adversarial matrix must run as root")
    parser = argparse.ArgumentParser()
    parser.add_argument("--assets-manifest", required=True, type=Path)
    parser.add_argument("--user", required=True)
    parser.add_argument("--tree-repetitions", type=int, default=100)
    parser.add_argument("--startup-repetitions", type=int, default=20)
    parser.add_argument("--restart-repetitions", type=int, default=20)
    parser.add_argument("--output", required=True, type=Path)
    args = parser.parse_args()
    if (
        (args.tree_repetitions, args.startup_repetitions, args.restart_repetitions) != (100, 20, 20)
        or args.output.exists()
        or args.output.is_symlink()
        or not args.output.parent.is_dir()
    ):
        raise SystemExit("qualification counts or output path are invalid")
    result: dict[str, Any] = {"restart": 0, "startup": 0, "tree": 0, "result": "FAIL"}
    try:
        runner, model, _manifest = assets(args.assets_manifest)
        with tempfile.TemporaryDirectory(prefix="lumi-content-adversarial-", dir="/tmp") as raw:
            root = Path(raw)
            os.chmod(root, 0o777)
            disguised_runner = root / secrets.token_hex(12)
            disguised_model = root / secrets.token_hex(12)
            pid_file = root / "children"
            shutil.copyfile(runner, disguised_runner)
            os.chmod(disguised_runner, 0o755)
            try:
                os.link(model, disguised_model)
            except OSError:
                shutil.copyfile(model, disguised_model)
            wrapper = root / f"{secrets.token_hex(8)}.py"
            wrapper.write_text(descendant_wrapper(pid_file), encoding="utf-8")
            argv = command(disguised_runner, disguised_model)
            for index in range(args.tree_repetitions):
                pid_file.unlink(missing_ok=True)
                canary = subprocess.Popen(["/bin/sleep", "30"], start_new_session=True)
                process: subprocess.Popen[bytes] | None = None
                try:
                    before = set(DETECTIONS.glob("*.json"))
                    process = launch(args.user, wrapper, argv, root / f"out-{index}")
                    receipt = receipt_after(before)
                    identities = child_identities(pid_file)
                    if receipt.get("detector", {}).get("profile") != "content.gguf-llama":
                        raise RuntimeError("content profile did not qualify")
                    if (
                        receipt["capture"]["captured_processes"] < 33
                        or len(identities) != 32
                        or canary.poll() is not None
                        or not wait_identities_terminated(identities)
                    ):
                        raise RuntimeError("content descendants or canary proof failed")
                    result["tree"] += 1
                finally:
                    stop(process)
                    stop(canary)
            for kind, repetitions in (
                ("startup", args.startup_repetitions),
                ("restart", args.restart_repetitions),
            ):
                for index in range(repetitions):
                    before = set(DETECTIONS.glob("*.json"))
                    process = None
                    try:
                        if kind == "startup":
                            subprocess.run(
                                ["/usr/bin/systemctl", "stop", SERVICE], check=True, timeout=30
                            )
                        process = launch(args.user, wrapper, argv, root / f"{kind}-{index}")
                        if kind == "startup":
                            subprocess.run(
                                ["/usr/bin/systemctl", "start", SERVICE], check=True, timeout=30
                            )
                        else:
                            subprocess.run(
                                [
                                    "/usr/bin/systemctl",
                                    "kill",
                                    "--kill-who=main",
                                    "-s",
                                    "SIGKILL",
                                    SERVICE,
                                ],
                                check=True,
                                timeout=30,
                            )
                        wait_service()
                        receipt = receipt_after(before, timeout=15)
                        if (
                            receipt.get("detector", {}).get("profile") != "content.gguf-llama"
                            or receipt.get("result") != "TERMINATED"
                        ):
                            raise RuntimeError(f"content {kind} rediscovery failed")
                        result[kind] += 1
                    finally:
                        stop(process)
        result["result"] = "PASS"
        args.output.write_text(json.dumps(result, sort_keys=True) + "\n", encoding="utf-8")
        return 0
    except (OSError, RuntimeError, subprocess.SubprocessError, json.JSONDecodeError) as error:
        result["error"] = str(error)
        if not args.output.exists():
            args.output.write_text(json.dumps(result, sort_keys=True) + "\n", encoding="utf-8")
        raise SystemExit(f"content adversarial matrix failed: {error}") from error


if __name__ == "__main__":
    raise SystemExit(main())