summaryrefslogtreecommitdiff
path: root/9player/test/adv_bridge_hostile.py
diff options
context:
space:
mode:
Diffstat (limited to '9player/test/adv_bridge_hostile.py')
-rwxr-xr-x9player/test/adv_bridge_hostile.py487
1 files changed, 487 insertions, 0 deletions
diff --git a/9player/test/adv_bridge_hostile.py b/9player/test/adv_bridge_hostile.py
new file mode 100755
index 0000000..b842a1a
--- /dev/null
+++ b/9player/test/adv_bridge_hostile.py
@@ -0,0 +1,487 @@
+#!/usr/bin/env python3
+"""A scriptable, hostile 9P2000 server on a Unix socket (stdlib only).
+
+Usage: adv_bridge_hostile.py SOCKET MODE
+
+Serves a tiny in-memory tree:
+ /f "hello world\\n"
+ /d/g "in d\\n"
+ /fids reading it returns the number of fids currently bound
+ /big 1 MiB of pseudo-random bytes
+plus create/write/remove/wstat so the scratch battery can run in `ok` mode.
+
+MODE selects one misbehaviour (see MODES below). Everything not covered by
+the mode behaves normally, so 9player gets through version/attach/stat(root).
+"""
+import os
+import random
+import socket
+import struct
+import sys
+import time
+
+NOTAG = 0xFFFF
+NOFID = 0xFFFFFFFF
+QTDIR = 0x80
+DMDIR = 0x80000000
+
+Tversion, Rversion = 100, 101
+Tauth, Rauth = 102, 103
+Tattach, Rattach = 104, 105
+Rerror = 107
+Tflush, Rflush = 108, 109
+Twalk, Rwalk = 110, 111
+Topen, Ropen = 112, 113
+Tcreate, Rcreate = 114, 115
+Tread, Rread = 116, 117
+Twrite, Rwrite = 118, 119
+Tclunk, Rclunk = 120, 121
+Tremove, Rremove = 122, 123
+Tstat, Rstat = 124, 125
+Twstat, Rwstat = 126, 127
+
+MODES = """
+ok behave (qid paths are recycled LIFO after remove, like many servers)
+trunc Rread on /f: send half the frame, then close
+short_frame Rread on /f: frame whose size field is 3
+huge_frame Rread on /f: frame whose size field is msize+1
+wrong_tag Rread on /f: reply carries tag+1
+wrong_type Tstat on /f: answer with an Rwalk
+rread_big Rread on /f: count = requested+1
+rwalk_many Twalk to f: nwqid = nwname+1
+rwalk_zero Twalk to nope: Rwalk nwqid=0 instead of Rerror
+rstat_garbage Tstat on /f: random bytes as the stat
+rstat_overlong Tstat on /f: inner stat size disagrees with outer
+dir_split Tread on /: a stat record split across two Rreads
+dir_forever Tread on /: ignore offset, always return the same records
+qid_collide every file and dir shares qid.path 7 (root keeps its own)
+qid_zero every qid.path is 0, including the root
+name_slash / has an entry "a/b"
+name_empty / has an entry ""
+name_huge / has an entry with a 60000-byte name
+name_dots / lists "." and ".." too
+rerror_big Twalk to nope: Rerror with 65535 bytes of text
+extra_reply Rread on /f: an unsolicited Rclunk (tag 9) precedes the real reply
+never Tread on /f: never reply (hang)
+close_mid Tread on /f: close the socket without replying
+renegotiate Tread on /f: an unsolicited Rversion precedes the real reply
+length_max Tstat on /f: length = 2**64-1
+iounit_one Ropen: iounit = 1
+rwrite_big Rwrite: count = requested+1
+msize_tiny Rversion msize = 64
+version_unknown Rversion "unknown"
+rename_fail Twstat with a new name always fails "file already exists"
+slow every reply delayed 20 ms (for interrupt tests)
+"""
+
+
+def s8(x): return struct.pack('<B', x)
+def s16(x): return struct.pack('<H', x)
+def s32(x): return struct.pack('<I', x)
+def s64(x): return struct.pack('<Q', x)
+def sstr(b):
+ if isinstance(b, str):
+ b = b.encode()
+ return s16(len(b)) + b
+
+
+class Node:
+ def __init__(self, name, isdir, path, content=b''):
+ self.name = name
+ self.isdir = isdir
+ self.path = path
+ self.content = bytearray(content)
+ self.children = {}
+ self.mode = 0o755 if isdir else 0o644
+ self.mtime = int(time.time())
+ self.removed = False
+
+ def qid(self, srv):
+ path = self.path
+ if srv.mode == 'qid_zero':
+ path = 0
+ elif srv.mode == 'qid_collide' and self is not srv.root:
+ path = 7
+ return s8(QTDIR if self.isdir else 0) + s32(1) + s64(path)
+
+ def stat_bytes(self, srv, name=None, length=None):
+ if name is None:
+ name = self.name
+ if length is None:
+ length = 0 if self.isdir else len(self.content)
+ body = (s16(0) + s32(0) + self.qid(srv) + s32((DMDIR if self.isdir else 0) | self.mode)
+ + s32(self.mtime) + s32(self.mtime) + s64(length)
+ + sstr(name) + sstr('u') + sstr('g') + sstr('u'))
+ return s16(len(body)) + body
+
+
+class Server:
+ def __init__(self, mode):
+ self.mode = mode
+ self.next_path = 100
+ self.free_paths = []
+ self.root = Node('', True, 1)
+ f = self.mk(self.root, 'f', False, b'hello world\n')
+ d = self.mk(self.root, 'd', True)
+ self.mk(d, 'g', False, b'in d\n')
+ self.mk(self.root, 'fids', False)
+ rnd = random.Random(4)
+ self.mk(self.root, 'big', False, bytes(rnd.getrandbits(8) for _ in range(1 << 20)))
+ self.fids = {}
+ self.msize = 8192
+ self.ops = 0
+
+ def alloc_path(self):
+ if self.free_paths:
+ return self.free_paths.pop()
+ self.next_path += 1
+ return self.next_path
+
+ def mk(self, parent, name, isdir, content=b''):
+ n = Node(name, isdir, self.alloc_path(), content)
+ parent.children[name] = n
+ return n
+
+ # -- framing ---------------------------------------------------------
+ def frame(self, typ, tag, body):
+ return s32(7 + len(body)) + s8(typ) + s16(tag) + body
+
+ def err(self, tag, text):
+ return self.frame(Rerror, tag, sstr(text))
+
+ def serve(self, conn):
+ buf = b''
+ while True:
+ if len(buf) >= 4:
+ n = struct.unpack('<I', buf[:4])[0]
+ if len(buf) >= n:
+ msg, buf = buf[:n], buf[n:]
+ out = self.handle(msg)
+ if out is None:
+ return # hang up / hang
+ if self.mode == 'slow':
+ time.sleep(0.02)
+ conn.sendall(out)
+ continue
+ data = conn.recv(65536)
+ if not data:
+ return
+ buf += data
+
+ def handle(self, msg):
+ typ = msg[4]
+ tag = struct.unpack('<H', msg[5:7])[0]
+ b = msg[7:]
+ self.ops += 1
+ r = Reader(b)
+ if typ == Tversion:
+ msize = r.u32()
+ ver = r.str()
+ self.msize = min(msize, 1 << 20)
+ self.fids = {}
+ if self.mode == 'msize_tiny':
+ self.msize = 64
+ if self.mode == 'version_unknown':
+ return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('unknown'))
+ return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('9P2000'))
+ if typ == Tauth:
+ return self.err(tag, 'authentication not required')
+ if typ == Tattach:
+ fid = r.u32()
+ r.u32()
+ r.str()
+ r.str()
+ if fid in self.fids:
+ return self.err(tag, 'fid in use')
+ self.fids[fid] = [self.root, False]
+ return self.frame(Rattach, tag, self.root.qid(self))
+ if typ == Tflush:
+ return self.frame(Rflush, tag, b'')
+ if typ == Twalk:
+ fid, newfid, nw = r.u32(), r.u32(), r.u16()
+ names = [r.str() for _ in range(nw)]
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ if newfid != fid and newfid in self.fids:
+ return self.err(tag, 'fid in use')
+ node = self.fids[fid][0]
+ qids = b''
+ n = 0
+ for name in names:
+ if not node.isdir:
+ break
+ if name == '..':
+ nxt = node # root's parent is itself; good enough
+ elif name in node.children:
+ nxt = node.children[name]
+ else:
+ break
+ node = nxt
+ qids += node.qid(self)
+ n += 1
+ if n < nw and n == 0:
+ if self.mode == 'rwalk_zero' and names == ['nope']:
+ return self.frame(Rwalk, tag, s16(0))
+ if self.mode == 'rerror_big' and names == ['nope']:
+ return self.err(tag, 'x' * 65535)
+ return self.err(tag, 'file does not exist')
+ if n == nw:
+ self.fids[newfid] = [node, False]
+ if self.mode == 'rwalk_many' and names == ['f']:
+ return self.frame(Rwalk, tag, s16(n + 1) + qids + node.qid(self))
+ return self.frame(Rwalk, tag, s16(n) + qids)
+ if typ == Topen:
+ fid, mode = r.u32(), r.u8()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node = self.fids[fid][0]
+ if node.isdir and (mode & 3) != 0:
+ return self.err(tag, 'is a directory')
+ if mode & 0x10 and not node.isdir:
+ node.content = bytearray()
+ self.fids[fid][1] = True
+ iounit = 1 if self.mode == 'iounit_one' else 0
+ return self.frame(Ropen, tag, node.qid(self) + s32(iounit))
+ if typ == Tcreate:
+ fid = r.u32()
+ name = r.str()
+ perm = r.u32()
+ mode = r.u8()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ parent = self.fids[fid][0]
+ if not parent.isdir:
+ return self.err(tag, 'not a directory')
+ if name in parent.children:
+ return self.err(tag, 'file already exists')
+ node = self.mk(parent, name, bool(perm & DMDIR))
+ node.mode = perm & 0o777
+ self.fids[fid] = [node, True]
+ return self.frame(Rcreate, tag, node.qid(self) + s32(0))
+ if typ == Tread:
+ fid, off, count = r.u32(), r.u64(), r.u32()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node, opened = self.fids[fid]
+ if not opened:
+ return self.err(tag, 'fid not open')
+ if node.isdir:
+ return self.readdir(tag, node, off, count)
+ if node.name == 'fids':
+ data = ('%d\n' % len(self.fids)).encode()
+ data = data[off:off + count]
+ return self.frame(Rread, tag, s32(len(data)) + data)
+ data = bytes(node.content[off:off + count])
+ if node is self.root.children.get('f'):
+ m = self.mode
+ if m == 'trunc':
+ fr = self.frame(Rread, tag, s32(len(data)) + data)
+ self.conn.sendall(fr[:len(fr) // 2])
+ return None
+ if m == 'short_frame':
+ return s32(3) + s8(Rread) + s16(tag)
+ if m == 'huge_frame':
+ return s32(self.msize + 1) + s8(Rread) + s16(tag) + s32(len(data)) + data
+ if m == 'wrong_tag':
+ return self.frame(Rread, (tag + 1) & 0xFFFF, s32(len(data)) + data)
+ if m == 'rread_big':
+ data = b'x' * (count + 1)
+ return self.frame(Rread, tag, s32(len(data)) + data)
+ if m == 'extra_reply':
+ return self.frame(Rclunk, 9, b'') + self.frame(Rread, tag, s32(len(data)) + data)
+ if m == 'never':
+ time.sleep(3600)
+ return None
+ if m == 'close_mid':
+ return None
+ if m == 'renegotiate':
+ return self.frame(Rversion, NOTAG, s32(self.msize) + sstr('9P2000')) + self.frame(Rread, tag, s32(len(data)) + data)
+ return self.frame(Rread, tag, s32(len(data)) + data)
+ if typ == Twrite:
+ fid, off = r.u32(), r.u64()
+ data = r.data()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node, opened = self.fids[fid]
+ if not opened or node.isdir:
+ return self.err(tag, 'fid not open for write')
+ if node.name == 'fids':
+ return self.err(tag, 'permission denied')
+ if off > len(node.content):
+ node.content.extend(b'\0' * (off - len(node.content)))
+ node.content[off:off + len(data)] = data
+ node.mtime = int(time.time())
+ n = len(data) + 1 if self.mode == 'rwrite_big' else len(data)
+ return self.frame(Rwrite, tag, s32(n))
+ if typ == Tclunk:
+ fid = r.u32()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ del self.fids[fid]
+ return self.frame(Rclunk, tag, b'')
+ if typ == Tremove:
+ fid = r.u32()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node = self.fids[fid][0]
+ del self.fids[fid]
+ if node is self.root:
+ return self.err(tag, 'cannot remove root')
+ if node.isdir and node.children:
+ return self.err(tag, 'directory not empty')
+ parent = self.find_parent(self.root, node)
+ if parent is not None:
+ del parent.children[node.name]
+ self.free_paths.append(node.path)
+ node.removed = True
+ return self.frame(Rremove, tag, b'')
+ if typ == Tstat:
+ fid = r.u32()
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node = self.fids[fid][0]
+ if node is self.root.children.get('f'):
+ m = self.mode
+ if m == 'wrong_type':
+ return self.frame(Rwalk, tag, s16(0))
+ if m == 'rstat_garbage':
+ junk = bytes([0xAB] * 60)
+ return self.frame(Rstat, tag, s16(len(junk)) + junk)
+ if m == 'rstat_overlong':
+ st = node.stat_bytes(self)
+ inner = st[2:]
+ return self.frame(Rstat, tag, s16(len(inner) + 5) + inner)
+ if m == 'length_max':
+ st = node.stat_bytes(self, length=2 ** 64 - 1)
+ return self.frame(Rstat, tag, s16(len(st)) + st)
+ st = node.stat_bytes(self)
+ return self.frame(Rstat, tag, s16(len(st)) + st)
+ if typ == Twstat:
+ fid = r.u32()
+ r.u16()
+ st = r.bytes(r.u16())
+ if fid not in self.fids:
+ return self.err(tag, 'unknown fid')
+ node = self.fids[fid][0]
+ sr = Reader(st)
+ sr.u16(); sr.u32(); sr.bytes(13)
+ mode = sr.u32(); sr.u32(); mtime = sr.u32(); length = sr.u64()
+ name = sr.str()
+ if name and name != node.name:
+ if self.mode == 'rename_fail':
+ return self.err(tag, 'file already exists')
+ parent = self.find_parent(self.root, node)
+ if name in parent.children:
+ return self.err(tag, 'file already exists')
+ del parent.children[node.name]
+ node.name = name
+ parent.children[name] = node
+ if mode != 0xFFFFFFFF:
+ node.mode = mode & 0o777
+ if mtime != 0xFFFFFFFF:
+ node.mtime = mtime
+ if length != 0xFFFFFFFFFFFFFFFF and not node.isdir:
+ if length < len(node.content):
+ del node.content[length:]
+ else:
+ node.content.extend(b'\0' * (length - len(node.content)))
+ return self.frame(Rwstat, tag, b'')
+ return self.err(tag, 'unsupported message')
+
+ def find_parent(self, cur, node):
+ for c in cur.children.values():
+ if c is node:
+ return cur
+ if c.isdir:
+ p = self.find_parent(c, node)
+ if p is not None:
+ return p
+ return None
+
+ def readdir(self, tag, node, off, count):
+ recs = []
+ if node is self.root:
+ m = self.mode
+ if m == 'name_slash':
+ recs.append(node.stat_bytes(self, name='a/b'))
+ if m == 'name_empty':
+ recs.append(node.stat_bytes(self, name=''))
+ if m == 'name_huge':
+ recs.append(node.stat_bytes(self, name='h' * 60000))
+ if m == 'name_dots':
+ recs.append(node.stat_bytes(self, name='.'))
+ recs.append(node.stat_bytes(self, name='..'))
+ for c in node.children.values():
+ recs.append(c.stat_bytes(self))
+ blob = b''.join(recs)
+ if node is self.root and self.mode == 'dir_forever':
+ return self.frame(Rread, tag, s32(len(blob)) + blob)
+ if node is self.root and self.mode == 'dir_split':
+ # first read: up to the middle of the second record; second read: the rest
+ cut = len(recs[0]) + len(recs[1]) // 2
+ if off == 0:
+ data = blob[:cut]
+ elif off == cut:
+ data = blob[cut:]
+ else:
+ data = b''
+ return self.frame(Rread, tag, s32(len(data)) + data)
+ # 9P rule: offset 0 or previous offset+count; never split a record.
+ out = b''
+ pos = 0
+ for rec in recs:
+ if pos >= off and len(out) + len(rec) <= count:
+ out += rec
+ elif pos >= off:
+ break
+ pos += len(rec)
+ return self.frame(Rread, tag, s32(len(out)) + out)
+
+
+class Reader:
+ def __init__(self, b):
+ self.b = b
+ self.i = 0
+
+ def bytes(self, n):
+ v = self.b[self.i:self.i + n]
+ self.i += n
+ return v
+
+ def u8(self): return struct.unpack('<B', self.bytes(1))[0]
+ def u16(self): return struct.unpack('<H', self.bytes(2))[0]
+ def u32(self): return struct.unpack('<I', self.bytes(4))[0]
+ def u64(self): return struct.unpack('<Q', self.bytes(8))[0]
+ def str(self): return self.bytes(self.u16()).decode('utf-8', 'surrogateescape')
+ def data(self): return self.bytes(self.u32())
+
+
+def main():
+ if len(sys.argv) != 3:
+ print(__doc__ + MODES)
+ sys.exit(2)
+ path, mode = sys.argv[1], sys.argv[2]
+ if mode not in [l.split()[0] for l in MODES.strip().splitlines()]:
+ print('unknown mode', mode)
+ sys.exit(2)
+ try:
+ os.unlink(path)
+ except FileNotFoundError:
+ pass
+ ls = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ ls.bind(path)
+ ls.listen(8)
+ while True:
+ conn, _ = ls.accept()
+ srv = Server(mode)
+ srv.conn = conn
+ try:
+ srv.serve(conn)
+ except (BrokenPipeError, ConnectionResetError):
+ pass
+ finally:
+ conn.close()
+
+
+if __name__ == '__main__':
+ main()