#!/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 9ns 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); the server stops reading, so a Tflush is never seen never_flush Tread on /f: never reply, but keep serving: log every Tflush and answer Rflush 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('= 4: n = struct.unpack('= 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(' 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('