diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-20 01:47:28 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-20 01:47:29 -0300 |
| commit | 66f2e492c348677ab3050f5e378b9eb4c04c98ce (patch) | |
| tree | cf06b32309eace8eb573925e9cf14e3dfc27bd66 /9proc/test/adv_9proc_hostile.py | |
| parent | 65209217b5b68f56bc0bd5bc6c4dce33911ded59 (diff) | |
| download | cloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.tar.gz cloud9-66f2e492c348677ab3050f5e378b9eb4c04c98ce.zip | |
9proc core becomes a backend of cloud9.fs; engine gains optional features
9proc's own fid table, walk loop and dir-read engine are replaced by
cloud9.fs.Server; the tree (static, vars, providers) is served through the
engine's Req/Reply contract with node ids that keep the old qid scheme.
Providers may answer later by returning error.Again (parked in the engine,
retried each step, Tflush -> EINTR); no new files are exposed.
Engine (backward compatible, all opt-in via Backend.features / Options):
create, remove, wstat, reference accounting for backends that count
handles, a salted fid index, name_capacity 0 (names from getattr),
Reply.ename for backend-chosen error text, Attr.path/version/atime.
Engine-level error strings and the 217-byte msize floor now apply to 9proc;
tests updated accordingly.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Diffstat (limited to '9proc/test/adv_9proc_hostile.py')
| -rwxr-xr-x | 9proc/test/adv_9proc_hostile.py | 125 |
1 files changed, 70 insertions, 55 deletions
diff --git a/9proc/test/adv_9proc_hostile.py b/9proc/test/adv_9proc_hostile.py index 934e757..dfa0d20 100755 --- a/9proc/test/adv_9proc_hostile.py +++ b/9proc/test/adv_9proc_hostile.py @@ -21,6 +21,22 @@ 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 @@ -303,37 +319,31 @@ def attack_framing(path): c.raw(frame(Tversion, 5, struct.pack("<I", 8192) + s16(b"9P2000"))) ok("Tversion with tag 5: closed", expect_dead(c)) c.close() - # Tversion msize below the resource floor - for ms in (0, 1, 23): + # Tversion msize below the engine's floor (one full Rwalk must fit): no Rversion + for ms in (0, 1, 23, 24, 64, MSIZE_MIN - 1): c = Nine(path) rt, _, _ = c.version(ms) ok(f"Tversion msize {ms}: no Rversion (closed or Rerror)", rt in (None, Rerror) or expect_dead(c)) c.close() - # tiny msize 24 is negotiable (Rversion fits); Tattach cannot fit, so use msize 64 for the rest - c = Nine(path) - rt, ms, ver = c.version(24) - ok("Tversion msize 24 accepted", rt == Rversion and ms == 24 and ver == b"9P2000", f"{rt} {ms} {ver}") - c.close() + # the floor itself is negotiable, and everything that fits is served at it c = Nine(path) - rt, ms, ver = c.version(64) - ok("Tversion msize 64 accepted", rt == Rversion and ms == 64, f"{rt} {ms} {ver}") + rt, ms, ver = c.version(MSIZE_MIN) + ok(f"Tversion msize {MSIZE_MIN} accepted", rt == Rversion and ms == MSIZE_MIN and ver == b"9P2000", f"{rt} {ms} {ver}") rt, _, _ = c.attach(uname=b"u") - ok("attach at msize 64", rt == Rattach, rt) - # Rstat of the root is ~70 bytes and cannot fit: must be an Rerror, not a dead socket + ok("attach at the floor", rt == Rattach, rt) rt, rb = c.stat(0) - ok("stat at msize 64 answers Rerror (reply does not fit), socket stays open", rt == Rerror, f"{rt} {rb!r}") - # Twalk with 5 names is 37 bytes (fits); Rwalk with 5 qids is 74 bytes (does not) - rt, _, rb = c.walk(0, 1, [b".", b".", b".", b".", b"."]) - ok("5-element walk at msize 64 answers Rerror, socket stays open", rt == Rerror, f"{rt} {rb!r}") - ok("newfid not bound by the failed walk", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("stat of the root at the floor fits", rt == Rstat, f"{rt} {rb!r}") + rt, _, rb = c.walk(0, 1, [b"."] * 16) + ok("16-element walk at the floor fits exactly", rt == Rwalk, f"{rt} {rb!r}") + c.clunk(1) rt, _, _ = c.walk(0, 1, [b"README"]) - ok("1-element walk at msize 64", rt == Rwalk, rt) + ok("1-element walk at the floor", rt == Rwalk, rt) rt, _, _ = c.open(1, OREAD) - ok("open at msize 64", rt == Ropen, rt) + ok("open at the floor", rt == Ropen, rt) rt, d = c.read(1, 0, 4096) - ok("read at msize 64 returns <= 40 bytes", rt == Rread and 0 < len(d) <= 40, f"{rt} {d!r}") + ok(f"read at the floor returns <= {MSIZE_MIN - 11} bytes", rt == Rread and 0 < len(d) <= MSIZE_MIN - 11, f"{rt} {d!r}") rt, _, _ = c.clunk(1) - ok("clunk at msize 64 still works", rt == Rclunk, rt) + ok("clunk at the floor still works", rt == Rclunk, rt) c.close() # huge msize is clamped to the server's max (1 MiB) c = Nine(path) @@ -413,11 +423,11 @@ def attack_walk(path): # partial walk: newfid not bound n = c.walk_ok(0, 1, [b"build", b"nope", b"x"]) ok("partial walk returns 1 qid", n == 1, n) - ok("partial walk does not bind newfid", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("partial walk does not bind newfid", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) # walk through a file n = c.walk_ok(0, 1, [b"build", b"target", b"x"]) ok("walk through a file is partial (2)", n == 2, n) - ok("newfid unbound after partial walk through file", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("newfid unbound after partial walk through file", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) # walk from a file with nwname>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("<IIH", 1, 2, 1) + s16(b"x")) == "not a directory") @@ -431,10 +441,10 @@ def attack_walk(path): c.clunk(1) # newfid in use c.walk_ok(0, 1, []) - ok("walk to a fid in use", c.err(Twalk, struct.pack("<IIH", 0, 1, 0)) == "fid in use") - ok("walk from unknown fid", c.err(Twalk, struct.pack("<IIH", 999, 2, 0)) == "unknown fid") + ok("walk to a fid in use", c.err(Twalk, struct.pack("<IIH", 0, 1, 0)) == E_FID_IN_USE) + ok("walk from unknown fid", c.err(Twalk, struct.pack("<IIH", 999, 2, 0)) == E_UNKNOWN_FID) # attach twice - ok("attach twice same fid", c.err(Tattach, struct.pack("<II", 0, NOFID) + s16(b"u") + s16(b"")) == "fid in use") + ok("attach twice same fid", c.err(Tattach, struct.pack("<II", 0, NOFID) + s16(b"u") + s16(b"")) == E_FID_IN_USE) ok("auth is refused", c.err(Tauth, struct.pack("<I", 5) + s16(b"u") + s16(b"")) is not None) c.close() ok("server healthy after walk attacks", healthy(path)) @@ -458,20 +468,20 @@ def attack_io(path): c.clunk(1) # read on unopened fid c.walk_ok(0, 2, [b"README"]) - ok("read on unopened fid", c.err(Tread, struct.pack("<IQI", 2, 0, 10)) == "file not open") - ok("write on unopened fid", c.err(Twrite, struct.pack("<IQI", 2, 0, 1) + b"x") == "file not open") - ok("read unknown fid", c.err(Tread, struct.pack("<IQI", 555, 0, 10)) == "unknown fid") + ok("read on unopened fid", c.err(Tread, struct.pack("<IQI", 2, 0, 10)) == E_BAD_USE) + ok("write on unopened fid", c.err(Twrite, struct.pack("<IQI", 2, 0, 1) + b"x") == E_BAD_USE) + ok("read unknown fid", c.err(Tread, struct.pack("<IQI", 555, 0, 10)) == E_UNKNOWN_FID) c.clunk(2) # directory: write/trunc/write on a dir c.walk_ok(0, 3, [b"build"]) - ok("open dir for write is 'is a directory'", c.err(Topen, struct.pack("<IB", 3, OWRITE)) == "is a directory") + ok("open dir for write is refused by the engine", c.err(Topen, struct.pack("<IB", 3, OWRITE)) == E_PERM) ok("open dir with OTRUNC is refused", c.err(Topen, struct.pack("<IB", 3, OREAD | OTRUNC)) is not None) rt, _, _ = c.open(3, OREAD) ok("write on open dir", c.err(Twrite, struct.pack("<IQI", 3, 0, 1) + b"x") is not None) rt, d = c.read(3, 0, 8192) ok("read dir", rt == Rread and len(d) > 0) - ok("read dir at bad offset", c.err(Tread, struct.pack("<IQI", 3, 3, 8192)) == "bad offset") - ok("read dir at 2^64-1 is bad offset", c.err(Tread, struct.pack("<IQI", 3, (1 << 64) - 1, 8192)) == "bad offset") + ok("read dir at bad offset", c.err(Tread, struct.pack("<IQI", 3, 3, 8192)) == E_BAD_OFFSET) + ok("read dir at 2^64-1 is bad offset", c.err(Tread, struct.pack("<IQI", 3, (1 << 64) - 1, 8192)) == E_BAD_OFFSET) rt, d2 = c.read(3, len(d), 8192) ok("read dir at end returns empty", rt == Rread and d2 == b"") # read of an open write-only file @@ -527,12 +537,12 @@ def attack_scratch(path): return c.walk_ok(0, fid, S + list(extra)) fresh(1) - ok("create name with '/'", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a/b") + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") - ok("create '.'", c.err(Tcreate, struct.pack("<I", 1) + s16(b".") + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") - ok("create '..'", c.err(Tcreate, struct.pack("<I", 1) + s16(b"..") + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") - ok("create empty name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"") + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") - ok("create NUL name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a\x00b") + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") - ok("create 256-byte name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a" * 256) + struct.pack("<IB", 0o644, OWRITE)) == "bad file name") + ok("create name with '/'", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a/b") + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) + ok("create '.'", c.err(Tcreate, struct.pack("<I", 1) + s16(b".") + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) + ok("create '..'", c.err(Tcreate, struct.pack("<I", 1) + s16(b"..") + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) + ok("create empty name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"") + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) + ok("create NUL name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a\x00b") + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) + ok("create 256-byte name", c.err(Tcreate, struct.pack("<I", 1) + s16(b"a" * 256) + struct.pack("<IB", 0o644, OWRITE)) == E_ILLEGAL_NAME) rt, _, _ = c.create(1, b"b" * 255, 0o644, OWRITE) ok("create 255-byte name ok", rt == Rcreate, rt) rt, st = c.stat(1) @@ -566,25 +576,28 @@ def attack_scratch(path): ok("wstat length 64MiB+1 is no space", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(length=(64 << 20) + 1))) == "no space left on device") ok("wstat length 2^64-2 is no space", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(length=(1 << 64) - 2))) == "no space left on device") # read on a write-only fid - ok("read on OWRITE fid", c.err(Tread, struct.pack("<IQI", 1, 0, 10)) == "file not open") + ok("read on OWRITE fid", c.err(Tread, struct.pack("<IQI", 1, 0, 10)) == E_BAD_USE) c.clunk(1) - # wstat with everything set to the current values: no-op + # wstat with name, mode, mtime and length equal to the current values: no-op fresh(1, [b"f"]) rt, st = c.stat(1) + same = mkstat(name=st["name"], mode=st["mode"], mtime=st["mtime"], length=st["length"]) + rt, _, _ = c.wstat(1, same) + ok("wstat with name/mode/mtime/length equal to current is ok", rt == Rwstat, rt) + rt, st2 = c.stat(1) + ok("stat/wstat round trip fidelity", st2 == st, f"{st}\n{st2}") + # the engine owns type, dev, qid, atime and the owner names: naming them at all is refused, even unchanged full = mkstat(name=st["name"], uid=st["uid"], gid=st["gid"], muid=st["muid"], typ=st["type"], dev=st["dev"], qtype=st["qid"][0], qvers=st["qid"][1], qpath=st["qid"][2], mode=st["mode"], atime=st["atime"], mtime=st["mtime"], length=st["length"]) - rt, _, _ = c.wstat(1, full) - ok("wstat with everything equal to current is ok", rt == Rwstat, rt) - rt, st2 = c.stat(1) - ok("stat/wstat round trip fidelity", st2 == st, f"{st}\n{st2}") + ok("wstat naming the engine-owned fields is 'wstat prohibited' even when equal", c.err(Twstat, struct.pack("<I", 1) + s16(full)) == E_WSTAT) # wstat changing immutable fields - ok("wstat changing qid.path", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(qpath=12345))) == "permission denied") - ok("wstat changing uid", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(uid=b"root"))) == "permission denied") - ok("wstat DMDIR on a file", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(mode=DMDIR | 0o755))) == "permission denied") - ok("wstat rename to '.'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"."))) == "bad file name") - ok("wstat rename to '..'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b".."))) == "bad file name") - ok("wstat rename to 'a/b'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"a/b"))) == "bad file name") + ok("wstat changing qid.path", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(qpath=12345))) == E_WSTAT) + ok("wstat changing uid", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(uid=b"root"))) == E_WSTAT) + ok("wstat DMDIR on a file", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(mode=DMDIR | 0o755))) == E_WSTAT) + ok("wstat rename to '.'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"."))) == E_ILLEGAL_NAME) + ok("wstat rename to '..'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b".."))) == E_ILLEGAL_NAME) + ok("wstat rename to 'a/b'", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"a/b"))) == E_ILLEGAL_NAME) ok("wstat rename to existing", c.err(Twstat, struct.pack("<I", 1) + s16(mkstat(name=b"b" * 255))) == "file already exists") rt, _, _ = c.wstat(1, mkstat(name=b"F")) rt2, st = c.stat(1) @@ -595,18 +608,18 @@ def attack_scratch(path): c.clunk(1) # remove of root / scratch root / static ok("remove of attach root", c.err(Tremove, struct.pack("<I", 0)) == "permission denied") - ok("fid clunked by failed remove", c.err(Tstat, struct.pack("<I", 0)) == "unknown fid") + ok("fid clunked by failed remove", c.err(Tstat, struct.pack("<I", 0)) == E_UNKNOWN_FID) c.attach() c.walk_ok(0, 1, [b"scratch"]) ok("remove of /scratch", c.err(Tremove, struct.pack("<I", 1)) == "permission denied") - ok("fid clunked by failed remove of /scratch", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("fid clunked by failed remove of /scratch", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) c.walk_ok(0, 1, [b"build", b"target"]) ok("remove of static file", c.err(Tremove, struct.pack("<I", 1)) == "permission denied") - ok("clunk unknown fid", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("clunk unknown fid", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) # remove non-empty dir; fid clunked fresh(1) ok("remove non-empty dir", c.err(Tremove, struct.pack("<I", 1)) == "directory not empty") - ok("fid clunked after failed remove", c.err(Tclunk, struct.pack("<I", 1)) == "unknown fid") + ok("fid clunked after failed remove", c.err(Tclunk, struct.pack("<I", 1)) == E_UNKNOWN_FID) # a fid on a removed file: everything but stat/clunk fails cleanly fresh(1, [b"F"]) fresh(2, [b"F"]) @@ -667,7 +680,9 @@ def attack_scratch(path): # directory read across offsets while the directory changes fresh(1) c.open(1, OREAD) - rt, d = c.read(1, 0, 120) # one or two records + rt, d = c.read(1, 0, 8192) + first = struct.unpack_from("<H", d)[0] + 2 + rt, d = c.read(1, 0, first) # exactly one record (the engine never splits one) fresh(2, [b"weird", b"inner"]) c.remove(2) fresh(2, [b"weird"]) @@ -700,7 +715,7 @@ def attack_scratch(path): rt, ms, _ = c.version(65536) ok("mid-session Tversion", rt == Rversion) ok("fids gone after Tversion", c.err(Tstat, struct.pack("<I", 1)) is not None) - ok("fids gone after Tversion (0)", c.err(Tstat, struct.pack("<I", 0)) == "unknown fid") + ok("fids gone after Tversion (0)", c.err(Tstat, struct.pack("<I", 0)) == E_UNKNOWN_FID) c.attach() # cleanup: remove everything under root c.walk_ok(0, 1, S) |
