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
|
|
@ -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