diff --git a/Makefile b/Makefile index e972c13..9a56822 100644 --- a/Makefile +++ b/Makefile @@ -55,7 +55,7 @@ KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/storage/usb_storage.o KERN_DRIVERS += $(BUILD)/kernel/drivers/usb/hub/usb_hub.o KERN_FS = $(addprefix $(BUILD)/kernel/fs/, \ - exec.o file.o fs.o log.o pipe.o procfs.o devfs.o sysfs.o) + exec.o file.o fs.o log.o pipe.o procfs.o devfs.o sysfs.o lock.o) # SchFS. Every one of these is the very same source the host tools compile, so # the kernel and mkschfs cannot disagree about the on-disk format: there is one @@ -128,7 +128,7 @@ KERN_NOENTRY = $(filter-out $(BUILD)/kernel/arch/x86_64/entry.o, $(KERNOBJS)) # Third-party software does NOT go here — see ports/. # ----------------------------------------------------------------------- ULIB = $(addprefix $(BUILD)/lib/, start.o glue.o usys.o) -CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready +CMDS = schinit schinitd fbinfo shmtest socktest inputtest nettest reflink schattr ping chvt loadkeys sigtest memfdtest pcitest vgpud schdbg schdbgd resolvconf httpd schmode netecho ldlsd tone hwreport dynhello evtest xready locktest CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS)) # ----------------------------------------------------------------------- @@ -1073,6 +1073,13 @@ $(BUILD)/cmd/xready: cmd/xready/main.c $(MUSLPREFIX)/lib/libc.so $(LINUXHDR) $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/xready.o \ -L$(MUSLPREFIX)/lib -lc $(PORTEND) +$(BUILD)/cmd/locktest: cmd/locktest/main.c $(MUSLPREFIX)/lib/libc.so + @mkdir -p $(BUILD)/cmd + $(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/locktest.o + $(DYNCC) -no-pie -nostdlib $(DYNLD) -o $@ \ + $(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/locktest.o \ + -L$(MUSLPREFIX)/lib -lc $(PORTEND) + $(BUILD)/cmd/libdynhello.so: cmd/dynhello/lib.c $(MUSLPREFIX)/lib/libc.so @mkdir -p $(BUILD)/cmd $(DYNCC) -fPIC -c $< -o $(BUILD)/cmd/libdynhello.o diff --git a/cmd/locktest/main.c b/cmd/locktest/main.c new file mode 100644 index 0000000..a9d477d --- /dev/null +++ b/cmd/locktest/main.c @@ -0,0 +1,152 @@ +// locktest: flock() and fcntl() record locks, each property a program +// relies on checked between a parent and a child -- the only way to see a +// lock is to be someone it keeps out. + +#include +#include +#include +#include +#include +#include +#include + +static int bad; + +#define CHECK(cond, ...) do { \ + if (!(cond)) { printf("locktest: FAIL " __VA_ARGS__); printf("\n"); bad++; } \ + } while (0) + +#define PATH "/tmp/locktest" + +// Run `fn` in a child and return its exit status. +static int +child(int (*fn)(void)) +{ + pid_t p = fork(); + if (p == 0) + _exit(fn()); + int st = 0; + waitpid(p, &st, 0); + return WIFEXITED(st) ? WEXITSTATUS(st) : 99; +} + +static int +try_flock_ex(void) +{ + int fd = open(PATH, O_RDWR); + return flock(fd, LOCK_EX | LOCK_NB) == 0 ? 0 : (errno == EWOULDBLOCK ? 1 : 2); +} + +static int +try_flock_sh(void) +{ + int fd = open(PATH, O_RDWR); + return flock(fd, LOCK_SH | LOCK_NB) == 0 ? 0 : 1; +} + +static int +set(int fd, int cmd, short type, off_t start, off_t len) +{ + struct flock fl = { .l_type = type, .l_whence = SEEK_SET, + .l_start = start, .l_len = len }; + return fcntl(fd, cmd, &fl); +} + +static int +try_range_10_19(void) +{ + int fd = open(PATH, O_RDWR); + return set(fd, F_SETLK, F_WRLCK, 10, 10) == 0 ? 0 : 1; +} + +static int +try_range_50_59(void) +{ + int fd = open(PATH, O_RDWR); + return set(fd, F_SETLK, F_WRLCK, 50, 10) == 0 ? 0 : 1; +} + +static int +wait_range_10_19(void) +{ + int fd = open(PATH, O_RDWR); + return set(fd, F_SETLKW, F_WRLCK, 10, 10) == 0 ? 0 : 1; +} + +static int +lock_and_exit(void) +{ + int fd = open(PATH, O_RDWR); + return set(fd, F_SETLK, F_WRLCK, 0, 0) == 0 ? 0 : 1; +} + +int +main(void) +{ + int fd = open(PATH, O_RDWR | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open: %s", strerror(errno)); + write(fd, "0123456789012345678901234567890123456789", 40); + + // flock: exclusive keeps everyone out, shared lets shared in. + CHECK(flock(fd, LOCK_EX) == 0, "flock LOCK_EX"); + CHECK(child(try_flock_ex) == 1, "a second LOCK_EX is refused with EWOULDBLOCK"); + CHECK(flock(fd, LOCK_SH) == 0, "convert to LOCK_SH"); + CHECK(child(try_flock_sh) == 0, "LOCK_SH alongside LOCK_SH"); + CHECK(child(try_flock_ex) == 1, "LOCK_EX against LOCK_SH"); + CHECK(flock(fd, LOCK_UN) == 0 && child(try_flock_ex) == 0, "LOCK_UN releases"); + + // Closing the last descriptor on the description drops a flock. + int fd2 = open(PATH, O_RDWR); + CHECK(flock(fd2, LOCK_EX) == 0, "flock on a second description"); + close(fd2); + CHECK(child(try_flock_ex) == 0, "close releases flock"); + + // flock and fcntl locks do not see each other. + CHECK(flock(fd, LOCK_EX) == 0 && child(try_range_10_19) == 0, + "fcntl ignores flock"); + flock(fd, LOCK_UN); + + // Ranges: a lock on 0..39 with 15..16 unlocked is two locks. + CHECK(set(fd, F_SETLK, F_WRLCK, 0, 40) == 0, "F_SETLK 0..39"); + CHECK(child(try_range_50_59) == 0, "outside the range is free"); + CHECK(child(try_range_10_19) == 1, "inside the range is not"); + CHECK(set(fd, F_SETLK, F_UNLCK, 10, 10) == 0, "unlock 10..19"); + CHECK(child(try_range_10_19) == 0, "the hole is free"); + + struct flock q = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = 0, .l_len = 5 }; + pid_t p = fork(); + if (p == 0) { + int cfd = open(PATH, O_RDWR); + _exit(fcntl(cfd, F_GETLK, &q) == 0 && q.l_type == F_WRLCK && + q.l_pid == getppid() && q.l_start == 0 && q.l_len == 10 ? 0 : 1); + } + int st; + waitpid(p, &st, 0); + CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "F_GETLK reports the holder"); + + // A process's record locks go when it closes *any* descriptor on the file. + int fd3 = open(PATH, O_RDONLY); + close(fd3); + CHECK(child(try_range_10_19) == 0 && child(try_range_50_59) == 0, + "closing another descriptor releases the process's locks"); + + // A blocked waiter is woken when the lock goes. + CHECK(set(fd, F_SETLK, F_WRLCK, 0, 40) == 0, "relock"); + p = fork(); + if (p == 0) + _exit(wait_range_10_19()); + usleep(100000); + CHECK(waitpid(p, &st, WNOHANG) == 0, "F_SETLKW waits"); + set(fd, F_SETLK, F_UNLCK, 0, 0); + waitpid(p, &st, 0); + CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "F_SETLKW is woken by the unlock"); + + // And they go when the process exits. + CHECK(child(lock_and_exit) == 0 && set(fd, F_SETLK, F_WRLCK, 0, 0) == 0, + "exit releases record locks"); + + close(fd); + unlink(PATH); + printf(bad ? "locktest: %d failed\n" : "locktest: ok\n", bad); + return bad != 0; +} diff --git a/include/core/defs.h b/include/core/defs.h index 237bba2..53c9ac9 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -110,6 +110,12 @@ struct file* filealloc(void); int fdalloc(struct file*); // sys_event.c: eventfd, timerfd, signalfd and epoll, as files +// kernel/fs/lock.c +int lock_flock(struct file *f, int op); +int lock_fcntl(struct file *f, int cmd, addr_t arg); +void lock_close_fd(struct file *f); +void lock_close_file(struct file *f); +void lock_exit(int pid); struct evobj; void ev_close(struct evobj *); int ev_poll(struct file *, int events); diff --git a/include/uapi/errno.h b/include/uapi/errno.h index a29d184..a061593 100644 --- a/include/uapi/errno.h +++ b/include/uapi/errno.h @@ -37,6 +37,7 @@ #define EPIPE 32 // Broken pipe #define ERANGE 34 // Math result not representable #define ENAMETOOLONG 36 // File name too long +#define ENOLCK 37 // No record locks available #define ENOSYS 38 // Function not implemented #define ENOTEMPTY 39 // Directory not empty #define ELOOP 40 // Too many symbolic links encountered diff --git a/include/uapi/fcntl.h b/include/uapi/fcntl.h index 19b4a0a..f6327b9 100644 --- a/include/uapi/fcntl.h +++ b/include/uapi/fcntl.h @@ -39,5 +39,12 @@ #define F_GETFL 3 #define F_SETFL 4 #define F_DUPFD_CLOEXEC 1030 // Linux value +// Record locks; see kernel/fs/lock.c. +#define F_GETLK 5 +#define F_SETLK 6 +#define F_SETLKW 7 +#define F_OFD_GETLK 36 +#define F_OFD_SETLK 37 +#define F_OFD_SETLKW 38 #define FD_CLOEXEC 1 diff --git a/kernel/fs/file.c b/kernel/fs/file.c index 50ab253..a696823 100644 --- a/kernel/fs/file.c +++ b/kernel/fs/file.c @@ -96,6 +96,9 @@ fileclose(struct file *f) release(&ftable.lock); return; } + // Its flock() and OFD locks go with it, before the slot can be handed to + // someone else: they are keyed by this address. + lock_close_file(f); ff = *f; f->ref = 0; f->type = FD_NONE; diff --git a/kernel/fs/lock.c b/kernel/fs/lock.c new file mode 100644 index 0000000..ef83350 --- /dev/null +++ b/kernel/fs/lock.c @@ -0,0 +1,351 @@ +// Advisory file locks: flock(2), and fcntl(2)'s record locks. +// +// Two kinds that Linux keeps entirely separate, and so does this: +// +// flock the whole file, owned by the open file description -- shared +// by dup() and fork(), released when the last descriptor on it +// is closed. +// fcntl byte ranges. F_SETLK and friends are owned by the *process* +// and released when it closes any descriptor on the file, or +// exits. F_OFD_SETLK and friends are owned by the open file +// description, like flock, but take ranges, like fcntl. +// +// They existed as nothing at all: fcntl answered F_SETLK with EINVAL and +// flock was not a system call, so Alpine's apk -- which takes a lock on its +// database before reading it -- stopped with "Unable to lock database: +// Function not implemented". +// +// One table for the machine, a sleeplock-free spinlock around it, and every +// waiter sleeping on the table: locks change rarely, and a release wakes +// everyone to look again. No deadlock detection. A lock is keyed by the +// inode's (dev, inum), not its pointer, because a process lock outlives the +// descriptor in the one case this file does not catch -- dup2() over it. + +#include "core/types.h" +#include "core/defs.h" +#include "core/param.h" +#include "arch/mmu.h" +#include "core/proc.h" +#include "core/spinlock.h" +#include "core/sleeplock.h" +#include "fs/fs.h" +#include "fs/file.h" +#include "core/uaccess.h" +#include "uapi/errno.h" +#include "uapi/fcntl.h" + +#define NLOCK 256 + +#define K_FLOCK 1 // whole file, owner a struct file +#define K_POSIX 2 // range, owner a pid +#define K_OFD 3 // range, owner a struct file + +#define L_RD 0 // F_RDLCK +#define L_WR 1 // F_WRLCK +#define L_UN 2 // F_UNLCK + +#define END_OF_TIME ((uint64)-1) + +struct flk { + int used; + int kind; + uint dev; + uint64 inum; + void *ofd; // K_FLOCK, K_OFD + int pid; // K_POSIX + int type; // L_RD or L_WR + uint64 start, end; // inclusive; end END_OF_TIME for "to EOF" +}; + +static struct { + struct spinlock lock; + struct flk l[NLOCK]; + int ready; +} lk; + +static void +lk_init(void) +{ + if (!lk.ready) { + initlock(&lk.lock, "flock"); + lk.ready = 1; + } +} + +static int +same_file(struct flk *a, struct inode *ip) +{ + return a->used && a->dev == ip->dev && a->inum == ip->inum; +} + +// flock() conflicts only with flock(); the record kinds only with each +// other -- POSIX and OFD locks do see one another, as on Linux. +static int +same_family(int a, int b) +{ + return (a == K_FLOCK) == (b == K_FLOCK); +} + +static int +mine(struct flk *a, int kind, void *ofd, int pid) +{ + if (a->kind != kind) + return 0; + return kind == K_POSIX ? a->pid == pid : a->ofd == ofd; +} + +// The first lock that would stop this one, or 0. +static struct flk * +conflict(struct inode *ip, int kind, void *ofd, int pid, int type, + uint64 s, uint64 e) +{ + for (int i = 0; i < NLOCK; i++) { + struct flk *a = &lk.l[i]; + if (!same_file(a, ip) || !same_family(a->kind, kind) || + mine(a, kind, ofd, pid)) + continue; + if (a->end < s || a->start > e) + continue; + if (a->type == L_WR || type == L_WR) + return a; + } + return 0; +} + +static struct flk * +slot(void) +{ + for (int i = 0; i < NLOCK; i++) + if (!lk.l[i].used) + return &lk.l[i]; + return 0; +} + +// Take [s, e] out of every lock this owner holds on the file, splitting a +// lock that straddles it. Returns -ENOLCK if a split needed a slot and there +// was none, having changed nothing. +static int +carve(struct inode *ip, int kind, void *ofd, int pid, uint64 s, uint64 e) +{ + int need = 0, free = 0; + + for (int i = 0; i < NLOCK; i++) { + struct flk *a = &lk.l[i]; + if (!a->used) + free++; + else if (same_file(a, ip) && mine(a, kind, ofd, pid) && + a->start < s && a->end > e) + need++; + } + if (need > free) + return -ENOLCK; + + for (int i = 0; i < NLOCK; i++) { + struct flk *a = &lk.l[i]; + if (!same_file(a, ip) || !mine(a, kind, ofd, pid)) + continue; + if (a->end < s || a->start > e) + continue; + if (a->start < s && a->end > e) { // the middle goes: two pieces + struct flk *b = slot(); + *b = *a; + b->start = e + 1; + a->end = s - 1; + } else if (a->start < s) { + a->end = s - 1; + } else if (a->end > e) { + a->start = e + 1; + } else { + a->used = 0; + } + } + return 0; +} + +static int +set_lock(struct inode *ip, int kind, void *ofd, int pid, int type, + uint64 s, uint64 e, int wait) +{ + lk_init(); + acquire(&lk.lock); + if (type != L_UN) { + while (conflict(ip, kind, ofd, pid, type, s, e)) { + if (!wait) { + release(&lk.lock); + return -EAGAIN; // EWOULDBLOCK, the same number + } + if (myproc()->killed || myproc()->pending_signals) { + release(&lk.lock); + return -EINTR; + } + sleep(&lk, &lk.lock); + } + } + int r = carve(ip, kind, ofd, pid, s, e); + if (r == 0 && type != L_UN) { + struct flk *a = slot(); + if (!a) { + r = -ENOLCK; + } else { + a->used = 1; + a->kind = kind; + a->dev = ip->dev; + a->inum = ip->inum; + a->ofd = ofd; + a->pid = pid; + a->type = type; + a->start = s; + a->end = e; + } + } + release(&lk.lock); + wakeup(&lk); + return r; +} + +// Every lock matching (owner, maybe file), gone. +static void +drop(struct inode *ip, int pid, void *ofd) +{ + int any = 0; + + if (!lk.ready) + return; + acquire(&lk.lock); + for (int i = 0; i < NLOCK; i++) { + struct flk *a = &lk.l[i]; + if (!a->used || (ip && !same_file(a, ip))) + continue; + if ((pid && a->kind == K_POSIX && a->pid == pid) || + (ofd && a->kind != K_POSIX && a->ofd == ofd)) { + a->used = 0; + any = 1; + } + } + release(&lk.lock); + if (any) + wakeup(&lk); +} + +// close() of one descriptor: the process's record locks on that file go, as +// POSIX requires -- including ones taken through a different descriptor. +void +lock_close_fd(struct file *f) +{ + if (f->type == FD_INODE && f->ip) + drop(f->ip, myproc()->pid, 0); +} + +// The open file description itself is going: its flock and OFD locks. +void +lock_close_file(struct file *f) +{ + drop(0, 0, f); +} + +// exit(): every record lock the process holds, on every file. +void +lock_exit(int pid) +{ + drop(0, pid, 0); +} + +// --------------------------------------------------------------------------- +// flock(2) +// --------------------------------------------------------------------------- + +#define LOCK_SH 1 +#define LOCK_EX 2 +#define LOCK_NB 4 +#define LOCK_UN 8 + +int +lock_flock(struct file *f, int op) +{ + // Nothing to lock but a file; a socket or a pipe accepts it and means + // nothing by it, as on Linux. + if (f->type != FD_INODE || !f->ip) + return 0; + + int type; + switch (op & ~LOCK_NB) { + case LOCK_SH: type = L_RD; break; + case LOCK_EX: type = L_WR; break; + case LOCK_UN: type = L_UN; break; + default: return -EINVAL; + } + // Converting a lock is not atomic on Linux either: the old one goes first. + if (type != L_UN) + set_lock(f->ip, K_FLOCK, f, 0, L_UN, 0, END_OF_TIME, 0); + return set_lock(f->ip, K_FLOCK, f, 0, type, 0, END_OF_TIME, + !(op & LOCK_NB)); +} + +// --------------------------------------------------------------------------- +// fcntl(2): F_GETLK, F_SETLK, F_SETLKW and the OFD forms +// --------------------------------------------------------------------------- + +struct linux_flock { + short l_type; + short l_whence; + int64 l_start; + int64 l_len; + int l_pid; +}; + +int +lock_fcntl(struct file *f, int cmd, addr_t arg) +{ + struct linux_flock *fl = user_ptr(arg, sizeof *fl); + if (!fl) + return -EFAULT; + if (f->type != FD_INODE || !f->ip) + return -EBADF; + if (fl->l_type != L_RD && fl->l_type != L_WR && fl->l_type != L_UN) + return -EINVAL; + if ((fl->l_type == L_RD && !f->readable) || (fl->l_type == L_WR && !f->writable)) + if (cmd != F_GETLK && cmd != F_OFD_GETLK) + return -EBADF; + + int64 base; + switch (fl->l_whence) { + case 0: base = 0; break; + case 1: base = (int64)f->off; break; + case 2: base = (int64)f->ip->size; break; + default: return -EINVAL; + } + int64 s = base + fl->l_start, len = fl->l_len; + uint64 e; + if (len < 0) { // a negative length counts backwards + s += len; + len = -len; + } + if (s < 0) + return -EINVAL; + e = len == 0 ? END_OF_TIME : (uint64)(s + len - 1); + + int ofd = cmd == F_OFD_GETLK || cmd == F_OFD_SETLK || cmd == F_OFD_SETLKW; + int kind = ofd ? K_OFD : K_POSIX; + int pid = myproc()->pid; + + if (cmd == F_GETLK || cmd == F_OFD_GETLK) { + if (ofd && fl->l_pid != 0) + return -EINVAL; + lk_init(); + acquire(&lk.lock); + struct flk *c = conflict(f->ip, kind, f, pid, fl->l_type, (uint64)s, e); + if (c) { + fl->l_type = (short)c->type; + fl->l_whence = 0; + fl->l_start = (int64)c->start; + fl->l_len = c->end == END_OF_TIME ? 0 : (int64)(c->end - c->start + 1); + fl->l_pid = c->kind == K_POSIX ? c->pid : -1; + } else { + fl->l_type = L_UN; + } + release(&lk.lock); + return 0; + } + return set_lock(f->ip, kind, f, pid, fl->l_type, (uint64)s, e, + cmd == F_SETLKW || cmd == F_OFD_SETLKW); +} diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index a562127..f707c266 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -657,6 +657,9 @@ exit(int status) } } + // Record locks belong to the process, not to any one descriptor. + lock_exit(myproc()->pid); + begin_op(); iput(myproc()->cwd); end_op(); diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index 85ad503..c90764b 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -332,10 +332,20 @@ addr_t sys_close(void) if (argfd(0, &fd, &f) < 0) return -EBADF; myproc()->ofile[fd] = 0; myproc()->ofile_flags[fd] = 0; + lock_close_fd(f); fileclose(f); return 0; } +addr_t sys_flock(void) +{ + struct file *f; + int op; + if (argfd(0, 0, &f) < 0) return -EBADF; + if (argint(1, &op) < 0) return -EINVAL; + return lock_flock(f, op); +} + addr_t sys_fcntl(void) { @@ -374,6 +384,13 @@ sys_fcntl(void) case F_SETFL: f->nonblock = (arg & O_NONBLOCK) != 0; return 0; + case F_GETLK: case F_SETLK: case F_SETLKW: + case F_OFD_GETLK: case F_OFD_SETLK: case F_OFD_SETLKW: { + // A pointer, which argint() above would have cut to 32 bits. + uint64 a; + if (argaddr(2, &a) < 0) return -EFAULT; + return lock_fcntl(f, cmd, (addr_t)a); + } } return -EINVAL; } diff --git a/kernel/sys/table.inc b/kernel/sys/table.inc index 83b1bf0..0002680 100644 --- a/kernel/sys/table.inc +++ b/kernel/sys/table.inc @@ -295,6 +295,7 @@ SYSCALL(202, 213, epoll_create) SYSCALL(203, 281, epoll_pwait) SYSCALL(204, 282, signalfd) SYSCALL(205, 284, eventfd) +SYSCALL(206, 73, flock) // --- mounts --- SYSCALL(185, 165, mount) diff --git a/shell.nix b/shell.nix index a51936c..0da3540 100644 --- a/shell.nix +++ b/shell.nix @@ -38,6 +38,18 @@ pkgs.mkShell { xorriso # make iso mtools # ESP staging curl gnutar # fetching ports + xz # ...some of which are .tar.xz + + # The X ports (docs/plan/xorg.md): half of them configure with autotools + # and half with meson, and all of them ask pkg-config for each other. + # libxcb generates its protocol code with python; libX11 and xkbcomp need + # m4 and bison at build time. + pkg-config + meson + ninja + gnum4 + bison + flex ]; shellHook = ''