diff options
Diffstat (limited to '9proc/test/adv_core_hostile.py')
| -rwxr-xr-x | 9proc/test/adv_core_hostile.py | 158 |
1 files changed, 82 insertions, 76 deletions
diff --git a/9proc/test/adv_core_hostile.py b/9proc/test/adv_core_hostile.py index 464876f..febda0d 100755 --- a/9proc/test/adv_core_hostile.py +++ b/9proc/test/adv_core_hostile.py @@ -4,7 +4,7 @@ Complements adv_9proc_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, +configured maximum, directory-read offsets, the msize floor, the ctl staging rule, and the demo's debug providers driven as black boxes. Usage: @@ -30,6 +30,8 @@ from adv_9proc_hostile import ( # noqa: E402 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, + MSIZE_MIN, E_UNKNOWN_FID, E_FID_IN_USE, E_TOO_MANY_FIDS, E_BAD_USE, E_ALREADY_OPEN, E_BAD_OFFSET, + E_PERM, E_WSTAT, E_INVAL, ) MAX_FIDS = 32768 # demo/main.zig cfg.max_fids @@ -88,7 +90,7 @@ def attack_vars(path): 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("<I", 1)) == "unknown fid") + ok(" and newfid stays unbound", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) for nm in (b"0", b"F", b"f\x00", b"ticks", b"value ", b"raw\x00"): n = c.walk_ok(0, 1, [b"vars", b"state", nm]) ok(f"walk /vars/state/{nm!r} is a partial walk (2)", n == 2, n) @@ -151,7 +153,7 @@ def attack_vars(path): for bad in (b"abc", b"99999999999999999999999", b"-1", b"1e3", b"", b" ", b"4\x002", b"1.5", b"0x", b"+", b"\xd9\xa1\xd9\xa2", b"12 34", b"0b102"): e = c.err(Twrite, struct.pack("<IQI", 1, 0, len(bad)) + bad) ok(f"write {bad!r} to u64 value is 'bad value'", e == "bad value", e) - ok("read on the write-only value fid is 'file not open'", c.err(Tread, struct.pack("<IQI", 1, 0, 10)) == "file not open") + ok("read on the write-only value fid is 'file not open'", c.err(Tread, struct.pack("<IQI", 1, 0, 10)) == E_BAD_USE) for good, want in ((b" 4200 \n", 4200), (b"0x10", 16), (b"+7", 7), (b"0b1010", 10), (b"0o17", 15), (b"1_000", 1000), (b"18446744073709551615", (1 << 64) - 1)): rt, _, rb = c.write(1, (1 << 64) - 1, good) # offset is ignored for values got = c.path_read([b"vars", b"state", b"f", b"ticks", b"value"]) @@ -200,9 +202,9 @@ def attack_vars(path): p = b"/".join(names).decode() ok(f"create in {p} is denied", c.err(base.Tcreate, struct.pack("<I", 1) + s16(b"x") + struct.pack("<IB", 0o644, OWRITE)) == "permission denied") ok(f"wstat of {p} is denied", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"y"))) == "permission denied") - ok(f"open {p} for write is 'is a directory'", c.err(Topen, struct.pack("<IB", 1, OWRITE)) == "is a directory") + ok(f"open {p} for write is refused by the engine", c.err(Topen, struct.pack("<IB", 1, OWRITE)) == E_PERM) ok(f"remove {p} is denied", c.err(Tremove, struct.pack("<I", 1)) == "permission denied") - ok(f" and the fid was clunked", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok(f" and the fid was clunked", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) for names in ([b"vars", b"state", b"value"], [b"vars", b"state", b"f", b"last_job", b"value"], [b"vars", b"state", b"type"]): c.walk_ok(0, 1, names) p = b"/".join(names).decode() @@ -249,7 +251,7 @@ def attack_snapshots(path): break ok(f"exactly {SNAPSHOT_SLOTS} dynamic files open per connection", opened == SNAPSHOT_SLOTS, opened) ok("the next open is 'too many open dynamic files'", err == "too many open dynamic files", err) - ok("the refused fid is still unopened (read is 'file not open')", c.err(Tread, struct.pack("<IQI", 100 + opened, 0, 10)) == "file not open") + ok("the refused fid is still unopened (read is 'file not open')", c.err(Tread, struct.pack("<IQI", 100 + opened, 0, 10)) == E_BAD_USE) # every held snapshot is still readable and consistent at offset 1 for i in range(opened): rt, d = c.read(100 + i, 0, 8192) @@ -278,11 +280,11 @@ def attack_snapshots(path): c.walk_ok(0, 60, dyn[0]) rt, _, _ = c.open(60, OREAD) ok("the failed-remove fid's slot was released", rt == Ropen, rt) - # a clone of an open dynamic fid takes no slot and is unopened - rt, _, _ = c.walk(60, 61, []) - ok("clone of an open dynamic fid is allowed", rt == Rwalk, rt) - ok("the clone is not open", c.err(Tread, struct.pack("<IQI", 61, 0, 10)) == "file not open") - ok("the clone cannot open (slots exhausted again)", c.err(Topen, struct.pack("<IB", 61, OREAD)) == "too many open dynamic files") + # an open fid cannot be cloned; a fresh walk to the same file takes no slot and is unopened + ok("clone of an open dynamic fid is refused", c.err(Twalk, struct.pack("<IIH", 60, 61, 0)) == E_BAD_USE) + c.walk_ok(0, 61, dyn[0]) + ok("the fresh fid is not open", c.err(Tread, struct.pack("<IQI", 61, 0, 10)) == E_BAD_USE) + ok("the fresh fid cannot open (slots exhausted again)", c.err(Topen, struct.pack("<IB", 61, OREAD)) == "too many open dynamic files") # Tversion releases everything: 8 opens succeed again rt, ms, _ = c.version(65536) ok("mid-session Tversion", rt == base.Rversion) @@ -310,12 +312,13 @@ def attack_static(path): ok(f"create in {p} is denied", c.err(base.Tcreate, struct.pack("<I", 1) + s16(b"x") + struct.pack("<IB", 0o644, OWRITE)) == "permission denied") ok(f"mkdir in {p} is denied", c.err(base.Tcreate, struct.pack("<I", 1) + s16(b"d") + struct.pack("<IB", DMDIR | 0o755, OREAD)) == "permission denied") ok(f"wstat of {p} is denied", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(mtime=1))) == "permission denied") - ok(f"wstat of {p} with all don't-care is denied too", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat())) == "permission denied") - ok(f"open {p} ORDWR is 'is a directory'", c.err(Topen, struct.pack("<IB", 1, ORDWR)) == "is a directory") - ok(f"open {p} OEXEC works like OREAD", c.open(1, OEXEC)[0] == Ropen) + ok(f"wstat of {p} with all don't-care is a no-op the engine answers itself", c.wstat(1, mkstat())[0] == Rwstat) + ok(f"open {p} ORDWR is refused by the engine", c.err(Topen, struct.pack("<IB", 1, ORDWR)) == E_PERM) + ok(f"open {p} OEXEC is refused by the engine", c.err(Topen, struct.pack("<IB", 1, OEXEC)) == E_PERM) + ok(f"open {p} OREAD", c.open(1, OREAD)[0] == Ropen) ok(f"write to open {p} is 'is a directory'", c.err(Twrite, struct.pack("<IQI", 1, 0, 1) + b"x") is not None) ok(f"remove {p} is denied", c.err(Tremove, struct.pack("<I", 1)) == "permission denied") - ok(f" and clunked", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok(f" and clunked", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) files = [[b"README"], [b"build", b"time"], [b"comptime", b"decls"], [b"comptime", b"types", b"Qid", b"fields"], [b"runtime", b"pid"], [b"runtime", b"fn", b"fib30"], [b"runtime", b"ctl"]] for names in files: p = "/" + b"/".join(names).decode() @@ -324,7 +327,7 @@ def attack_static(path): ok(f"'..' from {p} is 'not a directory'", c.err(Twalk, struct.pack("<IIH", 1, 2, 1) + s16(b"..")) == "not a directory") ok(f"wstat of {p} is denied", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(length=0))) == "permission denied") ok(f"remove {p} is denied", c.err(Tremove, struct.pack("<I", 1)) == "permission denied") - ok(f" and clunked", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok(f" and clunked", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) if names[-1] != b"ctl": c.walk_ok(0, 1, names) ok(f"open {p} OWRITE is denied", c.err(Topen, struct.pack("<IB", 1, OWRITE)) == "permission denied") @@ -563,11 +566,11 @@ def attack_fid_table(path): good, bad, dt, dead = flood(c, ids, [b"scratch"]) ok(f"{n} walks with adversarial fid numbers all succeed", good == n and bad == 0 and not dead, (good, bad, dead)) print(f" {n} clones (provider handles) in {dt:.2f}s") - ok("the next fid is 'too many fids'", c.err(Twalk, struct.pack("<IIH", 0, 7, 0)) == "too many fids") - ok("attach at the limit is 'too many fids'", c.err(base.Tattach, struct.pack("<II", 7, base.NOFID) + s16(b"u") + s16(b"")) == "too many fids") - ok("an existing id is 'fid in use'", c.err(Twalk, struct.pack("<IIH", 0, ids[12345], 0)) == "fid in use") + ok("the next fid is 'too many fids'", c.err(Twalk, struct.pack("<IIH", 0, 7, 0)) == E_TOO_MANY_FIDS) + ok("attach at the limit is 'too many fids'", c.err(base.Tattach, struct.pack("<II", 7, base.NOFID) + s16(b"u") + s16(b"")) == E_TOO_MANY_FIDS) + ok("an existing id is 'fid in use'", c.err(Twalk, struct.pack("<IIH", 0, ids[12345], 0)) == E_FID_IN_USE) ok("a self-walk at the limit works", c.walk_ok(ids[5], ids[5], [b".."]) == 1) - ok("an unknown fid at the limit is 'unknown fid'", c.err(Tstat, struct.pack("<I", 7)) == "unknown fid") + ok("an unknown fid at the limit is 'unknown fid'", c.err(Tstat, struct.pack("<I", 7)) == E_UNKNOWN_FID) # clunk all, pipelined, in a hostile order (every third first, then the rest reversed) order = ids[::3] + ids[1::3][::-1] + ids[2::3][::-1] got = [0] @@ -588,12 +591,12 @@ def attack_fid_table(path): t.join(120) ok(f"{n} clunks all answered", got[0] == n and not dead[0] and not t.is_alive(), (got[0], dead[0])) print(f" {n} clunks in {time.time() - t0:.2f}s") - ok("clunk of a clunked fid is 'unknown fid'", c.err(Tclunk, struct.pack("<I", ids[100])) == "unknown fid") + ok("clunk of a clunked fid is 'unknown fid'", c.err(Tclunk, struct.pack("<I", ids[100])) == E_UNKNOWN_FID) # reuse: the whole table is available again with dense ids good, bad, dt, dead = flood(c, list(range(1, n + 1)), []) ok(f"{n} clones with dense ids after the churn all succeed", good == n and bad == 0 and not dead, (good, bad, dead)) print(f" {n} clones (reuse) in {dt:.2f}s") - ok("still 'too many fids' at the limit", c.err(Twalk, struct.pack("<IIH", 0, n + 1, 0)) == "too many fids") + ok("still 'too many fids' at the limit", c.err(Twalk, struct.pack("<IIH", 0, n + 1, 0)) == E_TOO_MANY_FIDS) rt, _, _ = c.version(65536) ok("Tversion after the fid churn", rt == base.Rversion) c.attach() @@ -635,63 +638,66 @@ def attack_flush(path): ok("server healthy after the flush storm", healthy(path)) -# --------------------------------------------------------------------------- msize 24 +# --------------------------------------------------------------------------- msize floor -def attack_msize24(path): - print("# msize 24: everything that fits is served, everything else is an Rerror that fits") +def attack_msize_floor(path): + print(f"# msize floor: below {MSIZE_MIN} the connection dies; at {MSIZE_MIN} everything that fits is served") + for ms in (24, 64, MSIZE_MIN - 1): + c = Nine(path) + rt, _, _ = c.version(ms) + ok(f"Tversion msize {ms}: closed", rt is None or expect_dead(c), rt) + c.close() c = Nine(path) - rt, ms, ver = c.version(24) - ok("Tversion 24", rt == base.Rversion and ms == 24, (rt, ms)) - rt, _, _ = c.attach(uname=b"u") # 20 bytes; Rattach is 20 - ok("Tattach at msize 24", rt == base.Rattach, rt) - e = c.err(Tstat, struct.pack("<I", 0)) - ok("Tstat: Rerror truncated to 15 bytes ('reply too large')", e == "reply too large", e) - rt, _, rb = c.walk(0, 1, [b"build"]) # Twalk 24, Rwalk 22 - ok("Twalk of one 5-byte name", rt == Rwalk, rt) - e = c.err(Twalk, struct.pack("<IIH", 0, 2, 2) + s16(b".") + s16(b".")) # 23 bytes; Rwalk would be 35 - ok("Twalk of two names cannot be answered: 'reply too large'", e == "reply too large", e) - ok("newfid unbound after the refused walk", c.err(Tclunk, struct.pack("<I", 2)) == "unknown fid") - rt, _, _ = c.open(1, OREAD) # Ropen 24 - ok("Topen at msize 24", rt == Ropen, rt) - rt, d = c.read(1, 0, 4096) # count clamped to msize - iohdrsz = 0 - ok("Tread of a directory at msize 24 answers an empty Rread (no split record)", rt == Rread and d == b"", (rt, d)) - e = c.err(Tread, struct.pack("<IQI", 1, 1, 4096)) - ok("dir read at offset 1 is 'bad offset'", e == "bad offset", e) - rt, _, _ = c.clunk(1) - ok("Tclunk at msize 24", rt == Rclunk, rt) - rt, _, _ = c.walk(0, 1, [b"vars"]) # Twalk 23 - rt, _, _ = c.walk(1, 1, [b"state"]) # 23 - rt, _, _ = c.walk(1, 1, [b"value"]) # 23 - ok("walk to /vars/state/value in 3 self-walks", rt == Rwalk, rt) + rt, ms, ver = c.version(MSIZE_MIN) + ok(f"Tversion {MSIZE_MIN}", rt == base.Rversion and ms == MSIZE_MIN, (rt, ms)) + rt, _, _ = c.attach(uname=b"u") + ok("Tattach at the floor", rt == base.Rattach, rt) + rt, st = c.stat(0) + ok("Tstat of the root fits", rt == Rstat and st["name"] == b"/", (rt, st)) + rt, _, rb = c.walk(0, 1, [b"."] * 16) + ok("a full 16-element Rwalk is exactly the floor", rt == Rwalk and struct.unpack_from("<H", rb)[0] == 16, rt) + c.clunk(1) + # an Rstat that cannot fit is an Rerror, not a dead connection: a long name in /scratch + long_name = b"m" * 180 # Tcreate (18 + 180 bytes) fits; the Rstat (61 + 180) does not + c.walk_ok(0, 1, [b"scratch"]) + rt, _, _ = c.create(1, long_name, 0o644, OREAD) + ok("create a long name at the floor", rt == Rcreate, rt) + e = c.err(Tstat, struct.pack("<I", 1)) + ok("Tstat that does not fit is 'Invalid argument'", e == E_INVAL, e) + ok("remove it", c.remove(1)[0] == Rremove) + rt, _, _ = c.walk(0, 1, [b"build"]) rt, _, _ = c.open(1, OREAD) - ok("open a dynamic file at msize 24", rt == Ropen, rt) - rt, d = c.read(1, 0, 4096) - ok("read of a dynamic file at msize 24 is an empty Rread", rt == Rread and d == b"", (rt, d)) + ok("Topen at the floor", rt == Ropen, rt) + rt, d = c.read(1, 0, 4096) # count clamped to msize - 11 + ok("a directory read at the floor yields whole records", rt == Rread and 0 < len(d) <= MSIZE_MIN - 11 and records(d), (rt, len(d) if d else d)) + e = c.err(Tread, struct.pack("<IQI", 1, 1, 4096)) + ok("dir read at offset 1 is a bad offset", e == E_BAD_OFFSET, e) rt, _, _ = c.clunk(1) - for nm in (b"vars", b"state", b"f", b"ticks", b"value"): # each Twalk <= 24 bytes + ok("Tclunk at the floor", rt == Rclunk, rt) + for nm in (b"vars", b"state", b"f", b"ticks", b"value"): rt, _, _ = c.walk(0 if nm == b"vars" else 1, 1, [nm]) ok("walk to /vars/state/f/ticks/value in 5 self-walks", rt == Rwalk, rt) rt, _, _ = c.open(1, OWRITE) - ok("open a writable value at msize 24", rt == Ropen, rt) - rt, _, _ = c.write(1, 0, b"5") # Twrite 24, Rwrite 11 - ok("Twrite of one byte at msize 24", rt == Rwrite, rt) + ok("open a writable value at the floor", rt == Ropen, rt) + rt, _, _ = c.write(1, 0, b"5") + ok("Twrite of one byte at the floor", rt == Rwrite, rt) e = c.err(Twrite, struct.pack("<IQI", 1, 0, 0) + b"") - ok("empty write to a value at msize 24 is 'bad value'", e == "bad value", e) + ok("empty write to a value at the floor is 'bad value'", e == "bad value", e) rt, _, _ = c.clunk(1) - ok("clunk at msize 24", rt == Rclunk, rt) + ok("clunk at the floor", rt == Rclunk, rt) rt, ms, _ = c.version(65536) ok("renegotiate a big msize on the same connection", rt == base.Rversion and ms == 65536, (rt, ms)) c.attach() ok("normal service resumes", c.path_read([b"build", b"zig_version"]) not in (None, b"")) c.close() - # frames larger than 24 after negotiating 24 kill the connection + # frames larger than the negotiated msize kill the connection c = Nine(path) - c.version(24) - c.raw(frame(base.Tattach, 1, struct.pack("<II", 0, base.NOFID) + s16(b"longer-name") + s16(b""))) - ok("a 30-byte Tattach at msize 24: connection closed", expect_dead(c)) + c.version(MSIZE_MIN) + c.raw(frame(base.Tattach, 1, struct.pack("<II", 0, base.NOFID) + s16(b"u" * 210) + s16(b""))) + ok("an over-long Tattach at the floor: connection closed", expect_dead(c)) c.close() - ok("server healthy after msize-24 attacks", healthy(path)) + ok("server healthy after msize-floor attacks", healthy(path)) # --------------------------------------------------------------------------- directory offsets @@ -727,15 +733,15 @@ def attack_dir_offsets(path): rt, d1 = c.read(1, r0, r1) ok(f"{p}: read at the record boundary returns the next record", rt == Rread and d1 == recs[1][1], (rt, len(d1) if d1 else d1)) e = c.err(Tread, struct.pack("<IQI", 1, r0 + r1 + 1, 65000)) - ok(f"{p}: offset boundary+1 is 'bad offset'", e == "bad offset", e) + ok(f"{p}: offset boundary+1 is 'bad offset'", e == E_BAD_OFFSET, e) e = c.err(Tread, struct.pack("<IQI", 1, r0 + r1 - 1, 65000)) - ok(f"{p}: offset boundary-1 is 'bad offset'", e == "bad offset", e) + ok(f"{p}: offset boundary-1 is 'bad offset'", e == E_BAD_OFFSET, e) e = c.err(Tread, struct.pack("<IQI", 1, r0, 65000)) - ok(f"{p}: re-reading an earlier boundary is 'bad offset'", e == "bad offset", e) + ok(f"{p}: re-reading an earlier boundary is 'bad offset'", e == E_BAD_OFFSET, e) rt, rest = c.read(1, r0 + r1, 65000) ok(f"{p}: after a bad offset the good boundary still continues", rt == Rread and rest == d[r0 + r1:], rt) - rt, dd = c.read(1, 0, r0 - 1) - ok(f"{p}: count one short of a record returns nothing (no split)", rt == Rread and dd == b"", (rt, dd)) + e = c.err(Tread, struct.pack("<IQI", 1, 0, r0 - 1)) + ok(f"{p}: count one short of a record is refused (no split)", e == E_INVAL, e) rt, dd = c.read(1, 0, 65000) ok(f"{p}: offset 0 restarts and yields the same bytes", rt == Rread and dd == d) rt, dd = c.read(1, len(d), 65000) @@ -817,17 +823,17 @@ def attack_fid_states(path): 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("<IB", 1, OREAD)) == "file already open") - ok("walk with names from an open fid is 'file already open'", c.err(Twalk, struct.pack("<IIH", 1, 2, 1) + s16(b".")) == "file already open") - ok("create on an open fid is 'file already open'", c.err(base.Tcreate, struct.pack("<I", 1) + s16(b"g") + struct.pack("<IB", 0o644, OWRITE)) == "file already open") - rt, _, _ = c.walk(1, 2, []) - ok("clone of an open fid is allowed", rt == Rwalk) - ok("the clone is not open", c.err(Tread, struct.pack("<IQI", 2, 0, 10)) == "file not open") + ok("open of an open fid is 'file already open for I/O'", c.err(Topen, struct.pack("<IB", 1, OREAD)) == E_ALREADY_OPEN) + ok("walk with names from an open fid is 'bad use of fid'", c.err(Twalk, struct.pack("<IIH", 1, 2, 1) + s16(b".")) == E_BAD_USE) + ok("create on an open fid is 'file already open for I/O'", c.err(base.Tcreate, struct.pack("<I", 1) + s16(b"g") + struct.pack("<IB", 0o644, OWRITE)) == E_ALREADY_OPEN) + ok("clone of an open fid is refused", c.err(Twalk, struct.pack("<IIH", 1, 2, 0)) == E_BAD_USE) + ok("a fresh walk reaches the open file", c.walk_ok(0, 2, S + [b"f"]) == 3) + ok("the fresh fid is not open", c.err(Tread, struct.pack("<IQI", 2, 0, 10)) == E_BAD_USE) rt, _, _ = c.open(2, OREAD) - ok("the clone opens independently", rt == Ropen) + ok("the fresh fid opens independently", rt == Ropen) c.write(1, 0, b"data") rt, d = c.read(2, 0, 10) - ok("the clone sees the write", rt == Rread and d == b"data", d) + ok("the second fid sees the write", rt == Rread and d == b"data", d) rt, _, _ = c.wstat(2, mkstat(name=b"renamed")) ok("wstat through an open fid works", rt == Rwstat) rt, _, _ = c.remove(1) @@ -982,7 +988,7 @@ def main(): attack_dir_offsets(path) attack_ctl(path) attack_fid_states(path) - attack_msize24(path) + attack_msize_floor(path) attack_flush(path) attack_fid_table(path) if not args.fast: |
