From ba996acfcad1698adbf4a1834fe50e73b1c6cab9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Sat, 19 Sep 2026 23:28:22 -0300 Subject: Rename programs: 9player -> 9ns, introspect -> 9proc, app -> web (9web) Directories, binaries, build options (-D9ns, -D9proc), step names, module name (9proc), thread and fs names, env var NINEPLAYER_MOUNT -> NINE_MOUNT, docs and test scripts. Browser assets move to web/static. Co-Authored-By: Claude Fable 5.1 --- introspect/test/adv_core_hostile.py | 1018 ----------------------------------- 1 file changed, 1018 deletions(-) delete mode 100755 introspect/test/adv_core_hostile.py (limited to 'introspect/test/adv_core_hostile.py') diff --git a/introspect/test/adv_core_hostile.py b/introspect/test/adv_core_hostile.py deleted file mode 100755 index 56ef5a3..0000000 --- a/introspect/test/adv_core_hostile.py +++ /dev/null @@ -1,1018 +0,0 @@ -#!/usr/bin/env python3 -"""Hostile raw-9P2000 client aimed at the introspect *core* (stdlib only). - -Complements adv_introspect_hostile.py in this directory (framing, tags, scratch, floods) -with attacks on the freestanding engine's own paths: the /vars tree and its -comptime renderers, snapshot slots, the static tree, the fid table at its -configured maximum, directory-read offsets, msize 24, the ctl staging rule, -and the demo's debug providers driven as black boxes. - -Usage: - adv_core_hostile.py --server zig-out/bin/introspect # spawns it on a temp unix socket - adv_core_hostile.py --socket PATH # attacks a running server - -Exit status is non-zero if any check fails or the server dies. -""" -import argparse -import os -import signal -import struct -import subprocess -import sys -import tempfile -import threading -import time - -sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -import adv_introspect_hostile as base # noqa: E402 -from adv_introspect_hostile import ( # noqa: E402 - NOTAG, Tversion, Tflush, Rflush, Twalk, Rwalk, Topen, Ropen, Rcreate, - Tread, Rread, Twrite, Rwrite, Tclunk, Rclunk, Tremove, Rremove, Tstat, Rstat, Twstat, Rwstat, - Rerror, OREAD, OWRITE, ORDWR, OEXEC, OTRUNC, ORCLOSE, DMDIR, - Nine, frame, s16, mkstat, parse_stat, ok, healthy, expect_dead, -) - -MAX_FIDS = 32768 # demo/main.zig cfg.max_fids -SNAPSHOT_SLOTS = 8 # demo/main.zig cfg.snapshot_slots (per connection) -SCRATCH_BUDGET = 512 << 20 -SCRATCH_MAX_FILE = 64 << 20 - - -def records(d): - """Splits a directory read into (name, raw-record) pairs.""" - out = [] - while d: - n, = struct.unpack_from("/name.""" - c.walk_ok(0, 40, [b"threads"]) - c.open(40, OREAD) - d = c.read_all(40) - c.clunk(40) - for name, _ in records(d): - if c.path_read([b"threads", name, b"name"], fid=41) == b"worker": - return name - return None - - -# --------------------------------------------------------------------------- /vars - - -def attack_vars(path): - print("# /vars: deep walks, hostile names, renderer edge cases, hostile writes") - c = Nine(path) - c.session(1 << 20) - deep = [b"vars", b"state", b"f", b"last_job", b"f", b"id", b".", b"..", b"id", b".", b"..", b"id", b".", b"..", b"id", b"value"] - assert len(deep) == 16 - ok("16-element walk deep into /vars/state/f/... succeeds", c.walk_ok(0, 1, deep) == 16) - rt, _, _ = c.open(1, OREAD) - ok("deep walk lands on a readable value file", rt == Ropen, rt) - c.clunk(1) - up = [b"vars", b"state", b"f", b"inner"] if False else [b"vars", b"state", b"f", b"last_job"] + [b".."] * 12 - n = c.walk_ok(0, 1, up) - ok("12 x '..' from inside /vars climbs to the root and stays there", n == 16, n) - rt, st = c.stat(1) - ok("fid after the climb is the root directory", rt == Rstat and st["qid"][2] == 0xFF << 56, st) - c.clunk(1) - # names that are hex/decimal edge cases or otherwise hostile: never anything but Rerror/partial walk - for nm in (b"0", b"-1", b"0x", b"0x0", b"state\x00", b"State", b" state", b"state ", b"a" * 255, b"a" * 65535, b"\xff\xfe", b"..\x00", b"f", b"value"): - n = c.walk_ok(0, 1, [b"vars", nm]) - ok(f"walk /vars/{nm[:12]!r}{'...' if len(nm) > 12 else ''} is a partial walk (1)", n == 1, n) - ok(" and newfid stays unbound", c.err(Tclunk, struct.pack(" state -> /vars -> /", len(q) == 4 and q[3][2] == 0xFF << 56 and (q[1][0] & 0x80), q) - c.clunk(1) - c.clunk(2) - # every file under /vars/state reads; raw reads beyond @sizeOf are empty - size = int(c.path_read([b"vars", b"state", b"size"])) - ok("/vars/state/size is a number", size > 0, size) - raw = c.path_read([b"vars", b"state", b"raw"]) - ok("/vars/state/raw has exactly @sizeOf bytes", raw is not None and len(raw) == size, (len(raw) if raw else raw, size)) - c.walk_ok(0, 1, [b"vars", b"state", b"raw"]) - c.open(1, OREAD) - rt, d = c.read(1, size, 100) - ok("raw read at offset @sizeOf is empty", rt == Rread and d == b"", (rt, d)) - rt, d = c.read(1, size - 1, 100) - ok("raw read at @sizeOf-1 returns one byte", rt == Rread and len(d) == 1, (rt, d)) - rt, d = c.read(1, (1 << 64) - 1, 100) - ok("raw read at 2^64-1 is empty", rt == Rread and d == b"") - rt, d = c.read(1, 0, 0xFFFFFFFF) - ok("raw read with count 2^32-1 is clamped", rt == Rread and len(d) == size, (rt, len(d) if d else d)) - rt, st = c.stat(1) - ok("raw stat length is @sizeOf and mode 0444", rt == Rstat and st["length"] == size and st["mode"] == 0o444, st) - ok("raw is read-only", c.err(Twrite, struct.pack(" the refused one now opens; clunk via Tremove (denied) also frees the slot - c.clunk(100) - rt, _, _ = c.open(100 + opened, OREAD) - ok("after one clunk the refused open succeeds", rt == Ropen, rt) - ok("remove of an open dynamic file is denied", c.err(Tremove, struct.pack("/stack/x is 'not a directory'", c.walk_ok(0, 1, [b"threads", tid, b"stack"]) == 3 and c.err(Twalk, struct.pack("/stack is 'not a directory'", c.err(Twalk, struct.pack("/../..//name walks", c.walk_ok(0, 1, [b"threads", tid, b"..", b"..", b"threads", tid, b"name"]) == 7) - c.clunk(1) - c.walk_ok(0, 1, [b"threads", tid]) - ok("create under /threads/ is denied", c.err(base.Tcreate, struct.pack(" is denied", c.err(Twstat, struct.pack(" is denied", c.err(Tremove, struct.pack(" reads @sizeOf bytes", rt == Rread and len(d) == size, (rt, len(d) if d else d)) - rt, d = c.read(1, 0, 0xFFFFFFFF) - ok("/mem read with count 2^32-1 is clamped and answered", rt in (Rread, Rerror), rt) - c.clunk(1) - hexd = c.path_read([b"hex", hx]) - ok("/hex/ is a hexdump", hexd is not None and len(hexd) > 64, hexd[:40] if hexd else hexd) - # a value written through /mem must render, not trap: corrupt the phase enum and read /vars/state/value - phase_addr = int(c.path_read([b"vars", b"state", b"f", b"phase", b"addr"]), 16) - c.walk_ok(0, 1, [b"mem", b"%x" % phase_addr]) - c.open(1, OWRITE) - rt, _, _ = c.write(1, 0, b"\xee") - ok("write a corrupt enum byte through /mem", rt == Rwrite, rt) - c.clunk(1) - v = c.path_read([b"vars", b"state", b"value"]) - ok("/vars/state/value renders the corrupt enum as a number instead of trapping", v is not None and b"phase: 238" in v, v) - pv = c.path_read([b"vars", b"state", b"f", b"phase", b"value"]) - ok("/vars/state/f/phase/value renders 238", pv == b"238", pv) - c.walk_ok(0, 1, [b"vars", b"state", b"f", b"phase", b"value"]) - c.open(1, OWRITE) - rt, _, _ = c.write(1, 0, b"idle") - ok("the enum can be repaired through /vars", rt == Rwrite, rt) - c.clunk(1) - # /panic: ctl refuses reads and garbage; message/stack read - ok("/panic/message reads (empty, no panic)", c.path_read([b"panic", b"message"]) == b"") - ok("/panic/stack reads", c.path_read([b"panic", b"stack"]) is not None) - c.walk_ok(0, 1, [b"panic", b"ctl"]) - ok("/panic/ctl refuses OREAD", c.err(Topen, struct.pack("= 1, (len(held), err)) - for f in held: - c.clunk(f) - ok("after clunking, /addr opens again", c.path_read([b"addr", b"1000"]) is not None) - # Tversion with debug files open (hexdumps of the exposed state: mapped memory) - base_addr = int(addr, 16) if addr else 0 - opened = 0 - for i in range(4): - c.walk_ok(0, 300 + i, [b"hex", b"%x" % (base_addr + i)]) - opened += c.open(300 + i, OREAD)[0] == Ropen - ok("four /hex snapshots open", opened == 4, opened) - rt, _, _ = c.version(65536) - ok("Tversion with debug snapshots open", rt == base.Rversion) - c.attach() - ok("/hex still opens after the reset", c.path_read([b"hex", b"%x" % base_addr]) is not None) - ok("/hex of unmapped memory is an Rerror at open, not a crash", c.walk_ok(0, 1, [b"hex", b"3000"]) == 2 and c.open(1, OREAD)[0] == Rerror) - c.clunk(1) - c.close() - ok("server healthy after debug provider attacks", healthy(path)) - - -# --------------------------------------------------------------------------- fids at the maximum - - -def flood(c, ids, names): - """Pipelines one Twalk per id and counts the Rwalk replies; returns (ok_count, error_count, seconds).""" - got = [0, 0] - dead = [False] - - def reader(): - try: - for _ in ids: - rt, _, _ = c.recv_frame() - if rt == Rwalk: - got[0] += 1 - else: - got[1] += 1 - except (EOFError, OSError): - dead[0] = True - - t = threading.Thread(target=reader) - t.start() - t0 = time.time() - body = b"".join(frame(Twalk, i & 0xFFFE, struct.pack(" the handler's writer fails - rt2, d = c.read(1, 0, 100) - rt3, st5 = c.stat(1) - ok("an over-long result is an Rerror and the previous result survives", rt == Rerror and d == b"y" * 100 and st5["length"] == 60000, (rt, d[:10] if d else d, st5["length"])) - c.clunk(1) - c.close() - ok("server healthy after ctl attacks", healthy(path)) - - -# --------------------------------------------------------------------------- fid state machine on scratch - - -def attack_fid_states(path): - print("# fid state machine on /scratch") - c = Nine(path) - c.session() - tag = os.urandom(3).hex().encode() - root = b"fs-" + tag - c.walk_ok(0, 1, [b"scratch"]) - c.create(1, root, DMDIR | 0o755, OREAD) - c.clunk(1) - S = [b"scratch", root] - c.walk_ok(0, 1, S) - rt, _, _ = c.create(1, b"f", 0o644, ORDWR) - ok("create f", rt == Rcreate) - ok("open of an open fid is 'file already open'", c.err(Topen, struct.pack("> 20} MiB fill the {SCRATCH_BUDGET >> 20} MiB budget exactly", made == count, made) - print(f" filled the budget in {time.time() - t0:.1f}s") - c.walk_ok(0, 1, S) - c.create(1, b"one-more", 0o644, OWRITE) - ok("one more byte is 'no space left on device'", c.err(Twrite, struct.pack("