Users and permissions: accounts, login, enforced file permissions, setuid, su, passwd, doas

The kernel now checks owner/group/other on open, create, unlink, rename, exec
and path search; honours set-user-id and set-group-id; restricts kill, mount,
reboot and mknod; keeps supplementary groups; and implements chown. devtmpfs
nodes carry owners and modes that chmod/chown keep. Terminals run login, the
image has root and schos, X starts as the ordinary user.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
This commit is contained in:
Nexoniarz 2026-10-26 13:16:07 +01:00
commit 020aa6113c
34 changed files with 1342 additions and 120 deletions

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

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

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

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