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

@ -144,6 +144,7 @@ ptmx_open(struct inode *ip)
ptys[n].used = 0;
return 0;
}
devfs_pty_claim(n, myproc() ? myproc()->euid : 0);
iunlockput(ip);
ilock(m);
return m;

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)
{

View file

@ -5,6 +5,7 @@
#include "arch/mmu.h"
#include "core/proc.h"
#include "core/defs.h"
#include "uapi/stat.h"
#include "arch/x86.h"
#include "core/elf.h"
#include "uapi/fcntl.h"
@ -403,6 +404,10 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
begin_op();
int in_op = 1; // the error path closes the transaction only once
int ret = -1;
// What the file's mode asks for, read while it is still locked: a program
// with the set-user-id bit runs as its owner, set-group-id as its group.
int want_suid = 0, want_sgid = 0;
uint file_uid = 0, file_gid = 0;
int err;
if((ip = nameifollow(0, path, &err)) == 0){
@ -415,6 +420,20 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
ilock(ip);
pgdir = 0;
// To run a file is to have execute permission on it, and it has to be a
// plain file: a directory has the bit and is not a program.
if(ip->type == T_DIR || ip->type == T_DEV || inode_permission(ip, 1, 0) < 0){
ret = -EACCES;
goto bad;
}
{
uint m = inode_mode_bits(ip);
want_suid = (m & 04000) != 0;
want_sgid = (m & 02000) != 0 && (m & 010) != 0;
file_uid = ip->uid;
file_gid = ip->gid;
}
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf)){
ret = -ENOEXEC; // too short to be anything we can run
goto bad;
@ -572,6 +591,15 @@ exec_elf(char *path, struct argsrc *av, struct argsrc *ev)
myproc()->sz = sz;
pgdir = 0; // newmm owns it now; not bad:'s to free
// The new image's identity. The saved ids become the effective ones, which
// is what lets a program drop its privilege and take it back (su, sudo).
if (want_suid)
myproc()->euid = file_uid;
if (want_sgid)
myproc()->egid = file_gid;
myproc()->suid = myproc()->euid;
myproc()->sgid = myproc()->egid;
// New address space: reset all per-process mmap/signal/TLS state.
// sig_handler/sig_restorer point into the *old* image — POSIX resets
// handled signals to default on exec, and keeping them would make the

View file

@ -361,6 +361,82 @@ fs_readonly(uint dev)
return SCHFS_IS_SNAP_DEV(dev) || (FAT_IS_DEV(dev) && fat_readonly(dev));
}
// ===========================================================================
// Permissions
// ===========================================================================
// Is `gid` the caller's group, effective or supplementary? `real` asks about
// the real group instead of the effective one -- access(2) does.
int
in_group(uint gid, int real)
{
struct proc *p = myproc();
if (p == 0)
return 1;
if (gid == (real ? p->gid : p->egid))
return 1;
for (int i = 0; i < p->ngroups; i++)
if (p->groups[i] == gid)
return 1;
return 0;
}
// The permission bits of an inode, 07777, as stat reports them. A filesystem
// that keeps real ones (SchFS) supplies them; the rest are made up from the
// type, which is what stat has always done. A mode that was recorded as "no
// bits" is 01000000, which masks to zero here.
uint
inode_mode_bits(struct inode *ip)
{
if (ip->mode)
return ip->mode & 07777;
switch (ip->type) {
case T_DIR: return FAT_IS_DEV(ip->dev) ? 0777 : 0755;
case T_DEV: return 0666;
case T_SYMLINK: return 0777;
case T_SOCK: return 0755;
}
// A volume that cannot say who owns anything is everyone's: FAT on a USB
// stick was usable by whoever was logged in, and still is.
if (FAT_IS_DEV(ip->dev))
return 0777;
return (ip->rwmode & 2) ? 0755 : 0555;
}
// May the caller do `mask` (4 read, 2 write, 1 execute or search) to this
// inode? 0, or -EACCES. The caller holds the inode lock or knows it will not
// change under it.
//
// Root may read and write anything, and may execute a file only if somebody
// can -- the same rule Linux applies, so that a data file does not become a
// program because root asked.
int
inode_permission(struct inode *ip, int mask, int real)
{
struct proc *p = myproc();
uint m = inode_mode_bits(ip);
uint who = p ? (real ? p->uid : p->euid) : 0;
if (p == 0 || who == 0) {
if ((mask & 1) && ip->type != T_DIR && !(m & 0111))
return -EACCES;
return 0;
}
if (who == ip->uid)
m >>= 6;
else if (in_group(ip->gid, real))
m >>= 3;
return ((m & 7) & (uint)mask) == (uint)mask ? 0 : -EACCES;
}
// Superuser-only operations: mount, reboot, settimeofday and the rest.
int
capable(void)
{
return myproc() == 0 || myproc()->euid == 0;
}
static const struct fs_ops *
ops_for(uint dev)
{
@ -626,6 +702,7 @@ ilock(struct inode *ip)
if (ip->valid == 0) {
ip->mode = 0;
ip->uid = ip->gid = 0;
ip->ops->iload(ip);
}
}
@ -1970,6 +2047,15 @@ namex_from(struct inode *start, char *path, int nameiparent, char *name,
*err = ENOTDIR;
goto fail;
}
// Searching a directory takes its execute bit. The directory that holds
// the final name is searched too, even when the caller only wants its
// inode: nothing under a directory you cannot enter is yours to name.
if (inode_permission(ip, 1, 0) < 0) {
iunlockput(ip);
if (err)
*err = EACCES;
goto fail;
}
if (nameiparent && *spath == '\0') {
iunlock(ip);
goto out;

View file

@ -101,6 +101,8 @@ schfs_vfs_iload(struct inode *ip)
ip->size = in.size;
ip->rwmode = RW;
ip->mode = in.mode & 07777; // 0 on disk: never recorded
ip->uid = in.uid;
ip->gid = in.gid;
ip->supermode = 0;
ip->showmode = 1;
ip->atime = (uint)in.atime;
@ -137,6 +139,8 @@ schfs_vfs_istore(struct inode *ip)
if (ip->mode)
in.mode = ip->mode & 07777;
in.uid = ip->uid;
in.gid = ip->gid;
in.type = type_from_vfs(ip->type);
in.rdev_major = (uint16_t)ip->major;
in.rdev_minor = (uint16_t)ip->minor;

View file

@ -723,6 +723,14 @@ sockwrite_fds(struct socket *s, char *addr, int n, struct file **f, int nf)
// look for it. Same contract as a pipe with no reader: a signal, then an
// error. A process that writes to a dead connection and is not told about
// it spins forever believing it is making progress.
//
// A datagram socket is not a connection, so having no peer is not a broken
// pipe: syslog() sends to /dev/log whether or not anything listens, and a
// signal for that killed doas.
if(s->domain == AF_UNIX && s->type == SOCK_DGRAM && s->peer == 0){
release(&stab.lock);
return -ENOTCONN;
}
if(s->peer == 0 || s->wr_shut){
release(&stab.lock);
sigsend(myproc()->pid, SIGPIPE);

View file

@ -532,6 +532,7 @@ found:
p->sid = 0;
p->uid = p->euid = p->suid = 0;
p->gid = p->egid = p->sgid = 0;
p->ngroups = 0;
release(&ptable.lock);
@ -760,6 +761,8 @@ fork(void)
np->ctty = myproc()->ctty;
np->uid = myproc()->uid; np->euid = myproc()->euid; np->suid = myproc()->suid;
np->gid = myproc()->gid; np->egid = myproc()->egid; np->sgid = myproc()->sgid;
np->ngroups = myproc()->ngroups;
memmove(np->groups, myproc()->groups, sizeof(np->groups));
np->cwd = idup(myproc()->cwd);
np->root = myproc()->root ? idup(myproc()->root) : 0;
@ -1167,6 +1170,8 @@ clone_proc(uint64 flags, uint64 stack, uint64 ptid, uint64 ctid, uint64 tls)
np->ctty = cur->ctty;
np->uid = cur->uid; np->euid = cur->euid; np->suid = cur->suid;
np->gid = cur->gid; np->egid = cur->egid; np->sgid = cur->sgid;
np->ngroups = cur->ngroups;
memmove(np->groups, cur->groups, sizeof(np->groups));
np->cwd = idup(cur->cwd);
np->root = cur->root ? idup(cur->root) : 0;
memmove(np->cwd_path, cur->cwd_path, sizeof(cur->cwd_path));
@ -2614,8 +2619,21 @@ sig_to_threads(struct proc *p, int sig)
}
}
int
sigsend(int pid, int sig)
// May `from` signal `to`? Root may; anyone else only a process whose real or
// saved user id is one of their own ids -- kill(2)'s rule.
static int
sig_permitted(struct proc *from, struct proc *to)
{
if(from == 0 || from->euid == 0)
return 1;
return from->uid == to->uid || from->uid == to->suid ||
from->euid == to->uid || from->euid == to->suid;
}
// `check` is for a signal a process sends on purpose, by kill(2). The kernel's
// own -- SIGPIPE, the terminal's SIGINT -- is nobody's to refuse.
static int
sigsend_x(int pid, int sig, int check)
{
struct proc *p;
@ -2625,6 +2643,10 @@ sigsend(int pid, int sig)
acquire(&ptable.lock);
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
if(p->pid == pid && p->state != UNUSED && p->state != ZOMBIE){
if(check && !sig_permitted(myproc(), p)){
release(&ptable.lock);
return -2;
}
if(init_ignores(p, sig)){
release(&ptable.lock);
return 0; // silently dropped, as POSIX requires
@ -2652,16 +2674,29 @@ sigsend(int pid, int sig)
return -1;
}
int
sigsend(int pid, int sig)
{
return sigsend_x(pid, sig, 0);
}
// -1 if there is no such process, -2 if there is and the caller may not.
int
sigsend_user(int pid, int sig)
{
return sigsend_x(pid, sig, 1);
}
// Deliver a signal to every member of a process group. This is what job
// control actually needs: a shell sends SIGINT/SIGTSTP/SIGHUP to a *group*,
// never to individual pids. sys_kill() with pid <= 0 routes here.
//
// Returns 0 if at least one process was signalled, -1 if the group is empty.
int
sigsend_pgrp(int pgid, int sig)
static int
sigsend_pgrp_x(int pgid, int sig, int check)
{
struct proc *p;
int found = 0;
int found = 0, denied = 0;
if(sig < 0 || sig >= 32)
return -1;
@ -2672,6 +2707,10 @@ sigsend_pgrp(int pgid, int sig)
continue;
if(p->pgid != pgid)
continue;
if(check && !sig_permitted(myproc(), p)){
denied = 1;
continue;
}
found = 1;
if(sig == 0)
continue; // signal 0 is an existence probe only
@ -2701,7 +2740,19 @@ sigsend_pgrp(int pgid, int sig)
}
release(&ptable.lock);
return found ? 0 : -1;
return found ? 0 : denied ? -2 : -1;
}
int
sigsend_pgrp(int pgid, int sig)
{
return sigsend_pgrp_x(pgid, sig, 0);
}
int
sigsend_pgrp_user(int pgid, int sig)
{
return sigsend_pgrp_x(pgid, sig, 1);
}
// Look up a process's process-group id. Returns -1 if no such live process.

View file

@ -120,9 +120,11 @@ stati_linux(struct inode *ip, struct linux_stat *st)
}
// A filesystem that keeps real permission bits -- SchFS does -- reports
// those instead. tmux, ssh and gpg refuse a directory others can read.
if (ip->mode)
perm_bits = ip->mode & 07777;
(void)perm_bits;
perm_bits = inode_mode_bits(ip);
st->st_mode = type_bits | perm_bits;
st->st_uid = ip->uid;
st->st_gid = ip->gid;
// Which device this node actually is. Left at zero until now, so every
// entry in /dev printed as "0, 0" under ls -l -- harmless while nothing
@ -301,6 +303,24 @@ linux_arg_mode(int n)
return mode & 07777;
}
// May the caller add or remove names in directory `dp`? It takes write and
// search on the directory, and in a sticky one (/tmp) only the owner of the
// file or of the directory may remove or rename a name -- `victim` is the
// inode being removed, or 0 when a name is being added. -EACCES, or -EPERM for
// the sticky rule, as Linux answers them.
static int
dir_may_modify(struct inode *dp, struct inode *victim)
{
int r = inode_permission(dp, 3, 0);
if (r < 0)
return r;
if (victim && (inode_mode_bits(dp) & 01000) && myproc() &&
myproc()->euid != 0 && myproc()->euid != dp->uid &&
myproc()->euid != victim->uid)
return -EPERM;
return 0;
}
static struct inode*
createat_x(int dirfd, char *path, short type, short major, short minor,
int *err, int mode)
@ -345,6 +365,11 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
iunlockput(ip);
return 0;
}
if (dir_may_modify(dp, 0) < 0) {
iunlockput(dp);
if (err) *err = EACCES;
return 0;
}
// v1 and SchFS cannot fail either of these for a reason the caller could
// have avoided. FAT can: the volume fills up, the fixed root directory of
// FAT16 holds 512 names and no more, and a name may hold characters FAT
@ -357,6 +382,11 @@ createat_x(int dirfd, char *path, short type, short major, short minor,
ilock(ip);
ip->major = major; ip->minor = minor; ip->nlink = 1;
ip->showmode = 1;
// Owned by whoever made it. A directory with the setgid bit hands its
// group on, which is how a shared directory keeps one group.
ip->uid = myproc() ? myproc()->euid : 0;
ip->gid = (inode_mode_bits(dp) & 02000) ? dp->gid
: (myproc() ? myproc()->egid : 0);
// The permission bits asked for, less the umask -- or, from a caller that
// has none to give, what open() and mkdir() would ask for. A symlink's
// are 0777 on Linux whatever the umask, and nothing reads them.
@ -704,6 +734,20 @@ open_existing(int dirfd, char *path, int omode)
return at_nameif(dirfd, path, &err);
}
// May the caller open an existing file this way? Newly created files are not
// asked: a file made with O_CREAT and mode 0444 can still be written by the
// call that made it.
static int
open_perm(struct inode *ip, int omode)
{
int acc = omode & O_ACCMODE, mask = 0;
if (acc == O_RDONLY || acc == O_RDWR)
mask |= 4;
if (acc == O_WRONLY || acc == O_RDWR || (omode & O_TRUNC))
mask |= 2;
return inode_permission(ip, mask, 0);
}
addr_t sys_open(void)
{
char *path;
@ -722,6 +766,7 @@ addr_t sys_open(void)
if ((ip = open_existing(AT_FDCWD, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
if ((cerr = open_perm(ip, omode)) < 0) { iunlockput(ip); end_op(); return cerr; }
} else
ip = createat_x(AT_FDCWD, path,
(omode & O_EXTENT) ? T_EXTENT : T_FILE, 0, 0, &cerr,
@ -737,6 +782,7 @@ addr_t sys_open(void)
int err;
if (!(ip = open_lookup(AT_FDCWD, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
if ((err = open_perm(ip, omode)) < 0) { iunlockput(ip); end_op(); return err; }
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
@ -789,6 +835,7 @@ addr_t sys_openat(void)
if ((ip = open_existing(dirfd, path, omode)) != 0) {
ilock(ip);
if (ip->type == T_DIR) { iunlockput(ip); end_op(); return -EISDIR; }
if ((cerr = open_perm(ip, omode)) < 0) { iunlockput(ip); end_op(); return cerr; }
} else if (!(ip = createat_x(dirfd, path, T_FILE, 0, 0, &cerr,
linux_arg_mode(3)))) {
end_op();
@ -798,6 +845,7 @@ addr_t sys_openat(void)
int err;
if (!(ip = open_lookup(dirfd, path, omode, &err))) { end_op(); return -err; }
ilock(ip);
if ((err = open_perm(ip, omode)) < 0) { iunlockput(ip); end_op(); return err; }
// Directories may only be opened for reading, but callers (e.g.
// opendir()) legitimately OR in flags like O_DIRECTORY/O_CLOEXEC/
// O_NONBLOCK alongside O_RDONLY — only the access-mode bits matter.
@ -922,16 +970,38 @@ addr_t sys_lstat(void)
return do_stat(AT_FDCWD, path, st, 0);
}
// access(2) and friends: may the caller (by real ids, unless AT_EACCESS) do
// `amode` -- F_OK 0, X_OK 1, W_OK 2, R_OK 4 -- to this inode? Write on a
// read-only filesystem is EROFS rather than EACCES.
static int
access_check(struct inode *ip, int amode, int real)
{
int r = 0;
amode &= 7;
if (amode == 0)
return 0;
ilock(ip);
if ((amode & 2) && fs_readonly(ip->dev) && ip->type != T_DEV)
r = -EROFS;
else
r = inode_permission(ip, amode, real);
iunlock(ip);
return r;
}
addr_t sys_access(void)
{
char *path;
int amode = 0;
if (argstr(0, &path) < 0) return -1;
argint(1, &amode);
int err;
begin_op();
struct inode *ip = at_nameif(AT_FDCWD, path, &err);
if (!ip) { end_op(); return -err; }
err = access_check(ip, amode, 1);
iput(ip); end_op();
return 0;
return err;
}
addr_t sys_chdir(void)
@ -947,6 +1017,7 @@ addr_t sys_chdir(void)
// shell reported "Operation not permitted" -- which reads like a
// permissions problem and sends you looking in entirely the wrong place.
if (ip->type != T_DIR) { iunlockput(ip); end_op(); return -ENOTDIR; }
if ((err = inode_permission(ip, 1, 0)) < 0) { iunlockput(ip); end_op(); return err; }
iunlock(ip); iput(myproc()->cwd); end_op();
myproc()->cwd = ip;
if (path[0] == '/') {
@ -969,6 +1040,7 @@ addr_t sys_fchdir(void)
if (argfd(0, 0, &f) < 0 || f->type != FD_INODE) return -1;
ilock(f->ip);
if (f->ip->type != T_DIR) { iunlock(f->ip); return -1; }
if (inode_permission(f->ip, 1, 0) < 0) { iunlock(f->ip); return -EACCES; }
iunlock(f->ip);
iput(myproc()->cwd); idup(f->ip); myproc()->cwd = f->ip;
myproc()->cwd_path[0] = '/'; myproc()->cwd_path[1] = 0;
@ -1040,6 +1112,7 @@ do_mknod(int dirfd, char *path, uint32 mode, uint64 dev)
int major = (int)(((dev >> 8) & 0xfff) | ((dev >> 32) & ~0xfffull));
int minor = (int)((dev & 0xff) | ((dev >> 12) & ~0xffull));
if (type == T_FILE) major = minor = 0;
if (type == T_DEV && !capable()) return -EPERM;
begin_op();
// createat_x() opens an existing file when asked for one, as open(O_CREAT)
// wants. mknod() must not.
@ -1108,6 +1181,7 @@ addr_t sys_link(void)
ip->nlink++; iupdate(ip); iunlock(ip);
if (!(dp = nameiparent(new, name))) { err = ENOENT; goto bad_link; }
ilock(dp);
if (dir_may_modify(dp, 0) < 0) { iunlockput(dp); err = EACCES; goto bad_link; }
if (dp->dev != ip->dev) { iunlockput(dp); err = EXDEV; goto bad_link; }
if (dp->ops->no_hardlinks) { iunlockput(dp); err = EPERM; goto bad_link; }
if (dirlink(dp, name, ip->inum) < 0) { iunlockput(dp); err = EEXIST; goto bad_link; }
@ -1171,6 +1245,8 @@ unlink_path(char *path)
if (!(ip = dirlookup(dp, name, &off))) { err = ENOENT; goto bad; }
ilock(ip);
if (ip->nlink < 1) panic("unlink: nlink < 1");
if ((err = -dir_may_modify(dp, ip)) != 0) { iunlockput(ip); goto bad; }
err = EPERM;
if (ip->type == T_DIR && dirempty(ip) != 1) { iunlockput(ip); err = ENOTEMPTY; goto bad; }
if (dirunlink(dp, name, off) < 0) { iunlockput(ip); err = EIO; goto bad; }
if (ip->type == T_DIR) { dp->nlink--; iupdate(dp); }
@ -1246,6 +1322,7 @@ addr_t sys_truncate(void)
begin_op();
if (!(ip = at_nameif(AT_FDCWD, path, &err))) { end_op(); return -err; }
ilock(ip);
if ((err = inode_permission(ip, 2, 0)) < 0) { iunlockput(ip); end_op(); return err; }
itrunc_to(ip, (uint64)length);
inotify_event(ip->dev, ip->inum, IN_MODIFY, 0, 0);
iunlockput(ip); end_op();
@ -1306,8 +1383,22 @@ do_rename(int olddirfd, char *oldpath, int newdirfd, char *newpath)
uint new_dev = dp_new->dev; // for inotify, after dp_new is put
uint64 new_inum = dp_new->inum;
// Both directories are checked before anything changes: a refusal after
// the new name is in place would leave the file with two.
ilock(dp_old);
int perm = dir_may_modify(dp_old, ip);
iunlock(dp_old);
if (perm < 0) {
iput(dp_old); iput(dp_new);
err = EACCES; goto bad_rename;
}
// Clear the destination, if anything is there.
ilock(dp_new);
if (dir_may_modify(dp_new, 0) < 0) {
iunlockput(dp_new); iput(dp_old);
err = EACCES; goto bad_rename;
}
if ((ip_dst = dirlookup(dp_new, newname, &dstoff)) != 0) {
// Both names already reach the same file. POSIX: succeed and do
// nothing -- which includes not unlinking either name, since removing
@ -1955,7 +2046,15 @@ chmod_inode(struct inode *ip, int mode)
if (fs_readonly(ip->dev))
return -EROFS;
ilock(ip);
// The owner may change the mode, and so may root. Setting the group id bit
// on a file whose group you are not in is quietly dropped, as on Linux.
if (myproc()->euid != 0 && myproc()->euid != ip->uid) {
iunlock(ip);
return -EPERM;
}
ip->mode = (uint)mode & 07777;
if (myproc()->euid != 0 && !in_group(ip->gid, 0))
ip->mode &= ~02000u;
if (ip->mode == 0)
ip->mode = 01000000; // "no bits", as distinct from "never set"
iupdate(ip);
@ -1991,9 +2090,89 @@ addr_t sys_fchmod(void)
end_op();
return r;
}
addr_t sys_chown(void) { return 0; }
addr_t sys_fchown(void) { return 0; }
addr_t sys_lchown(void) { return 0; }
// chown(2): -1 leaves an id alone. Root may do anything. Anyone else may only
// give a file they own to a group they are in, and any change by a non-root
// caller drops the set-user-id and set-group-id bits, so that a program cannot
// be handed over and stay privileged.
static int
chown_inode(struct inode *ip, int uid, int gid)
{
uint nu = (uint)uid, ng = (uint)gid;
int root = myproc()->euid == 0;
if (fs_readonly(ip->dev))
return -EROFS;
ilock(ip);
if (!root) {
if (myproc()->euid != ip->uid ||
(uid != -1 && nu != ip->uid) ||
(gid != -1 && !in_group(ng, 0))) {
iunlock(ip);
return -EPERM;
}
}
if (uid != -1)
ip->uid = nu;
if (gid != -1)
ip->gid = ng;
if (!root || ip->type != T_DIR) {
uint m = inode_mode_bits(ip);
if (m & 06000 && (uid != -1 || gid != -1) && ip->type != T_DIR) {
ip->mode = m & ~06000u;
if (ip->mode == 0)
ip->mode = 01000000;
}
}
iupdate(ip);
inotify_event(ip->dev, ip->inum, IN_ATTRIB, 0, 0);
iunlock(ip);
return 0;
}
static int
do_chown(int dirfd, char *path, int uid, int gid, int nofollow)
{
struct inode *ip;
int err, r;
begin_op();
ip = nofollow ? at_namei(dirfd, path) : at_nameif(dirfd, path, &err);
if (!ip) { end_op(); return nofollow ? -ENOENT : -err; }
r = chown_inode(ip, uid, gid);
iput(ip);
end_op();
return r;
}
addr_t sys_chown(void)
{
char *path;
int uid, gid;
if (argstr(0, &path) < 0) return -EFAULT;
argint(1, &uid); argint(2, &gid);
return do_chown(AT_FDCWD, path, uid, gid, 0);
}
addr_t sys_lchown(void)
{
char *path;
int uid, gid;
if (argstr(0, &path) < 0) return -EFAULT;
argint(1, &uid); argint(2, &gid);
return do_chown(AT_FDCWD, path, uid, gid, 1);
}
addr_t sys_fchown(void)
{
struct file *f;
int uid, gid, r;
if (argfd(0, 0, &f) < 0) return -EBADF;
argint(1, &uid); argint(2, &gid);
if (f->type != FD_INODE || !f->ip) return 0;
begin_op();
r = chown_inode(f->ip, uid, gid);
end_op();
return r;
}
// rmdir: remove an empty directory (same as unlink for dirs).
addr_t sys_rmdir(void)
@ -2118,6 +2297,16 @@ addr_t sys_utimensat(void)
begin_op();
ilock(ip);
if (fs_readonly(ip->dev)) { iunlockput(ip); end_op(); return -EROFS; }
// Setting the times to "now" takes write access or ownership; setting them
// to anything else takes ownership.
if (myproc()->euid != 0 && myproc()->euid != ip->uid) {
int explicit_t = 0;
for (int i = 0; utimes && i < 2; i++)
if (ts[i].tv_nsec != UTIME_OMIT && ts[i].tv_nsec != UTIME_NOW)
explicit_t = 1;
int pr = explicit_t ? -EPERM : inode_permission(ip, 2, 0);
if (pr < 0) { iunlockput(ip); end_op(); return pr; }
}
for (int i = 0; i < 2; i++) {
long sec = utimes ? ts[i].tv_sec : (long)now;
if (utimes && ts[i].tv_nsec == UTIME_OMIT) continue;
@ -2238,6 +2427,7 @@ addr_t sys_chroot(void)
struct inode *ip;
if (argstr(0, &path) < 0) return -EFAULT;
if (!capable()) return -EPERM;
begin_op();
if ((ip = namei(path)) == 0) { end_op(); return -ENOENT; }
ilock(ip);
@ -2416,7 +2606,15 @@ addr_t sys_fchmodat(void)
end_op();
return r;
}
addr_t sys_fchownat(void) { return 0; } // stub
addr_t sys_fchownat(void)
{
int dirfd, uid, gid, flags;
char *path;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -EFAULT;
argint(2, &uid); argint(3, &gid); argint(4, &flags);
return do_chown(dirfd, path, uid, gid, flags & AT_SYMLINK_NOFOLLOW);
}
addr_t sys_faccessat(void)
{
@ -2424,14 +2622,15 @@ addr_t sys_faccessat(void)
char *path;
argint(0, &dirfd);
if (argstr(1, &path) < 0) return -1;
argint(2, &(int){0}); // amode — ignored
// arg3 flags — ignored
int amode = 0;
argint(2, &amode);
int err;
begin_op();
struct inode *ip = at_nameif(dirfd, path, &err);
if (!ip) { end_op(); return -err; }
err = access_check(ip, amode, 1);
iput(ip); end_op();
return 0;
return err;
}
// faccessat2: faccessat with the flags argument the original never had. musl
@ -2460,9 +2659,10 @@ addr_t sys_faccessat2(void)
ip = (flags & AT_SYMLINK_NOFOLLOW) ? at_namei(dirfd, path)
: at_nameif(dirfd, path, &err);
if (!ip) { end_op(); return -err; }
err = access_check(ip, mode, !(flags & AT_EACCESS));
iput(ip);
end_op();
return 0;
return err;
}
addr_t sys_fstatat(void)
@ -2556,6 +2756,8 @@ addr_t sys_mount(void)
if (argstr(0, &source) < 0 || argstr(1, &target) < 0 ||
argstr(2, &fstype) < 0)
return -EFAULT;
if (!capable())
return -EPERM;
// proc: a synthetic filesystem, so there is no source device to check and
// no volume to have found at boot. `mount -t proc proc /proc`.
@ -2697,6 +2899,8 @@ addr_t sys_umount2(void)
{
char *target;
if (argstr(0, &target) < 0) return -EFAULT;
if (!capable())
return -EPERM;
begin_op();
struct inode *dp = namei(target);

View file

@ -59,18 +59,27 @@ sys_kill(void)
// pid == 0 -> every process in the caller's own group
// pid == -1 -> every process we may signal (approximated as our group)
// pid < -1 -> the process group with id -pid
if (pid == 0 || pid == -1)
return sigsend_pgrp(myproc()->pgid, sig) < 0 ? -ESRCH : 0;
if (pid < -1)
return sigsend_pgrp(-pid, sig) < 0 ? -ESRCH : 0;
{
int r = 0;
if (pid == 0 || pid == -1)
r = sigsend_pgrp_user(myproc()->pgid, sig);
else if (pid < -1)
r = sigsend_pgrp_user(-pid, sig);
else
goto single;
return r == -2 ? -EPERM : r < 0 ? -ESRCH : 0;
}
single:
// sig == 0: existence check only
if (sig == 0)
return proc_exists(pid) ? 0 : -ESRCH;
// Raise the signal for real: pending bit + wakeup, handler delivery on
// the target's next syscall boundary, default action if unhandled.
return sigsend(pid, sig) < 0 ? -ESRCH : 0;
{
int r = sigsend_user(pid, sig);
return r == -2 ? -EPERM : r < 0 ? -ESRCH : 0;
}
}
// Defined with getrusage() further down; wait4() needs it too.
@ -208,6 +217,8 @@ sys_uptime(void)
addr_t sys_reboot(void)
{
if (!capable())
return -EPERM;
cprintf("System is rebooting...\n");
// A stick left mounted should come back readable, and marked clean.
schfs_ksync_all();
@ -223,6 +234,8 @@ addr_t sys_reboot(void)
addr_t sys_shutdown(void)
{
if (!capable())
return -EPERM;
cprintf("System is shutting down...\n");
// S5, through the firmware's own PM1 registers and its _PTS. The port
// writes after it are QEMU's, Bochs' and VirtualBox's fixed addresses,
@ -335,12 +348,19 @@ addr_t
sys_setreuid(void)
{
int ruid, euid;
struct proc *p = myproc();
argint(0, &ruid); argint(1, &euid);
if (myproc()->euid != 0)
return -EPERM;
if (ruid >= 0) myproc()->uid = (uint)ruid;
if (euid >= 0) myproc()->euid = (uint)euid;
myproc()->suid = myproc()->euid;
if (p->euid != 0) {
if (ruid >= 0 && (uint)ruid != p->uid && (uint)ruid != p->euid)
return -EPERM;
if (euid >= 0 && (uint)euid != p->uid && (uint)euid != p->euid &&
(uint)euid != p->suid)
return -EPERM;
}
if (ruid >= 0 || (euid >= 0 && (uint)euid != p->uid))
p->suid = euid >= 0 ? (uint)euid : p->euid;
if (ruid >= 0) p->uid = (uint)ruid;
if (euid >= 0) p->euid = (uint)euid;
return 0;
}
@ -348,12 +368,19 @@ addr_t
sys_setregid(void)
{
int rgid, egid;
struct proc *p = myproc();
argint(0, &rgid); argint(1, &egid);
if (myproc()->euid != 0)
return -EPERM;
if (rgid >= 0) myproc()->gid = (uint)rgid;
if (egid >= 0) myproc()->egid = (uint)egid;
myproc()->sgid = myproc()->egid;
if (p->euid != 0) {
if (rgid >= 0 && (uint)rgid != p->gid && (uint)rgid != p->egid)
return -EPERM;
if (egid >= 0 && (uint)egid != p->gid && (uint)egid != p->egid &&
(uint)egid != p->sgid)
return -EPERM;
}
if (rgid >= 0 || (egid >= 0 && (uint)egid != p->gid))
p->sgid = egid >= 0 ? (uint)egid : p->egid;
if (rgid >= 0) p->gid = (uint)rgid;
if (egid >= 0) p->egid = (uint)egid;
return 0;
}
@ -361,12 +388,19 @@ addr_t
sys_setresuid(void)
{
int r, e, sv;
struct proc *p = myproc();
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->uid = (uint)r;
if (e >= 0) myproc()->euid = (uint)e;
if (sv >= 0) myproc()->suid = (uint)sv;
// Unprivileged, each id may be set only to one the process already has.
if (p->euid != 0) {
int ids[3] = { r, e, sv };
for (int i = 0; i < 3; i++)
if (ids[i] >= 0 && (uint)ids[i] != p->uid &&
(uint)ids[i] != p->euid && (uint)ids[i] != p->suid)
return -EPERM;
}
if (r >= 0) p->uid = (uint)r;
if (e >= 0) p->euid = (uint)e;
if (sv >= 0) p->suid = (uint)sv;
return 0;
}
@ -374,12 +408,18 @@ addr_t
sys_setresgid(void)
{
int r, e, sv;
struct proc *p = myproc();
argint(0, &r); argint(1, &e); argint(2, &sv);
if (myproc()->euid != 0)
return -EPERM;
if (r >= 0) myproc()->gid = (uint)r;
if (e >= 0) myproc()->egid = (uint)e;
if (sv >= 0) myproc()->sgid = (uint)sv;
if (p->euid != 0) {
int ids[3] = { r, e, sv };
for (int i = 0; i < 3; i++)
if (ids[i] >= 0 && (uint)ids[i] != p->gid &&
(uint)ids[i] != p->egid && (uint)ids[i] != p->sgid)
return -EPERM;
}
if (r >= 0) p->gid = (uint)r;
if (e >= 0) p->egid = (uint)e;
if (sv >= 0) p->sgid = (uint)sv;
return 0;
}
@ -412,10 +452,39 @@ sys_getresgid(void)
addr_t sys_getgroups(void)
{
int size;
addr_t list;
struct proc *p = myproc();
argint(0, &size);
return 0; // no supplementary groups
argaddr(1, &list);
if (size < 0)
return -EINVAL;
if (size == 0)
return p->ngroups; // how many there are
if (size < p->ngroups)
return -EINVAL;
if (p->ngroups && copy_to_user(list, p->groups, p->ngroups * sizeof(uint)) < 0)
return -EFAULT;
return p->ngroups;
}
addr_t sys_setgroups(void)
{
int size;
addr_t list;
uint tmp[NGROUPS_MAX];
struct proc *p = myproc();
argint(0, &size);
argaddr(1, &list);
if (p->euid != 0)
return -EPERM;
if (size < 0 || size > NGROUPS_MAX)
return -EINVAL;
if (size && copy_from_user(tmp, list, size * sizeof(uint)) < 0)
return -EFAULT;
p->ngroups = size;
memmove(p->groups, tmp, size * sizeof(uint));
return 0;
}
addr_t sys_setgroups(void) { return 0; }
// --- Process/scheduling extensions ---
@ -478,7 +547,10 @@ addr_t sys_tgkill(void)
// the calling thread.
if (tid <= 0) return -EINVAL;
if (sig == 0) return proc_exists(tid) ? 0 : -ESRCH;
return sigsend(tid, sig) < 0 ? -ESRCH : 0;
{
int r = sigsend_user(tid, sig);
return r == -2 ? -EPERM : r < 0 ? -ESRCH : 0;
}
}
addr_t sys_tkill(void)
@ -487,7 +559,10 @@ addr_t sys_tkill(void)
argint(0, &tid); argint(1, &sig);
if (tid <= 0) return 0;
if (sig == 0) return proc_exists(tid) ? 0 : -ESRCH;
return sigsend(tid, sig) < 0 ? -ESRCH : 0;
{
int r = sigsend_user(tid, sig);
return r == -2 ? -EPERM : r < 0 ? -ESRCH : 0;
}
}
addr_t sys_pause(void)