summaryrefslogtreecommitdiff
path: root/9proc/test/adv_9proc_hostile.py
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-20 01:47:28 -0300
committerGabriel Schneider <[email protected]>2026-09-20 01:47:29 -0300
commit66f2e492c348677ab3050f5e378b9eb4c04c98ce (patch)
treecf06b32309eace8eb573925e9cf14e3dfc27bd66 /9proc/test/adv_9proc_hostile.py
parent65209217b5b68f56bc0bd5bc6c4dce33911ded59 (diff)
downloadcloud9-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-x9proc/test/adv_9proc_hostile.py125
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)