Processes and roles
Motor OS has no users, no groups, and no root account. A process's authority is a capability word fixed at creation, and three roles derived from that word decide what it may do with files.
Capabilities
Every process carries an immutable 64-bit capability word, set when the process is created and readable by the process itself without a syscall (it is in a read-only page the kernel maps into every process). The defined bits:
| Capability | Meaning |
|---|---|
CAP_SYS | System authority: root-like, unkillable from userspace, may grant any capability to a child. |
CAP_IO_MANAGER | May map device memory and contiguous physical memory; held by sys-io. |
CAP_SPAWN | May create processes. |
CAP_LOG | May write records to the kernel log and to the logging service. |
CAP_SHUTDOWN | May halt the machine. |
CAP_SPAWN_DETACHED | May create detached children that outlive the parent. |
CAP_INTERACTIVE | Acts with the authority of the logged-in user. |
The kernel enforces one rule at spawn: a parent without CAP_SYS may only
grant a subset of its own capabilities, and never CAP_SYS or
CAP_IO_MANAGER. A CAP_SYS parent may grant anything. Because a
capability can never be gained after creation, a process cannot escalate.
The three roles
A role is derived from the capability word: CAP_SYS means
System; otherwise CAP_INTERACTIVE means
Interactive; otherwise the role is None. The roles are
ordered, System above Interactive above None, and they are the only identity the
filesystem knows about: every file and directory stores one permission byte per role.
| Role | Meaning | Typical holders |
|---|---|---|
| System | The OS itself; owns installed content and system logs. | sys-io, sys-init, strobe |
| Interactive | The logged-in user's authority: may read and run installed programs, and owns /user. | sys-tty, russhd, console and SSH shells, the commands they run, user daemons |
| None | Least privilege: public read and execute, writes only to shared scratch directories. | dns-resolver, other services, deliberately sandboxed children |
A server that wants to know who it is talking to asks the kernel for the capabilities of the process at the other end of a connection; the answer comes from the kernel, not from the client, so it cannot be forged. sys-io does this once per filesystem connection and uses the result for every permission check on that connection. If the query fails, the connection is dropped rather than given a default role.
Spawning
There is no fork and no exec. std::process::Command
works, and behind it the runtime in the parent builds the child directly: it creates an
address space, loads the ELF executable into it, maps the runtime, writes the arguments
and environment, sets up stdio, creates the process object with the chosen capabilities,
and wakes the main thread. The program file's execute permission for the parent's role is
checked before loading; a script needs both the script and its interpreter to be
executable.
The child's capabilities come from an environment key that the parent's runtime consumes at spawn time and the child never sees:
MOTOR_OS_CAPS=<hex mask>replaces the default outright. It is the way to demote a child (for example, a shell running an untrusted binary as None) and the way an explicit session boundary is created. An unparsable value fails the spawn rather than falling back to the default.MOTOR_OS_DETACHEDmakes the child detached: it is owned by the kernel and survives its parent, which is how rmux's server outlives the SSH session that started it. The parent needsCAP_SPAWN_DETACHED. An ordinary child is killed when its parent is reaped.
Without MOTOR_OS_CAPS the default depends on the parent's role. An
Interactive parent gets an Interactive child (a shell and the commands it runs), without
CAP_LOG. A System parent gets a None child by default: a
system service must say explicitly when it is creating a user session, so that a
logged-in session cannot appear by accident. A None parent gets a None child. In every
case the default is intersected with what the parent holds, and CAP_SPAWN is
included so that children can have children.
How a session gets its role
The chain is explicit at every step. sys-init starts sys-tty and russhd with a mask
that includes CAP_INTERACTIVE; sys-tty starts the console shell with the
same role it has itself; russhd gives each authenticated session shell the Interactive
bit; and from there the default rule carries Interactive down to every command. The
shipped sys-init.cfg gives russhd the mask 124
(CAP_SPAWN | CAP_LOG | CAP_SHUTDOWN | CAP_SPAWN_DETACHED | CAP_INTERACTIVE)
and the DNS resolver the mask 8 (CAP_LOG only). A test image variant runs the
serial console as System instead, using tty:system: in the configuration.
russhd itself runs as Interactive, including its pre-authentication code and its in-process SFTP implementation. This is an explicit interim choice: a non-System parent cannot pass on a capability it does not hold, and SFTP needs filesystem authority. The authentication boundary protects that authority; a future change may keep the network front end at None and hand each authenticated session to an Interactive worker.
What roles mean day to day
psmarks each process's role:*System,+Interactive, blank for None.- Interactive can install programs and edit configuration under
/user, and edit sources under/devtools/srcon the developer image, but cannot replace anything in/system. - None can read and run public installed content and write only to
/system/tmp,/user/tmp, and/devtools/tmp; it cannot even list/user/cfgor/system/logs. - A runtime diagnostic (a panic message, a backtrace) goes to the process's stderr,
so an ordinary command sees it even though it has no
CAP_LOG.
The permission bits themselves, and the policy that assigns them when an image is
built, are described under Filesystem. The design documents
are docs/process-roles.md and
src/sys/lib/motor-fs/PERMISSIONS_DESIGN.md in the repository.
Threads, killing, and exit
Threads are kernel objects created with a syscall; the runtime implements
std::thread, thread-local storage, and futex-based synchronization on top.
A parent can kill its child (Child::kill) and wait for it; there is no
signal to catch, so a killed process simply stops. Ctrl+C from a terminal terminates a
process with status 130 unless it registered a handler; see
Terminals. Exit codes are reported to the parent as on other
systems, and rush treats 130 as an interrupt.