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:
parent
b3f72cd95e
commit
020aa6113c
34 changed files with 1342 additions and 120 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue