diff --git a/include/core/defs.h b/include/core/defs.h index 482bf73..0cc078f 100644 --- a/include/core/defs.h +++ b/include/core/defs.h @@ -797,6 +797,7 @@ void shm_ref(int id); char* shm_page(int id, int pg); uint64 memfd_size(int id); int memfd_truncate(int id, uint64 len); +int memfd_seals(int id, int add); int memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing); void shm_detach(int id); diff --git a/include/uapi/fcntl.h b/include/uapi/fcntl.h index f6327b9..00020b8 100644 --- a/include/uapi/fcntl.h +++ b/include/uapi/fcntl.h @@ -46,5 +46,15 @@ #define F_OFD_GETLK 36 #define F_OFD_SETLK 37 #define F_OFD_SETLKW 38 +// memfd seals (Linux values). +#define F_ADD_SEALS 1033 +#define F_GET_SEALS 1034 +#define F_SEAL_SEAL 0x01 +#define F_SEAL_SHRINK 0x02 +#define F_SEAL_GROW 0x04 +#define F_SEAL_WRITE 0x08 +#define F_SEAL_FUTURE_WRITE 0x10 +#define MFD_CLOEXEC 0x01 +#define MFD_ALLOW_SEALING 0x02 #define FD_CLOEXEC 1 diff --git a/include/uapi/user.h b/include/uapi/user.h index c2c2867..ea4ef76 100644 --- a/include/uapi/user.h +++ b/include/uapi/user.h @@ -46,8 +46,7 @@ int alarm(int seconds); int waitpid(int pid, int *status, int options); int setsid(void); void* mmap(void*, uint64, int, int, int, uint64); -// memfd_create(2). The flags argument exists for MFD_CLOEXEC and MFD_ALLOW_ -// SEALING, neither of which this kernel implements; pass 0. +// memfd_create(2). flags: MFD_CLOEXEC, MFD_ALLOW_SEALING. int memfd_create(const char *name, uint flags); // Shared memory. shmat returns the attach address, or a small negative value // on failure — there is no errno in this runtime, so check for < 0. diff --git a/kernel/proc/proc.c b/kernel/proc/proc.c index f64ce1c..789908a 100644 --- a/kernel/proc/proc.c +++ b/kernel/proc/proc.c @@ -1477,18 +1477,21 @@ waitpid(int pid, int *status, int options, uint64 *ut, uint64 *st) { struct proc *p; int havekids; + // Children belong to the process, not to the thread that made them: any + // thread may wait for them, and their exit wakes the leader's channel. + struct proc *me = proc_leader(myproc()); acquire(&ptable.lock); for(;;){ havekids = 0; for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ - if(p->parent != myproc()) + if(p->parent != me) continue; // POSIX pid selection: >0 one child, 0 = caller's group, // -1 = any child, < -1 = the process group with id -pid. if(pid > 0 && p->pid != pid) continue; - if(pid == 0 && p->pgid != myproc()->pgid) + if(pid == 0 && p->pgid != me->pgid) continue; if(pid < -1 && p->pgid != -pid) continue; @@ -1503,8 +1506,8 @@ waitpid(int pid, int *status, int options, uint64 *ut, uint64 *st) // RUSAGE_CHILDREN: the child's own time *and* whatever it had already // reaped, which is what makes the total transitive down a whole tree // of processes rather than one generation deep. - myproc()->cutime += p->utime + p->cutime; - myproc()->cstime += p->stime + p->cstime; + me->cutime += p->utime + p->cutime; + me->cstime += p->stime + p->cstime; reaped_utime += p->utime; reaped_stime += p->stime; // wait4()'s fourth argument wants the *child's* usage, not the @@ -1576,7 +1579,7 @@ waitpid(int pid, int *status, int options, uint64 *ut, uint64 *st) return 0; } - sleep(myproc(), &ptable.lock); + sleep(me, &ptable.lock); } } @@ -2699,7 +2702,7 @@ proc_setpgid(int pid, int pgid) continue; // A caller may only move itself or one of its own children, and never // across a session boundary. - if(p != myproc() && p->parent != myproc()) + if(p != myproc() && p->parent != proc_leader(myproc())) r = -ESRCH; else if(p->sid != myproc()->sid) r = -EPERM; diff --git a/kernel/sys/sys_fs.c b/kernel/sys/sys_fs.c index b572030..645406d 100644 --- a/kernel/sys/sys_fs.c +++ b/kernel/sys/sys_fs.c @@ -482,6 +482,13 @@ sys_fcntl(void) case F_SETFL: f->nonblock = (arg & O_NONBLOCK) != 0; return 0; + case F_ADD_SEALS: + case F_GET_SEALS: + if (f->type != FD_MEMFD) + return -EINVAL; + if (cmd == F_ADD_SEALS && !f->writable) + return -EPERM; + return memfd_seals(f->memfd_seg, cmd == F_ADD_SEALS ? arg : -1); case F_GETLK: case F_SETLK: case F_SETLKW: case F_OFD_GETLK: case F_OFD_SETLK: case F_OFD_SETLKW: { // A pointer, which argint() above would have cut to 32 bits. @@ -1547,6 +1554,28 @@ addr_t sys_ioctl(void) return 0; } + // The four that apply to any descriptor at all. Rust's standard library + // sets close-on-exec with FIOCLEX and non-blocking with FIONBIO rather + // than with fcntl, and on a pipe or a socket those fell through to the + // ENOTTY below: glycin's image loaders failed with "Not a tty" and GTK + // could load no icon. + if (request == 0x5451 || request == 0x5450) { // FIOCLEX, FIONCLEX + if (request == 0x5451) + myproc()->files->flags[fd] |= FD_CLOEXEC; + else + myproc()->files->flags[fd] &= ~FD_CLOEXEC; + return 0; + } + if (request == 0x5421) { // FIONBIO + int on; + if (copy_from_user(&on, argp, sizeof(on)) < 0) + return -EFAULT; + f->nonblock = on != 0; + return 0; + } + if (request == 0x5452) // FIOASYNC + return 0; + // /dev/fb0 (major 2) — geometry queries. This has to come before the // catch-all below, which answers every non-tty ioctl with 0: a program // asking FBIOGET_VSCREENINFO would have been told "success" and then diff --git a/kernel/sys/sys_mem.c b/kernel/sys/sys_mem.c index d859eb9..3908ee7 100644 --- a/kernel/sys/sys_mem.c +++ b/kernel/sys/sys_mem.c @@ -3,6 +3,7 @@ #include "gfx/gfx.h" #include "core/param.h" #include "uapi/stat.h" +#include "uapi/fcntl.h" #include "arch/memlayout.h" #include "arch/mmu.h" #include "core/spinlock.h" @@ -272,15 +273,21 @@ mmap_locked(void) // process that maps the same segment gets the same physical pages, and a // store by one is a load by the other because there is only one copy. // - // MAP_PRIVATE is refused rather than given a copy. Every caller of this is - // sharing on purpose, and a private mapping of an anonymous object is an - // expensive way to ask for MAP_ANONYMOUS. + // A private mapping is allowed read-only, and is then the same pages + // mapped without write access: until somebody writes, a private mapping + // sees the object's stores on Linux too. GLib's GMappedFile maps a memfd + // this way, and that is how gdk-pixbuf reads every image glycin decodes. + // Private and writable would need a copy that is not made; it is refused. if (f && f->type == FD_MEMFD) { int id = f->memfd_seg; - uint64 have = memfd_size(id); + uint64 have = PGROUNDUP(memfd_size(id)); // the tail page maps whole - if (!(flags & MAP_SHARED)) + if (!(flags & MAP_SHARED) && (prot & PROT_WRITE)) return -EINVAL; + int pte = (flags & MAP_SHARED) ? PTE_W | PTE_U : PTE_U; + if ((prot & PROT_WRITE) && + (memfd_seals(id, -1) & (F_SEAL_WRITE | F_SEAL_FUTURE_WRITE))) + return -EPERM; if (offset % PGSIZE) return -EINVAL; // Mapping past the end is not an error on Linux -- the pages simply @@ -295,7 +302,7 @@ mmap_locked(void) for (uint64 o = 0; o < length; o += PGSIZE) { char *pg = shm_page(id, (int)((offset + o) / PGSIZE)); if (!pg || mappages(myproc()->pgdir, (void*)(va + o), PGSIZE, - V2P((uint64)pg), PTE_W | PTE_U) < 0) { + V2P((uint64)pg), pte) < 0) { vm_unmap_norelease(myproc()->pgdir, va, o); shm_detach(id); return -ENOMEM; @@ -517,6 +524,8 @@ struct shmseg { int used; int key; uint64 size; // rounded up to a page + uint64 bytes; // a memfd's length as ftruncate gave it + int seals; // a memfd's F_SEAL_* bits int npages; int nattach; // live attachments across all processes char *pages[SHM_MAXPAGES]; // kernel virtual addresses @@ -605,6 +614,7 @@ shm_resize(int id, uint64 newsize) { struct shmseg *sg; int want, i; + uint64 len = newsize; if (id < 0 || id >= NSHM) return -EINVAL; @@ -644,6 +654,7 @@ shm_resize(int id, uint64 newsize) acquire(&shmlock); sg->npages = want; sg->size = newsize; + sg->bytes = len; release(&shmlock); return 0; } @@ -671,7 +682,11 @@ memfd_size(int id) return 0; shm_init_once(); acquire(&shmlock); - n = shm[id].used ? shm[id].size : 0; + // The length asked for, not the pages behind it. fstat reports this, and + // memmap2 -- which glycin uses to share every decoded image -- sizes its + // mapping by it: 24x24 icons came back as 4096 bytes instead of 2304, and + // the PNG decoder asserted. + n = shm[id].used ? shm[id].bytes : 0; release(&shmlock); return n; } @@ -679,9 +694,47 @@ memfd_size(int id) int memfd_truncate(int id, uint64 len) { + if (id < 0 || id >= NSHM) + return -EINVAL; + uint64 now = memfd_size(id); + if ((len < now && (shm[id].seals & F_SEAL_SHRINK)) || + (len > now && (shm[id].seals & F_SEAL_GROW))) + return -EPERM; return shm_resize(id, len); } +// File seals: promises about a memfd that whoever receives it can rely on. +// glycin seals each decoded image before handing it over, and gdk-pixbuf +// first asks for the seals just to learn the descriptor is a memfd at all. +// +// Linux refuses F_SEAL_WRITE while a writable mapping exists (EBUSY). That +// needs every mapping of the segment to be made read-only on the spot, which +// takes the page fault handling this kernel does not have, so the seal is +// recorded and later writes, truncates and writable mappings are refused -- +// the mapping that already exists keeps writing. +int +memfd_seals(int id, int add) +{ + int r; + if (id < 0 || id >= NSHM) + return -EINVAL; + acquire(&shmlock); + if (!shm[id].used) + r = -EINVAL; + else if (add < 0) + r = shm[id].seals; + else if (shm[id].seals & F_SEAL_SEAL) + r = -EPERM; + else if (add & ~(F_SEAL_SEAL | F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_FUTURE_WRITE)) + r = -EINVAL; + else { + shm[id].seals |= add; + r = 0; + } + release(&shmlock); + return r; +} + // read()/write() on a memfd, so it is a file rather than a token that only // mmap understands. Wayland clients use ftruncate and mmap, but a descriptor // that refuses read() is a surprise nobody needs. @@ -697,10 +750,12 @@ memfd_rw(int id, int user, uint64 buf, uint64 off, int n, int writing) sg = &shm[id]; if (!sg->used) return -EINVAL; - if (off >= sg->size) + if (writing && (sg->seals & (F_SEAL_WRITE | F_SEAL_FUTURE_WRITE))) + return -EPERM; + if (off >= sg->bytes) return 0; // EOF, or a write past the end - if (off + (uint64)n > sg->size) - n = (int)(sg->size - off); + if (off + (uint64)n > sg->bytes) + n = (int)(sg->bytes - off); while (done < n) { uint64 pos = off + (uint64)done; @@ -751,6 +806,8 @@ sys_memfd_create(void) shm[i].size = 0; shm[i].npages = 0; shm[i].nattach = 1; // the file holds this one + // Without MFD_ALLOW_SEALING the object is sealed against sealing. + shm[i].seals = (flags & MFD_ALLOW_SEALING) ? 0 : F_SEAL_SEAL; id = i; break; } @@ -761,6 +818,8 @@ sys_memfd_create(void) if ((f = filealloc()) == 0) { shm_detach(id); return -ENFILE; } if ((fd = fdalloc(f)) < 0) { fileclose(f); return -EMFILE; } + if (flags & MFD_CLOEXEC) + myproc()->files->flags[fd] = FD_CLOEXEC; f->type = FD_MEMFD; f->memfd_seg = id; f->readable = 1; diff --git a/kernel/sys/sys_net.c b/kernel/sys/sys_net.c index 3b9c358..0be29f3 100644 --- a/kernel/sys/sys_net.c +++ b/kernel/sys/sys_net.c @@ -459,7 +459,12 @@ SYSCALL_DEFINE(socketpair) return -EAFNOSUPPORT; int sflags = type & (SOCK_CLOEXEC | SOCK_NONBLOCK); type &= 0xFF; - if (type != SOCK_STREAM) + // Datagram and sequenced-packet pairs are carried as a stream. What they + // are used for between two processes is passing a descriptor or a status + // word -- Rust's process spawning asks for SOCK_SEQPACKET to hear back + // from its child -- and one message at a time is all of that. A reader + // that sends two and wants them kept apart would see them joined. + if (type != SOCK_STREAM && type != 2 /* DGRAM */ && type != 5 /* SEQPACKET */) return -EPROTOTYPE; if ((r = sockpair(&a, &b)) < 0) @@ -532,15 +537,18 @@ SYSCALL_DEFINE(sendto) return socksendto(s, p, len, ip, port); } +static int argsockfile(int n, struct socket **sp, struct file **fp); + SYSCALL_DEFINE(recvfrom) { struct socket *s; + struct file *file; addr_t ubuf, uaddr, ulenp; int len, flags, r; uint ip = 0; ushort port = 0; - if ((r = argsock(0, &s)) < 0) + if ((r = argsockfile(0, &s, &file)) < 0) return r; argaddr(1, &ubuf); argint(2, &len); @@ -554,6 +562,13 @@ SYSCALL_DEFINE(recvfrom) if (len && !p) return -EFAULT; + // As in recvmsg. This one blocked regardless, and musl's resolver -- which + // sends the A and AAAA queries together and reads its non-blocking socket + // until EAGAIN -- waited for ever after the first answer: NetSurf sat on + // "Loading" with the reply in hand. + if ((file->nonblock || (flags & MSG_DONTWAIT)) && !sockreadable(s)) + return -EAGAIN; + r = sockrecvfrom(s, p, len, &ip, &port); if (r < 0 || uaddr == 0) return r; diff --git a/kernel/sys/sys_proc.c b/kernel/sys/sys_proc.c index 1d0df2a..bc1eeb4 100644 --- a/kernel/sys/sys_proc.c +++ b/kernel/sys/sys_proc.c @@ -134,6 +134,39 @@ sys_wait4(void) return wpid; } +// Capabilities: all of them for root, none for anyone else -- which is what +// the permission checks here amount to. glycin asks before it sandboxes an +// image loader, and stopped at ENOSYS. +addr_t +sys_capget(void) +{ + addr_t hdrp, datap; + uint32 hdr[2]; + argaddr(0, &hdrp); + argaddr(1, &datap); + if (copy_from_user(hdr, hdrp, sizeof(hdr)) < 0) + return -EFAULT; + if (hdr[0] != 0x19980330 && hdr[0] != 0x20071026 && hdr[0] != 0x20080522) { + hdr[0] = 0x20080522; // the version this kernel speaks + put_user(hdr[0], hdrp); + return -EINVAL; + } + if (datap == 0) + return 0; + int words = hdr[0] == 0x19980330 ? 1 : 2; + uint32 all = myproc()->euid == 0 ? 0xffffffffu : 0; + for (int i = 0; i < words; i++) { + uint32 d[3] = { all, all, 0 }; // effective, permitted, inheritable + if (copy_to_user(datap + i * sizeof(d), d, sizeof(d)) < 0) + return -EFAULT; + } + return 0; +} + +// Dropping capabilities is accepted and changes nothing: there are none to +// drop that the uid does not already decide. +addr_t sys_capset(void) { return 0; } + addr_t sys_clone(void) { diff --git a/kernel/sys/table.inc b/kernel/sys/table.inc index c2ac019..a38ee7e 100644 --- a/kernel/sys/table.inc +++ b/kernel/sys/table.inc @@ -298,6 +298,8 @@ SYSCALL(205, 284, eventfd) SYSCALL(206, 73, flock) SYSCALL(207, 51, getsockname) SYSCALL(208, 52, getpeername) +SYSCALL(209, 125, capget) +SYSCALL(210, 126, capset) // --- mounts --- SYSCALL(185, 165, mount) diff --git a/shell.nix b/shell.nix index 2a63eb7..a201277 100644 --- a/shell.nix +++ b/shell.nix @@ -43,6 +43,7 @@ pkgs.mkShell { qemu # make qemu, and every test harness OVMF # firmware for the UEFI path; the Makefile searches for it + bubblewrap # mkaroot runs Alpine's triggers in the root as / xorriso # make iso mtools # ESP staging curl gnutar # fetching ports diff --git a/tools/posix/mkaroot.sh b/tools/posix/mkaroot.sh index 09497be..be6ca0e 100644 --- a/tools/posix/mkaroot.sh +++ b/tools/posix/mkaroot.sh @@ -168,6 +168,49 @@ Section "ServerLayout" EndSection 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 +# SchOS does not have. This takes the same arguments, keeps what they say +# about the environment and the directory, and runs the program as it is: +# no isolation at all, and it says so in its name. It replaces bubblewrap's own +# /usr/bin/bwrap, which glycin depends on and which cannot run here. +mkdir -p "$P/usr/bin" +cat > "$P/usr/bin/bwrap" <<'BEOF' +#!/bin/sh +# SchOS: bubblewrap's command line, with no sandbox behind it. SchOS has no +# namespaces; what the options say about the environment and the working +# directory is honoured, and everything about isolation is ignored. +clear= envs= +while [ $# -gt 0 ]; do + case "$1" in + --clearenv) clear=1; shift ;; + --setenv) envs="$envs $2=$3"; shift 3 ;; + --unsetenv) unset "$2"; shift 2 ;; + --chdir) cd "$2" || exit 1; shift 2 ;; + --bind|--bind-try|--ro-bind|--ro-bind-try|--dev-bind|--dev-bind-try|\ + --symlink|--bind-data|--ro-bind-data|--file|--chmod) shift 3 ;; + --dev|--proc|--tmpfs|--dir|--mqueue|--remount-ro|--seccomp|--add-seccomp-fd|\ + --uid|--gid|--hostname|--cap-add|--cap-drop|--lock-file|--sync-fd|\ + --info-fd|--json-status-fd|--block-fd|--userns-block-fd|--perms|--size|\ + --argv0|--exec-label|--file-label|--userns|--userns2|--pidns) shift 2 ;; + --unshare-*|--share-net|--die-with-parent|--new-session|--as-pid-1|\ + --disable-userns|--assert-userns-disabled) shift ;; + --) shift; break ;; + -*) echo "bwrap: $1: not understood (SchOS runs bwrap without a sandbox)" >&2 + exit 1 ;; + *) break ;; + esac +done +[ $# -gt 0 ] || { echo "bwrap: no command" >&2; exit 1; } +# shellcheck disable=SC2086 +if [ -n "$clear" ]; then exec env -i PATH="$PATH" $envs "$@"; fi +# shellcheck disable=SC2086 +[ -n "$envs" ] && exec env $envs "$@" +exec "$@" +BEOF +chmod 755 "$P/usr/bin/bwrap" + # Mount points and runtime directories are alpine-baselayout's; an empty # directory here only duplicates it. for d in dev proc sys run tmp srv; do @@ -197,7 +240,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" \ + --info "replaces:alpine-release alpine-baselayout zsh bubblewrap" \ --files "$P" \ --script "post-install:$PKG/post-install" \ --script "post-upgrade:$PKG/post-upgrade" \ @@ -234,6 +277,41 @@ ROOT_PI=$ROOT sh -c ' done grep -qw schos "$ROOT_PI/etc/hosts" || printf "127.0.1.1\tschos\n" >> "$ROOT_PI/etc/hosts"' +# The triggers --no-scripts skipped, and they are not optional: they build the +# caches half the desktop reads. Without shared-mime-info's, GLib cannot tell a +# PNG from anything else, so gdk-pixbuf -- which hands images to glycin by MIME +# type -- refused every icon and NetSurf died on its first toolbar button. +# Likewise gdk-pixbuf's loader list, GLib's compiled schemas and GIO modules, +# GTK's input methods, the font caches and busybox's applet links. +# +# They run in the root as /, which is what bubblewrap is for: no chroot, so no +# root needed. apk names each script by the hex form of the checksum the +# triggers database keys in base64, and passes the paths it watches as +# arguments, globs expanded. +if command -v bwrap >/dev/null 2>&1; then + TRIG=$(mktemp -d) + tar -xzf "$ROOT/lib/apk/db/scripts.tar.gz" -C "$TRIG" --wildcards '*.trigger' 2>/dev/null || true + mkdir -p "$ROOT/tmp" + for t in "$TRIG"/*.trigger; do + [ -e "$t" ] || continue + hex=${t##*.X1}; hex=${hex%.trigger} + q=Q1$(python3 -c 'import base64,sys; print(base64.b64encode(bytes.fromhex(sys.argv[1])).decode())' "$hex") + paths=$(grep "^$q " "$ROOT/lib/apk/db/triggers" | cut -d' ' -f2-) + cp "$t" "$ROOT/tmp/apk-trigger" + # --clearenv: the host's environment is not the root's. A Nix shell + # exports GDK_PIXBUF_MODULE_FILE, and the loader cache went there. + bwrap --bind "$ROOT" / --dev /dev --proc /proc --clearenv \ + --setenv PATH /usr/sbin:/usr/bin:/sbin:/bin --setenv HOME /root \ + /bin/sh -c 'a=; for g in '"$paths"'; do for p in $g; do [ -e "$p" ] && a="$a $p"; done; done + exec /bin/sh /tmp/apk-trigger $a' \ + || echo "mkaroot: trigger $(basename "$t") failed" >&2 + rm -f "$ROOT/tmp/apk-trigger" + done + rm -rf "$TRIG" +else + echo "mkaroot: no bwrap -- triggers not run; image and MIME caches are missing" >&2 +fi + # The shared scratch directories, world-writable and sticky as baselayout # means them. apk running as an ordinary user on the build host lands them as # 0755, and with the kernel reporting real modes that is what tmux and every