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

@ -161,7 +161,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 locktest
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 su passwd
CMDBINS = $(addprefix $(BUILD)/cmd/, $(CMDS))
# -----------------------------------------------------------------------
@ -879,13 +879,13 @@ $(BUILD)/kernel/kernel: $(BUILD)/kernel/kernel.pass1 $(BUILD)/kernel/ksym.o
$(BUILD)/fs-ram.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
@used=$$($(MKSCHFS) $@ --size 262144 --label schos \
@used=$$($(MKSCHFS) $@ --size 262144 --label schos --owners $(AROOT_DIR)/owners \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE) | \
sed -n 's/^ *\([0-9][0-9]*\) of [0-9][0-9]* blocks used.*/\1/p'); \
test -n "$$used" || { echo "fs-ram.img: mkschfs did not say how much it used" >&2; exit 1; }; \
rm -f $@; \
$(MKSCHFS) $@ --size $$((used + used / 8 + $(RAMDISK_SLACK))) --label schos \
$(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
--owners $(AROOT_DIR)/owners $(RAMDISK_EXCLUDE_ARGS) --sysroot $(ROOT_TREE)
# The image goes into the kernel as SCFF (include/fs/scff.h): 128 KB chunks,
# each its own zstd frame, unpacked by the kernel the first time a block in it
@ -1166,6 +1166,22 @@ $(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)
# su and passwd: musl, for getpwnam and crypt, and static -- they run setuid root,
# so there is no loader or library path to be led astray.
$(BUILD)/cmd/su: cmd/su/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/su.o
$(DYNCC) -static -no-pie -nostdlib -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/su.o \
-L$(MUSLPREFIX)/lib -lc $(PORTEND)
$(BUILD)/cmd/passwd: cmd/passwd/main.c $(MUSLPREFIX)/lib/libc.so
@mkdir -p $(BUILD)/cmd
$(DYNCC) -fno-pic -c $< -o $(BUILD)/cmd/passwd.o
$(DYNCC) -static -no-pie -nostdlib -o $@ \
$(MUSLPREFIX)/lib/crt1.o $(MUSLPREFIX)/lib/crti.o $(BUILD)/cmd/passwd.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
@ -1692,7 +1708,7 @@ SCHFS_BLOCKS ?= 262144
$(BUILD)/schfs.img: $(MKSCHFS) $(ROOT_STAMP)
@mkdir -p $(BUILD)
@rm -f $@
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --sysroot $(ROOT_TREE)
./$(MKSCHFS) $@ --size $(SCHFS_BLOCKS) --label schos --owners $(AROOT_DIR)/owners --sysroot $(ROOT_TREE)
# --- The apk-managed root ------------------------------------------------------
#

178
cmd/passwd/main.c Normal file
View file

@ -0,0 +1,178 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// passwd [USER]
//
// Change a password. Setuid root, like su and for the same reason a program of
// its own: it has to write /etc/shadow, which no one else can.
//
// Anyone may change their own password after proving they know the old one;
// root may change anyone's without it. The new one is stored as a SHA-512
// crypt hash with a fresh random salt, and the file is replaced by rename, so
// a crash leaves the old shadow or the new one and never half of each.
#define _GNU_SOURCE
#include <crypt.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
static int
read_password(const char *prompt, char *buf, size_t n)
{
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
struct termios t, old;
size_t i = 0;
char c;
if (fd < 0)
return -1;
if (tcgetattr(fd, &old) == 0) {
t = old;
t.c_lflag &= ~(ECHO | ECHONL);
tcsetattr(fd, TCSAFLUSH, &t);
}
if (write(fd, prompt, strlen(prompt)) < 0) {}
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
if (i + 1 < n)
buf[i++] = c;
buf[i] = 0;
tcsetattr(fd, TCSAFLUSH, &old);
if (write(fd, "\n", 1) < 0) {}
close(fd);
return 0;
}
static void
make_salt(char *out)
{
static const char a[] =
"./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
unsigned char r[16];
int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0 || read(fd, r, sizeof r) != (ssize_t)sizeof r) {
fprintf(stderr, "passwd: no random numbers\n");
exit(1);
}
close(fd);
strcpy(out, "$6$");
for (int i = 0; i < 16; i++)
out[3 + i] = a[r[i] & 63];
out[19] = '$';
out[20] = 0;
}
int
main(int argc, char **argv)
{
uid_t uid = getuid();
struct passwd *me = getpwuid(uid), *pw;
// getpwuid and getpwnam share one static buffer: keep the name we were
// given, not a pointer into it.
char *user = argc > 1 ? argv[1] : (me ? strdup(me->pw_name) : 0);
char oldp[256], p1[256], p2[256], salt[24], *hash;
FILE *in, *out;
char line[1024], tmp[] = "/etc/shadow.new";
long days = (long)(time(0) / 86400);
int found = 0;
if (!user || !(pw = getpwnam(user))) {
fprintf(stderr, "passwd: unknown user\n");
return 1;
}
{
static struct passwd copy;
copy = *pw;
copy.pw_name = strdup(pw->pw_name);
pw = &copy;
}
if (geteuid() != 0) {
fprintf(stderr, "passwd: must be setuid root\n");
return 1;
}
if (uid != 0 && pw->pw_uid != uid) {
fprintf(stderr, "passwd: you may only change your own password\n");
return 1;
}
if (uid != 0) {
struct spwd *sp = getspnam(pw->pw_name);
const char *cur = sp ? sp->sp_pwdp : "";
if (cur[0]) {
if (read_password("Current password: ", oldp, sizeof oldp) < 0)
return 1;
char *e = crypt(oldp, cur);
memset(oldp, 0, sizeof oldp);
if (!e || strcmp(e, cur) != 0) {
sleep(2);
fprintf(stderr, "passwd: incorrect password\n");
return 1;
}
}
}
if (read_password("New password: ", p1, sizeof p1) < 0 ||
read_password("Retype new password: ", p2, sizeof p2) < 0)
return 1;
if (strcmp(p1, p2) != 0) {
fprintf(stderr, "passwd: the passwords do not match\n");
return 1;
}
make_salt(salt);
// An empty password stays empty: crypt of "" is a hash, and the user asked
// for no password at all.
hash = p1[0] ? crypt(p1, salt) : "";
if (!hash) {
fprintf(stderr, "passwd: cannot hash\n");
return 1;
}
if (!(in = fopen("/etc/shadow", "r"))) {
fprintf(stderr, "passwd: /etc/shadow: %s\n", strerror(errno));
return 1;
}
umask(0077);
if (!(out = fopen(tmp, "w"))) {
fprintf(stderr, "passwd: %s: %s\n", tmp, strerror(errno));
return 1;
}
while (fgets(line, sizeof line, in)) {
size_t n = strlen(pw->pw_name);
if (strncmp(line, pw->pw_name, n) == 0 && line[n] == ':') {
// name:hash:lastchg:min:max:warn:inactive:expire:reserved
char *f = strchr(line + n + 1, ':');
fprintf(out, "%s:%s:%ld%s", pw->pw_name, hash, days,
f && (f = strchr(f + 1, ':')) ? f : ":0:::::\n");
found = 1;
} else {
fputs(line, out);
}
}
fclose(in);
if (!found)
fprintf(out, "%s:%s:%ld:0:::::\n", pw->pw_name, hash, days);
memset(p1, 0, sizeof p1);
memset(p2, 0, sizeof p2);
struct group *g = getgrnam("shadow");
fchmod(fileno(out), 0640);
if (g)
fchown(fileno(out), 0, g->gr_gid);
if (fclose(out) != 0 || rename(tmp, "/etc/shadow") < 0) {
fprintf(stderr, "passwd: could not write /etc/shadow: %s\n", strerror(errno));
unlink(tmp);
return 1;
}
printf("password updated\n");
return 0;
}

221
cmd/su/main.c Normal file
View file

@ -0,0 +1,221 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// su [-|-l] [-p] [-c COMMAND] [USER]
//
// Run a shell, or one command, as another user -- root by default. Setuid root,
// and a program of its own rather than a toybox applet: toybox is one binary
// under every name, and a setuid copy of it can be asked for any of its other
// applets by naming them, all as root.
//
// The rules are the ones su has always had, kept short:
//
// - root may become anyone, with no password;
// - anyone else is asked for the target's password, and gets three tries;
// - becoming root takes membership in `wheel` first, whatever the password
// is -- an account with no password is otherwise a door for every user.
//
// -, -l a login shell: a clean environment, the target's home, argv[0] "-sh"
// -p keep the environment (HOME, SHELL, USER and LOGNAME are still set
// unless the target is root)
// -c CMD run CMD with the shell's -c rather than an interactive shell
#define _GNU_SOURCE
#include <crypt.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <pwd.h>
#include <shadow.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
static int
in_wheel(uid_t uid, gid_t gid)
{
struct group *g = getgrnam("wheel");
struct passwd *pw = getpwuid(uid);
if (!g)
return 0;
if (gid == g->gr_gid)
return 1;
for (char **m = g->gr_mem; pw && m && *m; m++)
if (strcmp(*m, pw->pw_name) == 0)
return 1;
return 0;
}
// A line from the terminal with echo off. The password is read from the
// controlling terminal, not stdin, so a script cannot feed it.
static int
read_password(const char *prompt, char *buf, size_t n)
{
int fd = open("/dev/tty", O_RDWR | O_NOCTTY);
struct termios t, old;
size_t i = 0;
char c;
if (fd < 0)
return -1;
if (tcgetattr(fd, &old) == 0) {
t = old;
t.c_lflag &= ~(ECHO | ECHONL);
tcsetattr(fd, TCSAFLUSH, &t);
}
if (write(fd, prompt, strlen(prompt)) < 0) {}
while (read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
if (i + 1 < n)
buf[i++] = c;
buf[i] = 0;
tcsetattr(fd, TCSAFLUSH, &old);
if (write(fd, "\n", 1) < 0) {}
close(fd);
return 0;
}
// Does `pw` accept this password? An empty hash accepts anything, a locked one
// ("*", "!...") nothing.
static int
password_ok(const struct passwd *pw, const char *pass)
{
const char *hash = pw->pw_passwd;
struct spwd *sp;
char *enc;
if (hash[0] == 'x' && hash[1] == 0) {
sp = getspnam(pw->pw_name);
if (!sp)
return 0;
hash = sp->sp_pwdp;
}
if (hash[0] == 0)
return 1;
if (hash[0] == '*' || hash[0] == '!')
return 0;
enc = crypt(pass, hash);
return enc && strcmp(enc, hash) == 0;
}
int
main(int argc, char **argv)
{
const char *target = "root", *cmd = 0;
int login = 0, keep = 0, i;
struct passwd *pw, *me;
uid_t uid = getuid();
char pass[256];
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-") || !strcmp(argv[i], "-l"))
login = 1;
else if (!strcmp(argv[i], "-p"))
keep = 1;
else if (!strcmp(argv[i], "-c") && i + 1 < argc)
cmd = argv[++i];
else if (argv[i][0] == '-' && argv[i][1]) {
fprintf(stderr, "usage: su [-|-l] [-p] [-c COMMAND] [USER]\n");
return 2;
} else
target = argv[i];
}
if (geteuid() != 0) {
fprintf(stderr, "su: must be setuid root\n");
return 1;
}
// getpwnam and getpwuid return the same static buffer, so the second call
// overwrites the first answer. Keep our own copy of the one that matters.
if (!(pw = getpwnam(target))) {
fprintf(stderr, "su: unknown user %s\n", target);
return 1;
}
{
static struct passwd copy;
copy = *pw;
copy.pw_name = strdup(pw->pw_name);
copy.pw_passwd = strdup(pw->pw_passwd);
copy.pw_dir = strdup(pw->pw_dir);
copy.pw_shell = strdup(pw->pw_shell);
pw = &copy;
}
me = getpwuid(uid);
if (uid != 0) {
int tries, ok = 0;
if (pw->pw_uid == 0 && !in_wheel(uid, getgid())) {
fprintf(stderr, "su: %s is not in the wheel group\n",
me ? me->pw_name : "you");
return 1;
}
for (tries = 0; tries < 3 && !ok; tries++) {
if (pw->pw_passwd[0] == 0 ||
(pw->pw_passwd[0] == 'x' && getspnam(pw->pw_name) &&
getspnam(pw->pw_name)->sp_pwdp[0] == 0)) {
ok = 1;
break;
}
if (read_password("Password: ", pass, sizeof pass) < 0) {
fprintf(stderr, "su: no terminal\n");
return 1;
}
ok = password_ok(pw, pass);
memset(pass, 0, sizeof pass);
if (!ok) {
sleep(2);
fprintf(stderr, "su: incorrect password\n");
}
}
if (!ok)
return 1;
}
if (initgroups(pw->pw_name, pw->pw_gid) < 0 ||
setgid(pw->pw_gid) < 0 || setuid(pw->pw_uid) < 0) {
fprintf(stderr, "su: cannot become %s: %s\n", pw->pw_name, strerror(errno));
return 1;
}
{
const char *shell = pw->pw_shell && *pw->pw_shell ? pw->pw_shell : "/bin/sh";
char *sh_argv[5];
int n = 0;
char arg0[256];
if (login) {
clearenv();
setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1);
if (getenv("TERM") == 0)
setenv("TERM", "linux", 1);
}
if (login || !keep || pw->pw_uid != 0) {
setenv("HOME", pw->pw_dir, 1);
setenv("SHELL", shell, 1);
setenv("USER", pw->pw_name, 1);
setenv("LOGNAME", pw->pw_name, 1);
}
if (login && chdir(pw->pw_dir) < 0)
chdir("/");
if (login) {
const char *base = strrchr(shell, '/');
snprintf(arg0, sizeof arg0, "-%s", base ? base + 1 : shell);
sh_argv[n++] = arg0;
} else {
const char *base = strrchr(shell, '/');
sh_argv[n++] = (char *)(base ? base + 1 : shell);
}
if (cmd) {
sh_argv[n++] = "-c";
sh_argv[n++] = (char *)cmd;
}
sh_argv[n] = 0;
execv(shell, sh_argv);
fprintf(stderr, "su: %s: %s\n", shell, strerror(errno));
}
return 1;
}

View file

@ -35,6 +35,8 @@
["architecture/terminals.md", "Virtual terminals"],
["architecture/devices.md", "Device model"],
["architecture/usb.md", "USB"],
["architecture/udev.md", "udev"],
["architecture/users.md", "Users and permissions"],
["architecture/graphics.md", "Graphics"],
["architecture/audio.md", "Audio"],
["architecture/networking.md", "Networking"],

View file

@ -94,6 +94,7 @@ Read in this order if you are new:
| [fat.md](fat.md) | FAT12/16/32 with long names: USB sticks, SD cards, the ESP. |
| [devices.md](devices.md) | Buses, classes, binding, and the vtables consumers talk to. |
| [usb.md](usb.md) | xHCI, the USB core, and the three class drivers. |
| [users.md](users.md) | Accounts, login, and the permissions the kernel enforces. |
| [udev.md](udev.md) | eudev on SchOS: the device table, the `/sys` tree, uevents, datagram sockets. |
| [graphics.md](graphics.md) | Framebuffer, `/dev/fb0`, text rendering, the console, PSOGM and GMMOS. |
| [desktop.md](desktop.md) | Xorg on the framebuffer, Window Maker, and what they stand on. |

View file

@ -75,4 +75,5 @@ through ACPI, and reach X as a mouse: pointer, click and tap.
- **Touchpads are mice**: no two-finger scroll, no gestures, no palm rejection.
- **No resolution change from inside X.** The mode is the console's; `schmode` changes the
`vgpud` head only, and the loader's menu is where a real panel's mode is chosen.
- **Not a security boundary.** X runs as root on a system that enforces no file permissions.
- **Runs as an ordinary user.** `startx` as `schos` works by group permission alone
([users.md](users.md)); X itself is still a single seat with no logind or seatd.

View file

@ -435,9 +435,8 @@ allocation over 8 MB mapped straight into a neighbouring live mapping.
- **No page cache.** `mmap` of a file *reads the range in* at map time, and
writable `MAP_SHARED` of a file is refused outright rather than faked. Demand
paging covers anonymous memory only.
- **Permissions are tracked but not enforced.** uid/gid are maintained honestly
so `getuid()`/`setuid()` stop lying, and file access does not consult them;
`setpriority` is the one place the uids are checked.
- **Root is `euid == 0` and nothing finer.** No capabilities; file access, `kill`, `exec`
and the privileged calls are enforced as [users.md](users.md) says.
- **`mprotect` is accepted and not enforced** (`sys_mprotect` returns 0). RELRO is not read-only,
a `PROT_NONE` guard does not fault, and nothing has `NX` (`EFER.NXE` is never set), so every
mapping is executable.

View file

@ -102,9 +102,8 @@ tracked. Two rules, with the two errnos POSIX gives them:
alone. Without it nice is not a courtesy but a way for any process to
outrank the rest of the system by asking.
Both are inert today, because everything runs as root. They are written now
because the alternative is growing a priority knob and a permission model in
separate commits, and the window between the two is exactly the bug.
Both bite now that there are users ([users.md](users.md)); root is exempt from
both.
## Deliberately not a red-black tree

View file

@ -0,0 +1,79 @@
# Users and permissions
SchOS has accounts, a login prompt, and file permissions that the kernel
enforces. Until now every process was root and `uid` was bookkeeping; this is
the part that makes it a multi-user system.
## What you meet
- **`login` on tty1–tty5.** `etc/schos-getty` takes the terminal back from
whoever had it and runs `login`, which asks who is there and starts their
shell; when the shell exits `schinit` starts the prompt again.
- **tty6 is a root shell with no login**, on purpose: the way in when login
itself is what is broken.
- **Two accounts in the image.** `root`, and `schos` (uid 1000, home
`/home/schos`, in `wheel tty video input audio cdrom`). Both have empty
passwords, so `login` lets them in without asking — it is a live image.
`passwd` sets one.
- **`su` and `passwd`** (`cmd/su`, `cmd/passwd`) are setuid root and small
programs of their own, not toybox applets: toybox is one binary under every
name, and a setuid copy of it can be asked for any applet by naming it, as
root. `su` to root takes membership in `wheel`, whatever the password.
- **`doas`** (Alpine's, setuid) with `permit nopass :wheel` in `/etc/doas.conf`.
## What the kernel enforces
| | |
|---|---|
| open, create, unlink, rename, link | the usual owner/group/other bits, `w`+`x` on the directory, and the sticky bit (`/tmp`) |
| path walk | `x` on every directory searched |
| `exec` | `x` on the file; set-user-id and set-group-id make the new image run as its owner or group, and the saved ids follow |
| `chmod`, `chown`, `utimensat` | owner or root; a non-root `chown` drops the set-id bits |
| `access` / `faccessat` | by the *real* ids (`AT_EACCESS` for the effective ones) |
| `kill`, `tkill`, `tgkill` | root, or a target whose real or saved uid is one of yours. The kernel's own signals (`SIGPIPE`, the terminal's) are nobody's to refuse |
| `mount`, `umount`, `chroot`, `reboot`, `shutdown`, `setgroups`, `mknod` of a device | root |
| `setuid` family | root may set anything; anyone else only to an id they already hold |
Root reads and writes everything and executes a file only if some execute bit
is set. A process's supplementary groups (`setgroups`) are consulted along with
its effective group. Ownership is stored by SchFS (it always had the fields);
FAT volumes belong to everyone, since they cannot say otherwise, and `/proc`
and `/sys` are root's.
`inode_permission()` in `kernel/fs/fs.c` is the one check; the rest of this is
call sites.
## Devices
`/dev` is built by the kernel and each node has the owner, group and mode in the
`devnodes[]` table of `kernel/fs/devfs.c`: `null` and `zero` are 0666,
terminals `root:tty 0660`, `fb0` `root:video`, input `root:input`, raw disks
`root:disk`, `pci`, `dbg` and `netdev` root's alone. `chmod` and `chown` on a
node are kept (login claims its terminal this way, and udev sets modes) in an
override table applied whenever the inode is loaded. A new pseudo-terminal's
slave belongs to whoever opened `ptmx`.
udev would set a node it has no rule for to 0600, which made `/dev/null`
root-only, so `etc/udev-schos.rules` (installed as `90-schos.rules`) names the
kernel's devices with the answers the kernel gave.
## The image
Everything in the image is owned by root: the build host's user means nothing
inside it. `mkschfs --owners FILE` takes the exceptions, one `path uid gid
[mode]` per line (`tools/posix/mkaroot.sh` writes it: `/home/schos 1000 1000
0700`, and the modes of `/tmp`, `/var/tmp` and `/root`). The setuid bits are set
on `su`, `passwd` and `doas` in the root tree directly, because `apk` run as an
ordinary user may have dropped them.
## Not done
- **No PAM and no `/etc/securetty`.** `login` is busybox's and checks
`/etc/shadow` itself.
- **No capabilities.** Root is `euid == 0`, all of it.
- **No per-user `/run`, no logind.** X as a non-root user works (`startx` as `schos`, under
QEMU: Window Maker up) by group permission alone — video, input and tty — and
`/etc/X11/Xwrapper.config` lets anyone start it, because the wrapper's "console users" test
looks for a Linux VT on a standard descriptor that xinit's server child does not keep.
- **`/proc` and `/sys` hide nothing from other users.**
- **`mprotect` is still not enforced** ([kernel.md](kernel.md)).

View file

@ -1,11 +1,11 @@
# The interactive shell on /dev/tty1, the first virtual terminal.
Label = os.schos.console-shell
# zsh, not /bin/sh. mksh stays as /bin/sh and is what schinit falls back to
# when no manifest loads -- so if zsh ever fails to start, the recovery shell
# is a different program rather than the same broken one. See ports/zsh.
Program = /bin/zsh
Args = -zsh
# A login prompt, not a shell: /etc/schos-getty hands the terminal to login,
# which asks who is there and starts their shell. tty6 keeps a root shell
# with no login, for when this part of the system is what is broken.
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
@ -15,8 +15,6 @@ KeepAlive = yes
# most: a line editor with no TERM cannot ask terminfo how to move a cursor.
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
# This job reads the console, so it has to own it: schinit puts every job in
# its own session, and a session with no controlling terminal is not delivered

View file

@ -6,15 +6,13 @@
# what /dev/tty1 and /dev/tty2 being different terminals means.
Label = os.schos.tty2-shell
Program = /bin/zsh
Args = -zsh
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes

View file

@ -10,15 +10,13 @@
# for six lines each is the wrong trade.
Label = os.schos.tty3-shell
Program = /bin/zsh
Args = -zsh
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes

View file

@ -10,15 +10,13 @@
# for six lines each is the wrong trade.
Label = os.schos.tty4-shell
Program = /bin/zsh
Args = -zsh
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes

View file

@ -10,15 +10,13 @@
# for six lines each is the wrong trade.
Label = os.schos.tty5-shell
Program = /bin/zsh
Args = -zsh
Program = /bin/sh
Args = sh /etc/schos-getty
RunAtLoad = yes
KeepAlive = yes
Environment = TERM=linux
Environment = PATH=/bin:/usr/bin
Environment = HOME=/root
Environment = SHELL=/bin/zsh
ControllingTerminal = yes

View file

@ -1,3 +1,6 @@
# A root shell on /dev/tty6, with no login: the way in when login itself is
# what has broken. Every other terminal asks who is there.
#
# An interactive shell on /dev/tty6.
#
# NVT is 6 (include/dev/vt.h) and the kernel creates all six terminals, but only

10
etc/schos-getty Normal file
View file

@ -0,0 +1,10 @@
#!/bin/sh
# What a virtual terminal runs: take the terminal back from whoever had it, and
# ask for a login. login hands the terminal to the user it lets in, and this
# runs again when their shell exits.
t=$(tty 2>/dev/null)
if [ -n "$t" ]; then
chown root:tty "$t" 2>/dev/null
chmod 0660 "$t" 2>/dev/null
fi
exec /bin/login

14
etc/udev-schos.rules Normal file
View file

@ -0,0 +1,14 @@
# SchOS's own device permissions, applied after Alpine's (the name sorts last
# once installed as 90-schos.rules).
#
# The kernel makes these nodes with the permissions it means them to have
# (kernel/fs/devfs.c). udev, with no rule that names a node, would chmod it to
# 0600 -- which made /dev/null unusable to everyone but root -- so every kernel
# device is named here with the same answer the kernel gave.
SUBSYSTEM=="mem", MODE="0666"
SUBSYSTEM=="tty", KERNEL=="console", GROUP="root", MODE="0600"
SUBSYSTEM=="tty", KERNEL=="tty0|tty[0-9]|tty1[0-2]", GROUP="tty", MODE="0660"
SUBSYSTEM=="input", GROUP="input", MODE="0660"
SUBSYSTEM=="graphics", GROUP="video", MODE="0660"
SUBSYSTEM=="block", GROUP="disk", MODE="0660"

View file

@ -180,6 +180,10 @@ void mountinit(void);
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
// Ask before mutating, not after: the create path panics on a failed dirlink.
int fs_readonly(uint dev);
int in_group(uint gid, int real);
uint inode_mode_bits(struct inode *ip);
int inode_permission(struct inode *ip, int mask, int real);
int capable(void);
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
const char *path, const char *source, const char *type);
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
@ -601,6 +605,8 @@ int fork(void);
int growproc(int64);
int kill(int);
int sigsend_pgrp(int pgid, int sig);
int sigsend_user(int pid, int sig);
int sigsend_pgrp_user(int pgid, int sig);
int proc_pgid(int pid);
int proc_setpgid(int pid, int pgid);
int proc_setsid(void);

View file

@ -215,6 +215,8 @@ struct procinfo {
int nthreads; // threads in that process
};
#define NGROUPS_MAX 32
struct proc {
uint64 sz; // Size of process memory (bytes) [+0x00]
pml4e_t* pgdir; // Page table (PML4 on x86_64) [+0x08]
@ -335,6 +337,10 @@ struct proc {
// are tracked honestly so getuid()/setuid() stop lying to userland.
uint uid, euid, suid;
uint gid, egid, sgid;
// Supplementary groups, as setgroups(2) set them: a file's group is
// consulted against these as well as against egid.
int ngroups;
uint groups[NGROUPS_MAX];
// --- Fair-share scheduling --------------------------------------------
// The whole of the scheduler's per-process state. What each field means,

View file

@ -39,3 +39,4 @@ void pts_close(struct inode*);
// From kernel/fs/devfs.c: the inode of master N.
struct inode *devfs_pty_master(int n);
void devfs_pty_claim(int n, uint uid);

View file

@ -134,6 +134,9 @@ struct inode {
// that does not, in which case stat makes some up from the type. Bit 18 set
// alone means a mode of 0000, which chmod can ask for.
uint mode;
// Owner. Whatever the filesystem keeps: SchFS has stored them all along,
// FAT and the synthetic filesystems report root.
uint uid, gid;
uint supermode;
uint showmode;
uint atime;

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
{
int r = 0;
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;
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)
if (p->euid != 0) {
if (ruid >= 0 && (uint)ruid != p->uid && (uint)ruid != p->euid)
return -EPERM;
if (ruid >= 0) myproc()->uid = (uint)ruid;
if (euid >= 0) myproc()->euid = (uint)euid;
myproc()->suid = myproc()->euid;
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)
if (p->euid != 0) {
if (rgid >= 0 && (uint)rgid != p->gid && (uint)rgid != p->egid)
return -EPERM;
if (rgid >= 0) myproc()->gid = (uint)rgid;
if (egid >= 0) myproc()->egid = (uint)egid;
myproc()->sgid = myproc()->egid;
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)
// 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) myproc()->uid = (uint)r;
if (e >= 0) myproc()->euid = (uint)e;
if (sv >= 0) myproc()->suid = (uint)sv;
}
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)
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) myproc()->gid = (uint)r;
if (e >= 0) myproc()->egid = (uint)e;
if (sv >= 0) myproc()->sgid = (uint)sv;
}
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)

View file

@ -62,3 +62,6 @@ alsa-plugins
eudev
eudev-libs
udev-init-scripts
# Privilege for ordinary users: setuid doas (opendoas), configured in /etc/doas.d.
doas

View file

@ -22,6 +22,7 @@ cups-libs-2.4.19-r1.apk 5739c568d90f23006d74ff82619e1a18447a6f698ae2af61970debe6
curl-8.23.0-r0.apk 531ae298451a4c1c44f563fc3b2eedc2c1d7e0a26a33bdfc0efe6faaff6a5942 main
curl-zsh-completion-8.23.0-r0.apk 123ff531444fe6c3095c49553f8798bcf6f6ca8c7668b4140cc33daf24c351ec main
dbus-libs-1.16.2-r2.apk 105f6ba17454c587ca9345f9deb348012d1950588a22da42120c954d396c99b6 main
doas-6.8.2-r8.apk 1b0198d957fee06b484fc0e23783f03213cc97d8003c5f46b8d940d8ca9f57c8 main
e2fsprogs-libs-1.47.4-r0.apk 2e067395a6981b06cfd2cd3abf0e6c6574f7ef64d02e01f75fc5323d4a337fb9 main
encodings-1.1.0-r0.apk f7c0eb762638d0249b4fbbe07a8e4ed6aab8e86b74c239c40e47a36219777d52 main
eudev-3.2.14-r6.apk 540c78af1eed3bc411cae4e9171e911afc0704210c0b97315e978e05c3b39dce main

View file

@ -9,7 +9,7 @@
// construction — there is only one implementation, exercised on the host where
// a mistake costs a second instead of a boot.
//
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME] [--owners FILE]
//
// Two behaviours worth knowing about:
//
@ -289,6 +289,63 @@ die(const char *what, int r)
exit(1);
}
/* Who owns what. The host's owners mean nothing inside the image -- every file
came from whoever ran the build -- so everything is root's unless a rule in
the --owners file says otherwise. One rule per line, `path uid gid [mode]`,
with the path relative to the sysroot ("/home/schos"). uid and gid apply to
the path and to everything below it; a mode, in octal, to that path alone.
Later rules win. */
struct owner_rule { char *path; uint32_t uid, gid; int has_mode; uint32_t mode; };
static struct owner_rule *owner_rules;
static int nowner_rules;
static const char *cur_rel; /* the entry being added, set by add_entry */
static void
load_owners(const char *file)
{
FILE *f = fopen(file, "r");
char line[1024];
if (!f) { perror(file); exit(1); }
while (fgets(line, sizeof line, f)) {
char path[900];
unsigned uid, gid, mode;
int n;
if (line[0] == '#' || line[0] == '\n')
continue;
n = sscanf(line, "%899s %u %u %o", path, &uid, &gid, &mode);
if (n < 3) { fprintf(stderr, "mkschfs: bad owners line: %s", line); exit(1); }
owner_rules = realloc(owner_rules, (size_t)(nowner_rules + 1) * sizeof *owner_rules);
owner_rules[nowner_rules].path = strdup(path);
owner_rules[nowner_rules].uid = uid;
owner_rules[nowner_rules].gid = gid;
owner_rules[nowner_rules].has_mode = n == 4;
owner_rules[nowner_rules].mode = mode;
nowner_rules++;
}
fclose(f);
}
static void
apply_owners(struct schfs_inode *in)
{
if (!cur_rel)
return;
for (int i = 0; i < nowner_rules; i++) {
const char *r = owner_rules[i].path;
size_t n;
if (*r == '/')
r++; /* cur_rel is relative: "home/schos" */
n = strlen(r);
int exact = strcmp(cur_rel, r) == 0;
if (!exact && !(strncmp(cur_rel, r, n) == 0 && cur_rel[n] == '/'))
continue;
in->uid = owner_rules[i].uid;
in->gid = owner_rules[i].gid;
if (exact && owner_rules[i].has_mode)
in->mode = owner_rules[i].mode;
}
}
static void
inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
uint64_t parent)
@ -301,6 +358,7 @@ inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
: (type == SCHFS_T_DIR ? 0755u : 0644u);
in->uid = 0;
in->gid = 0;
apply_owners(in);
if (st) {
// All three come from mtime, deliberately.
//
@ -537,6 +595,7 @@ add_entry(uint64_t dirino, const char *hostdir, const char *name)
// lstat, not stat: a symlink must be stored as a symlink rather than
// silently turned into a second copy of whatever it points at.
if (lstat(path, &st) != 0) { perror(path); exit(1); }
cur_rel = strlen(path) >= sysroot_prefix ? path + sysroot_prefix : path;
if (strlen(name) > SCHFS_NAME_MAX) {
fprintf(stderr, "mkschfs: name too long (max %d): %s\n",
@ -631,6 +690,8 @@ main(int argc, char **argv)
nblocks = strtoull(argv[++i], NULL, 0);
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
label = argv[++i];
else if (strcmp(argv[i], "--owners") == 0 && i + 1 < argc)
load_owners(argv[++i]);
else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc)
snapname = argv[++i];
else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {

View file

@ -91,6 +91,19 @@ rm -rf "$P/usr/share/hwdata"
esac
done
# su and passwd are SchOS's own and run setuid root (cmd/su, cmd/passwd): the
# busybox ones are not setuid here, and a setuid toybox is one binary under
# every name. passwd goes where PATH finds it before busybox's link.
mkdir -p "$P/usr/bin"
cp "$SYSROOT/bin/su" "$P/bin/su"
cp "$SYSROOT/bin/passwd" "$P/usr/bin/passwd"
rm -f "$P/bin/passwd"
chmod 4755 "$P/bin/su" "$P/usr/bin/passwd"
# udev's device permissions for the kernel's own nodes.
mkdir -p "$P/etc/udev/rules.d"
mv "$P/etc/udev-schos.rules" "$P/etc/udev/rules.d/90-schos.rules"
# SchOS's profile without the three lines Alpine's already sets, and sets
# better: PATH with sbin, HOME from passwd, TERM from the console job.
mkdir -p "$P/etc/profile.d"
@ -157,6 +170,15 @@ Section "ServerLayout"
EndSection
XEOF
# Who may start X. The wrapper's default is "console users", decided by whether
# a standard descriptor is a Linux VT, which xinit's server child does not
# keep. SchOS has one seat, and /dev's group permissions (video, input, tty)
# are what actually guard the hardware, so anyone who can open them may run X.
cat > "$P/etc/X11/Xwrapper.config" <<'XEOF'
allowed_users=anybody
needs_root_rights=no
XEOF
# bwrap, without the sandbox. glycin -- which loads every image gdk-pixbuf is
# asked for, so every GTK icon -- runs its loaders under bubblewrap and has no
# other way to start them. Real bwrap needs user and mount namespaces, which
@ -255,7 +277,7 @@ cp "$PKG/post-install" "$PKG/post-upgrade"
--info url:https://github.com/Nexoniarz/SchOS \
--info origin:schos-base \
--info "depends:alpine-baselayout busybox musl apk-tools zsh" \
--info "replaces:alpine-release alpine-baselayout zsh bubblewrap" \
--info "replaces:alpine-release alpine-baselayout zsh bubblewrap busybox" \
--files "$P" \
--script "post-install:$PKG/post-install" \
--script "post-upgrade:$PKG/post-upgrade" \
@ -333,6 +355,65 @@ fi
# other program checking /tmp would see.
chmod 1777 "$ROOT/tmp" "$ROOT/var/tmp"
# --- accounts -------------------------------------------------------------------
#
# Alpine's baselayout has root and the system accounts, with root's password
# locked. A live image needs a way in, so: root with no password, and an
# ordinary user, `schos`, who can become root with doas and su (they are in the
# wheel group) and uses the desktop. Both have empty passwords -- login lets
# them in without asking -- and `passwd` sets one.
#
# The numbers are Alpine's: wheel 10, tty 5, video 27, input 23, audio 18.
add_member() { # add_member GROUP USER
if grep -q "^$1:[^:]*:[^:]*:$" "$ROOT/etc/group"; then
sed -i "s/^\($1:[^:]*:[^:]*:\)$/\1$2/" "$ROOT/etc/group"
else
sed -i "s/^\($1:[^:]*:[^:]*:.*\)$/\1,$2/" "$ROOT/etc/group"
fi
}
sed -i 's/^root:[^:]*:/root::/' "$ROOT/etc/shadow"
grep -q '^schos:' "$ROOT/etc/passwd" ||
echo 'schos:x:1000:1000:SchOS user:/home/schos:/bin/zsh' >> "$ROOT/etc/passwd"
grep -q '^schos:' "$ROOT/etc/group" ||
echo 'schos:x:1000:' >> "$ROOT/etc/group"
grep -q '^schos:' "$ROOT/etc/shadow" ||
echo 'schos::0:0:::::' >> "$ROOT/etc/shadow"
for g in wheel tty video input audio cdrom; do
grep -q "^$g:.*[:,]schos\(,\|\$\)" "$ROOT/etc/group" || add_member $g schos
done
chmod 640 "$ROOT/etc/shadow"
# Who may become root with doas: the wheel group, without being asked again.
# The file has to be root's and not writable by anyone else, or doas refuses it.
cat > "$ROOT/etc/doas.conf" <<'DEOF'
# doas: see doas.conf(5). Members of wheel run anything as anyone.
permit nopass :wheel
DEOF
chmod 644 "$ROOT/etc/doas.conf"
# The user's home, with what startx wants there. root's are in /root.
mkdir -p "$ROOT/home/schos"
cp "$ROOT/root/.xinitrc" "$ROOT/root/.xserverrc" "$ROOT/home/schos/"
chmod 755 "$ROOT/home/schos/.xserverrc"
# zsh offers to write one on first login; /etc/zsh is where the settings are.
: > "$ROOT/home/schos/.zshrc"
chmod 700 "$ROOT/home/schos" "$ROOT/root"
# The setuid programs. Run as an ordinary user, apk may have dropped the bit,
# and nothing here should depend on whether it did.
for f in bin/su usr/bin/passwd usr/bin/doas; do
[ -f "$ROOT/$f" ] && chmod 4755 "$ROOT/$f"
done
# Who owns what inside the image: everything is root's -- the build host's
# user means nothing here -- except the user's home.
cat > "$OUT/owners" <<'OEOF'
/home/schos 1000 1000 0700
/root 0 0 0700
/tmp 0 0 1777
/var/tmp 0 0 1777
OEOF
# The repository goes in too, so the installed system can reinstall and
# upgrade schos-base, and Alpine's are listed for everything else.
mkdir -p "$ROOT/var/lib/schos"