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
24
Makefile
24
Makefile
|
|
@ -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
178
cmd/passwd/main.c
Normal 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 = ©
|
||||
}
|
||||
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
221
cmd/su/main.c
Normal 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 = ©
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
79
docs/architecture/users.md
Normal file
79
docs/architecture/users.md
Normal 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)).
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
10
etc/schos-getty
Normal 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
14
etc/udev-schos.rules
Normal 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"
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue