Flash 250 lines
// login — interactive credential gate + session supervisor.
//
// PID-1 execs /bin/login instead of the shell. login prompts for a
// username (echoed) and a password (masked with '*' via
// SYS_SET_CONSOLE_MODE), asks the kernel to verify the password against
// the active shadow database (sys_authenticate — the KDF lives in the
// kernel), looks the user up in /etc/passwd for the uid / gid / shell,
// and then runs the session as a CHILD process: the child drops
// privilege (setgid + setuid) and execs the shell; login itself stays
// root, waits, reaps, and prompts again. `exit` in the shell therefore
// returns to the `login:` prompt instead of ending the boot — the
// re-prompt lifecycle.
//
// The privilege drop MUST live in the child: setuid is one-way for a
// non-root process, so a login that dropped itself could never
// authenticate a second session. The parent staying root is what makes
// it a supervisor.
//
// argv[1] (optional) is a decimal session limit: login exits cleanly
// after that many completed sessions. The [TEST] auth scenario drives a
// full login->shell->exit->login cycle through this real binary with limit
// "2" and then reaps it for the free-page baseline check. No argv (the
// real boot) means loop forever. A non-numeric argv[1] is ignored.
//
// Under the CI boot watchdog PID-1 console-injects the test credentials
// so this real path authenticates unattended; on hardware the user types
// them. Same coreutil recipe as dmesg / ls (flibc _start shim, single
// PT_LOAD, no heap allocator — only fixed stack buffers).
use flibc
use pwfile
link "flibc_start"
link "flibc_mem"
const PASSWD_PATH cstr = "/etc/passwd"
fn emit(s []u8) void {
_ = flibc.sys.write_fd(1, s.ptr, s.len)
}
// Read a masked secret from fd 0 into `buf`. Drives flibc's pure, host-tested
// line-editor `step` so backspace actually pops a byte, but echoes one '*' per
// accepted byte and the rubout "\x08 \x08" on backspace instead of the byte
// itself — the secret never reaches the serial console. Submits on CR / LF,
// stops on EOF, drops the line on ^C. Returns the byte count, excluding the
// terminator. The caller leaves the console in echo-off mode, so this loop is
// the only echo (no kernel double-echo).
fn readMasked(buf []mut u8) usize {
var state = flibc.readline_mod.State.init(buf)
var ch [1]u8 = undefined
while true {
if flibc.sys.read(0, &ch, 1) <= 0 {
break
}
switch flibc.readline_mod.step(&state, ch[0]) {
.echo => emit("*"),
.backspace => emit("\x08 \x08"),
.submit, .eof => { break },
.abandon => {
state.len = 0
break
},
.none, .complete => {},
}
}
return state.len
}
fn strLen(s cstr) usize {
var n usize = 0
while s[n] != 0 {
n += 1
}
return n
}
fn parseU32(s []u8) ?u32 {
if s.len == 0 {
return null
}
var v u64 = 0
for c in s {
if c < '0' || c > '9' {
return null
}
v = v * 10 + (c - '0')
if v > 0xffff_ffff {
return null
}
}
return #intCast(v)
}
// One authenticated session: fork; the child drops privilege and execs
// the user's shell; the parent waits for it to exit (logout). Returns
// true when a session actually ran (a fork/exec failure returns false so
// the caller does not count it against the session limit).
fn runSession(uid u32, gid u32, shell_z cstr) bool {
pid := flibc.fork()
if pid == 0 {
// Child: drop privilege — gid first (while still root), then uid —
// and become the shell. Credentials are inherited by everything
// the shell forks.
if flibc.sys.setgid(gid) != 0 || flibc.sys.setuid(uid) != 0 {
emit("login: cannot drop privilege\n")
flibc.exit()
}
sh_argv := [_:null]?cstr{ shell_z }
_ = flibc.sys.exec_path(shell_z, &sh_argv)
// exec_path only returns on failure; the child must die, not loop.
emit("login: exec failed\n")
flibc.exit()
}
if pid < 0 {
emit("login: fork failed\n")
return false
}
// Parent (still root): the wait returning is the logout event.
_ = flibc.wait()
return true
}
export fn main(argc usize, argv argv) noreturn {
var user_buf [64]u8 = undefined
var pass_buf [128]u8 = undefined
var pw_buf [512]u8 = undefined
var shell_buf [64]u8 = undefined
// login mints a session by dropping privilege from root, and setuid is
// one-way — so a login that is not already root can only ever re-grant the
// euid it inherited. Run as a normal command from a privilege-dropped shell
// it would still authenticate (the kernel verifier does not gate on the
// caller's uid) and only then fail the drop with a misleading "cannot drop
// privilege". Refuse up front: minting sessions is the PID-1 supervisor's
// job, reached as root via initramfs exec. The proper user-switch is
// `logout` back to that supervisor, then log in as the other account.
// (Matches Unix, where login is getty/PID-1-only with no user entry point;
// FlashOS has no setuid-root bit to make a user-invoked login safe.)
if flibc.sys.geteuid() != 0 {
emit("login: must be root\n")
flibc.exit()
}
// Optional session limit (argv[1], decimal). 0 = loop forever.
var max_sessions u32 = 0
if argc >= 2 {
if argv[1] |arg| {
if parseU32(arg[0..strLen(arg)]) |n| {
max_sessions = n
}
}
}
var sessions_done u32 = 0
// Blank line before the first `login:` prompt, separating it from the
// kernel's last boot status line (or the -Dboot-selftest tally).
emit("\n")
while true {
// Username — echo off so login owns the echo through flibc's line
// editor: it echoes each byte and rubs out a backspace, so a typo is
// correctable. The kernel's raw echo could not erase a mistake, which
// made a single slip uncorrectable.
_ = flibc.sys.set_console_mode(0)
emit("login: ")
ulen := switch flibc.readline(&user_buf) {
.line => |l| l.len,
.eof, .abandoned => 0,
}
emit("\n")
// A bare Enter / empty username re-prompts silently, getty-style:
// no password challenge, no "Login incorrect". This also absorbs a
// stray newline left in the console RX at boot (e.g. a residual byte
// from the [TEST] login scenario's scripted sessions), so the first
// real prompt is a clean `login:` instead of a phantom failed attempt.
if ulen == 0 {
continue
}
// Password — still echo off; readMasked owns the echo, printing one
// '*' per accepted byte and rubbing it out on backspace, so the secret
// stays hidden on the serial console yet a typo is correctable. The
// console stays echo-off straight into the shell, where fsh's own
// readline owns the echo (no mask leak, no double-echo).
emit("Password: ")
plen := readMasked(&pass_buf)
emit("\n")
if flibc.sys.authenticate(&user_buf, ulen, &pass_buf, plen) != 0 {
emit("Login incorrect\n")
continue
}
// Pull uid / gid / shell from /etc/passwd (fresh read per session).
fd := flibc.sys.open(PASSWD_PATH)
if fd < 0 {
emit("login: /etc/passwd missing\n")
continue
}
var pn usize = 0
while pn < pw_buf.len {
r := flibc.sys.read(fd, pw_buf[pn..].ptr, pw_buf.len - pn)
if r <= 0 {
break
}
pn += #intCast(r)
}
_ = flibc.sys.close(fd)
// (orelse with a multi-statement block handler is not expressible; an
// explicit null check yields the same divergent re-prompt.)
maybe_entry := pwfile.lookupByName(pw_buf[0..pn], user_buf[0..ulen])
if maybe_entry == null {
emit("login: no passwd entry\n")
continue
}
entry := maybe_entry.?
// Copy + NUL-terminate the shell path for execve.
if entry.shell.len == 0 || entry.shell.len >= shell_buf.len {
emit("login: bad shell\n")
continue
}
var si usize = 0
while si < entry.shell.len {
shell_buf[si] = entry.shell[si]
si += 1
}
shell_buf[si] = 0
const shell_z cstr = #ptrCast(&shell_buf)
// Blank line separating the password prompt from the shell's
// homescreen, which the session child execs into next.
emit("\n")
if !runSession(entry.uid, entry.gid, shell_z) {
continue
}
// Logout: the session child has been reaped. Honour the session
// limit (the [TEST] auth hook), then fall through to re-prompt.
sessions_done += 1
if max_sessions != 0 && sessions_done >= max_sessions {
flibc.exit()
}
}
}