code: 9ferno

Download patch

ref: 9e6359c9bdb5c65d6476a836a7dfe6487b69d135
parent: 8ba2d1ffd2838e1e4c92d455a8cd1a4af0d4a457
author: 9ferno <gophone2015@gmail.com>
date: Sat Aug 28 13:44:33 EDT 2021

kfs 64 bit version

--- /dev/null
+++ b/appl/cmd/disk/kfs64.b
@@ -1,0 +1,4069 @@
+implement Kfs64;
+
+#
+# TO DO:
+#	- sync proc; Bmod; process structure
+#	- swiz?
+
+# Differences from kfs
+#	updating all file offsets, sizes and block numbers to 64 bits
+#	triple, quadruple, quintiple and sextuple-indirect blocks
+#	filename size 228 bytes TODO variable
+#	store 128 bytes of data in Dentry
+#	using a default bufsize of 512 bytes, small but not too small
+#	as locks are per block, do not have to worry about locking out other files' access
+
+include "sys.m";
+	sys: Sys;
+	Qid, Dir: import Sys;
+	DMEXCL, DMAPPEND, DMDIR: import Sys;
+	QTEXCL, QTAPPEND, QTDIR: import Sys;
+
+include "draw.m";
+
+include "styx.m";
+	styx: Styx;
+	Tmsg, Rmsg: import styx;
+	NOFID, OEXEC, ORCLOSE, OREAD, OWRITE, ORDWR, OTRUNC: import Styx;
+	IOHDRSZ: import Styx;
+
+include "daytime.m";
+	daytime: Daytime;
+	now: import daytime;
+
+include "arg.m";
+
+Kfs64: module
+{
+	init:	fn(nil: ref Draw->Context, nil: list of string);
+};
+
+MAXBUFSIZE:	con 16*1024;
+
+#
+#  fundamental constants
+#
+	# keeping Dentrysize to 512 and 128 bytes for data in Dentry leaves 228 bytes
+NAMELEN:	con 224; # ext2, ext3, ext4, zfs - 255 bytes
+NDBLOCK:	con 8;	# number of direct blocks in Dentry
+NIBLOCK:	con 6;	# max depth of indirect blocks in Dentry - sextuple-indirect blocks
+MAXFILESIZE:	con big 16r7FFFFFFFFFFFFFFF;	# Plan 9's limit (kfs's size is signed)
+
+SUPERADDR: con big 1;	# block address of Super block
+ROOTADDR: con big 2;	# block address of root Dentry
+
+QPDIR:	con (big 1<<31);
+QPNONE: con big 0;
+QPROOT: con big 1;
+QPSUPER: con big 2;
+
+#
+# don't change, these are the mode bits on disc
+#
+DALLOC: con 16r8000;
+DDIR:	con 16r4000;
+DAPND:	con 16r2000;
+DLOCK:	con 16r1000;
+DREAD:	con 4;
+DWRITE:	con 2;
+DEXEC:	con 1;
+
+#
+# other constants
+#
+
+MINUTE:	con 60;
+TLOCK:	con 5*MINUTE;
+NTLOCK:	con 200;	# number of active file locks
+
+Buffering: con 1;
+
+FID1, FID2, FID3: con 1+iota;
+
+None: con 0;	# user ID for "none"
+Noworld: con 9999;	# conventional id for "noworld" group
+
+Lock: adt
+{
+	c: chan of int;
+	new:	fn(): ref Lock;
+	lock:	fn(c: self ref Lock);
+	canlock:	fn(c: self ref Lock): int;
+	unlock:	fn(c: self ref Lock);
+};
+
+Dentry: adt
+{
+	name:	string;
+	uid:	int;
+	gid:	int;
+	muid:	int;	# not set by plan 9's kfs
+	mode:	int;	# mode bits on disc: DALLOC etc
+	qid:	Qid;	# 9p1 format on disc
+	size:	big;	# only 64-bits on disc, and Plan 9 limits it to signed
+	atime:	int;
+	mtime:	int;
+
+	iob:	ref Iobuf;	# locked block containing directory entry
+	buf:	array of byte;	# pointer into block to packed directory entry
+	mod:	int;	# bits of buf that need updating
+
+	unpack:	fn(a: array of byte): ref Dentry;
+		# TODO slot is obsolete with this design of leaving each block for a Dentry
+		# leave the slot as int as it can be used as an index and it is rare to have 1TB memory
+	get:	fn(p: ref Iobuf, slot: int): ref Dentry; # read Dentry from a slot in the iobuf array
+		# read from device addr into iobuf array
+		# then read Dentry from a slot in that array
+	geta:	fn(d: ref Device, addr: big, slot: int, qpath: big, mode: int): (ref Dentry, string);
+	getd:	fn(f: ref File, mode: int): (ref Dentry, string); # refresh the Direntry from device
+	put:	fn(d: self ref Dentry); # update packed Dentry in memory and save it to device
+	access:	fn(d: self ref Dentry, f: int, uid: int); # update Dentry times
+	change:	fn(d: self ref Dentry, f: int); # tag Dentry as changed
+	release:	fn(d: self ref Dentry); # release Dentry and packed memory of it
+
+		# a is the block number of the Direntry being retrieved
+	getblk:	fn(d: self ref Dentry, a: big, tag: int): ref Iobuf;
+	getblk1:	fn(d: self ref Dentry, a: big, tag: int): ref Iobuf;
+	rel2abs:	fn(d: self ref Dentry, a: big, tag: int, putb: int): big;
+	trunc:	fn(d: self ref Dentry, uid: int);
+	update:	fn(d: self ref Dentry);
+	print:	fn(d: self ref Dentry, msg: string);
+};
+
+Uname, Uids, Umode, Uqid, Usize, Utime: con 1<<iota;	# Dentry.mod
+
+#
+# disc structure:
+#	Tag:	pad[2] tag[2] path[8] TODO change pad?
+Tagsize: con 2+2+8;
+
+Tag: adt
+{
+	tag:	int;
+	path:	big;
+
+	unpack:	fn(a: array of byte): Tag;
+	pack:	fn(t: self Tag, a: array of byte);
+};
+
+Superb: adt
+{
+	iob:	ref Iobuf;	# buffer pool?
+
+	fstart:	big;	# starting block
+	fsize:	big;	# size in blocks
+	tfree:	big;	# total number of free blocks
+	qidgen:	big;	# generator for unique ids
+
+	fsok:	int;
+
+		# free block pointers list until the block end
+		# nfree[4] number of free block pointers in this list
+		# free[FEPERBLK*8]
+	fbuf:	array of byte;	# nfree[4] free[FEPERBLK*8]; aliased into containing block
+
+	get:	fn(dev: ref Device, flags: int): ref Superb;
+	touched:	fn(s: self ref Superb);
+	put:	fn(s: self ref Superb);
+	print:	fn(s: self ref Superb);
+
+	pack:	fn(s: self ref Superb, a: array of byte);
+	unpack:	fn(a: array of byte): ref Superb;
+};
+
+Device: adt
+{
+	fd:	ref Sys->FD;
+	ronly:	int;
+	# could put locks here if necessary
+	# partitioning by ds(3)
+};
+
+#
+# one for each locked qid
+#
+Tlock: adt
+{
+	dev:	ref Device;
+	time:	int;
+	qpath:	big;
+	file:	cyclic ref File;	# TO DO: probably not needed
+};
+
+File: adt
+{
+	qlock:	chan of int;
+	qid:	Qid;
+	wpath:	ref Wpath;
+	tlock:	cyclic ref Tlock;		# if file is locked
+	fs:	ref Device;
+	addr:	big;		# block number, not the actual block address on the disk
+	slot:	int;		# only useful when DIRPERBUF > 1
+	lastra:	big;		# read ahead address, block number
+	fid:	int;
+	uid:	int;
+	open:	int;
+	cons:	int;	# if opened by console
+	doffset: big;	# directory reading
+	dvers:	int;
+	daddr:	big;	# state for next read, different from kfs
+	dslot:	int;	# state for next read
+
+	new:	fn(fid: int): ref File;
+	access:	fn(f: self ref File, d: ref Dentry, mode: int): int;
+	lock:	fn(f: self ref File);
+	unlock:	fn(f: self ref File);
+};
+
+FREAD, FWRITE, FREMOV, FWSTAT: con 1<<iota;	# File.open
+
+Chan: adt
+{
+	fd:	ref Sys->FD;			# fd request came in on
+#	rlock, wlock: QLock;		# lock for reading/writing messages on cp
+	flags:	int;
+	flist:	list of ref File;			# active files
+	fqlock:	chan of int;
+#	reflock:	RWLock;		# lock for Tflush
+	msize:	int;			# version
+
+	new:	fn(fd: ref Sys->FD): ref Chan;
+	getfid:	fn(c: self ref Chan, fid: int, flag: int): ref File;
+	putfid:	fn(c: self ref Chan, f: ref File);
+	flock: fn(nil: self ref Chan);
+	funlock:	fn(nil: self ref Chan);
+};
+
+# buffer pool = array of Hiob's = hiob
+#	implements a locality based cache (lru cache).
+#	hb := hiob[addr%len hiob]; is used to figure out
+#		which array element to use
+# wrapper over a list of IO buffers
+Hiob: adt
+{
+	link:	ref Iobuf;	# TO DO: eliminate circular list
+	lk:	ref Lock;		# used while scrolling the list
+	niob: int;
+
+	newbuf:	fn(h: self ref Hiob): ref Iobuf;
+};
+
+# memory unit corresponding to a block maintained in a doubly linked list
+Iobuf: adt
+{
+	qlock:	chan of int;
+	dev:	ref Device;
+	fore:	cyclic ref Iobuf;		# lru hash chain
+	back:	cyclic ref Iobuf;		# for lru
+	iobuf:	array of byte;		# only active while locked
+	xiobuf:	array of byte;	# "real" buffer pointer
+	addr:	big;
+	flags:	int;
+
+	get:	fn(dev: ref Device, addr: big, flags: int):ref Iobuf;
+	put:	fn(iob: self ref Iobuf);
+	lock:	fn(iob: self ref Iobuf);
+	canlock:	fn(iob: self ref Iobuf): int;
+	unlock:	fn(iob: self ref Iobuf);
+
+	checktag:	fn(iob: self ref Iobuf, tag: int, qpath: big): int;
+	settag:	fn(iob: self ref Iobuf, tag: int, qpath: big);
+	print: fn(iob: self ref Iobuf, msg: string);
+};
+
+Wpath: adt
+{
+	up: cyclic ref Wpath;		# pointer upwards in path
+	addr: big;		# directory entry addr
+	slot: int;		# directory entry slot
+};
+
+#
+#  error codes generated from the file server
+#
+Eaccess: con "access permission denied";
+Ealloc: con "phase error -- directory entry not allocated";
+Eauth: con "authentication failed";
+Eauthmsg: con "kfs: authentication not required";
+Ebadspc: con "attach -- bad specifier";
+Ebadu: con "attach -- privileged user";
+Ebroken: con "close/read/write -- lock is broken";
+Echar: con "bad character in directory name";
+Econvert: con "protocol botch";
+Ecount: con "read/write -- count too big";
+Edir1: con "walk -- in a non-directory";
+Edir2: con "create -- in a non-directory";
+Edot: con "create -- . and .. illegal names";
+Eempty: con "remove -- directory not empty";
+Eentry: con "directory entry not found";
+Eexist: con "create -- file exists";
+Efid: con "unknown fid";
+Efidinuse: con "fid already in use";
+Efull: con "file system full";
+Elocked: con "open/create -- file is locked";
+Emode: con "open/create -- unknown mode";
+Ename: con "create/wstat -- bad character in file name";
+Enotd: con "wstat -- attempt to change directory";
+Enotg: con "wstat -- not in group";
+Enotl: con "wstat -- attempt to change length";
+Enotm: con "wstat -- unknown type/mode";
+Enotu: con "wstat -- not owner";
+Eoffset: con "read/write -- offset negative";
+Eopen: con "read/write -- on non open fid";
+Ephase: con "phase error -- cannot happen";
+Eqid: con "phase error -- qid does not match";
+Eqidmode: con "wstat -- qid.qtype/dir.mode mismatch";
+Eronly: con "file system read only";
+Ersc: con "it's russ's fault.  bug him.";
+Esystem: con "kfs system error";
+Etoolong: con "name too long";
+Etoobig: con "write -- file size limit";
+Ewalk: con "walk -- too many (system wide)";
+
+#
+#  tags on block
+#
+Tnone,
+Tsuper,			# the super block
+Tdir,			# directory contents
+Tind1,			# points to blocks - direct pointer
+Tind2,			# points to Tind1 - indirect pointer
+Tind3,			# points to Tind2 - double indirect pointer
+Tind4,			# points to Tind3 - triple indirect pointer
+Tind5,			# points to Tind4 - quadruple indirect pointer
+Tind6,			# points to Tind5 - quintuple indirect pointer
+Tind7,			# points to Tind6 - sextuple indirect pointer
+Tfile,			# file contents
+Tfree,			# in free list
+Tbuck,			# cache fs bucket - why?
+Tvirgo,			# fake worm virgin bits - why?
+Tcache,			# cw cache things - why?
+MAXTAG: con iota;
+
+#
+#  flags to Iobuf.get
+#
+	Bread,	# read the block if miss
+	Bprobe,	# return null if miss
+	Bmod,	# set modified bit in buffer
+	Bimm,	# set immediate bit in buffer
+	Bres:		# never renamed
+	con 1<<iota;
+
+#
+#  check flags
+#
+	Crdall,	# read all files
+	Ctag,	# rebuild tags
+	Cpfile,	# print files
+	Cpdir,	# print directories
+	Cfree,	# rebuild free list
+	Cream,	# clear all bad tags
+	Cbad,	# clear all bad blocks
+	Ctouch,	# touch old dir and indir
+	Cquiet:	# report just nasty things
+	con 1<<iota;
+
+#
+#  buffer size variables, determined by RBUFSIZE
+#
+# could be named better but left alone for conformity
+#	BUF = block in these variables
+RBUFSIZE	:= 512;				# block size, real block size
+BUFSIZE		:= 500; # RBUFSIZE-Tagsize; # usable block size
+DIRPERBUF	:= 1;				# number of Dentries per block
+INDPERBUF	:= big 62; # BUFSIZE/8; # number of pointers in a block
+	# number of blocks representable by a double indirect block of pointers
+INDPERBUF2	:= big 3844; # INDPERBUF^2;
+	# number of blocks representable by a triple indirect block of pointers
+INDPERBUF3	:= big 238328; # INDPERBUF^3;
+	# number of blocks representable by a quadruple indirect block of pointers
+INDPERBUF4	:= big 14776336; # INDPERBUF^4;
+	# number of blocks representable by a quintuple indirect block of pointers
+INDPERBUF5	:= big 916132832; # INDPERBUF^5;
+	# number of blocks representable by a sextuple indirect block of pointers
+INDPERBUF6	:= big 56800235584; # INDPERBUF^6;
+	# -4 for the nfree[4] of Fbuf
+	# list of free blocks maintained in a Tfree block
+FEPERBUF	:= 57; # (BUFSIZE - Super1size -4)/8;
+
+emptyblock: array of byte;
+
+wrenfd: ref Sys->FD;
+thedevice: ref Device;
+devnone: ref Device;
+wstatallow := 0;
+writeallow := 0;
+writegroup := 0;
+
+ream := 0;
+readonly := 0;
+noatime := 0;
+localfs: con 1;
+conschan: ref Chan;
+consuid := -1;
+consgid := -1;
+debug := 0;
+kfsname: string;
+consoleout: chan of string;
+mainlock: ref Lock;
+pids: list of int;
+
+noqid: Qid;
+
+init(nil: ref Draw->Context, args: list of string)
+{
+	sys = load Sys Sys->PATH;
+	styx = load Styx Styx->PATH;
+	daytime = load Daytime Daytime->PATH;
+
+	styx->init();
+
+	arg := load Arg Arg->PATH;
+	if(arg == nil)
+		error(sys->sprint("can't load %s: %r", Arg->PATH));
+	arg->init(args);
+	arg->setusage("disk/kfs [-r [-b blocksize]] [-cADPRW] [-n name] kfsfile");
+	bufsize := 512;
+	nocheck := 0;
+	while((o := arg->opt()) != 0)
+		case o {
+		'c' => nocheck = 1;
+		'r' =>	ream = 1;
+		'b' => bufsize = int arg->earg();
+		'D' => debug = !debug;
+		'P' => writeallow = 1;
+		'W' => wstatallow = 1;
+		'R' => readonly = 1;
+		'A' => noatime = 1;	# mainly useful for flash
+		'n' => kfsname = arg->earg();
+		* =>	arg->usage();
+		}
+	args = arg->argv();
+	if(args == nil)
+		arg->usage();
+	arg = nil;
+
+	devnone = ref Device(nil, 1);
+	mainlock = Lock.new();
+
+	conschan = Chan.new(nil);
+	conschan.msize = Styx->MAXRPC;
+
+	mode := Sys->ORDWR;
+	if(readonly)
+		mode = Sys->OREAD;
+	wrenfd = sys->open(hd args, mode);
+	if(wrenfd == nil)
+		error(sys->sprint("can't open %s: %r", hd args));
+	thedevice = ref Device(wrenfd, readonly);
+	if(ream){
+		if(bufsize <= 0 || bufsize % 512 || bufsize > MAXBUFSIZE)
+			error(sys->sprint("invalid block size %d", bufsize));
+		wrenream(thedevice);
+	}else{
+		if(!wreninit(thedevice))
+			error("kfs magic in trouble");
+	}
+	BUFSIZE = RBUFSIZE - Tagsize;
+	DIRPERBUF = BUFSIZE / Dentrysize;
+	INDPERBUF = big (BUFSIZE/8); # number of pointers in a block
+		# number of blocks representable by a double indirect block of pointers
+	INDPERBUF2 = INDPERBUF^ big 2;
+		# number of blocks representable by a triple indirect block of pointers
+	INDPERBUF3 = INDPERBUF^ big 3;
+		# number of blocks representable by a quadruple indirect block of pointers
+	INDPERBUF4 = INDPERBUF^ big 4;
+		# number of blocks representable by a quintuple indirect block of pointers
+	INDPERBUF5 = INDPERBUF^ big 5;
+		# number of blocks representable by a sextuple indirect block of pointers
+	INDPERBUF6 = INDPERBUF^ big 6;
+		# -4 for the nfree[4] of Fbuf
+		# number of possible free block pointers in super block 
+		# the -4 to store the number of freeblockpointers
+	FEPERBUF = (BUFSIZE - Super1size - 4) / 8;
+	emptyblock = array[RBUFSIZE] of {* => byte 0};
+
+	if(debug){
+		sys->print("QPDIR 0x%bx %bd QPNONE %bx QPROOT %bx QPSUPER %bx\n", QPDIR, QPDIR, QPNONE, QPROOT, QPSUPER);
+		sys->print("RBUFSIZE %d Tagsize %d BUFSIZE %d Dentrysize %d\n	DIRPERBUF %d INDPERBUF %bd FEPERBUF %d\n",
+					RBUFSIZE, Tagsize, BUFSIZE, Dentrysize, DIRPERBUF, INDPERBUF, FEPERBUF);
+	}
+	iobufinit(30);	# initialize buffer pool of 30 buffers in groups of 5
+
+	if(ream){
+		superream(thedevice, SUPERADDR);
+		rootream(thedevice, ROOTADDR);
+		wstatallow = writeallow = 1;
+	}
+	if(wrencheck(wrenfd))
+		error("kfs super/root in trouble");
+
+	if(debug)
+		sys->print("ream %d readonly %d !read %d !readonly %d\n",
+					ream, readonly, !ream, !readonly);
+	if(!ream && !readonly && !superok(0)){
+		sys->print("kfs needs check\n");
+		if(!nocheck)
+			check(thedevice, Cquiet|Cfree);
+	}
+
+	(d, e) := Dentry.geta(thedevice, ROOTADDR, 0, QPROOT, Bread);
+	if(d != nil && !(d.mode & DDIR))
+		e = "not a directory";
+	if(e != nil)
+		error("bad root: "+e);
+	if(debug)
+		d.print();
+	d.put();
+
+	sys->pctl(Sys->FORKFD|Sys->NEWPGRP, nil);
+
+	sys->pctl(Sys->NEWFD, wrenfd.fd :: 0 :: 1 :: 2 :: nil);
+	wrenfd = sys->fildes(wrenfd.fd);
+	thedevice.fd = wrenfd;
+
+	c := chan of int;
+
+	if(Buffering){
+		spawn syncproc(c);
+		pid := <-c;
+		if(pid)
+			pids = pid :: pids;
+	}
+	spawn consinit(c);
+	pid := <- c;
+	if(pid)
+		pids = pid :: pids;
+
+	spawn kfs(sys->fildes(0));
+}
+
+error(s: string)
+{
+	sys->fprint(sys->fildes(2), "kfs: %s\n", s);
+	for(; pids != nil; pids = tl pids)
+		kill(hd pids);
+	raise "fail:error";
+}
+
+panic(s: string)
+{
+	sys->fprint(sys->fildes(2), "kfs: panic: %s\n", s);
+	for(; pids != nil; pids = tl pids)
+		kill(hd pids);
+	raise "panic";
+}
+
+syncproc(c: chan of int)
+{
+	c <-= 0;
+}
+
+shutdown()
+{
+	for(; pids != nil; pids = tl pids)
+		kill(hd pids);
+	# TO DO: when Bmod deferred, must sync
+	# sync super block
+	if(!readonly && superok(1)){
+		# ;
+	}
+	iobufclear();
+}
+
+kill(pid: int)
+{
+	fd := sys->open("#p/"+string pid+"/ctl", Sys->OWRITE);
+	if(fd != nil)
+		sys->fprint(fd, "kill");
+}
+
+#
+# limited file system support for console
+#
+kattach(fid: int): string
+{
+	return applycons(ref Tmsg.Attach(1, fid, NOFID, "adm", "")).t1;
+}
+
+kopen(oldfid: int, newfid: int, names: array of string, mode: int): string
+{
+	(r1, e1) := applycons(ref Tmsg.Walk(1, oldfid, newfid, names));
+	if(r1 != nil){
+		pick m := r1 {
+		Walk =>
+			if(len m.qids != len names){
+				kclose(newfid);
+				cprint(Eexist);
+				return Eexist;
+			}
+		* =>
+			return "unexpected reply";
+		}
+		(r1, e1) = applycons(ref Tmsg.Open(1, newfid, mode));
+		if(e1 != nil){
+			kclose(newfid);
+			cprint(sys->sprint("open: %s", e1));
+		}
+	}
+	return e1;
+}
+
+kread(fid: int, offset: big, nbytes: int): (array of byte, string)
+{
+	(r, e) := applycons(ref Tmsg.Read(1, fid, offset, nbytes));
+	if(r != nil){
+		pick m := r {
+		Read =>
+			return (m.data, nil);
+		* =>
+			return (nil, "unexpected reply");
+		}
+	}
+	cprint(sys->sprint("read error: %s", e));
+	return (nil, e);
+}
+
+kclose(fid: int)
+{
+	applycons(ref Tmsg.Clunk(1, fid));
+}
+
+applycons(t: ref Tmsg): (ref Rmsg, string)
+{
+	r := apply(conschan, t);
+	pick m := r {
+	Error =>
+		if(debug)
+			cprint(sys->sprint("%s: %s\n", t.text(), m.ename));
+		return (nil, m.ename);
+	}
+	return (r, nil);
+}
+
+#
+# always reads /adm/users in userinit(), then
+# optionally serves the command file, if used.
+#
+Req: adt {
+	nbytes:	int;
+	rc:	chan of (array of byte, string);
+};
+
+consinit(c: chan of int)
+{
+	kattach(FID1);
+	userinit();
+	if(kfsname == nil){
+		c <-= 0;
+		exit;
+	}
+	cfname := "kfs."+kfsname+".cmd";
+	sys->bind("#s", "/chan", Sys->MBEFORE);
+	file := sys->file2chan("/chan", cfname);
+	if(file == nil)
+		error(sys->sprint("can't create /chan/%s: %r", cfname));
+	c <-= sys->pctl(0, nil);
+	consc := chan of string;
+	checkend := chan of int;
+	cdata: array of byte;
+	pending: ref Req;
+	cfid := -1;
+	for(;;) alt{
+	(nil, nbytes, fid, rc) := <-file.read =>
+		if(rc == nil)
+			break;
+		if(cfid == -1)
+			cfid = fid;
+		if(fid != cfid || pending != nil){
+			rc <-= (nil, "kfs.cmd is busy");
+			break;
+		}
+		if(cdata != nil){
+			cdata = reply(rc, nbytes, cdata);
+			break;
+		}
+		if(nbytes <= 0 || consoleout == nil){
+			rc <-= (nil, nil);
+			break;
+		}
+		pending = ref Req(nbytes, rc);
+		consc = consoleout;
+	(nil, data, fid, wc) := <-file.write =>
+		if(cfid == -1)
+			cfid = fid;
+		if(wc == nil){
+			if(fid == cfid){
+				cfid = -1;
+				pending = nil;
+				cdata = nil;	# discard unread data from last command
+				if((consc = consoleout) == nil)
+					consc = chan of string;
+			}
+			break;
+		}
+		if(fid != cfid){
+			wc <-= (0, "kfs.cmd is busy");
+			break;
+		}
+		(nf, fld) := sys->tokenize(string data, " \t\n\r");
+		if(nf < 1){
+			wc <-= (0, "illegal kfs request");
+			break;
+		}
+		case hd fld {
+		"check" =>
+			if(consoleout != nil){
+				wc <-= (0, "check in progress");
+				break;
+			}
+			f := 0;
+			if(nf > 1){
+				f = checkflags(hd tl fld);
+				if(f < 0){
+					wc <-= (0, "illegal check flag: "+hd tl fld);
+					break;
+				}
+			}
+			consoleout = chan of string;
+			spawn checkproc(checkend, f);
+			wc <-= (len data, nil);
+			consc = consoleout;
+		"users" or "user" =>
+			cmd_users();
+			wc <-= (len data, nil);
+		"sync" =>
+			# nothing TO DO until writes are buffered
+			wc <-= (len data, nil);
+		"allow" =>
+			wstatallow = writeallow = 1;
+			wc <-= (len data, nil);
+		"allowoff" or "disallow" =>
+			wstatallow = writeallow = 0;
+			wc <-= (len data, nil);
+		* =>
+			wc <-= (0, "unknown kfs request");
+			continue;
+		}
+	<-checkend =>
+		consoleout = nil;
+		consc = chan of string;
+	s := <-consc =>
+		#sys->print("<-%s\n", s);
+		req := pending;
+		pending = nil;
+		if(req != nil)
+			cdata = reply(req.rc, req.nbytes, array of byte s);
+		else
+			cdata = array of byte s;
+		if(cdata != nil && cfid != -1)
+			consc = chan of string;
+	}
+}
+
+reply(rc: chan of (array of byte, string), nbytes: int, a: array of byte): array of byte
+{
+	if(len a < nbytes)
+		nbytes = len a;
+	rc <-= (a[0:nbytes], nil);
+	if(nbytes == len a)
+		return nil;
+	return a[nbytes:];
+}
+
+checkproc(c: chan of int, flags: int)
+{
+	mainlock.lock();
+	check(thedevice, flags);
+	mainlock.unlock();
+	c <-= 1;
+}
+
+#
+# normal kfs service
+#
+kfs(rfd: ref Sys->FD)
+{
+	cp := Chan.new(rfd);
+	while((t := Tmsg.read(rfd, cp.msize)) != nil){
+		if(debug)
+			sys->print("<- %s\n", t.text());
+		r := apply(cp, t);
+		pick m := r {
+		Error =>
+			r.tag = t.tag;
+		}
+		if(debug)
+			sys->print("-> %s\n", r.text());
+		rbuf := r.pack();
+		if(rbuf == nil)
+			panic("Rmsg.pack");
+		if(sys->write(rfd, rbuf, len rbuf) != len rbuf)
+			panic("mount write");
+	}
+	shutdown();
+}
+
+apply(cp: ref Chan, t: ref Tmsg): ref Rmsg
+{
+	mainlock.lock();	# TO DO: this is just to keep console and kfs from colliding
+	r: ref Rmsg;
+	pick m := t {
+	Readerror =>
+		error(sys->sprint("mount read error: %s", m.error));
+	Version =>
+		r = rversion(cp, m);
+	Auth =>
+		r = rauth(cp, m);
+	Flush =>
+		r = rflush(cp, m);
+	Attach =>
+		r = rattach(cp, m);
+	Walk =>
+		r = rwalk(cp, m);
+	Open =>
+		r = ropen(cp, m);
+	Create =>
+		r = rcreate(cp, m);
+	Read =>
+		r = rread(cp, m);
+	Write =>
+		r = rwrite(cp, m);
+	Clunk =>
+		r = rclunk(cp, m);
+	Remove =>
+		r = rremove(cp, m);
+	Stat =>
+		r = rstat(cp, m);
+	Wstat =>
+		r = rwstat(cp, m);
+	* =>
+		panic("Styx mtype");
+		return nil;
+	}
+	mainlock.unlock();
+	return r;
+}
+
+rversion(cp: ref Chan, t: ref Tmsg.Version): ref Rmsg
+{
+	cp.msize = RBUFSIZE+IOHDRSZ;
+	if(cp.msize < Styx->MAXRPC)
+		cp.msize = Styx->MAXRPC;
+	(msize, version) := styx->compatible(t, Styx->MAXRPC, Styx->VERSION);
+	if(msize < 256)
+		return ref Rmsg.Error(t.tag, "message size too small");
+	return ref Rmsg.Version(t.tag, msize, version);
+}
+
+rauth(nil: ref Chan, t: ref Tmsg.Auth): ref Rmsg
+{
+	return ref Rmsg.Error(t.tag, Eauthmsg);
+}
+
+rflush(nil: ref Chan, t: ref Tmsg.Flush): ref Rmsg
+{
+	# runlock(cp.reflock);
+	# wlock(cp.reflock);
+	# wunlock(cp.reflock);
+	# rlock(cp.reflock);
+	return ref Rmsg.Flush(t.tag);
+}
+
+err(t: ref Tmsg, s: string): ref Rmsg.Error
+{
+	return ref Rmsg.Error(t.tag, s);
+}
+
+ferr(t: ref Tmsg, s: string, file: ref File, p: ref Iobuf): ref Rmsg.Error
+{
+	if(p != nil)
+		p.put();
+	if(file != nil)
+		file.unlock();
+	return ref Rmsg.Error(t.tag, s);
+}
+
+File.new(fid: int): ref File
+{
+	f := ref File;
+	f.qlock = chan[1] of int;
+	f.fid = fid;
+	f.cons = 0;
+	f.tlock = nil;
+	f.wpath = nil;
+	f.doffset = big 0;
+	f.dvers = 0;
+	f.daddr = big 0;
+	f.dslot = 0;
+	f.uid = None;
+	f.cons = 0;
+#	f.cuid = None;
+	return f;
+}
+
+#
+# returns a locked file structure
+#
+
+Chan.getfid(cp: self ref Chan, fid: int, flag: int): ref File
+{
+	if(fid == NOFID)
+		return nil;
+	cp.flock();
+	for(l := cp.flist; l != nil; l = tl l){
+		f := hd l;
+		if(f.fid == fid){
+			cp.funlock();
+			if(flag)
+				return nil;	# fid in use
+			f.lock();
+			if(f.fid == fid)
+				return f;
+			f.unlock();
+			cp.flock();
+		}
+	}
+	if(flag == 0){
+		sys->print("kfs: cannot find %H.%ud", cp, fid);
+		cp.funlock();
+		return nil;
+	}
+	f := File.new(fid);
+	f.lock();
+	cp.flist = f :: cp.flist;
+	cp.funlock();
+	return f;
+}
+
+Chan.putfid(cp: self ref Chan, f: ref File)
+{
+	cp.flock();
+	nl: list of ref File;
+	for(x := cp.flist; x != nil; x = tl x)
+		if(hd x != f)
+			nl = hd x :: nl;
+	cp.flist = nl;
+	cp.funlock();
+	f.unlock();
+}
+
+File.lock(f: self ref File)
+{
+	f.qlock <-= 1;
+}
+
+File.unlock(f: self ref File)
+{
+	<-f.qlock;
+}
+
+Chan.new(fd: ref Sys->FD): ref Chan
+{
+	c := ref Chan;
+	c.fd = fd;
+	c.fqlock = chan[1] of int;
+#	rlock, wlock: QLock;		# lock for reading/writing messages on cp
+	c.flags = 0;
+#	reflock:	RWLock;		# lock for Tflush
+	c.msize = 0;	# set by rversion
+	return c;
+}
+
+Chan.flock(c: self ref Chan)
+{
+	c.fqlock <-= 1;
+}
+
+Chan.funlock(c: self ref Chan)
+{
+	<-c.fqlock;
+}
+
+rattach(cp: ref Chan, t: ref Tmsg.Attach): ref Rmsg
+{
+	if(t.aname != "" && t.aname != "main")
+		return err(t, Ebadspc);
+	file := cp.getfid(t.fid, 1);
+	if(file == nil)
+		return err(t, Efidinuse);
+	p := Iobuf.get(thedevice, ROOTADDR, Bread);
+	if(p == nil){
+		cp.putfid(file);
+		return err(t, "can't access root block");
+	}
+	d := Dentry.get(p, 0);
+	if(d == nil || p.checktag(Tdir, QPROOT) || (d.mode & DALLOC) == 0 || (d.mode & DDIR) == 0){
+		p.put();
+		cp.putfid(file);
+		return err(t, Ealloc);
+	}
+	if(file.access(d, DEXEC)){
+		p.put();
+		cp.putfid(file);
+		return err(t, Eaccess);
+	}
+	d.access(FREAD, file.uid);
+	file.fs = thedevice;
+	file.qid = d.qid;
+	file.addr = p.addr;
+	file.slot = 0;
+	file.open = 0;
+	file.uid = strtouid(t.uname);
+	file.wpath = nil;
+	p.put();
+	qid := file.qid;
+	file.unlock();
+	return ref Rmsg.Attach(t.tag, qid);
+}
+
+clone(nfile: ref File, file: ref File)
+{
+	nfile.qid = file.qid;
+	nfile.wpath = file.wpath;
+	nfile.fs = file.fs;
+	nfile.addr = file.addr;
+	nfile.slot = file.slot;
+	nfile.uid = file.uid;
+#	nfile.cuid = None;
+	nfile.open = file.open & ~FREMOV;
+}
+
+walkname(file: ref File, wname: string): (string, Qid)
+{
+	#
+	# File must not have been opened for I/O by an open
+	# or create message and must represent a directory.
+	#
+	if(file.open != 0)
+		return (Emode, noqid);
+
+	(d, e) := Dentry.getd(file, Bread);
+	if(d == nil)
+		return (e, noqid);
+	if(!(d.mode & DDIR)){
+		d.put();
+		return (Edir1, noqid);
+	}
+
+	#
+	# For walked elements the implied user must
+	# have permission to search the directory.
+	#
+	if(file.access(d, DEXEC)){
+		d.put();
+		return (Eaccess, noqid);
+	}
+	d.access(FREAD, file.uid);
+
+	if(wname == "." || wname == ".." && file.wpath == nil){
+		d.put();
+		return (nil, file.qid);
+	}
+
+	d1: ref Dentry;	# entry for wname, if found
+	slot: int;
+
+	if(wname == ".."){
+		d.put();
+		addr := file.wpath.addr;
+		slot = file.wpath.slot;
+		(d1, e) = Dentry.geta(file.fs, addr, slot, QPNONE, Bread);
+		if(d1 == nil)
+			return (e, noqid);
+		file.wpath = file.wpath.up;
+	}else{
+
+	Search:
+		for(addr := big 0; ; addr++){
+			if(d.iob == nil){
+				(d, e) = Dentry.getd(file, Bread);
+				if(d == nil)
+					return (e, noqid);
+			}
+			p1 := d.getblk1(addr, 0);
+			if(p1 == nil || p1.checktag(Tdir, d.qid.path)){
+				if(p1 != nil)
+					p1.put();
+				return (Eentry, noqid);
+			}
+			for(slot = 0; slot < DIRPERBUF; slot++){
+				d1 = Dentry.get(p1, slot);
+				if(!(d1.mode & DALLOC))
+					continue;
+				if(wname != d1.name)
+					continue;
+				#
+				# update walk path
+				#
+				file.wpath = ref Wpath(file.wpath, file.addr, file.slot);
+				# TODO test is this needed? slot += DIRPERBUF*addr;
+				break Search;
+			}
+			p1.put();
+		}
+		d.put();
+	}
+
+	file.addr = d1.iob.addr;
+	file.slot = slot;
+	file.qid = d1.qid;
+	d1.put();
+	return (nil, file.qid);
+}
+
+rwalk(cp: ref Chan, t: ref Tmsg.Walk): ref Rmsg
+{
+	nfile, tfile: ref File;
+	q: Qid;
+
+	# The file identified by t.fid must be valid in the
+	# current session and must not have been opened for I/O
+	# by an open or create message.
+
+	if((file := cp.getfid(t.fid, 0)) == nil)
+		return err(t, Efid);
+	if(file.open != 0)
+		return ferr(t, Emode, file, nil);
+
+	# If newfid is not the same as fid, allocate a new file;
+	# a side effect is checking newfid is not already in use (error);
+	# if there are no names to walk this will be equivalent to a
+	# simple 'clone' operation.
+	# Otherwise, fid and newfid are the same and if there are names
+	# to walk make a copy of 'file' to be used during the walk as
+	# 'file' must only be updated on success.
+	# Finally, it's a no-op if newfid is the same as fid and t.nwname
+	# is 0.
+
+	nwqid := 0;
+	if(t.newfid != t.fid){
+		if((nfile = cp.getfid(t.newfid, 1)) == nil)
+			return ferr(t, Efidinuse, file, nil);
+	}
+	else if(len t.names != 0)
+		nfile = tfile = File.new(NOFID);
+	else{
+		file.unlock();
+		return ref Rmsg.Walk(t.tag, nil);
+	}
+	clone(nfile, file);
+
+	r := ref Rmsg.Walk(t.tag, array[len t.names] of Qid);
+	error: string;
+	for(nwname := 0; nwname < len t.names; nwname++){
+		(error, q) = walkname(nfile, t.names[nwname]);
+		if(error != nil)
+			break;
+		r.qids[nwqid++] = q;
+	}
+
+	if(len t.names == 0){
+
+		# Newfid must be different to fid (see above)
+		# so this is a simple 'clone' operation - there's
+		# nothing to do except unlock unless there's
+		# an error.
+
+		nfile.unlock();
+		if(error != nil)
+			cp.putfid(nfile);
+	}else if(nwqid < len t.names){
+		#
+		# Didn't walk all elements, 'clunk' nfile
+		# and leave 'file' alone.
+		# Clear error if some of the elements were
+		# walked OK.
+		#
+		if(nfile != tfile)
+			cp.putfid(nfile);
+		if(nwqid != 0)
+			error = nil;
+		r.qids = r.qids[0:nwqid];
+	}else{
+		#
+		# Walked all elements. If newfid is the same
+		# as fid must update 'file' from the temporary
+		# copy used during the walk.
+		# Otherwise just unlock (when using tfile there's
+		# no need to unlock as it's a local).
+		#
+		if(nfile == tfile){
+			file.qid = nfile.qid;
+			file.wpath = nfile.wpath;
+			file.addr = nfile.addr;
+			file.slot = nfile.slot;
+		}else
+			nfile.unlock();
+	}
+	file.unlock();
+
+	if(error != nil)
+		return err(t, error);
+	return r;
+}
+
+ropen(cp: ref Chan, f: ref Tmsg.Open): ref Rmsg
+{
+	wok := cp == conschan || writeallow;
+
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+
+	#
+	# if remove on close, check access here
+	#
+	ro := isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup));
+	if(f.mode & ORCLOSE){
+		if(ro)
+			return ferr(f, Eronly, file, nil);
+		#
+		# check on parent directory of file to be deleted
+		#
+		if(file.wpath == nil || file.wpath.addr == file.addr)
+			return ferr(f, Ephase, file, nil);
+		p := Iobuf.get(file.fs, file.wpath.addr, Bread);
+		if(p == nil || p.checktag(Tdir, QPNONE))
+			return ferr(f, Ephase, file, p);
+		if((d := Dentry.get(p, file.wpath.slot)) == nil || !(d.mode & DALLOC))
+			return ferr(f, Ephase, file, p);
+		if(file.access(d, DWRITE))
+			return ferr(f, Eaccess, file, p);
+		p.put();
+	}
+	(d, e) := Dentry.getd(file, Bread);
+	if(d == nil)
+		return ferr(f, e, file, nil);
+	p := d.iob;
+	qid := d.qid;
+	fmod: int;
+	case f.mode & 7 {
+
+	OREAD =>
+		if(file.access(d, DREAD) && !wok)
+			return ferr(f, Eaccess, file, p);
+		fmod = FREAD;
+
+	OWRITE =>
+		if((d.mode & DDIR) || (file.access(d, DWRITE) && !wok))
+			return ferr(f, Eaccess, file, p);
+		if(ro)
+			return ferr(f, Eronly, file, p);
+		fmod = FWRITE;
+
+	ORDWR =>
+		if((d.mode & DDIR)
+		|| (file.access(d, DREAD) && !wok)
+		|| (file.access(d, DWRITE) && !wok))
+			return ferr(f, Eaccess, file, p);
+		if(ro)
+			return ferr(f, Eronly, file, p);
+		fmod = FREAD+FWRITE;
+
+	OEXEC =>
+		if((d.mode & DDIR) || (file.access(d, DEXEC) && !wok))
+			return ferr(f, Eaccess, file, p);
+		fmod = FREAD;
+
+	* =>
+		return ferr(f, Emode, file, p);
+	}
+	if(f.mode & OTRUNC){
+		if((d.mode & DDIR) || (file.access(d, DWRITE) && !wok))
+			return ferr(f, Eaccess, file, p);
+		if(ro)
+			return ferr(f, Eronly, file, p);
+	}
+	if(d.mode & DLOCK){
+		if((t := tlocked(file, d)) == nil)
+			return ferr(f, Elocked, file, p);
+		file.tlock = t;
+		t.file = file;
+	}
+	if(f.mode & ORCLOSE)
+		fmod |= FREMOV;
+	file.open = fmod;
+	if((f.mode & OTRUNC) && !(d.mode & DAPND)){
+		d.trunc(file.uid);
+		qid.vers = d.qid.vers;
+	}
+	file.lastra = big 1;
+	p.put();
+	file.unlock();
+	return ref Rmsg.Open(f.tag, qid, cp.msize-IOHDRSZ);
+}
+
+rcreate(cp: ref Chan, f: ref Tmsg.Create): ref Rmsg
+{
+	wok := cp == conschan || writeallow;
+
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
+		return ferr(f, Eronly, file, nil);
+
+	(d, e) := Dentry.getd(file, Bread);
+	if(e != nil)
+		return ferr(f, e, file, nil);
+	p := d.iob;
+	if(!(d.mode & DDIR))
+		return ferr(f, Edir2, file, p);
+	if(file.access(d, DWRITE) && !wok)
+		return ferr(f, Eaccess, file, p);
+	d.access(FREAD, file.uid);
+
+	#
+	# Check the name is valid and will fit in an old
+	# directory entry.
+	#
+	if((l := checkname9p2(f.name)) == 0)
+		return ferr(f, Ename, file, p);
+	if(l+1 > NAMELEN)
+		return ferr(f, Etoolong, file, p);
+	if(f.name == "." || f.name == "..")
+		return ferr(f, Edot, file, p);
+
+	addr1 := big 0;	# block with first empty slot, if any
+	slot1 := 0;
+	for(addr := big 0; ; addr++){
+		if((p1 := d.getblk(addr, 0)) == nil){
+			if(addr1 != big 0)
+				break;
+			p1 = d.getblk(addr, Tdir);
+		}
+		if(p1 == nil)
+			return ferr(f, Efull, file, p);
+		if(p1.checktag(Tdir, d.qid.path)){
+			p1.put();
+			return ferr(f, Ephase, file, p);
+		}
+		for(slot := 0; slot < DIRPERBUF; slot++){
+			d1 := Dentry.get(p1, slot);
+			if(!(d1.mode & DALLOC)){
+				if(addr1 == big 0){
+					addr1 = p1.addr;
+					slot1 = slot; # + addr*DIRPERBUF; # TODO
+				}
+				continue;
+			}
+			if(f.name == d1.name){
+				p1.put();
+				return ferr(f, Eexist, file, p);
+			}
+		}
+		p1.put();
+	}
+
+	fmod: int;
+
+	case f.mode & 7 {
+	OEXEC or
+	OREAD =>		# seems only useful to make directories
+		fmod = FREAD;
+
+	OWRITE =>
+		fmod = FWRITE;
+
+	ORDWR =>
+		fmod = FREAD+FWRITE;
+
+	* =>
+		return ferr(f, Emode, file, p);
+	}
+	if(f.perm & DMDIR)
+		if((f.mode & OTRUNC) || (f.perm & DMAPPEND) || (fmod & FWRITE))
+			return ferr(f, Eaccess, file, p);
+
+	# do it
+
+	path := qidpathgen(file.fs);
+	if((p1 := Iobuf.get(file.fs, addr1, Bread|Bimm|Bmod)) == nil)
+		return ferr(f, Ephase, file, p);
+	d1 := Dentry.get(p1, slot1);
+	if(d1 == nil || p1.checktag(Tdir, d.qid.path)){
+		p.put();
+		return ferr(f, Ephase, file, p1);
+	}
+	if(d1.mode & DALLOC){
+		p.put();
+		return ferr(f, Ephase, file, p1);
+	}
+
+	d1.name = f.name;
+	if(cp == conschan){
+		d1.uid = consuid;
+		d1.gid = consgid;
+	}
+	else{
+		d1.uid = file.uid;
+		d1.gid = d.gid;
+		f.perm &= d.mode | ~8r666;
+		if(f.perm & DMDIR)
+			f.perm &= d.mode | ~8r777;
+	}
+	d1.qid.path = big path;
+	d1.qid.vers = 0;
+	d1.mode = DALLOC | (f.perm & 8r777);
+	if(f.perm & DMDIR)
+		d1.mode |= DDIR;
+	if(f.perm & DMAPPEND)
+		d1.mode |= DAPND;
+	t: ref Tlock;
+	if(f.perm & DMEXCL){
+		d1.mode |= DLOCK;
+		t = tlocked(file, d1);
+		# if nil, out of tlock structures
+	}
+	d1.access(FWRITE, file.uid);
+	d1.change(~0);
+	d1.update();
+	qid := mkqid(path, 0, d1.mode);
+	p1.put();
+	d.change(~0);
+	d.access(FWRITE, file.uid);
+	d.update();
+	p.put();
+
+	#
+	# do a walk to new directory entry
+	#
+	file.wpath = ref Wpath(file.wpath, file.addr, file.slot);
+	file.qid = qid;
+	file.tlock = t;
+	if(t != nil)
+		t.file = file;
+	file.lastra = big 1;
+	if(f.mode & ORCLOSE)
+		fmod |= FREMOV;
+	file.open = fmod;
+	file.addr = addr1;
+	file.slot = slot1;
+	file.unlock();
+	return ref Rmsg.Create(f.tag, qid, cp.msize-IOHDRSZ);
+}
+
+dirread(cp: ref Chan, f: ref Tmsg.Read, file: ref File, d: ref Dentry): ref Rmsg
+{
+	p1: ref Iobuf;
+	d1: ref Dentry;
+
+	count := f.count;
+	data := array[count] of byte;
+	offset := f.offset;
+	iounit := cp.msize-IOHDRSZ;
+	if(count > iounit)
+		count = iounit;
+
+	# Pick up where we left off last time if nothing has changed,
+	# otherwise must scan from the beginning.
+
+	addr: big;
+	start: big;
+	slot: int;
+
+	if(offset == file.doffset){	# && file.qid.vers == file.dvers
+		addr = file.daddr;
+		slot = file.dslot;
+		start = offset;
+	}
+	else{
+		addr = big 0;
+		slot = 0;
+		start = big 0;
+	}
+
+	nread := 0;
+Dread:
+	for(;;){
+		if(d.iob == nil){
+			#
+			# This is just a check to ensure the entry hasn't
+			# gone away during the read of each directory block.
+			#
+			e: string;
+			(d, e) = Dentry.getd(file, Bread);
+			if(d == nil)
+				return ferr(f, e, file, nil);
+		}
+		p1 = d.getblk1(addr, 0);
+		if(p1 == nil)
+			break;
+		if(p1.checktag(Tdir, QPNONE))
+			return ferr(f, Ephase, file, p1);
+
+		for(; slot < DIRPERBUF; slot++){
+			d1 = Dentry.get(p1, slot);
+			if(!(d1.mode & DALLOC))
+				continue;
+			dir := dir9p2(d1);
+			n := styx->packdirsize(dir);
+			if(n > count-nread){
+				p1.put();
+				break Dread;
+			}
+			data[nread:] = styx->packdir(dir);
+			start += big n;
+			if(start < offset)
+				continue;
+			if(count < n){
+				p1.put();
+				break Dread;
+			}
+			count -= n;
+			nread += n;
+			offset += big n;
+		}
+		p1.put();
+		slot = 0;
+		addr++;
+	}
+
+	file.doffset = offset;
+	file.dvers = file.qid.vers;
+	file.daddr = addr;
+	file.dslot = slot;
+
+	d.put();
+	file.unlock();
+	return ref Rmsg.Read(f.tag, data[0:nread]);
+}
+
+rread(cp: ref Chan, f: ref Tmsg.Read): ref Rmsg
+{
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+	if(!(file.open & FREAD))
+		return ferr(f, Eopen, file, nil);
+	count := f.count;
+	iounit := cp.msize-IOHDRSZ;
+	if(count < 0 || count > iounit)
+		return ferr(f, Ecount, file, nil);
+	offset := f.offset;
+	if(offset < big 0)
+		return ferr(f, Eoffset, file, nil);
+
+	(d, e) := Dentry.getd(file, Bread);
+	if(d == nil)
+		return ferr(f, e, file, nil);
+	if((t := file.tlock) != nil){
+		tim := now();
+		if(t.time < tim || t.file != file){
+			d.put();
+			return ferr(f, Ebroken, file, nil);
+		}
+		# renew the lock
+		t.time = tim + TLOCK;
+	}
+	d.access(FREAD, file.uid);
+	if(d.mode & DDIR)
+		return dirread(cp, f, file, d);
+
+	if(offset+big count > d.size)
+		count = int (d.size - offset);
+	if(count < 0)
+		count = 0;
+	data := array[count] of byte;
+	nread := 0;
+	if(d.iob == nil){
+		# must check and reacquire entry
+		(d, e) = Dentry.getd(file, Bread);
+		if(d == nil)
+			return ferr(f, e, file, nil);
+	}
+	# read data in Dentry
+	if(offset < big Dentrydatasize){
+		if(offset + big count <= big Dentrydatasize){
+			nread = int offset+count;
+			data[0:] = d.buf[Odata+int offset:Odata+nread];
+			count -= nread;
+			offset += big nread;
+		}else{
+			nread = Dentrydatasize-int offset;
+			data[0:] = d.buf[Odata+int offset:Odata+Dentrydatasize];
+			count -= nread;
+			offset += big nread;
+		}
+	}
+	while(count > 0){
+		if(d.iob == nil){
+			# must check and reacquire entry
+			(d, e) = Dentry.getd(file, Bread);
+			if(d == nil)
+				return ferr(f, e, file, nil);
+		}
+		addr := offset / big BUFSIZE;
+		if(addr == file.lastra+big 1)
+			;	# dbufread(p, d, addr+1);
+		file.lastra = addr;
+		o := int (offset % big BUFSIZE);
+		n := BUFSIZE - o;
+		if(n > count)
+			n = count;
+		p1 := d.getblk1(addr, 0);
+		if(p1 != nil){
+			if(p1.checktag(Tfile, QPNONE)){
+				p1.put();
+				return ferr(f, Ephase, file, nil);
+			}
+			data[nread:] = p1.iobuf[o:o+n];
+			p1.put();
+		}else
+			data[nread:] = emptyblock[0:n];
+		count -= n;
+		nread += n;
+		offset += big n;
+	}
+	d.put();
+	file.unlock();
+	return ref Rmsg.Read(f.tag, data[0:nread]);
+}
+
+rwrite(cp: ref Chan, f: ref Tmsg.Write): ref Rmsg
+{
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+	if(!(file.open & FWRITE))
+		return ferr(f, Eopen, file, nil);
+	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
+		return ferr(f, Eronly, file, nil);
+	count := len f.data;
+	if(count < 0 || count > cp.msize-IOHDRSZ)
+		return ferr(f, Ecount, file, nil);
+	offset := f.offset;
+	if(offset < big 0)
+		return ferr(f, Eoffset, file, nil);
+
+	(d, e) := Dentry.getd(file, Bread|Bmod);
+	if(d == nil)
+		return ferr(f, e, file, nil);
+	if((t := file.tlock) != nil){
+		tim := now();
+		if(t.time < tim || t.file != file){
+			d.put();
+			return ferr(f, Ebroken, file, nil);
+		}
+		# renew the lock
+		t.time = tim + TLOCK;
+	}
+	d.access(FWRITE, file.uid);
+	if(d.mode & DAPND)
+		offset = d.size;
+	end := offset + big count;
+	if(end > d.size){
+		if(end > MAXFILESIZE)
+			return ferr(f, Etoobig, file, nil);
+		d.size = end;
+		d.change(Usize);
+	}
+	d.update();
+
+	nwrite := 0;
+	# write data to Dentry
+	if(d.iob == nil){
+		# must check and reacquire entry
+		(d, e) = Dentry.getd(file, Bread|Bmod);
+		if(d == nil)
+			return ferr(f, e, file, nil);
+	}
+	if(offset < big Dentrydatasize){
+		if(offset + big count <= big Dentrydatasize){
+			nwrite = int offset+count;
+			d.buf[Odata+int offset:] = f.data[0:nwrite];
+			count -= nwrite;
+			offset += big nwrite;
+		}else{
+			nwrite = Dentrydatasize-int offset;
+			d.buf[Odata+int offset:] = f.data[0:nwrite];
+			count -= nwrite;
+			offset += big nwrite;
+		}
+	}
+	while(count > 0){
+		if(d.iob == nil){
+			# must check and reacquire entry
+			(d, e) = Dentry.getd(file, Bread|Bmod);
+			if(d == nil)
+				return ferr(f, e, file, nil);
+		}
+		addr := offset / big BUFSIZE;
+		o := int (offset % big BUFSIZE);
+		n := BUFSIZE - o;
+		if(n > count)
+			n = count;
+		qpath := d.qid.path;
+		p1 := d.getblk1(addr, Tfile);
+		if(p1 == nil)
+			return ferr(f, Efull, file, nil);
+		if(p1.checktag(Tfile, qpath)){
+			p1.put();
+			return ferr(f, Ealloc, file, nil);
+		}
+		p1.iobuf[o:] = f.data[nwrite:nwrite+n];
+		p1.flags |= Bmod;
+		p1.put();
+		count -= n;
+		nwrite += n;
+		offset += big n;
+	}
+	d.put();
+	file.unlock();
+	return ref Rmsg.Write(f.tag, nwrite);
+}
+
+doremove(f: ref File, iscon: int): string
+{
+	if(isro(f.fs) || f.cons == 0 && (writegroup && !ingroup(f.uid, writegroup)))
+		return Eronly;
+	#
+	# check permission on parent directory of file to be deleted
+	#
+	if(f.wpath == nil || f.wpath.addr == f.addr)
+		return Ephase;
+	(d1, e1) := Dentry.geta(f.fs, f.wpath.addr, f.wpath.slot, QPNONE, Bread);
+	if(e1 != nil)
+		return e1;
+	if(!iscon && f.access(d1, DWRITE)){
+		d1.put();
+		return Eaccess;
+	}
+	d1.access(FWRITE, f.uid);
+	d1.put();
+
+	#
+	# check on file to be deleted
+	#
+	(d, e) := Dentry.getd(f, Bread);
+	if(e != nil)
+		return e;
+
+	#
+	# if deleting a directory, make sure it is empty
+	#
+	if(d.mode & DDIR)
+	for(addr:=big 0; (p1 := d.getblk(addr, 0)) != nil; addr++){
+		if(p1.checktag(Tdir, d.qid.path)){
+			p1.put();
+			d.put();
+			return Ephase;
+		}
+		for(slot:=0; slot<DIRPERBUF; slot++){
+			d1 = Dentry.get(p1, slot);
+			if(!(d1.mode & DALLOC))
+				continue;
+			p1.put();
+			d.put();
+			return Eempty;
+		}
+		p1.put();
+	}
+
+	#
+	# do it
+	#
+	d.trunc(f.uid);
+	d.buf[0:] = emptyblock[0:Dentrysize];
+	d.put();
+	return nil;
+}
+
+clunk(cp: ref Chan, file: ref File, remove: int, wok: int): string
+{
+	if((t := file.tlock) != nil){
+		if(t.file == file)
+			t.time = 0;		# free the lock
+		file.tlock = nil;
+	}
+	if(remove)
+		error := doremove(file, wok);
+	file.open = 0;
+	file.wpath = nil;
+	cp.putfid(file);
+
+	return error;
+}
+
+rclunk(cp: ref Chan, t: ref Tmsg.Clunk): ref Rmsg
+{
+	if((file := cp.getfid(t.fid, 0)) == nil)
+		return err(t, Efid);
+	clunk(cp, file, file.open & FREMOV, 0);
+	return ref Rmsg.Clunk(t.tag);
+}
+
+rremove(cp: ref Chan, t: ref Tmsg.Remove): ref Rmsg
+{
+	if((file := cp.getfid(t.fid, 0)) == nil)
+		return err(t, Efid);
+	e :=  clunk(cp, file, 1, cp == conschan);
+	if(e != nil)
+		return err(t, e);
+	return ref Rmsg.Remove(t.tag);
+}
+
+rstat(cp: ref Chan, f: ref Tmsg.Stat): ref Rmsg
+{
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+	(d, e) := Dentry.getd(file, Bread);
+	if(d == nil)
+		return ferr(f, e, file, nil);
+	dir := dir9p2(d);
+	if(d.qid.path == QPROOT)	# stat of root gives time
+		dir.atime = now();
+	d.put();
+	if(styx->packdirsize(dir) > cp.msize-IOHDRSZ)
+		return ferr(f, Ersc, file, nil);
+	file.unlock();
+
+	return ref Rmsg.Stat(f.tag, dir);
+}
+
+rwstat(cp: ref Chan, f: ref Tmsg.Wstat): ref Rmsg
+{
+	if((file := cp.getfid(f.fid, 0)) == nil)
+		return err(f, Efid);
+
+	# if user none, can't do anything unless in allow mode
+
+	if(file.uid == None && !wstatallow)
+		return ferr(f, Eaccess, file, nil);
+
+	if(isro(file.fs) || (writegroup && !ingroup(file.uid, writegroup)))
+		return ferr(f, Eronly, file, nil);
+
+	#
+	# first get parent
+	#
+	p1: ref Iobuf;
+	d1: ref Dentry;
+	if(file.wpath != nil){
+		p1 = Iobuf.get(file.fs, file.wpath.addr, Bread);
+		if(p1 == nil)
+			return ferr(f, Ephase, file, p1);
+		d1 = Dentry.get(p1, file.wpath.slot);
+		if(d1 == nil || p1.checktag(Tdir, QPNONE) || !(d1.mode & DALLOC))
+			return ferr(f, Ephase, file, p1);
+	}
+
+	#
+	# now the file
+	#
+	(d, e) := Dentry.getd(file, Bread);
+	if(d == nil)
+		return ferr(f, e, file, p1);
+
+	#
+	# Convert the message and fix up
+	# fields not to be changed.
+	#
+	dir := f.stat;
+	if(dir.uid == nil)
+		uid := d.uid;
+	else
+		uid = strtouid(dir.uid);
+	if(dir.gid == nil)
+		gid := d.gid;
+	else
+		gid = strtouid(dir.gid);
+	if(dir.name == nil)
+		dir.name = d.name;
+	else{
+		if((l := checkname9p2(dir.name)) == 0){
+			d.put();
+			return ferr(f, Ename, file, p1);
+		}
+		if(l+1 > NAMELEN){
+			d.put();
+			return ferr(f, Etoolong, file, p1);
+		}
+	}
+
+	# Before doing sanity checks, find out what the
+	# new 'mode' should be:
+	# if 'type' and 'mode' are both defaults, take the
+	# new mode from the old directory entry;
+	# else if 'type' is the default, use the new mode entry;
+	# else if 'mode' is the default, create the new mode from
+	# 'type' or'ed with the old directory mode;
+	# else neither are defaults, use the new mode but check
+	# it agrees with 'type'.
+	# checking for the default value of 16rFFFFFFFF also as the
+	# conversion from Dir.mode (u32) to Sys_Dir.mode(intptr)
+	# loses the top 4 bytes and ~0 will not match in those circumstances
+	if(dir.qid.qtype == 16rFF && (dir.mode == ~0||big dir.mode == big 16rFFFFFFFF)){
+		dir.mode = d.mode & 8r777;
+		if(d.mode & DLOCK)
+			dir.mode |= DMEXCL;
+		if(d.mode & DAPND)
+			dir.mode |= DMAPPEND;
+		if(d.mode & DDIR)
+			dir.mode |= DMDIR;
+	}
+	else if(dir.qid.qtype == 16rFF){
+		# nothing to do
+	}
+	else if(dir.mode == ~0||big dir.mode == big 16rFFFFFFFF){
+		dir.mode = (dir.qid.qtype<<24)|(d.mode & 8r777);
+	}
+	else if(dir.qid.qtype != ((dir.mode>>24) & 16rFF)){
+		d.put();
+		return ferr(f, Eqidmode, file, p1);
+	}
+
+	# Check for unknown type/mode bits
+	# and an attempt to change the directory bit.
+
+	if(dir.mode & ~(DMDIR|DMAPPEND|DMEXCL|8r777)){
+		d.put();
+		return ferr(f, Enotm, file, p1);
+	}
+	if(d.mode & DDIR)
+		mode := DMDIR;
+	else
+		mode = 0;
+	if((dir.mode^mode) & DMDIR){
+		d.put();
+		return ferr(f, Enotd, file, p1);
+	}
+
+	if(dir.mtime == ~0)
+		dir.mtime = d.mtime;
+	if(dir.length == ~big 0)
+		dir.length = big d.size;
+
+
+	# Currently, can't change length.
+
+	if(dir.length != big d.size){
+		d.put();
+		return ferr(f, Enotl, file, p1);
+	}
+
+
+	# if chown,
+	# must be god
+	# wstatallow set to allow chown during boot
+
+	if(uid != d.uid && !wstatallow){
+		d.put();
+		return ferr(f, Enotu, file, p1);
+	}
+
+	# if chgroup,
+	# must be either
+	#	a) owner and in new group
+	#	b) leader of both groups
+	# wstatallow and writeallow are set to allow chgrp during boot
+
+	while(gid != d.gid){
+		if(wstatallow || writeallow)
+			break;
+		if(d.uid == file.uid && ingroup(file.uid, gid))
+			break;
+		if(leadgroup(file.uid, gid))
+			if(leadgroup(file.uid, d.gid))
+				break;
+		d.put();
+		return ferr(f, Enotg, file, p1);
+	}
+
+	# if rename,
+	# must have write permission in parent
+	# TODO if NAMELEN is variable, this has to be changed to a mv or cp
+	while(d.name != dir.name){
+
+		# drop entry to prevent deadlock, then
+		# check that destination name is valid and unique
+
+		d.put();
+		if(checkname9p2(dir.name) == 0 || d1 == nil)
+			return ferr(f, Ename, file, p1);
+		if(dir.name == "." || dir.name == "..")
+			return ferr(f, Edot, file, p1);
+
+
+		for(addr := big 0; ; addr++){
+			if((p := d1.getblk(addr, 0)) == nil)
+				break;
+			if(p.checktag(Tdir, d1.qid.path)){
+				p.put();
+				continue;
+			}
+			for(slot := 0; slot < DIRPERBUF; slot++){
+				d = Dentry.get(p, slot);
+				if(!(d.mode & DALLOC))
+					continue;
+				if(dir.name == d.name){
+					p.put();
+					return ferr(f, Eexist, file, p1);
+				}
+			}
+			p.put();
+		}
+
+		# reacquire entry
+
+		(d, nil) = Dentry.getd(file, Bread);
+		if(d == nil)
+			return ferr(f, Ephase, file, p1);
+
+		if(wstatallow || writeallow) # set to allow rename during boot
+			break;
+		if(d1 == nil || file.access(d1, DWRITE)){
+			d.put();
+			return ferr(f, Eaccess, file, p1);
+		}
+		break;
+	}
+
+	# if mode/time, either
+	#	a) owner
+	#	b) leader of either group
+
+	mode = dir.mode & 8r777;
+	if(dir.mode & DMAPPEND)
+		mode |= DAPND;
+	if(dir.mode & DMEXCL)
+		mode |= DLOCK;
+	while(d.mtime != dir.mtime || ((d.mode^mode) & (DAPND|DLOCK|8r777))){
+		if(wstatallow)			# set to allow chmod during boot
+			break;
+		if(d.uid == file.uid)
+			break;
+		if(leadgroup(file.uid, gid))
+			break;
+		if(leadgroup(file.uid, d.gid))
+			break;
+		d.put();
+		return ferr(f, Enotu, file, p1);
+	}
+	d.mtime = dir.mtime;
+	d.uid = uid;
+	d.gid = gid;
+	d.mode = (mode & (DAPND|DLOCK|8r777)) | (d.mode & (DALLOC|DDIR));
+
+	d.name = dir.name;
+	d.access(FWSTAT, file.uid);
+	d.change(~0);
+	d.put();
+
+	if(p1 != nil)
+		p1.put();
+	file.unlock();
+
+	return ref Rmsg.Wstat(f.tag);
+}
+
+superok(set: int): int
+{
+	sb := Superb.get(thedevice, Bread|Bmod|Bimm);
+	ok := sb.fsok;
+	sb.fsok = set;
+	if(debug){
+		sys->print("superok ok %d\n", ok);
+		sb.print();
+	}
+	sb.touched();
+	sb.put();
+	return ok;
+}
+
+# little-endian
+get2(a: array of byte, o: int): int
+{
+	return (int a[o+1]<<8) | int a[o];
+}
+
+get2s(a: array of byte, o: int): int
+{
+	v := (int a[o+1]<<8) | int a[o];
+	if(v & 16r8000)
+		v |= ~0 << 8;
+	return v;
+}
+
+get4(a: array of byte, o: int): int
+{
+	return (int a[o+3]<<24) | (int a[o+2] << 16) | (int a[o+1]<<8) | int a[o];
+}
+
+get8(a: array of byte, o: int): big
+{
+	return (big a[o+7]<<56) | (big a[o+6] << 48) | (big a[o+5]<<40) | (big a[o+4]<<32) | (big a[o+3]<<24) | (big a[o+2] << 16) | (big a[o+1]<<8) | big a[o];
+}
+
+put2(a: array of byte, o: int, v: int)
+{
+	a[o] = byte v;
+	a[o+1] = byte (v>>8);
+}
+
+put4(a: array of byte, o: int, v: int)
+{
+	a[o] = byte v;
+	a[o+1] = byte (v>>8);
+	a[o+2] = byte (v>>16);
+	a[o+3] = byte (v>>24);
+}
+
+put8(a: array of byte, o: int, v: big)
+{
+	a[o] = byte v;
+	a[o+1] = byte (v>>8);
+	a[o+2] = byte (v>>16);
+	a[o+3] = byte (v>>24);
+	a[o+4] = byte (v>>32);
+	a[o+5] = byte (v>>40);
+	a[o+6] = byte (v>>48);
+	a[o+7] = byte (v>>56);
+}
+
+Tag.unpack(a: array of byte): Tag
+{
+	return Tag(get2(a,2), get8(a,4));
+}
+
+Tag.pack(t: self Tag, a: array of byte)
+{
+	put2(a, 0, 0);
+	put2(a, 2, t.tag);
+	if(t.path != QPNONE)
+		put8(a, 4, t.path & ~QPDIR);
+}
+
+Superb.get(dev: ref Device, flags: int): ref Superb
+{
+	p := Iobuf.get(dev, SUPERADDR, flags);
+	if(p == nil)
+		return nil;
+	if(p.checktag(Tsuper, QPSUPER)){
+		p.put();
+		return nil;
+	}
+	sb := Superb.unpack(p.iobuf);
+	sb.iob = p;
+	return sb;
+}
+
+Superb.touched(s: self ref Superb)
+{
+	s.iob.flags |= Bmod;
+}
+
+Superb.put(sb: self ref Superb)
+{
+	if(sb.iob == nil)
+		return;
+	if(sb.iob.flags & Bmod)
+		sb.pack(sb.iob.iobuf);
+	sb.iob.put();
+	sb.iob = nil;
+}
+
+#  this is the disk structure
+# Superb:
+#	Super1;
+#	Fbuf	fbuf;
+# Fbuf:
+#	nfree[4]	# 4 bytes of this and 4 bytes of fsok gets the below to a multiple of 8 address
+#	free[]	# based on BUFSIZE
+#  Super1:
+#	u64	fstart;		# starting block address, mostly 1. leaving out 0 if someone craps
+#	u64	fsize;		# total number of blocks on this device
+#	u64	tfree;		# total number of free blocks on this device
+#	u64	qidgen;		# generator for unique ids
+#	s32	fsok;		# file system ok
+#	u64	roraddr;	# dump root addr 		- obsolete, unused in this program
+#	u64	last;		# last super block addr	- obsolete, unused in this program
+#	u64	next;		# next super block addr	- obsolete, unused in this program
+
+Ofstart: con 0;
+Ofsize: con Ofstart+8;
+Otfree: con Ofsize+8;
+Oqidgen: con Otfree+8;
+Ofsok: con Oqidgen+8;
+#	Ororaddr: con Ofsok+8;
+#	Olast: con Ororaddr+8;
+#	Onext: con Olast+8;
+Super1size: con Ofsok+4; # 36
+
+Superb.unpack(a: array of byte): ref Superb
+{
+	s := ref Superb;
+	s.fstart = get8(a, Ofstart);
+	s.fsize = get8(a, Ofsize);
+	s.tfree = get8(a, Otfree);
+	s.qidgen = get8(a, Oqidgen);
+	s.fsok = get4(a, Ofsok);
+	s.fbuf = a[Super1size:];
+	return s;
+}
+
+Superb.pack(s: self ref Superb, a: array of byte)
+{
+	put8(a, Ofstart, s.fstart);
+	put8(a, Ofsize, s.fsize);
+	put8(a, Otfree, s.tfree);
+	put8(a, Oqidgen, s.qidgen);
+	put4(a, Ofsok, s.fsok);
+}
+
+Superb.print(sb: self ref Superb)
+{
+	sys->print("fstart=%bud fsize=%bud tfree=%bud qidgen=%bud fsok=%d\n",
+		sb.fstart, sb.fsize, sb.tfree, sb.qidgen, sb.fsok);
+}
+
+Dentry.get(p: ref Iobuf, slot: int): ref Dentry
+{
+	if(p == nil)
+		return nil;
+	buf := p.iobuf[slot*Dentrysize:];
+	d := Dentry.unpack(buf);
+	d.iob = p;
+	d.buf = buf;
+	return d;
+}
+
+Dentry.geta(fs: ref Device, addr: big, slot: int, qpath: big, mode: int): (ref Dentry, string)
+{
+	p := Iobuf.get(fs, addr, mode);
+	if(p == nil || p.checktag(Tdir, qpath)){
+		if(p != nil)
+			p.put();
+		return (nil, Ealloc);
+	}
+	d := Dentry.get(p, slot);
+	if(d == nil || !(d.mode & DALLOC)){
+		p.put();
+		return (nil, Ealloc);
+	}
+	return (d, nil);
+}
+
+Dentry.getd(file: ref File, mode: int): (ref Dentry, string)
+{
+	(d, e) := Dentry.geta(file.fs, file.addr, file.slot, QPNONE, mode);	# QPNONE should be file.wpath's path
+	if(e != nil)
+		return (nil, e);
+	if(file.qid.path != d.qid.path || (file.qid.qtype&QTDIR) != (d.qid.qtype&QTDIR)){
+		d.put();
+		return (nil, Eqid);
+	}
+	return (d, nil);
+}
+
+#  this is the disk structure:
+#	u16	uid;
+#	u16	gid;		[2*2]
+#	u16	mode;
+#		#define	DALLOC	0x8000
+#		#define	DDIR	0x4000
+#		#define	DAPND	0x2000
+#		#define	DLOCK	0x1000
+#		#define	DREAD	0x4
+#		#define	DWRITE	0x2
+#		#define	DEXEC	0x1
+#	[u16 muid]		[2*2]
+#	Qid.path;			[8]	16
+#	Qid.version;		[4]	20
+#	u64	size;			[8]	28
+#	u64	dblock[NDBLOCK];[8*8]	92
+#	u64	iblock[NIBLOCK];[8*6]	140
+#	u32	atime;					144
+#	u32	mtime;					148
+#	u8	name[NAMELEN];
+#	u8 data in Dentry size = 512 RBUFSIZE - 224 NAMELEN - 148 -Tagsize = 128
+
+Ouid: con 0;
+Ogid: con Ouid+2;
+Omode: con Ogid+2;
+Omuid: con Omode+2;
+Opath: con Omuid+2;
+Overs: con Opath+8;
+Osize: con Overs+4;
+Odblock: con Osize+8;
+Oiblock: con Odblock+NDBLOCK*8;
+Oatime: con Oiblock+NIBLOCK*8;
+Omtime: con Oatime+4;
+Oname: con Omtime+4;
+Odata: con Oname+NAMELEN;
+Dentrydatasize: con 128;
+Dentrysize: con Odata+Dentrydatasize;	# should be 500 = BUFSIZE = RBUFSIZE - Tagsize
+
+Dentry.unpack(a: array of byte): ref Dentry
+{
+	d := ref Dentry;
+	for(i:=0; i<NAMELEN; i++)
+		if(int a[Oname+i] == 0)
+			break;
+	d.name = string a[Oname:Oname+i];
+	d.uid = get2s(a, Ouid);
+	d.gid = get2s(a, Ogid);
+	d.mode = get2(a, Omode);
+	d.muid = get2(a, Omuid);	# note: not set by Plan 9's kfs
+	d.qid = mkqid(get8(a, Opath), get4(a, Overs), d.mode);
+	d.size = big get8(a, Osize);
+	d.atime = get4(a, Oatime);
+	d.mtime = get4(a, Omtime);
+	d.mod = 0;
+	return d;
+}
+
+Dentry.change(d: self ref Dentry, f: int)
+{
+	d.mod |= f;
+}
+
+Dentry.update(d: self ref Dentry)
+{
+	f := d.mod;
+	d.mod = 0;
+	if(d.iob == nil || (d.iob.flags & Bmod) == 0){
+		if(f != 0)
+			panic("Dentry.update");
+		return;
+	}
+	a := d.buf;
+	if(f & Uname){
+		b := array of byte d.name;
+		for(i := 0; i < NAMELEN; i++)
+			if(i < len b)
+				a[Oname+i] = b[i];
+			else
+				a[Oname+i] = byte 0;
+	}
+	if(f & Uids){
+		put2(a, Ouid, d.uid);
+		put2(a, Ogid, d.gid);
+	}
+	if(f & Umode)
+		put2(a, Omode, d.mode);
+	if(f & Uqid){
+		path := d.qid.path;
+		if(d.mode & DDIR)
+			path |= QPDIR;
+		put8(a, Opath, path);
+		put4(a, Overs, d.qid.vers);
+	}
+	if(f & Usize)
+		put8(a, Osize, d.size);
+	if(f & Utime){
+		put4(a, Omtime, d.mtime);
+		put4(a, Oatime, d.atime);
+	}
+	d.iob.flags |= Bmod;
+}
+
+Dentry.access(d: self ref Dentry, f: int, uid: int)
+{
+	if((p := d.iob) != nil && !readonly){
+		if((f & (FWRITE|FWSTAT)) == 0 && noatime)
+			return;
+		if(f & (FREAD|FWRITE|FWSTAT)){
+			d.atime = now();
+			put4(d.buf, Oatime, d.atime);
+			p.flags |= Bmod;
+		}
+		if(f & FWRITE){
+			d.mtime = now();
+			put4(d.buf, Omtime, d.mtime);
+			d.muid = uid;
+			put2(d.buf, Omuid, uid);
+			d.qid.vers++;
+			put4(d.buf, Overs, d.qid.vers);
+			p.flags |= Bmod;
+		}
+	}
+}
+
+#
+# release the directory entry buffer and thus the
+# lock on both buffer and entry, typically during i/o,
+# to be reacquired later if needed
+#
+Dentry.release(d: self ref Dentry)
+{
+	if(d.iob != nil){
+		d.update();
+		d.iob.put();
+		d.iob = nil;
+		d.buf = nil;
+	}
+}
+
+Dentry.getblk(d: self ref Dentry, a: big, tag: int): ref Iobuf
+{
+	addr := d.rel2abs(a, tag, 0);
+	if(addr == big 0)
+		return nil;
+	return Iobuf.get(thedevice, addr, Bread);
+}
+
+#
+# same as Dentry.buf but calls d.release
+# to reduce interference.
+#
+Dentry.getblk1(d: self ref Dentry, a: big, tag: int): ref Iobuf
+{
+	addr := d.rel2abs(a, tag, 1);
+	if(addr == big 0)
+		return nil;
+	return Iobuf.get(thedevice, addr, Bread);
+}
+
+Dentry.rel2abs(d: self ref Dentry, a: big, tag: int, putb: int): big
+{
+	if(a < big 0){
+		sys->print("Dentry.rel2abs: neg\n");
+		return big 0;
+	}
+	p := d.iob;
+	if(p == nil || d.buf == nil)
+		panic("nil iob");
+	data := d.buf;
+	qpath := d.qid.path;
+	dev := p.dev;
+	if(a < big NDBLOCK){
+		addr := get8(data, Odblock+int a*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, tag, qpath);
+			put8(data, Odblock+int a*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		return addr;
+	}
+	a -= big NDBLOCK;
+	if(a < INDPERBUF){
+		addr := get8(data, Oiblock);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind1, qpath);
+			put8(data, Oiblock, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		return  indfetch(dev, qpath, addr, a, Tind1, tag);
+	}
+	a -= INDPERBUF;
+	if(a < INDPERBUF2){
+		addr := get8(data, Oiblock+1*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind2, qpath);
+			put8(data, Oiblock+1*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
+		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
+	}
+	a -= INDPERBUF2;
+	if(a < INDPERBUF3){
+		addr := get8(data, Oiblock+2*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind3, qpath);
+			put8(data, Oiblock+2*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF2, Tind3, Tind2);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
+		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
+	}
+	a -= INDPERBUF3;
+	if(a < INDPERBUF4){
+		addr := get8(data, Oiblock+3*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind4, qpath);
+			put8(data, Oiblock+3*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF3, Tind4, Tind3);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF2, Tind3, Tind2);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
+		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
+	}
+	a -= INDPERBUF4;
+	if(a < INDPERBUF5){
+		addr := get8(data, Oiblock+4*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind5, qpath);
+			put8(data, Oiblock+4*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF4, Tind5, Tind4);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF3, Tind4, Tind3);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF2, Tind3, Tind2);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
+		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
+	}
+	a -= INDPERBUF5;
+	if(a < INDPERBUF6){
+		addr := get8(data, Oiblock+5*8);
+		if(addr == big 0 && tag){
+			addr = balloc(dev, Tind6, qpath);
+			put8(data, Oiblock+5*8, addr);
+			p.flags |= Bmod|Bimm;
+		}
+		if(putb)
+			d.release();
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF5, Tind6, Tind5);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF4, Tind5, Tind4);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF3, Tind4, Tind3);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF2, Tind3, Tind2);
+		addr = indfetch(dev, qpath, addr, a/INDPERBUF, Tind2, Tind1);
+		return indfetch(dev, qpath, addr, a%INDPERBUF, Tind1, tag);
+	}
+	if(putb)
+		d.release();
+	sys->print("Dentry.buf: trip indirect a %bd tag %d putb %d\n", a, tag, putb);
+	return big 0;
+}
+
+indfetch(dev: ref Device, path: big, addr: big, a: big, itag: int, tag: int): big
+{
+	if(addr == big 0)
+		return big 0;
+	bp := Iobuf.get(dev, addr, Bread);
+	if(bp == nil){
+		sys->print("ind fetch bp = nil\n");
+		return big 0;
+	}
+	if(bp.checktag(itag, path)){
+		sys->print("ind fetch tag\n");
+		bp.put();
+		return big 0;
+	}
+	addr = get8(bp.iobuf, int a*8);
+	if(addr == big 0 && tag){
+		addr = balloc(dev, tag, path);
+		if(addr != big 0){
+			put8(bp.iobuf, int a*8, addr);
+			bp.flags |= Bmod;
+			if(localfs || tag == Tdir)
+				bp.flags |= Bimm;
+			bp.settag(itag, path);
+		}
+	}
+	bp.put();
+	return addr;
+}
+
+# buffer pool management routines
+balloc(dev: ref Device, tag: int, qpath: big): big
+{
+	# TO DO: cache superblock to reduce pack/unpack
+	sb := Superb.get(dev, Bread|Bmod);
+	if(sb == nil)
+		panic("balloc: super block");
+	n := get4(sb.fbuf, 0);
+	n--;
+	sb.tfree--;
+	if(n < 0 || n >= FEPERBUF)
+		panic("balloc: bad freelist");
+	a := get8(sb.fbuf, 4+n*8);
+	if(n == 0){
+		if(a == big 0){
+			sb.tfree = big 0;
+			sb.touched();
+			sb.put();
+			return big 0;
+		}
+		bp := Iobuf.get(dev, a, Bread);
+		if(bp == nil || bp.checktag(Tfree, QPNONE)){
+			if(bp != nil)
+				bp.put();
+			sb.put();
+			return big 0;
+		}
+		sb.fbuf[0:] = bp.iobuf[0:4+FEPERBUF*8];
+		sb.touched();
+		bp.put();
+	}else{
+		put4(sb.fbuf, 0, n);
+		sb.touched();
+	}
+	bp := Iobuf.get(dev, a, Bmod);
+	bp.iobuf[0:] = emptyblock;
+	bp.settag(tag, qpath);
+	if(tag == Tind1 || tag == Tind2  ||tag == Tind3 ||
+		tag == Tind4 || tag == Tind5 || tag == Tind6 ||
+		tag == Tdir)
+		bp.flags |= Bimm;
+	bp.put();
+	sb.put();
+	return a;
+}
+
+bfree(dev: ref Device, addr: big, d: int)
+{
+	if(addr == big 0)
+		return;
+	if(d > 0){
+		d--;
+		p := Iobuf.get(dev, addr, Bread);
+		if(p != nil){
+			for(i:=int INDPERBUF-1; i>=0; i--){
+				a := get8(p.iobuf, i*8);
+				bfree(dev, a, d);
+			}
+			p.put();
+		}
+	}
+
+	# stop outstanding i/o
+	p := Iobuf.get(dev, addr, Bprobe);
+	if(p != nil){
+		p.flags &= ~(Bmod|Bimm);
+		p.put();
+	}
+
+	s := Superb.get(dev, Bread|Bmod);
+	if(s == nil)
+		panic("bfree: super block");
+	addfree(dev, addr, s);
+	s.put();
+}
+
+addfree(dev: ref Device, addr: big, sb: ref Superb)
+{
+	if(addr >= sb.fsize){
+		sys->print("addfree: bad addr %bud\n", addr);
+		return;
+	}
+	n := get4(sb.fbuf, 0);
+	if(n < 0 || n > FEPERBUF)
+		panic("addfree: bad freelist");
+	if(n >= FEPERBUF){
+		p := Iobuf.get(dev, addr, Bmod);
+		if(p == nil)
+			panic("addfree: Iobuf.get");
+		p.iobuf[0:] = sb.fbuf[0:4+FEPERBUF*8];
+		sb.fbuf[0:] = emptyblock[0:4+FEPERBUF*8];	# clear it for debugging
+		p.settag(Tfree, QPNONE);
+		p.put();
+		n = 0;
+	}
+	put8(sb.fbuf, 4+n*8, addr);
+	put4(sb.fbuf, 0, n+1);
+	sb.tfree++;
+	if(addr >= sb.fsize)
+		sb.fsize = addr+big 1;
+	sb.touched();
+}
+
+qidpathgen(dev: ref Device): big
+{
+	sb := Superb.get(dev, Bread|Bmod);
+	if(sb == nil)
+		panic("qidpathgen: super block");
+	sb.qidgen++;
+	path := sb.qidgen;
+	sb.touched();
+	sb.put();
+	return path;
+}
+
+Dentry.trunc(d: self ref Dentry, uid: int)
+{
+	p := d.iob;
+	data := d.buf;
+	bfree(p.dev, get8(data, Oiblock+5*8), 6);
+	put8(data, Oiblock+5*8, big 0);
+	bfree(p.dev, get8(data, Oiblock+4*8), 5);
+	put8(data, Oiblock+4*8, big 0);
+	bfree(p.dev, get8(data, Oiblock+3*8), 4);
+	put8(data, Oiblock+3*8, big 0);
+	bfree(p.dev, get8(data, Oiblock+2*8), 3);
+	put8(data, Oiblock+2*8, big 0);
+	bfree(p.dev, get8(data, Oiblock+1*8), 2);
+	put8(data, Oiblock+1*8, big 0);
+	bfree(p.dev, get8(data, Oiblock), 1);
+	put8(data, Oiblock, big 0);
+	for(i:=NDBLOCK-1; i>=0; i--){
+		bfree(p.dev, get8(data, Odblock+i*8), 0);
+		put8(data, Odblock+i*8, big 0);
+	}
+	d.size = big 0;
+	d.change(Usize);
+	p.flags |= Bmod|Bimm;
+	d.access(FWRITE, uid);
+	d.update();
+}
+
+Dentry.put(d: self ref Dentry)
+{
+	p := d.iob;
+	if(p == nil || d.buf == nil)
+		return;
+	d.update();
+	p.put();
+	d.iob = nil;
+	d.buf = nil;
+}
+
+Dentry.print(d: self ref Dentry, msg: string)
+{
+	sys->print("%s name=%#q len name %d len array of byte d.name %d uid=%d gid=%d mode=#%8.8ux qid.path=#%bux qid.vers=%ud size=%bud\n",
+		msg, d.name, len d.name, len array of byte d.name, d.uid, d.gid, d.mode, d.qid.path, d.qid.vers, d.size);
+	p := d.iob;
+	if(p != nil && (data := p.iobuf) != nil){
+		sys->print("\tdblock=");
+		for(i := 0; i < NDBLOCK; i++)
+			sys->print(" %bd", get8(data, Odblock+i*8));
+		sys->print("\tiblock=");
+		for(i = 0; i < NIBLOCK; i++)
+			sys->print(" %bd", get8(data, Oiblock+i*8));
+		sys->print("\n");
+	}
+}
+
+HWidth: con 5;	# initial number of buffers in each doubly linked list
+
+# buffer pool = array of Hiob's = hiob
+#	implements a locality based cache (lru cache).
+#	hb := hiob[addr%len hiob]; is used to figure out
+#		which array element to use
+hiob: array of ref Hiob;
+
+iobufinit(niob: int)
+{
+	nhiob := niob/HWidth;
+	while(!prime(nhiob))
+		nhiob++;
+	hiob = array[nhiob] of {* => ref Hiob(nil, Lock.new(), 0)};
+	# allocate the buffers now
+	for(i := 0; i < len hiob; i++){
+		h := hiob[i];
+		while(h.niob < HWidth)
+			h.newbuf();
+	}
+}
+
+iobufclear()
+{
+	# eliminate the cyclic references
+	for(i := 0; i < len hiob; i++){
+		h := hiob[i];
+		while(--h.niob >= 0){
+			p := hiob[i].link;
+			hiob[i].link = p.fore;
+			p.fore = p.back = nil;
+			p = nil;
+		}
+	}
+}
+
+prime(n: int): int
+{
+	if((n%2) == 0)
+		return 0;
+	for(i:=3;; i+=2) {
+		if((n%i) == 0)
+			return 0;
+		if(i*i >= n)
+			return 1;
+	}
+}
+
+Hiob.newbuf(hb: self ref Hiob): ref Iobuf
+{
+	# hb must be locked
+	p := ref Iobuf;
+	p.qlock = chan[1] of int;
+	q := hb.link;
+	if(q != nil){
+		p.fore = q;
+		p.back = q.back;
+		q.back = p;
+		p.back.fore = p;
+	}else{
+		hb.link = p;
+		p.fore = p;
+		p.back = p;
+	}
+	p.dev = devnone;
+	p.addr = big -1;
+	p.flags = 0;
+	p.xiobuf = array[RBUFSIZE] of byte;
+	hb.niob++;
+	return p;
+}
+
+Iobuf.get(dev: ref Device, addr: big, flags: int): ref Iobuf
+{
+	hb := hiob[int (addr%big len hiob)];
+	p: ref Iobuf;
+Search:
+	for(;;){
+		hb.lk.lock();
+		s := hb.link;
+
+		# see if it's active
+		p = s;
+		do{
+			if(p.addr == addr && p.dev == dev){
+				if(p != s){
+					p.back.fore = p.fore;
+					p.fore.back = p.back;
+					p.fore = s;
+					p.back = s.back;
+					s.back = p;
+					p.back.fore = p;
+					hb.link = p;
+				}
+				hb.lk.unlock();
+				p.lock();
+				if(p.addr != addr || p.dev != dev){
+					# lost race
+					p.unlock();
+					continue Search;
+				}
+				p.flags |= flags;
+				p.iobuf = p.xiobuf;
+				return p;
+			}
+		}while((p = p.fore) != s);
+		if(flags == Bprobe){
+			hb.lk.unlock();
+			return nil;
+		}
+
+		# steal the oldest unlocked buffer
+		do{
+			p = s.back;
+			if(p.canlock()){
+				# TO DO: if Bmod, write it out and restart Hashed
+				# for now we needn't because Iobuf.put is synchronous
+				if(p.flags & Bmod)
+					sys->print("Bmod unexpected (%bud)\n", p.addr);
+				hb.link = p;
+				p.dev = dev;
+				p.addr = addr;
+				p.flags = flags;
+				break Search;
+			}
+			s = p;
+		}while(p != hb.link);
+
+		# no unlocked blocks available; add a new one
+		p = hb.newbuf();
+		p.lock();	# return it locked
+		break;
+	}
+
+	p.dev = dev;
+	p.addr = addr;
+	p.flags = flags;
+	hb.lk.unlock();
+	p.iobuf = p.xiobuf;
+	if(flags & Bread){
+		if(wrenread(dev.fd, addr, p.iobuf)){
+			eprint(sys->sprint("error reading block %bud: %r", addr));
+			p.flags = 0;
+			p.dev = devnone;
+			p.addr = big -1;
+			p.iobuf = nil;
+			p.unlock();
+			return nil;
+		}
+	}
+	return p;
+}
+
+Iobuf.put(p: self ref Iobuf)
+{
+	if(p.flags & Bmod)
+		p.flags |= Bimm;	# temporary; see comment in Iobuf.get
+	if(p.flags & Bimm){
+		if(!(p.flags & Bmod))
+			eprint(sys->sprint("imm and no mod (%bd)", p.addr));
+		if(!wrenwrite(p.dev.fd, p.addr, p.iobuf))
+			p.flags &= ~(Bmod|Bimm);
+		else
+			panic(sys->sprint("error writing block %bud: %r", p.addr));
+	}
+	p.iobuf = nil;
+	p.unlock();
+}
+
+Iobuf.lock(p: self ref Iobuf)
+{
+	p.qlock <-= 1;
+}
+
+Iobuf.canlock(p: self ref Iobuf): int
+{
+	alt{
+	p.qlock <-= 1 =>
+		return 1;
+	* =>
+		return 0;
+	}
+}
+
+Iobuf.unlock(p: self ref Iobuf)
+{
+	<-p.qlock;
+}
+
+File.access(f: self ref File, d: ref Dentry, m: int): int
+{
+	if(wstatallow)
+		return 0;
+
+	# none gets only other permissions
+
+	if(f.uid != None){
+		if(f.uid == d.uid)	# owner
+			if((m<<6) & d.mode)
+				return 0;
+		if(ingroup(f.uid, d.gid))	# group membership
+			if((m<<3) & d.mode)
+				return 0;
+	}
+
+	#
+	# other access for everyone except members of group "noworld"
+	#
+	if(m & d.mode){
+		#
+		# walk directories regardless.
+		# otherwise it's impossible to get
+		# from the root to noworld's directories.
+		#
+		if((d.mode & DDIR) && (m == DEXEC))
+			return 0;
+		if(!ingroup(f.uid, Noworld))
+			return 0;
+	}
+	return 1;
+}
+
+tagname(t: int): string
+{
+	case t {
+	Tnone =>	return "Tnone";
+	Tsuper =>	return "Tsuper";
+	Tdir => return "Tdir";
+	Tind1 => return "Tind1";
+	Tind2 => return "Tind2";
+	Tind3 => return "Tind3";
+	Tind4 => return "Tind4";
+	Tind5 => return "Tind5";
+	Tind6 => return "Tind6";
+	Tind7 => return "Tind7";
+	Tfile => return "Tfile";
+	Tfree => return "Tfree";
+	Tbuck => return "Tbuck";
+	Tvirgo => return "Tvirgo";
+	Tcache => return "Tcache";
+	* =>	return sys->sprint("%d", t);
+	}
+}
+
+Iobuf.checktag(p: self ref Iobuf, tag: int, qpath: big): int
+{
+	t := Tag.unpack(p.iobuf[BUFSIZE:]);
+	if(t.tag != tag){
+		if(1)
+			eprint(sys->sprint("	tag = %s; expected %s; addr = %bud\n",
+				tagname(t.tag), tagname(tag), p.addr));
+		return 2;
+	}
+	if(qpath != QPNONE){
+		qpath &= ~QPDIR;
+		if(qpath != t.path){
+			if(qpath == (t.path&~QPDIR))	# old bug
+				return 0;
+			if(1)
+				eprint(sys->sprint("	tag/path = %bux; expected %s/%bux\n",
+					t.path, tagname(tag), qpath));
+			return 1;
+		}
+	}
+	return 0;
+}
+
+Iobuf.settag(p: self ref Iobuf, tag: int, qpath: big)
+{
+	Tag(tag, qpath).pack(p.iobuf[BUFSIZE:]);
+	p.flags |= Bmod;
+}
+
+badmagic := 0;
+wmagic := "kfs wren device\n";
+
+wrenream(dev: ref Device)
+{
+	if(RBUFSIZE % 512)
+		panic(sys->sprint("kfs: bad buffersize(%d): restart a multiple of 512", RBUFSIZE));
+	if(RBUFSIZE > MAXBUFSIZE)
+		panic(sys->sprint("kfs: bad buffersize(%d): must be at most %d", RBUFSIZE, MAXBUFSIZE));
+	sys->print("kfs: reaming the file system using %d byte blocks\n", RBUFSIZE);
+	buf := array[RBUFSIZE] of {* => byte 0};
+	buf[256:] = sys->aprint("%s%d\n", wmagic, RBUFSIZE);
+	if(sys->seek(dev.fd, big 0, 0) < big 0 || sys->write(dev.fd, buf, len buf) != len buf)
+		panic("can't ream disk");
+}
+
+wreninit(dev: ref Device): int
+{
+	(ok, nil) := sys->fstat(dev.fd);
+	if(ok < 0)
+		return 0;
+	buf := array[MAXBUFSIZE] of byte;
+	sys->seek(dev.fd, big 0, 0);
+	n := sys->read(dev.fd, buf, len buf);
+	if(n < len buf)
+		return 0;
+	badmagic = 0;
+	RBUFSIZE = 512;
+	if(string buf[256:256+len wmagic] != wmagic){
+		badmagic = 1;
+		return 0;
+	}
+	RBUFSIZE = int string buf[256+len wmagic:256+len wmagic+12];
+	if(RBUFSIZE % 512)
+		error("bad block size");
+	return 1;
+}
+
+wrenread(fd: ref Sys->FD, addr: big, a: array of byte): int
+{
+	return sys->pread(fd, a, len a, addr * big RBUFSIZE) != len a;
+}
+
+wrenwrite(fd: ref Sys->FD, addr: big, a: array of byte): int
+{
+	return sys->pwrite(fd, a, len a, addr * big RBUFSIZE) != len a;
+}
+
+wrentag(buf: array of byte, tag: int, qpath: big): int
+{
+	t := Tag.unpack(buf[BUFSIZE:]);
+	return t.tag != tag || (qpath&~QPDIR) != t.path;
+}
+
+wrencheck(fd: ref Sys->FD): int
+{
+	if(badmagic)
+		return 1;
+	buf := array[RBUFSIZE] of byte;
+	if(wrenread(fd, SUPERADDR, buf) || wrentag(buf, Tsuper, QPSUPER) ||
+	    wrenread(fd, ROOTADDR, buf) || wrentag(buf, Tdir, QPROOT))
+		return 1;
+	d0 := Dentry.unpack(buf);
+	if(d0.mode & DALLOC)
+		return 0;
+	return 1;
+}
+
+wrensize(dev: ref Device): big
+{
+	(ok, d) := sys->fstat(dev.fd);
+	if(ok < 0)
+		return big -1;
+	return (d.length / big RBUFSIZE);
+}
+
+checkname9p2(s: string): int
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] <= 8r40)
+			return 0;
+	return styx->utflen(s);
+}
+
+isro(d: ref Device): int
+{
+	return d == nil || d.ronly;
+}
+
+tlocks: list of ref Tlock;
+
+tlocked(f: ref File, d: ref Dentry): ref Tlock
+{
+	tim := now();
+	path := d.qid.path;
+	t1: ref Tlock;
+	for(l := tlocks; l != nil; l = tl l){
+		t := hd l;
+		if(t.qpath == path && t.time >= tim && t.dev == f.fs)
+			return nil;	# it's locked
+		if(t.file == nil || t1 == nil && t.time < tim)
+			t1 = t;
+	}
+	t := t1;
+	if(t == nil)
+		t = ref Tlock;
+	t.dev = f.fs;
+	t.qpath = path;
+	t.time = tim + TLOCK;
+	tlocks = t :: tlocks;
+	return t;
+}
+
+mkqid(path: big, vers: int, mode: int): Qid
+{
+	qid: Qid;
+
+	qid.path = (path & ~QPDIR);
+	qid.vers = vers;
+	qid.qtype = 0;
+	if(mode & DDIR)
+		qid.qtype |= QTDIR;
+	if(mode & DAPND)
+		qid.qtype |= QTAPPEND;
+	if(mode & DLOCK)
+		qid.qtype |= QTEXCL;
+	return qid;
+}
+
+dir9p2(d: ref Dentry): Sys->Dir
+{
+	dir: Sys->Dir;
+
+	dir.name = d.name;
+	dir.uid = uidtostr(d.uid);
+	dir.gid = uidtostr(d.gid);
+	dir.muid = uidtostr(d.muid);
+	dir.qid = d.qid;
+	dir.mode = d.mode & 8r777;
+	if(d.mode & DDIR)
+		dir.mode |= DMDIR;
+	if(d.mode & DAPND)
+		dir.mode |= DMAPPEND;
+	if(d.mode & DLOCK)
+		dir.mode |= DMEXCL;
+	dir.atime = d.atime;
+	dir.mtime = d.mtime;
+	dir.length = d.size;
+	dir.dtype = 0;
+	dir.dev = 0;
+	return dir;
+}
+
+rootream(dev: ref Device, addr: big)
+{
+	p := Iobuf.get(dev, addr, Bmod|Bimm);
+	p.iobuf[0:] = emptyblock;
+	p.settag(Tdir, QPROOT);
+	d := Dentry.get(p, 0);
+	d.name = "/";
+	d.uid = -1;
+	d.gid = -1;
+	d.mode = DALLOC | DDIR |
+		((DREAD|DWRITE|DEXEC) << 6) |
+		((DREAD|DWRITE|DEXEC) << 3) |
+		((DREAD|DWRITE|DEXEC) << 0);
+	d.qid.path = QPROOT;
+	d.qid.vers = 0;
+	d.qid.qtype = QTDIR;
+	d.atime = now();
+	d.mtime = d.atime;
+	d.change(~0);
+	d.access(FREAD|FWRITE, -1);
+	d.update();
+	p.put();
+}
+
+superream(dev: ref Device, addr: big)
+{
+	fsize := wrensize(dev);
+	if(fsize <= big 0)
+		panic("file system device size");
+	p := Iobuf.get(dev, addr, Bmod|Bimm);
+	p.iobuf[0:] = emptyblock;
+	p.settag(Tsuper, QPSUPER);
+	sb := ref Superb;
+	sb.iob = p;
+	sb.fstart = big 1;
+	sb.fsize = fsize;
+	sb.qidgen = big 10;
+	sb.tfree = big 0;
+	sb.fsok = 0;
+	sb.fbuf = p.iobuf[Super1size:];
+	put4(sb.fbuf, 0, 1);	# nfree = 1
+	for(i := fsize-big 1; i>=addr+big 2; i--){
+		#sys->print("superream i %bd\n", i);
+		addfree(dev, i, sb); # what does this do?
+	}
+	sb.put();
+}
+
+eprint(s: string)
+{
+	sys->print("kfs: %s\n", s);
+}
+
+#
+# /adm/users
+#
+# uid:user:leader:members[,...]
+
+User: adt {
+	uid:	int;
+	name:	string;
+	leader:	int;
+	mem:	list of int;
+};
+
+users: list of ref User;
+
+admusers := array[] of {
+	(-1, "adm", "adm"),
+	(None, "none", "adm"),
+	(Noworld, "noworld", nil),
+	(10000, "sys", nil),
+	(10001, "upas", "upas"),
+	(10002, "bootes", "bootes"),
+	(10006, "inferno", nil),
+};
+
+userinit()
+{
+	if(!cmd_users() && users == nil){
+		cprint("initializing minimal user table");
+		defaultusers();
+	}
+	writegroup = strtouid("write");
+}
+
+cmd_users(): int
+{
+	if(kopen(FID1, FID2, array[] of {"adm", "users"}, OREAD) != nil)
+		return 0;
+	buf: array of byte;
+	for(off := big 0;;){
+		(a, e) := kread(FID2, off, Styx->MAXFDATA);
+		if(e != nil){
+			cprint("/adm/users read error: "+e);
+			return 0;
+		}
+		if(len a == 0)
+			break;
+		off += big len a;
+		if(buf != nil){
+			c := array[len buf + len a] of byte;
+			if(buf != nil)
+				c[0:] = buf;
+			c[len buf:] = a;
+			buf = c;
+		}else
+			buf = a;
+	}
+	kclose(FID2);
+
+	# (uid:name:lead:mem,...\n)+
+	(nl, lines) := sys->tokenize(string buf, "\n");
+	if(nl == 0){
+		cprint("empty /adm/users");
+		return 0;
+	}
+	oldusers := users;
+	users = nil;
+
+	# first pass: enter id:name
+	for(l := lines; l != nil; l = tl l){
+		uid, name, r: string;
+		s := hd l;
+		if(s == "" || s[0] == '#')
+			continue;
+		(uid, r) = field(s, ':');
+		(name, r) = field(r, ':');
+		if(uid == nil || name == nil || string int uid != uid){
+			cprint("invalid /adm/users line: "+hd l);
+			users = oldusers;
+			return 0;
+		}
+		adduser(int uid, name, nil, nil);
+	}
+
+	# second pass: groups and leaders
+	for(l = lines; l != nil; l = tl l){
+		s := hd l;
+		if(s == "" || s[0] == '#')
+			continue;
+		name, lead, mem, r: string;
+		(nil, r) = field(s, ':');	# skip id
+		(name, r) = field(r, ':');
+		(lead, mem) = field(r, ':');
+		(nil, mems) := sys->tokenize(mem, ",\n");
+		if(name == nil || lead == nil && mems == nil)
+			continue;
+		u := finduname(name);
+		if(lead != nil){
+			lu := strtouid(lead);
+			if(lu != None)
+				u.leader = lu;
+			else if(lead != nil)
+				u.leader = u.uid;	# mimic kfs not fs
+		}
+		mids: list of int = nil;
+		for(; mems != nil; mems = tl mems){
+			lu := strtouid(hd mems);
+			if(lu != None)
+				mids = lu :: mids;
+		}
+		u.mem = mids;
+	}
+
+	if(debug)
+	for(x := users; x != nil; x = tl x){
+		u := hd x;
+		sys->print("%d : %q : %d :", u.uid, u.name, u.leader);
+		for(y := u.mem; y != nil; y = tl y)
+			sys->print(" %d", hd y);
+		sys->print("\n");
+	}
+	return 1;
+}
+
+field(s: string, c: int): (string, string)
+{
+	for(i := 0; i < len s; i++)
+		if(s[i] == c)
+			return (s[0:i], s[i+1:]);
+	return (s, nil);
+}
+
+defaultusers()
+{
+	for(i := 0; i < len admusers; i++){
+		(id, name, leader) := admusers[i];
+		adduser(id, name, leader, nil);
+	}
+}
+
+finduname(s: string): ref User
+{
+	for(l := users; l != nil; l = tl l){
+		u := hd l;
+		if(u.name == s)
+			return u;
+	}
+	return nil;
+}
+
+uidtostr(id: int): string
+{
+	if(id == None)
+		return "none";
+	for(l := users; l != nil; l = tl l){
+		u := hd l;
+		if(u.uid == id)
+			return u.name;
+	}
+	return sys->sprint("#%d", id);
+}
+
+leadgroup(ui: int, gi: int): int
+{
+	for(l := users; l != nil; l = tl l){
+		u := hd l;
+		if(u.uid == gi){
+			if(u.leader == ui)
+				return 1;
+			if(u.leader == 0)
+				return ingroup(ui, gi);
+			return 0;
+		}
+	}
+	return 0;
+}
+
+strtouid(s: string): int
+{
+	if(s == "none")
+		return None;
+	u := finduname(s);
+	if(u != nil)
+		return u.uid;
+	return 0;
+}
+
+ingroup(uid: int, gid: int): int
+{
+	if(uid == gid)
+		return 1;
+	for(l := users; l != nil; l = tl l){
+		u := hd l;
+		if(u.uid == gid){
+			for(m := u.mem; m != nil; m = tl m)
+				if(hd m == uid)
+					return 1;
+			return 0;
+		}
+	}
+	return 0;
+}
+
+baduname(s: string): int
+{
+	n := checkname9p2(s);
+	if(n == 0 || n+1 > NAMELEN || s == "." || s == ".."){
+		sys->print("kfs: illegal user name %q\n", s);
+		return 1;
+	}
+	return 0;
+}
+
+adduser(id: int, name: string, leader: string, mem: list of string)
+{
+	if(baduname(name))
+		return;
+	for(l := users; l != nil; l = tl l){
+		u := hd l;
+		if(u.uid == id){
+			sys->print("kfs: duplicate user ID %d (name %q)\n", id, u.name);
+			return;
+		}else if(u.name == name){
+			sys->print("kfs: duplicate user name %q (id %d)\n", name, u.uid);
+			return;
+		}
+	}
+	if(name == leader)
+		lid := id;
+	else if(leader == nil)
+		lid = 0;
+	else if(!baduname(leader))
+		lid = strtouid(leader);
+	else
+		return;
+	memid: list of int;
+	for(; mem != nil; mem = tl mem){
+		if(baduname(hd mem))
+			return;
+		x := strtouid(hd mem);
+		if(x != 0)
+			memid = x :: memid;
+	}
+	u := ref User(id, name, lid, memid);
+	users = u :: users;
+}
+
+Lock.new(): ref Lock
+{
+	return ref Lock(chan[1] of int);
+}
+
+Lock.lock(l: self ref Lock)
+{
+	l.c <-= 1;
+}
+
+Lock.canlock(l: self ref Lock): int
+{
+	alt{
+	l.c <-= 1 =>
+		return 1;
+	* =>
+		return 0;
+	}
+}
+
+Lock.unlock(l: self ref Lock)
+{
+	<-l.c;
+}
+
+#
+# kfs check, could be a separate module if that seemed important
+#
+
+MAXDEPTH: con 100;
+MAXNAME: con 4000;
+
+# bit map
+# bit array with each bit representing a block
+Map: adt {
+	lo, hi:	big;
+	bits:	array of byte;
+	nbad:	big;
+	ndup:	big;
+	nmark:	big;
+
+	new:	fn(lo, hi: big): ref Map;
+	isset:	fn(b: self ref Map, a: big): int;
+	mark:	fn(b: self ref Map, a: big): string;
+};
+
+Check: adt {
+	dev:	ref Device;
+
+	amap:	ref Map;
+	qmap:	ref Map;
+
+	name:	string;
+	nfiles:	big;
+	maxq:	big;
+
+	mod:	int;
+	flags:	int;
+	oldblock:	big;
+
+	depth:	int;
+	maxdepth:	int;
+
+	check:	fn(c: self ref Check);
+	touch:	fn(c: self ref Check, a: big): int;
+	checkdir:	fn(c: self ref Check, a: big, qpath: big): int;
+	checkindir:	fn(c: self ref Check, a: big, d: ref Dentry, qpath: big, indir: int): int;
+	maked:	fn(c: self ref Check, a: big, s: int, qpath: big): ref Dentry;
+	modd:	fn(c: self ref Check, a: big, s: int, d: ref Dentry);
+	fsck:		fn(c: self ref Check, d: ref Dentry): int;
+	xread:	fn(c: self ref Check, a: big, qpath: big);
+	xtag:		fn(c: self ref Check, a: big, tag: int, qpath: big): ref Iobuf;
+	ckfreelist:	fn(c: self ref Check, sb: ref Superb);
+	mkfreelist:	fn(c: self ref Check, sb: ref Superb);
+	amark:	fn(c: self ref Check, a: big): int;
+	fmark:	fn(c: self ref Check, a: big): int;
+	missing:	fn(c: self ref Check, sb: ref Superb);
+	qmark:	fn(c: self ref Check, q: big);
+};
+
+check(dev: ref Device, flag: int)
+{
+	#mainlock.wlock();
+	#mainlock.wunlock();
+	c := ref Check;
+	c.dev = dev;
+	c.nfiles = big 0;
+	c.maxq = big 0;
+	c.mod = 0;
+	c.flags = flag;
+	c.oldblock = big 0;
+	c.depth = 0;
+	c.maxdepth = 0;
+	c.check();
+}
+
+checkflags(s: string): int
+{
+	f := 0;
+	for(i := 0; i < len s; i++)
+		case s[i] {
+		'r' =>	f |= Crdall;
+		't' => f |= Ctag;
+		'P' => f |= Cpfile;
+		'p' => f |= Cpdir;
+		'f' => f |= Cfree;
+		'c' => f |= Cream;
+		'd' => f |= Cbad;
+		'w' => f |= Ctouch;
+		'q' => f |= Cquiet;
+		'v' => ;	# old verbose flag; ignored
+		* =>	return -1;
+	}
+	return f;
+}
+
+Check.check(c: self ref Check)
+{
+	sbaddr := SUPERADDR;
+	p := c.xtag(sbaddr, Tsuper, QPSUPER);
+	if(p == nil){
+		cprint(sys->sprint("bad superblock"));
+		return;
+	}
+	sb := Superb.unpack(p.iobuf);
+	sb.iob = p;
+
+	fstart := sb.fstart;
+	if(fstart != big 1){
+		cprint(sys->sprint("invalid superblock"));
+		return;
+	}
+	fsize := sb.fsize;
+	if(fsize < fstart || fsize > wrensize(c.dev)){
+		cprint(sys->sprint("invalid size in superblock"));
+		return;
+	}
+	c.amap = Map.new(fstart, fsize);
+
+	nqid := sb.qidgen+big 100;		# not as much of a botch
+	if(nqid > big (1024*1024*8))
+		nqid = big (1024*1024*8);
+	if(nqid < big (64*1024))
+		nqid = big (64*1024);
+	c.qmap = Map.new(big 0, nqid);
+
+	c.mod = 0;
+	c.depth = 0;
+	c.maxdepth = 0;
+
+	if(c.amark(sbaddr))
+		{}
+
+	if(!(c.flags & Cquiet))
+		cprint(sys->sprint("checking file system: %s", "main"));
+	c.nfiles = big 0;
+	c.maxq = big 0;
+
+	d := c.maked(ROOTADDR, 0, QPROOT);
+	if(d != nil){
+		if(c.amark(ROOTADDR))
+			{}
+		if(c.fsck(d))
+			c.modd(ROOTADDR, 0, d);
+		if(--c.depth != 0)
+			cprint("depth not zero on return");
+	}
+	if(sb.qidgen < c.maxq)
+		cprint(sys->sprint("qid generator low path=%bd maxq=%bd", sb.qidgen, c.maxq));
+
+	nqbad := c.qmap.nbad + c.qmap.ndup;
+	c.qmap = nil;	# could use to implement resequence
+
+	ndup := c.amap.ndup;
+	nused := c.amap.nmark;
+
+	c.amap.ndup = c.amap.nmark = big 0;	# reset for free list counts
+	if(c.flags & Cfree){
+		c.name = "free list";
+		c.mkfreelist(sb);
+		sb.qidgen = c.maxq;
+		p.settag(Tsuper, QPNONE);
+	}else
+		c.ckfreelist(sb);
+
+	nbad := c.amap.nbad;
+	nfdup := c.amap.ndup;
+	nfree := c.amap.nmark;
+	# leave amap for missing, below
+
+	if(c.mod){
+		cprint("file system was modified");
+		p.settag(Tsuper, QPNONE);
+	}
+
+	if(!(c.flags & Cquiet)){
+		cprint(sys->sprint("%8bd files", c.nfiles));
+		cprint(sys->sprint("%8bd blocks in the file system", fsize-fstart));
+		cprint(sys->sprint("%8bd used blocks", nused));
+		cprint(sys->sprint("%8bd free blocks", sb.tfree));
+	}
+	if(!(c.flags & Cfree)){
+		if(nfree != sb.tfree)
+			cprint(sys->sprint("%8bd free blocks found", nfree));
+		if(nfdup > big 0)
+			cprint(sys->sprint("%8bd blocks duplicated in the free list", nfdup));
+		if(fsize-fstart-nused-nfree > big 0)
+			cprint(sys->sprint("%8bd missing blocks", fsize-fstart-nused-nfree));
+	}
+	if(ndup > big 0)
+		cprint(sys->sprint("%8bd address duplications", ndup));
+	if(nbad > big 0)
+		cprint(sys->sprint("%8bd bad block addresses", nbad));
+	if(nqbad > big 0)
+		cprint(sys->sprint("%8bd bad qids", nqbad));
+	if(!(c.flags & Cquiet))
+		cprint(sys->sprint("%8bd maximum qid path", c.maxq));
+	c.missing(sb);
+
+	sb.put();
+}
+
+Check.touch(c: self ref Check, a: big): int
+{
+	if((c.flags&Ctouch) && a > big 0){
+		p := Iobuf.get(c.dev, a, Bread|Bmod);
+		if(p != nil)
+			p.put();
+		return 1;
+	}
+	return 0;
+}
+
+# check directory Dentry at address a
+Check.checkdir(c: self ref Check, a: big, qpath: big): int
+{
+	 ns := len c.name;
+	 dmod := c.touch(a);
+	 for(i:=0; i<DIRPERBUF; i++){
+		nd := c.maked(a, i, qpath);
+		if(nd == nil)
+			break;
+		if(c.fsck(nd)){
+			c.modd(a, i, nd);
+			dmod++;
+		}
+		c.depth--;
+		c.name = c.name[0:ns];
+	 }
+	 c.name = c.name[0:ns];
+	 return dmod;
+}
+
+# check a block of pointers to blocks
+#	the block pointed to has a Dentry if indirection = 1
+#	else it is anoher block of pointers - multiple levels of indirection
+#	recursively drills through each level of indirection
+Check.checkindir(c: self ref Check, a: big, d: ref Dentry, qpath: big, indir: int): int
+{
+	dmod := c.touch(a);
+	ind := 0;
+	case indir
+	{
+		1 => ind = Tind1;
+		2 => ind = Tind2;
+		3 => ind = Tind3;
+		4 => ind = Tind4;
+		5 => ind = Tind5;
+		6 => ind = Tind6;
+		* => cprint(sys->sprint("Check.checkindir invalid indirection indir %d\n", indir));
+			 return dmod+1; # TODO is this correct?
+	}
+	p := c.xtag(a, ind, qpath);
+	if(p == nil)
+		return dmod;
+	for(i:=0; i<int INDPERBUF; i++){
+		a = get8(p.iobuf, i*8);
+		if(a == big 0)
+			continue;
+		if(c.amark(a)){
+			if(c.flags & Cbad){
+				put8(p.iobuf, i*8, big 0);
+				p.flags |= Bmod;
+			}
+			continue;
+		}
+		if(d.mode & DDIR){
+			if(indir == 1)
+				dmod += c.checkdir(a, qpath);
+			else
+				dmod += c.checkindir(a, d, qpath, indir-1);
+		}else if(c.flags & Crdall)
+			c.xread(a, qpath);
+	}
+	p.put();
+	return dmod;
+}
+
+Check.fsck(c: self ref Check, d: ref Dentry): int
+{
+	# p: ref Iobuf;
+	i: int;
+	a, qpath: big;
+
+	if(++c.depth >= c.maxdepth){
+		c.maxdepth = c.depth;
+		if(c.maxdepth >= MAXDEPTH){
+			cprint(sys->sprint("max depth exceeded: %s", c.name));
+			return 0;
+		}
+	}
+	dmod := 0;
+	if(!(d.mode & DALLOC))
+		return 0;
+	c.nfiles++;
+
+	ns := len c.name;
+	i = styx->utflen(d.name);
+	if(i >= NAMELEN){
+		d.name[NAMELEN-1] = 0;	# TO DO: not quite right
+		cprint(sys->sprint("%q.name (%q) not terminated", c.name, d.name));
+		return 0;
+	}
+	ns += i;
+	if(ns >= MAXNAME){
+		cprint(sys->sprint("%q.name (%q) name too large", c.name, d.name));
+		return 0;
+	}
+	c.name += d.name;
+
+	if(d.mode & DDIR){
+		if(ns > 1)
+			c.name += "/";
+		if(c.flags & Cpdir)
+			cprint(sys->sprint("%s", c.name));
+	} else if(c.flags & Cpfile)
+		cprint(sys->sprint("%s", c.name));
+
+	qpath = d.qid.path & ~QPDIR;
+	c.qmark(qpath);
+	if(qpath > c.maxq)
+		c.maxq = qpath;
+	for(i=0; i<NDBLOCK; i++){
+		a = get8(d.buf, Odblock+i*8);
+		if(a == big 0)
+			continue;
+		if(c.amark(a)){
+			put8(d.buf, Odblock+i*8, big 0); # erasing the block, something wrong
+			dmod++;
+			continue;
+		}
+		if(d.mode & DDIR)
+			dmod += c.checkdir(a, qpath);
+		else if(c.flags & Crdall)
+			c.xread(a, qpath);
+	}
+	for(i=0; i<NIBLOCK; i++){
+		a = get8(d.buf, Oiblock+i*8);
+		if(a == big 0)
+			continue;
+		if(c.amark(a)){
+			put8(d.buf, Oiblock+i*8, big 0); # erasing the block, something wrong
+			dmod++;
+			continue;
+		}
+		if(d.mode & DDIR)
+			dmod += c.checkindir(a, d, qpath, i+1);
+		else if(c.flags & Crdall)
+			c.xread(a, qpath);
+	}
+	return dmod;
+}
+
+Check.ckfreelist(c: self ref Check, sb: ref Superb)
+{
+	c.name = "free list";
+	cprint(sys->sprint("check %s", c.name));
+	fb := sb.fbuf;
+	a := SUPERADDR;
+	p: ref Iobuf;
+	lo := big 0;
+	hi := big 0;
+	for(;;){
+		n := get4(fb, 0);		# nfree
+		if(n < 0 || n > FEPERBUF){
+			cprint(sys->sprint("check: nfree bad %bd", a));
+			break;
+		}
+		for(i:=1; i<n; i++){
+			a = get8(fb, 4+i*8);	# free[i]
+			if(a > big 0 && !c.fmark(a)){
+				if(lo == big 0 || lo > a)
+					lo = a;
+				if(hi == big 0 || hi < a)
+					hi = a;
+			}
+		}
+		a = get8(fb, 4);	# free[0]
+		if(a == big 0)
+			break;
+		if(c.fmark(a))
+			break;
+		if(lo == big 0 || lo > a)
+			lo = a;
+		if(hi == big 0 || hi < a)
+			hi = a;
+		if(p != nil)
+			p.put();
+		p = c.xtag(a, Tfree, QPNONE);
+		if(p == nil)
+			break;
+		fb = p.iobuf;
+	}
+	if(p != nil)
+		p.put();
+	cprint(sys->sprint("lo = %bd; hi = %bd", lo, hi));
+}
+
+#
+# make freelist from scratch
+#
+Check.mkfreelist(c: self ref Check, sb: ref Superb)
+{
+	sb.fbuf[0:] = emptyblock[0:4+FEPERBUF*8];
+	sb.tfree = big 0;
+	put4(sb.fbuf, 0, 1);	# nfree = 1
+	for(a:=sb.fsize-sb.fstart-big 1; a >= big 0; a--){
+		i := a>>3;
+		if(i < big 0 || i >= big len c.amap.bits)
+			continue;
+		b := byte (1 << int (a&big 7));
+		if((c.amap.bits[int i] & b) != byte 0)
+			continue;
+		addfree(c.dev, sb.fstart+a, sb);
+		c.amap.bits[int i] |= b;
+	}
+	sb.iob.flags |= Bmod;
+}
+
+#
+# makes a copy of a Dentry's representation on disc so that
+# the rest of the much larger iobuf can be freed.
+#
+Check.maked(c: self ref Check, a: big, s: int, qpath: big): ref Dentry
+{
+	p := c.xtag(a, Tdir, qpath);
+	if(p == nil)
+		return nil;
+	d := Dentry.get(p, s);
+	if(d == nil)
+		return nil;
+	copy := array[len d.buf] of byte;
+	copy[0:] = d.buf;
+	d.put();
+	d.buf = copy;
+	return d;
+}
+
+Check.modd(c: self ref Check, a: big, s: int, d1: ref Dentry)
+{
+	if(!(c.flags & Cbad))
+		return;
+	p := Iobuf.get(c.dev, a, Bread);
+	d := Dentry.get(p, s);
+	if(d == nil){
+		if(p != nil)
+			p.put();
+		return;
+	}
+	d.buf[0:] = d1.buf;
+	p.flags |= Bmod;
+	p.put();
+}
+
+Check.xread(c: self ref Check, a: big, qpath: big)
+{
+	p := c.xtag(a, Tfile, qpath);
+	if(p != nil)
+		p.put();
+}
+
+Iobuf.print(iob: self ref Iobuf, msg: string)
+{
+	sys->print("%s show Iobuf\n", msg);
+	buf := iob.iobuf[0:];
+	if(iob != nil)
+		for(i:= 0; i<RBUFSIZE/8; i++){
+			sys->print("%d 0x%bux	", i*8, get8(buf, i*8));
+		}
+	sys->print("\n");
+}
+
+Check.xtag(c: self ref Check, a: big, tag: int, qpath: big): ref Iobuf
+{
+	if(a == big 0)
+		return nil;
+	p := Iobuf.get(c.dev, a, Bread);
+	if(p == nil){
+		cprint(sys->sprint("check: \"%s\": xtag: p null", c.name));
+		if(c.flags & (Cream|Ctag)){
+			p = Iobuf.get(c.dev, a, Bmod);
+			if(p != nil){
+				p.iobuf[0:] = emptyblock;
+				p.settag(tag, qpath);
+				c.mod++;
+				return p;
+			}
+		}
+		return nil;
+	}
+	if(p.checktag(tag, qpath)){
+		cprint(sys->sprint("check: \"%s\": xtag: checktag", c.name));
+		if(c.flags & Cream)
+			p.iobuf[0:] = emptyblock;
+		if(c.flags & (Cream|Ctag)){
+			p.settag(tag, qpath);
+			c.mod++;
+		}
+		return p;
+	}
+	return p;
+}
+
+Check.amark(c: self ref Check, a: big): int
+{
+	e := c.amap.mark(a);
+	if(e != nil){
+		cprint(sys->sprint("check: \"%s\": %s %bd", c.name, e, a));
+		return e != "dup";	# don't clear dup blocks because rm might repair
+	}
+	return 0;
+}
+
+Check.fmark(c: self ref Check,a: big): int
+{
+	e := c.amap.mark(a);
+	if(e != nil){
+		cprint(sys->sprint("check: \"%s\": %s %bd", c.name, e, a));
+		return 1;
+	}
+	return 0;
+}
+
+Check.missing(c: self ref Check, sb: ref Superb)
+{
+	n := 0;
+	for(a:=sb.fsize-sb.fstart-big 1; a>=big 0; a--){
+		i := a>>3;
+		b := byte (1 << int (a&big 7));
+		if((c.amap.bits[int i] & b) == byte 0){
+			cprint(sys->sprint("missing: %bd", sb.fstart+a));
+			n++;
+		}
+		if(n > 10){
+			cprint(sys->sprint(" ..."));
+			break;
+		}
+	}
+}
+
+Check.qmark(c: self ref Check, qpath: big)
+{
+	e := c.qmap.mark(qpath);
+	if(e != nil){
+		if(c.qmap.nbad+c.qmap.ndup < big 20)
+			cprint(sys->sprint("check: \"%s\": qid %s 0x%bux", c.name, e, qpath));
+	}
+}
+
+Map.new(lo, hi: big): ref Map
+{
+	m := ref Map;
+	n := (hi-lo+big 7)>>3;
+	m.bits = array[int n] of {* => byte 0};
+	m.lo = lo;
+	m.hi = hi;
+	m.nbad = big 0;
+	m.ndup = big 0;
+	m.nmark = big 0;
+	return m;
+}
+
+Map.isset(m: self ref Map, i: big): int
+{
+	if(i < m.lo || i >= m.hi)
+		return -1;	# hard to say
+	i -= m.lo;
+	return (m.bits[int (i>>3)] & byte (1<< int (i&big 7))) != byte 0;
+}
+
+Map.mark(m: self ref Map, i: big): string
+{
+	if(i < m.lo || i >= m.hi){
+		m.nbad++;
+		return "out of range";
+	}
+	i -= m.lo;
+	b := byte (1 << int (i&big 7));
+	i >>= 3;
+	if((m.bits[int i] & b) != byte 0){
+		m.ndup++;
+		return "dup";
+	}
+	m.bits[int i] |= b;
+	m.nmark++;
+	return nil;
+}
+
+cprint(s: string)
+{
+	if(consoleout != nil)
+		consoleout <-= s+"\n";
+	else
+		eprint(s);
+}
--- a/appl/cmd/disk/mkfile
+++ b/appl/cmd/disk/mkfile
@@ -4,6 +4,7 @@
 	prep\
 
 TARG=\
+	kfs64.dis\
 	kfs.dis\
 	mbr.dis\
 	mkext.dis\