// 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() } } }