shithub: ext4srv

ref: 7e6af6b00ee3034daa6a1a1e9c6cb97c94b57196
dir: /ext4srv.c/

View raw version
#include <ext4.h>
#include <ext4_inode.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include "group.h"
#include "common.h"

int mainstacksize = 65536;

typedef struct Aux Aux;

struct Aux {
	Part *p;
	u32int uid;
	char *path;
	union {
		ext4_file *file;
		ext4_dir *dir;
	};
	int type;
};

enum {
	Adir,
	Afile,

	Root = 0,
};

static Opts opts;

static char Eperm[] = "permission denied";

static char *
linkresolve(Aux *a, char *s, char **value)
{
	char *q, buf[4096+1];
	ulong sz;

	if(ext4_readlink(s, buf, sizeof(buf), &sz) == 0){
		if(sz == sizeof(buf)){
			werrstr("readlink: %s: path too long", s);
			free(s);
			return nil;
		}
		if(opts.linkmode == Lhide){
			werrstr("linkresolve: %s: links are hidden", s);
			free(s);
			return nil;
		}

		buf[sz] = 0;
		if(value != nil)
			*value = strdup(buf);
		cleanname(buf);
		if(buf[0] == '/'){
			free(s);
			s = smprint("%M%s", a->p, buf);
		}else{
			q = strrchr(s, '/');
			*q = 0;
			q = s;
			s = smprint("%s/%s", q, buf);
			cleanname(s);
			free(q);
		}
	}else if(value != nil){
		*value = nil;
	}

	return s;
}

static char *
fullpath(Aux *a)
{
	return linkresolve(a, smprint("%M/%s", a->p, a->path), nil);
}

static int
haveperm(Aux *a, int p, struct ext4_inode *inodeout)
{
	struct ext4_inode inode;
	u32int ino, id;
	int m, fm, r;
	Group *g;
	char *s;

	switch(p & 3){
	case OREAD:
		p = AREAD;	
		break;
	case OWRITE:
		p = AWRITE;
		break;
	case ORDWR:
		p = AREAD|AWRITE;
		break;
	case OEXEC:
		p = AEXEC;	
		break;
	default:
		return 0;
	}

	if((s = fullpath(a)) == nil)
		return -1;
	if((r = ext4_raw_inode_fill(s, &ino, &inode)) != 0){
		werrstr("%s: %s", s, errno2s(r));
		free(s);
		return -1;
	}
	free(s);

	if(inodeout != nil)
		memmove(inodeout, &inode, sizeof(inode));

	fm = ext4_inode_get_mode(a->p->sb, &inode);

	/* other */
	m = fm & 7;
	if((p & m) == p)
		return 1;

	/* owner */
	id = ext4_inode_get_uid(&inode);
	if(a->uid == Root || ((g = findgroupid(&a->p->groups, id)) != nil && ingroup(g, a->uid))){
		m |= (fm >> 6) & 7;
		if((p & m) == p)
			return 1;
	}

	/* group */
	id = ext4_inode_get_gid(&inode);
	if(a->uid == Root || ((g = findgroupid(&a->p->groups, id)) != nil && ingroup(g, a->uid))){
		m |= (fm >> 3) & 7;
		if((p & m) == p)
			return 1;
	}

	return 0;
}

static void
rattach(Req *r)
{
	char err[ERRMAX];
	Aux *a;

	if((a = calloc(1, sizeof(*a))) == nil)
		respond(r, "memory");
	else if((a->p = openpart(r->ifcall.aname, &opts)) == nil){
		free(a);
		rerrstr(err, sizeof(err));
		respond(r, err);
	}else{
		if(opts.asroot || findgroup(&a->p->groups, r->ifcall.uname, &a->uid) == nil)
			a->uid = Root;

		incref(a->p);
		a->type = Adir;
		a->path = strdup("");
		r->ofcall.qid = a->p->qidmask;
		r->fid->qid = a->p->qidmask;
		r->fid->aux = a;
		respond(r, nil);
	}
}

static u32int
toext4mode(u32int mode, u32int perm, int creat)
{
	u32int e;

	e = 0;
	mode &= ~OCEXEC;

	if(mode & OTRUNC){
		mode &= ~OTRUNC;
		e |= O_TRUNC;
	}
	if(mode == OWRITE)
		e |= O_WRONLY;
	else if(mode == ORDWR)
		e |= O_RDWR;

	if(creat)
		e |= O_CREAT;

	if(perm & DMEXCL)
		e |= O_EXCL;
	if(perm & DMAPPEND)
		e |= O_APPEND;

	return e;
}

static void
ropen(Req *r)
{
	char *path;
	int res;
	Aux *a;

	a = r->fid->aux;
	switch(a->type){
	case Adir:
		if(r->ifcall.mode != OREAD || !haveperm(a, r->ifcall.mode, nil)){
			respond(r, Eperm);
			return;
		}
		if(a->dir != nil){
			respond(r, "double open");
			return;
		}
		if((a->dir = malloc(sizeof(*a->dir))) == nil)
			goto Nomem;
		if((path = smprint("%M/%s", a->p, a->path)) == nil){
			free(a->dir);
			a->dir = nil;
			goto Nomem;
		}
		res = ext4_dir_open(a->dir, path);
		free(path);
		if(res != 0){
			free(a->dir);
			a->dir = nil;
			respond(r, errno2s(res));
			return;
		}
		break;

	case Afile:
		if(!haveperm(a, r->ifcall.mode, nil)){
			respond(r, Eperm);
			return;
		}
		if(a->file != nil){
			respond(r, "double open");
			return;
		}
		if((a->file = malloc(sizeof(*a->file))) == nil)
			goto Nomem;
		if((path = smprint("%M/%s", a->p, a->path)) == nil){
			free(a->file);
			a->file = nil;
			goto Nomem;
		}
		res = ext4_fopen2(a->file, path, toext4mode(r->ifcall.mode, 0, 0));
		free(path);
		if(res != 0){
			free(a->file);
			a->file = nil;
			respond(r, errno2s(res));
			return;
		}
		break;

Nomem:
		respond(r, "memory");
		return;
	}

	r->ofcall.iounit = 0;

	respond(r, nil);
}

static void
rcreate(Req *r)
{
	struct ext4_inode inode;
	u32int perm, dirperm, t;
	char *s, *err, *q;
	int mkdir, res;
	Aux *a;

	a = r->fid->aux;
	err = nil;
	s = nil;

	if(a->file != nil || a->dir != nil){
		err = "double create";
		goto error;
	}
	if(!haveperm(a, OWRITE, &inode)){
		err = Eperm;
		goto error;
	}

	/* first make sure this is a directory */
	t = ext4_inode_type(a->p->sb, &inode);
	if((t & EXT4_INODE_MODE_DIRECTORY) == 0){
		err = "create in non-directory";
		goto error;
	}

	if((s = fullpath(a)) == nil)
		goto error;
	ext4_mode_get(s, &dirperm);

	/* check if the entry already exists */
	if((q = smprint("%s/%s", s, r->ifcall.name)) == nil){
Nomem:
		err = "memory";
		goto error;
	}
	free(s);
	s = q;
	if(ext4_inode_exist(s, EXT4_DE_UNKNOWN) == 0){
		err = "already exists";
		goto error;
	}

	mkdir = r->ifcall.perm & DMDIR;
	perm = mkdir ? 0666 : 0777;
	perm = r->ifcall.perm & (~perm | (dirperm & perm));

	if(mkdir){
		a->type = Adir;
		if((res = ext4_dir_mk(s)) != 0)
			goto ext4error;
		if((a->dir = malloc(sizeof(*a->dir))) == nil)
			goto Nomem;
		if((res = ext4_dir_open(a->dir, s)) != 0){
			free(a->dir);
			a->dir = nil;
			goto ext4errorrm;
		}
	}else{
		a->type = Afile;
		if((a->file = malloc(sizeof(*a->file))) == nil)
			goto Nomem;
		if((res = ext4_fopen2(a->file, s, toext4mode(r->ifcall.mode, perm, 1))) != 0){
			free(a->file);
			a->file = nil;
			goto ext4error;
		}
	}

	if((res = ext4_mode_set(s, perm)) != 0)
		goto ext4errorrm;
	ext4_owner_set(s, a->uid, ext4_inode_get_gid(&inode));

	a->path = strdup(strchr(s+1, '/')+1);
	free(s);
	r->ofcall.iounit = 0;
	respond(r, nil);
	return;

ext4errorrm:
	if(mkdir)
		ext4_dir_rm(s);
	else
		ext4_fremove(s);
ext4error:
	err = errno2s(res);
error:
	free(s);
	respond(r, err);
}

static int
dirfill(Dir *dir, Aux *a, char *path)
{
	struct ext4_inode inode;
	u32int t, ino, id;
	char tmp[16];
	char *s, *q;
	Group *g;
	int r;

	memset(dir, 0, sizeof(*dir));

	if(path == nil){
		path = a->path;
		s = smprint("%M/%s", a->p, a->path);
	}else{
		if(*a->path == 0 && *path == 0)
			path = "/";
		s = smprint("%M%s%s/%s", a->p, *a->path ? "/" : "", a->path, path);
	}
	if((s = linkresolve(a, s, nil)) == nil)
		return -1;
	if((r = ext4_raw_inode_fill(s, &ino, &inode)) != 0){
		werrstr("%s: %s", s, errno2s(r));
		free(s);
		return -1;
	}

	dir->mode = ext4_inode_get_mode(a->p->sb, &inode) & 0x1ff;
	dir->qid.path = a->p->qidmask.path | ino;
	dir->qid.vers = ext4_inode_get_generation(&inode);
	t = ext4_inode_type(a->p->sb, &inode);
	if(t & EXT4_INODE_MODE_DIRECTORY){
		dir->qid.type |= QTDIR;
		dir->mode |= DMDIR;
	}else
		dir->length = ext4_inode_get_size(a->p->sb, &inode);
	if(ext4_inode_get_flags(&inode) & EXT4_INODE_FLAG_APPEND){
		dir->qid.type |= QTAPPEND;
		dir->mode |= DMAPPEND;
	}

	if((q = strrchr(path, '/')) != nil)
		path = q+1;
	dir->name = estrdup9p(path);
	dir->atime = ext4_inode_get_access_time(&inode);
	dir->mtime = ext4_inode_get_modif_time(&inode);

	sprint(tmp, "%ud", id = ext4_inode_get_uid(&inode));
	dir->uid = estrdup9p((g = findgroupid(&a->p->groups, id)) != nil ? g->name : tmp);

	sprint(tmp, "%ud", id = ext4_inode_get_gid(&inode));
	dir->gid = estrdup9p((g = findgroupid(&a->p->groups, id)) != nil ? g->name : tmp);

	free(s);

	return 0;
}

static int
dirgen(int n, Dir *dir, void *aux)
{
	const ext4_direntry *e;
	Aux *a;

	a = aux;
	if(n == 0)
		ext4_dir_entry_rewind(a->dir);

	for(;;){
		do{
			if((e = ext4_dir_entry_next(a->dir)) == nil)
				return -1;
		}while(strcmp((char*)e->name, ".") == 0 || strcmp((char*)e->name, "..") == 0);

		if(dirfill(dir, a, (char*)e->name) == 0)
			return 0;
	}
}

static void
rread(Req *r)
{
	ulong n;
	Aux *a;

	a = r->fid->aux;
	if(a->type == Adir && a->dir != nil){
		dirread9p(r, dirgen, a);
		respond(r, nil);
		return;
	}else if(a->type == Afile && a->file != nil){
		ext4_fseek(a->file, r->ifcall.offset, 0);
		if(ext4_fread(a->file, r->ofcall.data, r->ifcall.count, &n) != 0){
			respond(r, "i/o error");
		}else{
			r->ofcall.count = n;
			respond(r, nil);
		}
		return;
	}

	respond(r, "eh?");
}

static void
rwrite(Req *r)
{
	ulong n;
	int res;
	Aux *a;

	a = r->fid->aux;
	if(a->type == Adir){
		respond(r, "can't write to dir");
		return;
	}else if(a->type == Afile){
		ext4_fseek(a->file, r->ifcall.offset, SEEK_SET);
		if((res = ext4_fwrite(a->file, r->ifcall.data, r->ifcall.count, &n)) != 0)
			respond(r, errno2s(res));
		else{
			r->ofcall.count = n;
			respond(r, nil);
		}
		return;
	}

	respond(r, "eh?");
}

static void
rremove(Req *r)
{
	struct ext4_inode inode;
	const ext4_direntry *e;
	u32int ino, t, empty;
	char *s, *err;
	ext4_dir dir;
	Group *g;
	int res;
	Aux *a;

	a = r->fid->aux;
	err = nil;

	/* do not resolve links here as most likely it's JUST the link we want to remove */
	if((s = smprint("%M/%s", a->p, a->path)) == nil){
		err = "memory";
		goto end;
	}
	if((res = ext4_raw_inode_fill(s, &ino, &inode)) != 0){
ext4error:
		err = errno2s(res);
		goto end;
	}

	if(a->uid == Root || ((g = findgroupid(&a->p->groups, ext4_inode_get_uid(&inode))) != nil && g->id == a->uid)){
		t = ext4_inode_type(a->p->sb, &inode);
		if((t & EXT4_INODE_MODE_DIRECTORY) != 0 && ext4_dir_open(&dir, s) == 0){
			do{
				e = ext4_dir_entry_next(&dir);
				empty = e == nil;
				if(empty)
					break;
			}while(strcmp((char*)e->name, ".") == 0 || strcmp((char*)e->name, "..") == 0);
			ext4_dir_close(&dir);
			if(!empty)
				err = "directory not empty";
			else if((res = ext4_dir_rm(s)) != 0)
				goto ext4error;
		}else if((res = ext4_fremove(s)) != 0)
			goto ext4error;
	}else{
		err = Eperm;
	}

end:
	free(s);
	respond(r, err);
}

static void
rstat(Req *r)
{
	char err[ERRMAX];
	Aux *a;

	a = r->fid->aux;
	if(dirfill(&r->d, a, nil) != 0){
		rerrstr(err, sizeof(err));
		respond(r, err);
	}else{
		respond(r, nil);
	}
}

static void
rwstat(Req *r)
{
	int res, isdir, wrperm, isowner;
	char *old, *new, *err, *s;
	struct ext4_inode inode;
	u32int uid, gid;
	ext4_file f;
	Aux *a, o;
	Group *g;

	a = r->fid->aux;
	old = nil;
	new = nil;
	err = nil;

	/* can't do anything to root, can't change the owner */
	if(a->path[0] == 0 || (r->d.uid != nil && r->d.uid[0] != 0)){
		err = Eperm;
		goto error;
	}

	if((old = smprint("%M/%s", a->p, a->path)) == nil){
		err = "memory";
		goto error;
	}
	new = old;

	wrperm = haveperm(a, OWRITE, &inode);
	uid = ext4_inode_get_uid(&inode);
	isowner = a->uid == Root || a->uid == uid;

	/* permission to truncate */
	isdir = ext4_inode_type(a->p->sb, &inode) & EXT4_INODE_MODE_DIRECTORY;
	if(r->d.length >= 0 && (!wrperm || isdir || !ext4_inode_can_truncate(a->p->sb, &inode))){
		err = Eperm;
		goto error;
	}

	/* permission to rename */
	if(r->d.name != nil && r->d.name[0] != 0){
		if((s = strrchr(old, '/')) != nil)
			*s = 0;
		if((new = smprint("%M/%s%s%s", a->p, s ? old : "", s ? "/" : "", r->d.name)) == nil){
			err = "memory";
			goto error;
		}

		/* check parent write permission */
		o = *a;
		o.path = old;
		if(!haveperm(&o, OWRITE, nil)){
			err = Eperm;
			goto error;
		}
		*s = '/';
	}

	/* permission to change mode */
	if(r->d.mode != ~0){
		/* has to be owner and can't change dir bit */
		if(!isowner || (!!isdir != !!(r->d.mode & DMDIR))){
			err = Eperm;
			goto error;
		}
	}

	/* permission to change mtime */
	if(r->d.mtime != ~0 && !isowner){
		err = Eperm;
		goto error;
	}

	/* permission to change gid */
	if(r->d.gid != nil && r->d.gid[0] != 0){
		/* has to be the owner, group has to exist, must be in that group */
		if(a->uid == Root || (!isowner || (g = findgroup(&a->p->groups, r->d.gid, &gid)) == nil || !ingroup(g, a->uid))){
			err = Eperm;
			goto error;
		}
	}

	/* done checking permissions, now apply all the changes and hope it all works */

	/* rename */
	if(r->d.name != nil && r->d.name[0] != 0){
		if((res = ext4_frename(old, new)) != 0)
			goto ext4error;

		free(old);
		old = new;
		new = nil;

		free(a->path);
		a->path = strdup(strchr(old+1, '/')+1);
	}

	/* truncate */
	if(r->d.length >= 0){
		if((res = ext4_fopen2(&f, new, toext4mode(OWRITE, 0, 0))) != 0 ||
		   (res = ext4_ftruncate(&f, r->d.length)) != 0){
			goto ext4error;
		}
		ext4_fclose(&f);
	}

	/* mode */
	if(r->d.mode != ~0 && (res = ext4_mode_set(new, r->d.mode & 0x1ff)) != 0)
		goto ext4error;

	/* mtime */
	if(r->d.mtime != ~0 && (res = ext4_mtime_set(new, r->d.mtime)) != 0)
		goto ext4error;

	/* gid */
	if(r->d.gid != nil && r->d.gid[0] != 0 && (res = ext4_owner_set(new, uid, gid)) != 0)
		goto ext4error;

	goto error; /* not really an error */

ext4error:
	err = errno2s(res);
error:
	free(old);
	if(new != old)
		free(new);
	respond(r, err);
}

static char *
rwalk1(Fid *fid, char *name, Qid *qid)
{
	static char errbuf[ERRMAX];
	struct ext4_inode inode;
	u32int ino, t;
	Aux *a, dir;
	char *s, *q;
	int r;

	a = fid->aux;

	/* try walking to the real file first */
	if((s = fullpath(a)) == nil){
		/* else try link itself. might want to just remove it anyway */
		if((s = smprint("%M/%s", a->p, a->path)) == nil){
			r = ENOMEM;
			goto error;
		}
	}
	if((r = ext4_raw_inode_fill(s, &ino, &inode)) != 0)
		goto error;
	t = ext4_inode_type(a->p->sb, &inode);
	if((t & EXT4_INODE_MODE_DIRECTORY) == 0){
		free(s);
		return "not a directory";
	}
	dir = *a;
	dir.path = s;
	if(!haveperm(&dir, OEXEC, nil)){
		free(s);
		return Eperm;
	}

	q = s;
	s = smprint("%s/%s", q, name);
	cleanname(s);
	free(q);
	if((s = linkresolve(a, s, nil)) == nil){
		rerrstr(errbuf, sizeof(errbuf));
		return errbuf;
	}
	if((r = ext4_raw_inode_fill(s, &ino, &inode)) != 0)
		goto error;
	qid->type = 0;
	qid->path = a->p->qidmask.path | ino;
	qid->vers = ext4_inode_get_generation(&inode);
	t = ext4_inode_type(a->p->sb, &inode);
	if(t & EXT4_INODE_MODE_DIRECTORY){
		qid->type |= QTDIR;
		a->type = Adir;
	}else
		a->type = Afile;
	if(ext4_inode_get_flags(&inode) & EXT4_INODE_FLAG_APPEND)
		qid->type |= QTAPPEND;
	free(a->path);
	a->path = strdup(strchr(s+1, '/')+1);
	free(s);
	fid->qid = *qid;

	return nil;
error:
	free(s);
	return errno2s(r);
}

static char *
rclone(Fid *oldfid, Fid *newfid)
{
	Aux *a, *c;

	a = oldfid->aux;
	switch(a->type){
	case Afile:
	case Adir:
		if((c = calloc(1, sizeof(*c))) == nil)
			return "memory";
		memmove(c, a, sizeof(*c));
		c->path = strdup(a->path);
		c->file = nil;
		c->dir = nil;
		break;

	default:
		return "unknown aux type";
	}
	incref(c->p);
	newfid->aux = c;

	return nil;
}

static void
rdestroyfid(Fid *fid)
{
	Aux *a;

	a = fid->aux;
	if(a == nil)
		return;
	fid->aux = nil;

	if(a->type == Adir){
		if(a->dir != nil){
			ext4_dir_close(a->dir);
			free(a->dir);
		}
	}else if(a->type == Afile){
		if(a->file != nil){
			ext4_fclose(a->file);
			free(a->file);
		}
	}else{
		/* that would be a BUG */
		return;
	}

	if(decref(a->p) == 0)
		closepart(a->p);
	free(a->path);
	free(a);
}

static int
note(void *, char *s)
{
	if(strncmp(s, "sys:", 4) != 0){
		closeallparts();
		return 1;
	}

	return 0;
}

static void
rstart(Srv *)
{
	threadnotify(note, 1);
}

static void
rend(Srv *)
{
	closeallparts();
	threadexitsall(nil);
}

static Srv fs = {
	.attach = rattach,
	.open = ropen,
	.create = rcreate,
	.read = rread,
	.write = rwrite,
	.remove = rremove,
	.stat = rstat,
	.wstat = rwstat,
	.walk1 = rwalk1,
	.clone = rclone,
	.destroyfid = rdestroyfid,
	.start = rstart,
	.end = rend,
};

static void
usage(void)
{
	fprint(2, "usage: %s [-C] [-R] [-g GROUPFILE] [-l hide] [-s SRVNAME]\n", argv0);
	threadexitsall("usage");
}

static int
linkmode(char *m)
{
	if(strcmp(m, "hide") == 0)
		return Lhide;

	usage();
	return -1;
}

void
threadmain(int argc, char **argv)
{
	char *srv, *gr;
	vlong sz;
	int f;

	srv = "ext4";
	ARGBEGIN{
	case 'D':
		chatty9p++;
		break;
	case 'C':
		opts.cachewb = 1;
		break;
	case 'l':
		opts.linkmode = linkmode(EARGF(usage()));
		break;
	case 's':
		srv = EARGF(usage());
		break;
	case 'g':
		gr = EARGF(usage());
		if((f = open(gr, OREAD)) < 0)
			sysfatal("%r");
		if((sz = seek(f, 0, 2)) < 0)
			sysfatal("%s: invalid group file", gr);
		if((opts.group = malloc(sz+1)) == nil)
			sysfatal("memory");
		seek(f, 0, 0);
		if(readn(f, opts.group, sz) != sz)
			sysfatal("%s: read failed", gr);
		close(f);
		opts.group[sz] = 0;
		break;
	case 'R':
		opts.asroot = 1;
		break;
	}ARGEND

	if(argc != 0)
		usage();

	threadpostmountsrv(&fs, srv, nil, 0);
	threadexits(nil);
}