diff options
-rw-r--r-- | 9bind.c | 45 | ||||
-rw-r--r-- | 9mount.1 | 115 | ||||
-rw-r--r-- | 9mount.c | 273 | ||||
-rw-r--r-- | 9umount.c | 140 | ||||
-rw-r--r-- | COPYING | 13 | ||||
-rw-r--r-- | ChangeLog | 21 | ||||
-rw-r--r-- | Makefile | 28 | ||||
-rw-r--r-- | test.sh | 61 |
8 files changed, 696 insertions, 0 deletions
@@ -0,0 +1,45 @@ +/* © 2008 sqweek <sqweek@gmail.com> + * See COPYING for details. + */ +#include <err.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <sys/stat.h> +#include <sys/mount.h> + +int +main(int argc, char **argv) +{ + char *old = NULL, *new = NULL; + struct stat stbuf; + + while (*++argv) { + if (!old) { + old = *argv; + } else if (!new) { + new = *argv; + } else { + errx(1, "%s: too many arguments", *argv); + } + } + + if (!old || !new) { + errx(1, "usage: 9bind old new"); + } + + /* Make sure mount exists, is writable, and not sticky */ + if (stat(new, &stbuf) || access(new, W_OK)) { + err(1, "%s", new); + } + if (stbuf.st_mode & S_ISVTX) { + errx(1, "%s: refusing to bind over sticky directory", new); + } + + if (mount(old, new, NULL, MS_BIND, NULL)) { + err(1, "mount"); + } + + return 0; +} diff --git a/9mount.1 b/9mount.1 new file mode 100644 index 0000000..a4884b0 --- /dev/null +++ b/9mount.1 @@ -0,0 +1,115 @@ +.TH "9mount" "1" "04 September 2007" "9mount" "User commands" +.SH NAME +9mount, 9bind, 9umount \- mount/unmount 9p filesystems +.SH SYNOPSIS +.B 9mount +[ insuvx ] [ -a SPEC ] [ -c CACHE ] [ -d DEBUG ] [ -m MSIZE ] DIAL MOUNTPT +.PP +.B 9bind +OLD NEW +.PP +.B 9umount +MOUNTPT +.SH DESCRIPTION +.B 9mount +mounts a 9p filesystem served at DIAL on MOUNTPT. MOUNTPT must be +writable by you and not sticky. DIAL is a dial string assuming one of +the forms: +.PP +unix!SOCKET +.br +tcp!HOST[!PORT] +.br +virtio!CHANNEL +.br +- +.PP +where SOCKET is the name of a file representing a socket, HOST is a +hostname, PORT is a port number or service name, and CHANNEL is a +virtio channel name (currently ignored). - indicates that 9p messages +should be read/written on stdin/stdout. +.B 9mount +has several options: +.TP +-i +mount the file system with your uid/gid +.TP +-n +dry-run, print mount command to stderr but don't actually mount anything +.TP +-s +single attach mode - all users accessing the mount point see the same +filesystem (by default they'll each see a unique attach) +.TP +-u +use the 9P2000.u extensions +.TP +-v +use device mapping +.TP +-x +exclusive access - other users cannot access the mount point +.TP +-a SPEC +SPEC determines which file tree to mount when attaching to file servers that +export multiple trees +.TP +-c CACHE +turns on caching using CACHE mode. Currently only +.I loose +cache mode is available, which is suitable for exclusive read-only mounts. +.TP +-d DEBUG +comma seperated list of channels for which to enable debug output. Possible +channels include: err, devel, 9p, vfs, conv, mux, trans, alloc, fcall. +.TP +-m MSIZE +specifies the maximum length of a single 9p message in bytes. +.PP +.B 9bind +performs a bind mount, making the tree visible at directory OLD also visible +at mount point NEW. +.PP +.B 9umount +unmounts a 9p filesystem previously mounted by you. +.SH ENVIRONMENT +.TP +$USER +the uname to provide to the server. +.SH EXAMPLES +.TP +9mount -i 'unix!/tmp/ns.'$USER'.:0/factotum' $HOME/n/factotum +mount p9p's factotum interface +.TP +9mount 'tcp!sources.cs.bell-labs.com' $HOME/n/sources +import plan 9's "sources" +.TP +9mount -u -a/home/sqweek/mail 'tcp!wren!5640' $HOME/mail +import my maildir from my server(wren), being served by ufs +.TP +9mount -i 'tcp!wren' $HOME/n/wren; 9bind $HOME/n/wren/home/sqweek/mail $HOME/mail +again importing my maildir, this time serving via u9fs +.SH BUGS +.B 9mount +truncates user names and SPECs to 249 characters. +.B 9umount +doesn't know this, so you won't be able to unmount anything +outside your home directory. But you probably never bother +logging out if your user name is that long. +.PP +.B 9mount +doesn't update /etc/mtab. +.PP +.B 9bind +only does a "shallow", non-recursive bind - any mounted filesystems +under the OLD tree will not appear mounted in the NEW tree. +.PP +If you +.B 9bind +a non-9p filesystem outside your home directory, +.B 9umount +won't let you unmount it. +.SH AUTHOR +sqweek@gmail.com +.SH SEE ALSO +.BR mount (1) diff --git a/9mount.c b/9mount.c new file mode 100644 index 0000000..65bccac --- /dev/null +++ b/9mount.c @@ -0,0 +1,273 @@ +/* © 2008 sqweek <sqweek@gmail.com> + * See COPYING for details. + */ +#include <err.h> +#include <mntent.h> +#include <string.h> +#include <stdlib.h> +#include <stdio.h> +#include <unistd.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/socket.h> +#include <sys/mount.h> +#include <arpa/inet.h> +#include <pwd.h> +#include <netdb.h> + +#define nelem(x) (sizeof(x)/sizeof(*(x))) + +struct {char *mnemonic; int mask;} debug_flags[] = { + {"err", 0x001}, + {"devel", 0x002}, + {"9p", 0x004}, + {"vfs", 0x008}, + {"conv", 0x010}, + {"mux", 0x020}, + {"trans", 0x040}, + {"alloc", 0x080}, + {"fcall", 0x100} +}; + +char* +append(char **dest, char *src, int *destlen) +{ + while (strlen(*dest) + 1 + strlen(src) > *destlen) + *destlen *= 2; + if (!(*dest=realloc(*dest, *destlen))) + errx(1, "out of memory"); + + if (**dest) + strcat(*dest, ","); + strcat(*dest, src); + return *dest; +} + +char* +getarg(char opt, char *cp, char*** argv) +{ + if (*(cp+1)) { + return cp+1; + } else if (*(*argv+1)) { + return *++(*argv); + } else { + errx(1, "-%c: expected argument", opt); + } + return NULL; +} + +void +parsedial(char *dial, char **network, char **netaddr, int *port) +{ + char *cp; + if (!(*network=strtok(dial, "!"))) { + errx(1, "empty dial string"); + } + if (strcmp(*network, "unix") != 0 + && strcmp(*network, "tcp") != 0 + && strcmp(*network, "virtio") != 0) { + errx(1, "%s: unknown network (expecting unix, tcp or virtio)", *network); + } + if (!(*netaddr=strtok(NULL, "!"))) { + errx(1, "missing dial netaddress"); + } + if (strcmp(*network, "tcp") == 0) { + char *service; + if ((service=strtok(NULL, "!"))) { + if (strspn(service, "0123456789") == strlen(service)) { + *port = atoi(service); + } else { + struct servent *sv; + if ((sv=getservbyname(service, *network))) { + /* sv->s_port is a 16-bit big endian masquerading as an int */ + *port = ntohs((uint16_t)sv->s_port); + endservent(); + } else { + errx(1, "%s: unknown service", service); + } + } + } + } + if ((cp=strtok(NULL, "!"))) { + errx(1, "%s: junk trailing dial string", cp); + } +} + +int +main(int argc, char **argv) +{ + char buf[256], *opts, *dial = NULL, *mountpt = NULL; + int optlen = 64, port = 0, i; + struct stat stbuf; + struct passwd *pw; + int axess = 0, dotu = 0, uidgid = 0, dev = 0, debug = 0, dryrun = 0; + char *debugstr = NULL, *msize = NULL, *cache = NULL, *aname = NULL; + char *cp, *proto, *addr; + + if (!(opts=calloc(optlen, 1))) { + err(1, "calloc"); + } + while (*++argv) { + if (**argv == '-' && (*argv)[1] != '\0') { + for (cp=*argv+1; *cp; ++cp) { + switch (*cp) { + case 'i': uidgid = 1; break; + case 'n': dryrun = 1; break; + case 's': axess = -1; break; + case 'u': dotu = 1; break; + case 'v': dev = 1; break; + case 'x': axess = getuid(); break; + case 'a': + aname = getarg('a', cp, &argv); + *cp-- = '\0'; /* breaks out of for loop */ + break; + case 'c': + cache = getarg('c', cp, &argv); + *cp-- = '\0'; + break; + case 'd': + debugstr = getarg('d', cp, &argv); + *cp-- = '\0'; + break; + case 'm': + msize = getarg('m', cp, &argv); + *cp-- = '\0'; + break; + } + } + } else if (!dial) { + dial = *argv; + } else if (!mountpt) { + mountpt = *argv; + } else { + errx(1, "%s: too many arguments", *argv); + } + } + + if (!dial || !mountpt) { + errx(1, "usage: 9mount [ -insuvx ] [ -a spec ] [ -c cache ] [ -d debug ] [ -m msize ] dial mountpt"); + } + + if(!(pw=getpwuid(getuid()))) { + err(1, "who are you?? getpwuid failed"); + } + /* Make sure mount exists, is writable, and not sticky */ + if (stat(mountpt, &stbuf) || access(mountpt, W_OK)) { + err(1, "%s", mountpt); + } + if (stbuf.st_mode & S_ISVTX) { + errx(1, "%s: refusing to mount over sticky directory", mountpt); + } + + if (strcmp(dial, "-") == 0) { + proto = "fd"; + addr = "nodev"; + append(&opts, "rfdno=0,wrfdno=1", &optlen); + } else { + parsedial(dial, &proto, &addr, &port); + } + + /* set up mount options */ + append(&opts, proto, &optlen); /* < 2.6.24 */ + snprintf(buf, sizeof(buf), "trans=%s", proto); + append(&opts, buf, &optlen); /* >= 2.6.24 */ + + if (aname) { + if (strchr(aname, ',')) { + errx(1, "%s: spec can't contain commas", aname); + } + snprintf(buf, sizeof(buf), "aname=%s", aname); + append(&opts, buf, &optlen); + } + + if (cache) { + if (strcmp(cache, "loose") != 0) { + errx(1, "%s: unknown cache mode (expecting loose)", cache); + } + snprintf(buf, sizeof(buf), "cache=%s", cache); + append(&opts, buf, &optlen); + } + + if (debugstr) { + for (cp=strtok(debugstr, ","); cp; cp=strtok(NULL, ",")) { + for (i=0; i<nelem(debug_flags); ++i) { + if (strcmp(cp, debug_flags[i].mnemonic) == 0) { + debug |= debug_flags[i].mask; + break; + } + } + if (i >= nelem(debug_flags)) { + errx(1, "%s: unrecognised debug channel", cp); + } + } + snprintf(buf, sizeof(buf), "debug=0x%04x", debug); + append(&opts, buf, &optlen); + } + + if (msize) { + if (strspn(msize, "0123456789") < strlen(msize)) { + errx(1, "%s: msize must be an integer", msize); + } + snprintf(buf, sizeof(buf), "msize=%s", msize); + append(&opts, buf, &optlen); + } + + snprintf(buf, sizeof(buf), "name=%s", pw->pw_name); + append(&opts, buf, &optlen); + + if (getenv("USER")) { + snprintf(buf, sizeof(buf), "uname=%s", getenv("USER")); + } else { + snprintf(buf, sizeof(buf), "uname=%s", pw->pw_name); + } + if (strchr(buf, ',')) { + errx(1, "%s: username can't contain commas", buf+6); + } + append(&opts, buf, &optlen); + + if (axess == -1) { + append(&opts, "access=any", &optlen); + } else if (axess) { + snprintf(buf, sizeof(buf), "access=%d", axess); + append(&opts, buf, &optlen); + } + if (!dotu) { + append(&opts, "noextend", &optlen); + } + if (!dev) { + append(&opts, "nodev", &optlen); + } + if (uidgid) { + snprintf(buf, sizeof(buf), "uid=%d,gid=%d", getuid(), getgid()); + append(&opts, buf, &optlen); /* < 2.6.24 */ + snprintf(buf, sizeof(buf), "dfltuid=%d,dfltgid=%d", getuid(), getgid()); + append(&opts, buf, &optlen); /* >= 2.6.24 */ + } + if (port) { + snprintf(buf, sizeof(buf), "port=%d", port); + append(&opts, buf, &optlen); + } + + if (strcmp(proto, "tcp") == 0) { + struct addrinfo *ai; + int r; + if ((r=getaddrinfo(addr, NULL, NULL, &ai))) { + errx(1, "getaddrinfo: %s", gai_strerror(r)); + } + if ((r=getnameinfo(ai->ai_addr, ai->ai_addrlen, buf, + sizeof(buf), NULL, 0, NI_NUMERICHOST))) { + errx(1, "getnameinfo: %s", gai_strerror(r)); + } + } else { /* unix socket, virtio device or fd transport */ + snprintf(buf, sizeof(buf), "%s", addr); + } + + if(dryrun) { + fprintf(stderr, "mount -t 9p -o %s %s %s\n", opts, buf, mountpt); + } else if (mount(buf, mountpt, "9p", 0, (void*)opts)) { + err(1, "mount"); + } + + return 0; +} diff --git a/9umount.c b/9umount.c new file mode 100644 index 0000000..1adc9fe --- /dev/null +++ b/9umount.c @@ -0,0 +1,140 @@ +/* © 2008 sqweek <sqweek@gmail.com> + * See COPYING for details. + */ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <err.h> + +#include <sys/types.h> +#include <sys/mount.h> +#include <mntent.h> +#include <pwd.h> + +char* +canonpath(char *rel) +{ + char *abs, *cp, *prev; + + if (rel[0] == '/') { + if (!(abs=malloc(strlen(rel) + 2))) { + return NULL; + } + abs[0] = '\0'; + strcat(abs, rel); + strcat(abs, "/"); + } else { + char *cwd = NULL; + if (!(cwd=getcwd(NULL, 0))) { + return NULL; + } + if (!(abs=malloc(strlen(cwd) + 1 + strlen(rel) + 2))) { + free(cwd); + return NULL; + } + abs[0] = '\0'; + strcat(abs, cwd); + strcat(abs, "/"); + strcat(abs, rel); + strcat(abs, "/"); + free(cwd); + } + + /* abs is now an absolute path which begins and ends with a slash */ + prev = abs; + while ((cp=strchr(prev+1, '/'))) { + if (cp == prev+1) { + /* remove consecutive slashes */ + memmove(prev, cp, strlen(cp)+1); + } else if (strncmp(prev, "/./", cp-prev+1) == 0) { + memmove(prev, cp, strlen(cp)+1); + } else if (strncmp(prev, "/../", cp-prev+1) == 0) { + char *parent; + if (prev == abs) { + parent = abs; /* just eat .. in root dir */ + } else { + for (parent=prev-1; *parent != '/'; --parent); + } + memmove(parent, cp, strlen(cp)+1); + prev = parent; + } else { + prev = cp; + } + } + abs[strlen(abs)-1] = '\0'; /* remove trailing slash */ + + return abs; +} + +int +indir(struct mntent *mnt, char *dir) +{ + return (strncmp(mnt->mnt_dir, dir, strlen(dir)) == 0); +} + +int +mountedby(struct mntent *mnt, char *username) +{ + char *s, *cp; + if (!(s=strdup(mnt->mnt_opts))) { + errx(1, "out of memory"); + } + for (cp=strtok(s, ","); cp; cp=strtok(NULL, ",")) { + if (strncmp(cp, "name=", 5) == 0) { + int eq = (strcmp(cp+5, username) == 0); + free(s); + return eq; + } + } + free(s); + return 0; +} + +int +main(int argc, char **argv) +{ + int ret = 0; + char *path; + FILE *fp; + struct mntent *mnt; + struct passwd *pw; + + if (!(pw=getpwuid(getuid()))) { + err(1, "who are you?? getpwuid failed"); + } + + while (*++argv) { + if (!(path=canonpath(*argv))) { + warn("%s: canonpath", *argv); + continue; + } + if (!(fp=fopen("/proc/mounts", "r"))) { + err(1, "couldn't open /proc/mounts"); + } + while ((mnt=getmntent(fp))) { + if (strcmp(path, mnt->mnt_dir) == 0) { + int inhomedir; + inhomedir = indir(mnt, pw->pw_dir); + if (!inhomedir && strcmp(mnt->mnt_type, "9p") != 0) { + warnx("%s: refusing to unmount non-9p fs", path); + ret = 1; + } else if (!inhomedir && !mountedby(mnt, pw->pw_name)) { + warnx("%s: not mounted by you", path); + ret = 1; + } else if (umount(mnt->mnt_dir)) { + warn("umount %s", mnt->mnt_dir); + ret = 1; + } + goto done; + } + } + warnx("%s not found in /proc/mounts", path); + ret = 1; + +done: + free(path); + fclose(fp); + } + return ret; +} @@ -0,0 +1,13 @@ +Copyright (c) 2008, sqweek <sqweek@gmail.com> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..7701503 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,21 @@ +2008-07-22 sqweek <sqweek@gmail.com> + * 1.3 release + * 9mount: support for custom uname, cache, msize, virtio, fd transport, + access modes, -d more useful + * 9umount: allow unmounting of even non-9p filesystems in your home dir + * introduced some test cases + * added ISC license + +2008-06-20 sqweek <sqweek@gmail.com> + * 1.2 release + * 9mount: support for linux kernels >= 2.6.24 + * 9umount: allow unmounting of anything in your home dir + +2007-10-16 sqweek <sqweek@gmail.com> + * 1.1 release + * 9bind: new SUID utility for bind mounting + * Makefile: actually install the man page + * 9mount: check that the initial calloc actually succeeds + +2007-09-08 sqweek <sqweek@gmail.com> + * 1.0 release diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e7a9948 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +prefix=/usr/local +bindir=$(prefix)/bin +mandir=$(prefix)/share/man + +CFLAGS=-Wall + + +all: 9mount 9umount 9bind + +9mount: 9mount.c +9umount: 9umount.c +9bind: 9bind.c + +%: %.c + $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $^ -o $@ + +clean: + rm -f 9mount 9umount 9bind + +install: all + mkdir -p $(bindir) + cp -f 9mount $(bindir) + cp -f 9umount $(bindir) + cp -f 9bind $(bindir) + chown root:users $(bindir)/9mount $(bindir)/9umount $(bindir)/9bind + chmod 4755 $(bindir)/9mount $(bindir)/9umount $(bindir)/9bind + mkdir -p $(mandir)/man1 + cp -f 9mount.1 $(mandir)/man1 @@ -0,0 +1,61 @@ +#!/bin/sh +expect() { + addr=$1 + opts=$(echo $2 |tr , '\n' |sort |tr '\n' , |sed 's/,$//') + mtpt=$(eval 'echo $'$#) + expected="mount -t 9p -o $opts $addr $mtpt" + shift; shift + actual=$(9mount -n "$@" 2>&1) + aopts=$(echo $actual |sed 's/.*-o \([^ ]*\) .*/\1/' |tr , '\n' |sort |tr '\n' , |sed 's/,$//') + actual=$(echo $actual |sed 's/-o [^ ]*/-o '"$aopts"'/') + if [ "$expected" != "$actual" ]; then + echo ' '9mount "$@" + echo $expected' #expected' + echo $actual' #actual' + exit 1 + fi +} + +mtpt=/tmp/9mount +mkdir -p $mtpt +trap 'rmdir $mtpt' EXIT +dfltopts="name=$USER,uname=$USER,noextend,nodev" + +expect 127.0.0.1 $dfltopts,tcp,trans=tcp,port=888 tcp!localhost!888 $mtpt +expect /tmp/ns.$USER.:0/foo $dfltopts,unix,trans=unix unix!/tmp/ns.$USER.:0/foo $mtpt +expect /dev/bar $dfltopts,virtio,trans=virtio virtio!/dev/bar $mtpt +expect nodev $dfltopts,fd,trans=fd,rfdno=0,wrfdno=1 - $mtpt + +dfltopts="tcp,trans=tcp,$dfltopts" + +testflag() { + opts=$1 + shift + expect 127.0.0.1 $dfltopts,$opts "$@" tcp!localhost $mtpt +} + +testflag "uid=$(id -u),gid=$(id -g),dfltuid=$(id -u),dfltgid=$(id -g)" -i +testflag "access=any" -s +expect 127.0.0.1 $(echo $dfltopts |sed 's/,noextend//') -u tcp!localhost $mtpt +expect 127.0.0.1 $(echo $dfltopts |sed 's/,nodev//') -v tcp!localhost $mtpt +testflag "access=$(id -u)" -x +testflag "aname=abcdef" -a abcdef +testflag "cache=loose" -c loose +testflag "debug=0x0130" -d fcall,conv,mux +testflag "msize=16384" -m 16384 + +shouldfail() { + output=$(9mount -n "$@" 2>&1) && { + echo ' '9mount "$@" + echo $output' #should have failed!' + } +} + +shouldfail -a tcp!localhost $mtpt +shouldfail -a main/active,bwahaha tcp!localhost $mtpt +shouldfail -d lol tcp!localhost $mtpt +shouldfail -m z tcp!localhost $mtpt +shouldfail udp!localhost $mtpt +shouldfail unix!/tmp/9mount!qux $mtpt +shouldfail virtio!/dev/chan!bar $mtpt +shouldfail tcp!localhost!564!foo $mtpt |