Users and permissions: accounts, login, enforced file permissions, setuid, su, passwd, doas

The kernel now checks owner/group/other on open, create, unlink, rename, exec
and path search; honours set-user-id and set-group-id; restricts kill, mount,
reboot and mknod; keeps supplementary groups; and implements chown. devtmpfs
nodes carry owners and modes that chmod/chown keep. Terminals run login, the
image has root and schos, X starts as the ordinary user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 13:16:07 +01:00
commit 020aa6113c
34 changed files with 1342 additions and 120 deletions

View file

@ -58,6 +58,14 @@
#define DEVFS_PTS0 8192ULL
#define DEVFS_PTM0 12288ULL
// Groups, by the numbers Alpine's /etc/group gives them.
#define G_ROOT 0
#define G_TTY 5
#define G_DISK 6
#define G_AUDIO 18
#define G_INPUT 23
#define G_VIDEO 27
// What udev adds to /dev: directories (/dev/input/by-path, /dev/disk/...) and
// symlinks (/dev/fd, /dev/stdin, /dev/char/13:64, /dev/disk/by-id/...). The
// device nodes themselves stay the kernel's; this is a small table of names
@ -95,6 +103,71 @@ extras_init(void)
}
}
// What chmod and chown have done to the kernel's nodes. A node is built from
// the table above every time its inode is loaded, so a change has to be kept
// somewhere that outlives the inode: here, by inode number, applied on the way
// in. login claims its terminal this way, and udev sets modes on input nodes.
#define NOVR 128
static struct {
struct spinlock lock;
int ready;
struct { uint64 inum; uint mode, uid, gid; } e[NOVR];
} ovr;
static void
ovr_init(void)
{
if (!ovr.ready) {
initlock(&ovr.lock, "devfsovr");
ovr.ready = 1;
}
}
static void
ovr_apply(struct inode *ip)
{
ovr_init();
acquire(&ovr.lock);
for (int i = 0; i < NOVR; i++)
if (ovr.e[i].inum == ip->inum) {
ip->mode = ovr.e[i].mode;
ip->uid = ovr.e[i].uid;
ip->gid = ovr.e[i].gid;
break;
}
release(&ovr.lock);
}
static void
ovr_set(uint64 inum, uint mode, uint uid, uint gid)
{
int slot = -1;
ovr_init();
acquire(&ovr.lock);
for (int i = 0; i < NOVR; i++) {
if (ovr.e[i].inum == inum) { slot = i; break; }
if (slot < 0 && ovr.e[i].inum == 0)
slot = i;
}
if (slot >= 0) {
ovr.e[slot].inum = inum;
ovr.e[slot].mode = mode;
ovr.e[slot].uid = uid;
ovr.e[slot].gid = gid;
}
release(&ovr.lock);
}
// A new pseudo-terminal's slave belongs to whoever opened ptmx, in group tty,
// readable and writable by the owner and writable by the group -- what grantpt
// does on Linux.
void
devfs_pty_claim(int n, uint uid)
{
ovr_set(DEVFS_PTS0 + (uint64)n, 0620, uid, G_TTY);
}
static int
extra_of(uint64 inum)
{
@ -115,8 +188,11 @@ struct devnode {
short major;
short minor;
short dir;
ushort mode; // permission bits a fresh boot starts with
ushort gid; // owner is always root; the group by name
};
// The devices this kernel implements, in the order they appear in a listing.
//
// Minor numbers under major 1 follow Linux's mem-major layout where it costs
@ -125,65 +201,65 @@ struct devnode {
//
// The majors are devsw[] indices and nothing else; see kernel/dev/cdev.c.
static const struct devnode devnodes[] = {
{ "console", 1, 1, D_ROOT },
{ "null", 1, 2, D_ROOT },
{ "keyboard",1, 3, D_ROOT },
{ "zero", 1, 5, D_ROOT },
{ "full", 1, 7, D_ROOT },
{ "random", 1, 8, D_ROOT },
{ "urandom", 1, 9, D_ROOT },
{ "console", 1, 1, D_ROOT , 0600, G_ROOT },
{ "null", 1, 2, D_ROOT , 0666, G_ROOT },
{ "keyboard",1, 3, D_ROOT , 0660, G_INPUT },
{ "zero", 1, 5, D_ROOT , 0666, G_ROOT },
{ "full", 1, 7, D_ROOT , 0666, G_ROOT },
{ "random", 1, 8, D_ROOT , 0666, G_ROOT },
{ "urandom", 1, 9, D_ROOT , 0666, G_ROOT },
/* /dev/tty is the caller's *controlling* terminal, which is a different
thing from the console and used to be the same thing only because there
was one terminal. Minor 10 resolves through proc->ctty; see
vt_for_minor() in kernel/core/console.c. */
{ "tty", 1, 10, D_ROOT },
{ "tty", 1, 10, D_ROOT , 0666, G_ROOT },
/* The terminal on screen, as on Linux. Xorg opens it to find a free VT and
switch to it, and without one ran on no VT at all: the console went on
drawing into the framebuffer over the X screen. Any minor that is not a
numbered terminal or the controlling one resolves to vt_current(). */
{ "tty0", 1, 0, D_ROOT },
{ "tty1", 1, 11, D_ROOT },
{ "tty2", 1, 12, D_ROOT },
{ "tty3", 1, 13, D_ROOT },
{ "tty4", 1, 14, D_ROOT },
{ "tty5", 1, 15, D_ROOT },
{ "tty6", 1, 16, D_ROOT },
{ "tty7", 1, 17, D_ROOT },
{ "tty8", 1, 18, D_ROOT },
{ "tty9", 1, 19, D_ROOT },
{ "tty10", 1, 20, D_ROOT },
{ "tty11", 1, 21, D_ROOT },
{ "tty12", 1, 22, D_ROOT },
{ "stdin", 1, 1, D_ROOT },
{ "stdout", 1, 1, D_ROOT },
{ "stderr", 1, 1, D_ROOT },
{ "fb0", 2, 0, D_ROOT },
{ "pci", 9, 0, D_ROOT },
{ "dbg", 10, 0, D_ROOT },
{ "tty0", 1, 0, D_ROOT , 0660, G_TTY },
{ "tty1", 1, 11, D_ROOT , 0660, G_TTY },
{ "tty2", 1, 12, D_ROOT , 0660, G_TTY },
{ "tty3", 1, 13, D_ROOT , 0660, G_TTY },
{ "tty4", 1, 14, D_ROOT , 0660, G_TTY },
{ "tty5", 1, 15, D_ROOT , 0660, G_TTY },
{ "tty6", 1, 16, D_ROOT , 0660, G_TTY },
{ "tty7", 1, 17, D_ROOT , 0660, G_TTY },
{ "tty8", 1, 18, D_ROOT , 0660, G_TTY },
{ "tty9", 1, 19, D_ROOT , 0660, G_TTY },
{ "tty10", 1, 20, D_ROOT , 0660, G_TTY },
{ "tty11", 1, 21, D_ROOT , 0660, G_TTY },
{ "tty12", 1, 22, D_ROOT , 0660, G_TTY },
{ "stdin", 1, 1, D_ROOT , 0666, G_ROOT },
{ "stdout", 1, 1, D_ROOT , 0666, G_ROOT },
{ "stderr", 1, 1, D_ROOT , 0666, G_ROOT },
{ "fb0", 2, 0, D_ROOT , 0660, G_VIDEO },
{ "pci", 9, 0, D_ROOT , 0600, G_ROOT },
{ "dbg", 10, 0, D_ROOT , 0600, G_ROOT },
// The same device, answering only the views that read. Minor 1 rather than
// a flag, because the minor is the one piece of per-open state a character
// device already sees -- so which one you opened *is* the permission, and
// nothing has to be remembered between the write and the read. See
// include/core/dbg.h and cmd/schdbgd.
{ "dbgro", 10, 1, D_ROOT },
{ "netdev", 11, 0, D_ROOT },
{ "dbgro", 10, 1, D_ROOT , 0644, G_ROOT },
{ "netdev", 11, 0, D_ROOT , 0600, G_ROOT },
// Audio, spelled the way UNIX spells it. See include/dev/audio.h.
{ "dsp", 12, 0, D_ROOT },
{ "mouse", 3, 1, D_ROOT },
{ "dsp", 12, 0, D_ROOT , 0660, G_AUDIO },
{ "mouse", 3, 1, D_ROOT , 0660, G_INPUT },
// Opening it makes a new pseudo-terminal pair; see kernel/dev/pty.c.
{ "ptmx", PTMX_MAJOR, 0, D_ROOT },
{ "ptmx", PTMX_MAJOR, 0, D_ROOT , 0666, G_ROOT },
// Raw disks. Created whether or not a drive is present: opening one with no
// disk behind it fails with ENXIO, which is a better answer than a missing
// path -- and with no node at all, `echo x > /dev/hdb` silently creates an
// ordinary file of that name and appears to work.
{ "hda", 8, 0, D_ROOT },
{ "hdb", 8, 1, D_ROOT },
{ "hda", 8, 0, D_ROOT , 0660, G_DISK },
{ "hdb", 8, 1, D_ROOT , 0660, G_DISK },
// Raw input events, Linux evdev layout. The minor is the device index, so
// event0 is the keyboard and event1 the mouse.
{ "event0", 7, 0, D_INPUT },
{ "event1", 7, 1, D_INPUT },
{ "event0", 7, 0, D_INPUT , 0660, G_INPUT },
{ "event1", 7, 1, D_INPUT , 0660, G_INPUT },
};
#define NDEVNODES ((int)(sizeof(devnodes) / sizeof(devnodes[0])))
@ -206,6 +282,7 @@ devfs_iload(struct inode *ip)
ip->size = 0;
int x = extra_of(ip->inum);
ip->mode = 0;
if (x >= 0) {
extras_init();
acquire(&extras.lock);
@ -240,20 +317,33 @@ devfs_iload(struct inode *ip)
ip->nlink = 1;
ip->major = devnodes[i].major;
ip->minor = devnodes[i].minor;
ip->mode = devnodes[i].mode;
ip->gid = devnodes[i].gid;
} else if (ip->inum >= DEVFS_BLK0 &&
blkdev_present((uint)(ip->inum - DEVFS_BLK0))) {
ip->type = T_DEV;
ip->nlink = 1;
ip->major = 8;
ip->minor = (short)(ip->inum - DEVFS_BLK0);
ip->mode = 0660;
ip->gid = G_DISK;
} else {
ip->type = 0; /* no such node */
ip->nlink = 0;
}
if (x < 0 && ip->type != 0)
ovr_apply(ip);
ip->valid = 1;
}
static void devfs_istore(struct inode *ip) { (void)ip; }
// iupdate() lands here after a chmod or chown; see ovr above. Directories and
// udev's links keep nothing.
static void
devfs_istore(struct inode *ip)
{
if (extra_of(ip->inum) < 0 && ip->type == T_DEV)
ovr_set(ip->inum, ip->mode, ip->uid, ip->gid);
}
static void
devfs_ifree(struct inode *ip)
{