flock, fcntl record locks

There were none: fcntl answered F_SETLK with EINVAL and flock was not a
system call. Alpine's apk, which runs unmodified on SchOS through
Alpine's own musl, locks its database before reading it and stopped
there.

kernel/fs/lock.c keeps one table of advisory locks keyed by (dev, inum):
flock() for the whole file, owned by the open file description;
F_SETLK/F_SETLKW/F_GETLK for byte ranges, owned by the process and
released when it closes any descriptor on the file or exits; and the
F_OFD_* forms, ranges owned by the description. flock and record locks
do not see each other, as on Linux. Waiters sleep until any release and
are interrupted by a signal.

cmd/locktest checks each between a parent and a child, including range
splitting, F_GETLK's report, and a blocked waiter being woken.

shell.nix gains pkg-config, meson, ninja, m4, bison, flex and xz, for
the X ports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 12:22:36 +02:00
commit 6f3175d8a5
11 changed files with 562 additions and 2 deletions

351
kernel/fs/lock.c Normal file
View file

@ -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);
}