Decisions
0033: The system service greets first
Apps are identified the moment they connect, and both sides confirm each other before any request.
Status: Accepted. Builds on 0020 (identity from the kernel). Closes audit-security A2 (identity after parse).
On this page
Context
wold decides grants on who the kernel says is on the other end of the control socket
(/var/run/wold.sock): SO_PEERCRED's uid and pid, and the executable of that pid. Since v0.12 the
identity is captured at accept and pinned with a /proc/<pid> descriptor (crates/api/src/peer.rs),
and the executable is read again before any grant, so a peer that exec()s after accept is refused.
One gap was left: a socket accepts data before the server calls accept(). A process could connect,
write a request, then exec() a trusted binary (setuid or not) before wold accepted. wold would then
read a request the old image wrote, attributed to the new image's executable. A process's pid and
start time survive exec, so neither a pidfd nor /proc/<pid>/stat can tell the two apart.
Decision
- wold writes
hello <nonce>(128 random bits, hex;shared::jobs::send_hello) on every accepted control connection before it reads anything. The client's first line must echo that line exactly (shared::jobs::answer_hello); otherwise the connection is dropped without an answer. Only after the echo does wold read the request (crates/api/src/server.rs,serve). - The hello is mandatory, deny by default. There is no fallback for clients that do not speak it.
- In-tree clients connect through
shared::jobs::connect(connect + handshake). Test fakes of wold callsend_helloand skip the echo line. - Either side waits at most
shared::jobs::HELLO_TIMEOUT(5 s) for the other's hello.
Consequences
- A request written before accept can never be the first line after the hello, because the nonce did not exist yet. A process that exec()ed before accept cannot answer: the new image does not know about the pending connection (Rust and libwol sockets are close-on-exec).
- The control socket costs one extra round trip per connection (one line each way).
- Fork gap closed in userspace (audit S5): a process that forks, keeps the socket in the child, and
lets the parent (the pid the kernel recorded at connect) exec a trusted binary before accept
cannot answer the hello from the child.
shared::jobs::greetturns SO_PASSCRED on before it sends the hello, so the kernel stamps every byte the client writes with the writing process (SCM_CREDENTIALS; an unprivileged sender can claim only its own pid), and drops the connection unless every byte of the echo carries the SO_PEERCRED pid. Clients change nothing. Tests:shared::jobsgreet_refuses_an_echo_from_another_process. - Residual: a client that legitimately forks between connect and its hello (none in tree) is refused; it must connect after the fork. A trusted binary whose parent is hostile is ADR 0020's concern (audit S6), not the handshake's.
- The memsaver and qos sockets greet the same way (
crates/api/src/bin/wold.rscallsgreet), and the realtime agent answers throughshared::jobs::answer_hello. session.approve: a helper's admin tool (wol mcp) no longer dials the shell; it asks wold, which names the caller by the executable the kernel reports for it and forwards the sheet, and the shell answers anapproveline only from wold (crates/controller/src/approve.rs,crates/shell/src/ui/ipc.rs).
Reference synced from wonderlandos/wolos docs/adr/0033-identity-handshake.md.