summaryrefslogtreecommitdiff
path: root/9proc/test
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
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')
-rwxr-xr-x9proc/test/adv_9proc_hostile.py125
-rwxr-xr-x9proc/test/adv_core_hostile.py158
2 files changed, 152 insertions, 131 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)
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: