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). Ten bits are defined: system authority, device access, spawning, logging, shutdown, detached spawning, the user's authority, vsock, the network, and filesystem writes. The kernel decides what a parent may grant a child, and a capability can never be gained after creation, so a process cannot escalate. Capabilities describes every bit, the grant rules, network and filesystem-write access, and how rush and rmux use them.
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. Servers that clients find by name check each other the same way; see Named IPC services.
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. A failed spawn says why: PermissionDenied for a program the
parent's role cannot execute, InvalidInput for a file that is not a loadable
executable, and OutOfMemory when the kernel refuses memory for the program's
segments.
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. CAP_VSOCK, CAP_NET,
and CAP_FS_WRITE follow whenever the parent holds them, so a restriction
applies to all descendants. The full rules are under
Capabilities.
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 1020
(CAP_SPAWN | CAP_LOG | CAP_SHUTDOWN | CAP_SPAWN_DETACHED | CAP_INTERACTIVE |
CAP_VSOCK | CAP_NET | CAP_FS_WRITE) and the DNS resolver the mask 264
(CAP_LOG | CAP_NET). 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.