SchOS/cmd/su/main.c
Nexoniarz 020aa6113c 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
2026-10-26 13:16:07 +01:00

221 lines
6.5 KiB
C

// 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;
}