LeonOS Documentation

Privilege elevation: sudo, su and the Fileman prompt

Upstream integration work, 2026-09-11

The normal image still uses the broker described below. Fixed official sudo, Linux-PAM, util-linux and libxcrypt now build into isolated staging; PAM development files are also packaged in the SDK. Kernel ID setters now follow capability checks and no longer change account metadata or cwd. Login/launch consumers perform group and cwd setup explicitly. Ordinary exec applies saved/fs-ID and capability transitions. Ext2 set-ID, NNP/nosuid, secure musl loader environment, and focused nonroot official sudo probes now pass on Linux v6.12 and LeonOS. Standard shadow account authority and normal consumers remain unfinished. Current evidence and required gaps are maintained in SUDOERS_PAM_STATUS.md. The earlier statement that no account migration is needed describes only the old broker repair, not the required upstream integration.

Security repair, 2026-09-11

The initial implementation described below contained security defects; its old host tests did not exercise the production password adapter. The repaired adapter explicitly converts authd_check_password()'s boolean success into the broker's zero-on-success convention. Only authentication of an administrator can create an administrator cache entry. Root callers need no password.

The cache is now keyed by requester UID and the kernel session ID obtained using the accepted peer PID. It is not a sudoers per-TTY timestamp or a per-command authorization policy. sudo -k still revokes all entries for the UID. Account changes and logout revoke all entries.

RUN now requires three preceding RUN_FD messages on a private connection, carrying stdin/stdout/stderr independently, plus cwd and TERM in RUN. Old RUN layouts are rejected; rebuild and deploy authd and all libc consumers together. No persistent account database migration is required. Connections are CLOEXEC; the child closes all descriptors above stderr except a CLOEXEC error pipe, which reports setup/exec failures before a successful RUN reply. Slots are reserved before spawn. WAIT and RUN_SIGNAL require the originating UID and PID. Signal forwarding currently targets the direct command process; full process-group job control and descendant supervision remain unverified.

Guest testing exposed a kernel prerequisite missed by the original audit: SCM_RIGHTS rejected PTY and implicit terminal descriptors because they are not stored as task_file entries. The socket layer now captures explicit PTY endpoints (including legacy standard-stream bindings), holds a PTY reference while queued, and installs a receiver endpoint with its own CLOEXEC flag. PTY reclaim and master reference accounting include queued transfers. Kernel PTY and socket tests cover queued lifetime, receive and discard paths. These changes do not claim complete Linux shared-OFD semantics for all PTY status flags, which remain a broader kernel ABI limitation.

Commands without slashes are searched in a fixed system PATH. Normal commands preserve cwd; su - enters HOME and invokes /bin/sh as -sh. TERM and SHELL are initialized. Password input uses the controlling terminal, rejects overflow, and restores echo before propagating interruption signals.

FILEOP argv is NULL terminated. Its root-private result file is opened by authd, passed read-only to the authorized client using SCM_RIGHTS, then unlinked after a successful send. The client never opens that private path. The worker pins parent directories using openat/O_DIRECTORY/O_NOFOLLOW and uses mkdirat/renameat/unlinkat for mutations; redundant path components and protected tree spellings are rejected. This relies on the kernel's dirfd and O_NOFOLLOW implementation; it is not protection against privileged mount or rename operations performed concurrently by another root process.

python3 tools/test_sudo_policy.py includes the real PBKDF2 adapter, cache isolation, slot exhaustion, exec fd isolation, three streams/cwd/TERM/PATH, private socket lifecycle, result fd lifetime, path traversal and real PTY password interruption tests under ASan/UBSan. See the current run evidence in build/sudo-host-tests.log. Guest evidence is recorded separately; host tests are not a claim of complete Linux sudo/su compatibility.

Remaining intentional differences include target-account password policy, no sudoers/PAM policy, sudo's administrator-only target restriction, fixed argument limits, and no full login-shell selection from arbitrary passwd shells. The historical sections below describe the original design and must not override the repaired protocol and security rules in this section.

LeonOS 4 authenticates users properly and enforces POSIX file permissions in the kernel, but until this change there was no path from an ordinary user to root. This document describes the mechanism that was added, what it defends, and what it deliberately does not.

Why a privileged broker, and not setuid

Repair validation evidence

The following original design rationale is retained for context.

Three facts force the design:

Therefore elevation cannot be something a process does to itself. It has to be something a process that is already uid 0 does on another's behalf. That is authd, the only long-lived uid 0 process besides init.

Architecture


ordinary user (uid 1000)                       authd (uid 0, always running)
  sudo / su / fileman ─┐                            │
                       │ RUN / WAIT / FILEOP        │
                       ├───────────────────────────▶│ 1. verify the target
                       │                            │    account's password
                       │                            │ 2. fork, assume the
                       │◀───────────────────────────┤    target identity
                       │  ack {child_pid}           │ 3. execve on the
                       │                            │    caller's terminal
                       │ WAIT                       │
                       └───────────────────────────▶│ 4. reap and report

The client never gains a credential and cannot assert one. Every request carries only what to run; who is asking comes from SO_PEERCRED, recorded when the connection was accepted.

Commands


sudo id                     # run as root after verifying an administrator
sudo -u alice id            # a non-root target is refused by sudo (see below)
sudo -k                     # discard this user's cached authorization
sudo -n id                  # never prompt; fail if a password is required
su                          # root shell
su -c 'id'                  # run one command as root
su alice                    # switch to an ordinary account

Password rules

callertargetpassword required
any useradministrator (root)the target account's password
uid 0ordinary accountnone — real Unix su alice semantics
ordinary userordinary accountthat target account's password
ordinary user, within the 5 minute windowadministratornone

Two invariants are worth stating explicitly because they are the parts a regression would turn into a privilege escalation:

  1. A cached window never authorizes assuming a different ordinary account. A user who has just run sudo cannot then su into another normal user without that user's password.
  2. sudo always elevates. sudo -u <ordinary user> is refused even when that account's own password is supplied. Switching to an ordinary account is su's job, where the password proves the target.

The refusal message does not distinguish "wrong password" from "that account may not be elevated": saying which would let the dialog enumerate administrators.

Credential window

A verified password opens a 300 second window for that requester uid, held in authd and not in any client. It is discarded by sudo -k, by leonos_auth_logout(), at authd start, and whenever the account is updated or its password changed. The window records that someone authenticated, never what for: every request still re-checks the target account, its role and the requested path.

The Fileman prompt

When Fileman cannot open a directory (open returns EACCES/EPERM) or a write verb is denied, it shows the administrator username and password dialogs, then asks the broker to perform the operation. chdir can never succeed for the unprivileged process, so a verified directory is adopted as the current path and enumerated through the broker from then on.

The broker answers every file operation with the affected directory's fresh contents, so the list view refreshes from one reply instead of racing a second listing.

Supported verbs: LIST, MKDIR, RENAME, UNLINK. Recursive deletion of a directory additionally requires the confirmation word DELETE, checked both by the daemon and by the worker.

Not supported: copying file contents out of a protected directory. The existing copy/paste implementation still runs as the calling user and reports a permission error. This is a deliberate boundary, not an oversight.

Protocol

Private to authd (userland/libc/include/leonos/authd.h); not public ABI.

messagevaluepurpose
RUN30verify, then fork/exec on the caller's terminal
WAIT31collect the child's exit status
SUDO_KILL32drop this requester's window
SUDO_CHECK33is a window open for this requester?
FILEOP34one privileged file operation
SUDO_VERIFY35open a window without running anything

RUN and FILEOP answer on their own message type with the errno in the same code field a success reply uses: one request, one reply, one type.

The RUN frame carries the caller's terminal descriptor over SCM_RIGHTS, and the child inherits it as stdin, stdout and stderr. WAIT exists because sched_wait_reap() only lets a parent reap a child (kernel/ntclks/sched/sched.c:2880) and the child belongs to authd; the daemon therefore reports the status.

The result-file path is chosen by the daemon inside a root-only directory, so a client cannot make the worker write where it likes.

Security boundaries

Defended:

Not defended:

Implementation notes

Verification


python3 build.py test sudo-policy          # policy unit + authd handlers
python3 tools/test_sudo_policy.py           # same suites directly
python3 build.py test oobe                  # authentication/session regressions
python3 build.py test installer-setup       # account and PBKDF2 regressions
python3 tools/test_security_regressions.py --strict

The security audit greps that every privileged message is dispatched to a *_from_peer handler and that the daemon passes the accept-time client->uid into it, so a new message cannot quietly skip the trust boundary.

Verified by this change

Not verified here

Appendix: the "update existing system" mount failure (ret=-22)

A report of the installer's update flow failing with 挂载失败 ret=-22 was investigated. Conclusion: it is not caused by the elevation change.

Evidence from the serial log:

The elevation change adds no block-device code path: it is userland-only, and the kernel/ABI/devtools trees were not touched. The same ISO boots, installs and reaches a working desktop.

What is genuinely at fault:

  1. The update path has never been exercised. The reporter had never used it before this attempt, so there is no baseline showing it working.
  2. The failure was undiagnosable. set_status() wrote only to the GUI (userland/apps/installer/main.c:702), so the specific check that returned -EINVAL never reached the log. That is now fixed: set_status() mirrors to the serial console, so the next attempt names the failing check and its path.

To distinguish "the update logic is broken" from "that VM's disk reads were failing", boot the installer with the scratch disk attached and repeat the update in QEMU, where storage is reliable:


python3 tools/test_sudo_e2e_qemu.py --output build/sudo-e2e --skip-install

If the update then succeeds, the -22 was a transport failure in that VM. If it still fails, the new serial line names the check, which is the information this investigation could not obtain.

Historical Record: Retired authd Broker

This document preserves the previous implementation and its evidence. It does not describe the current production system and must not be used as an active authorization or compatibility contract. See ../SUDO_AND_ELEVATION.md.