#!/usr/bin/env python3 """Hostile raw-9P2000 client for the 9proc-demo server (stdlib only). Usage: adv_9proc_hostile.py --server zig-out/bin/9proc-demo # spawns it on a temp unix socket adv_9proc_hostile.py --socket PATH # attacks a running server Every attack is followed by a "server still healthy" probe on a fresh connection. Exit status is non-zero if any check fails, the server dies, or a probe hangs. """ import argparse import os import signal import socket import struct import subprocess import sys import tempfile import threading import time NOTAG = 0xFFFF NOFID = 0xFFFFFFFF # The Rerror strings of the conditions cloud9.fs (the file-server engine the # core is a backend of) decides itself; the tree's own refusals keep the # Plan 9 strings ("file does not exist", "bad command", ...). MSIZE_MIN = 217 # one full Rwalk must fit E_UNKNOWN_FID = "fid unknown or out of range" E_FID_IN_USE = "fid already in use" E_TOO_MANY_FIDS = "Too many open files in system" E_BAD_USE = "bad use of fid" # I/O on an unopened fid; a walk or clone from an open one E_ALREADY_OPEN = "file already open for I/O" # open or create on an open fid E_BAD_OFFSET = "bad offset in directory read" E_PERM = "permission denied" # the engine's own refusals: dirs for write, OEXEC, static trees E_WSTAT = "wstat prohibited" # engine-owned stat fields, or a length on a directory E_ILLEGAL_NAME = "illegal name" # names of creates and renames E_INVAL = "Invalid argument" # a reply that cannot fit msize; a dir read count below one record E_INTERRUPTED = "Interrupted system call" Tversion, Rversion, Tauth, Rauth, Tattach, Rattach, Rerror = 100, 101, 102, 103, 104, 105, 107 Tflush, Rflush, Twalk, Rwalk, Topen, Ropen, Tcreate, Rcreate = 108, 109, 110, 111, 112, 113, 114, 115 Tread, Rread, Twrite, Rwrite, Tclunk, Rclunk, Tremove, Rremove = 116, 117, 118, 119, 120, 121, 122, 123 Tstat, Rstat, Twstat, Rwstat = 124, 125, 126, 127 OREAD, OWRITE, ORDWR, OEXEC, OTRUNC, ORCLOSE = 0, 1, 2, 3, 0x10, 0x40 DMDIR, DMAPPEND, DMEXCL = 0x80000000, 0x40000000, 0x20000000 NAMES = {v: k for k, v in globals().items() if k[:1] in "TR" and isinstance(v, int) and 100 <= v <= 127} FAILS = [] PASSES = 0 def ok(name, cond, detail=""): global PASSES if cond: PASSES += 1 print(f"ok - {name}") else: FAILS.append(name) print(f"FAIL - {name} {detail}") def s16(b): return struct.pack(" connection closed c.raw(frame(Twalk, 1, struct.pack("0 ok("walk to file", c.walk_ok(0, 1, [b"build", b"target"]) == 2) ok("walk from file fails 'not a directory'", c.err(Twalk, struct.pack(" 0) ok("read dir at bad offset", c.err(Tread, struct.pack(" 6: return if c.walk_ok(0, 9, names) != len(names): ok("walk " + b"/".join(names).decode(), False) return rt, st = c.stat(9) if st["mode"] & DMDIR: c.open(9, OREAD) d = c.read_all(9, 512) c.clunk(9) while d: n, = struct.unpack_from(" 0: ok("server process still running", proc.poll() is None, proc.poll()) finally: if proc is not None: proc.send_signal(signal.SIGTERM) try: _, err = proc.communicate(timeout=5) except subprocess.TimeoutExpired: proc.kill() _, err = proc.communicate() lines = [ln for ln in err.decode("utf-8", "replace").splitlines() if "connection ended" not in ln and "read: " not in ln] if lines: print("# server stderr (filtered):") for ln in lines[:40]: print(" " + ln) if tmp: try: os.unlink(path) os.rmdir(tmp) except OSError: pass print(f"# {PASSES} passed, {len(FAILS)} failed") for f in FAILS: print("# FAIL " + f) sys.exit(1 if FAILS else 0) if __name__ == "__main__": main()