<feed xmlns='http://www.w3.org/2005/Atom'>
<title>procd/stdio-fds.h, branch master</title>
<subtitle>OpenWrt service / process manager</subtitle>
<id>https://git.openwrt.org/project/procd/atom?h=master</id>
<link rel='self' href='https://git.openwrt.org/project/procd/atom?h=master'/>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/procd/'/>
<updated>2026-08-21T13:20:28Z</updated>
<entry>
<title>jail, uxc: report the exit status and signal the invoker</title>
<updated>2026-08-21T13:20:28Z</updated>
<author>
<name>Daniel Golle</name>
</author>
<published>2026-08-21T04:35:52Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/procd/commit/?id=30393174b60507d355feb946f1b1ecec89ff60e6'/>
<id>urn:sha1:30393174b60507d355feb946f1b1ecec89ff60e6</id>
<content type='text'>
conmon learns a container's exit status by waiting for the pid it reads from
the runtime's pid file, but with ujail that process is a child of procd and
never of conmon, so conmon never obtains a status at all: podman reports 0
for a container that exited 42, and podman exec fails even when the command
succeeded. A shim would give conmon something to wait for at the price of
putting a process other than the container's own in the pid file, which
tooling needs for introspecting namespaces, cgroups and seccomp state.

Write the decoded status, WEXITSTATUS or 128 plus the terminating signal, to
an exit_status file in the directory the pid file lives in. The value goes to
a temporary file and is renamed into place, so a reader either sees the
previous value or the whole new one, and it is written before the SIGCHLD
that ends conmon's loop. Exec sessions do the same with their own pid file,
so podman exec has a status to report as well. A stale value from an earlier
run is removed when a pid file is written, and nothing is written when no pid
file was asked for, which leaves the behaviour of a caller that does not want
any of this unchanged.

The status file on its own ends no wait: the container is procd's child, so
its death raises no SIGCHLD in conmon, and a detached container's streams
never reach end of file, leaving conmon to sit until its own timeout. The
runtime therefore signals the process that asked for the container. conmon
execs the runtime, so uxc runs as conmon's child and its parent is the
waiting process; uxc opens a pidfd on getppid() with pidfd_open(), called
through syscall() since musl wraps neither it nor pidfd_send_signal(), and
sends the descriptor along with the create and exec requests. procd keeps it
on the instance beside the stdio descriptors and lets ujail inherit it
across the execve() that starts the jail, -a naming the descriptor number,
and ujail sends SIGCHLD through it with pidfd_send_signal() once the
container is gone, after the status has been written. An exec session
signals through the descriptor its own request carried once the session has
been reaped, and a respawned instance carries the same descriptor into the
next ujail, so the invoker also learns when a later incarnation dies. This
holds for any invoker instead of leaning on the conmon.pid filename, which
is merely podman's default for --conmon-pidfile and silently defeated by
overriding it.

A pid would identify the invoker only for as long as it lives. Callers such
as uxc.init and interactive shells routinely exit long before their
container does, leaving behind a number the kernel may hand out again, and
any scheme that re-checks the pid before the kill still leaves a window
between the check and the signal. A pidfd pins the identity at the instant
it is opened: from that instant on it is the only process a signal through it
can ever reach, however much later it is sent. One window remains, and it is
not one a descriptor can close: getppid() is read before the descriptor
exists, so an invoker that died first yields the reaper instead, and a number
already recycled by then names a stranger. uxc therefore re-reads getppid()
after opening and sends nothing if it changed, which leaves only the two
adjacent syscalls in between, against the whole lifetime of a container in
the old scheme. ujail still polls the descriptor for POLLIN first, the same
way exec_jail() watches the pidfd of its own parent, and stays silent for an
invoker that has already gone. A caller that supplies no descriptor is never
signalled; when pidfd_open() fails uxc warns and sends none rather than
falling back to a pid, since a caller which then waits for a wake-up that
never comes deserves to see why.

The descriptor set now carries its own count, so procd and uxc must be
upgraded together: the previous revision of this commit accepted exactly
three descriptors and nothing else. Losing the carrier now loses the
notification with it, where the pid had travelled separately in the request.

Two limits are worth naming. The signal is sent as ujail begins tearing
down, before cgroups and the network are dismantled, so a woken manager can
observe a container whose traces have not all gone yet. And an exec session
still in flight when the container itself dies is never signalled, because
the supervisor exits first; such a session is left to end on stream EOF, as
it was before.

ubus carries a single descriptor per request, and the three standard
descriptors already travel as SCM_RIGHTS over a socket pair whose receiving
end goes to ubus_invoke_fd(). That carrier now takes a counted set instead
of a fixed trio, the count riding in the payload byte, because the stdio
descriptors are only sent when pass-through is wanted while the notification
descriptor is wanted independently of that: a create sends stdio and, when
the invoker could be named, the pidfd; an exec session with a terminal sends
the pidfd alone. The receiver tells the layouts apart by the count, which
stays unambiguous because stdio is all or nothing.

The invoker's identity is thereby no longer configuration. procd used to
compare the notifypid attribute like the other jail attributes, restarting a
running instance when a re-add named a different invoker; the descriptor is
runtime state like the stdio descriptors, so a re-add replaces the stored
descriptor for the next start while the running jail keeps the one it
inherited. The descriptor stays close-on-exec everywhere except across the
one execve() that starts the instance's own ujail, and ujail marks it
close-on-exec again as soon as it parses the option, so neither hooks nor
the container itself ever inherit it.

conmon ends its loop when the streams it handed the runtime reach end of file
and only then looks for the status, so an exec session's descriptors stay with
the session and are closed once its status has been written. Recording it from
the process that waits for the session is no alternative: that one has joined
the container's mount namespace, where the path the status belongs at does not
exist.

Signed-off-by: Daniel Golle &lt;daniel@makrotopia.org&gt;
</content>
</entry>
<entry>
<title>service, jail: accept the caller's stdio for containers</title>
<updated>2026-08-21T04:34:20Z</updated>
<author>
<name>Daniel Golle</name>
</author>
<published>2026-08-21T04:34:20Z</published>
<link rel='alternate' type='text/html' href='https://git.openwrt.org/project/procd/commit/?id=7419d653d858b0e35936c906b7ec290ea3ffbb59'/>
<id>urn:sha1:7419d653d858b0e35936c906b7ec290ea3ffbb59</id>
<content type='text'>
An OCI runtime gives the container the standard descriptors of the
process that created it: that is how conmon collects a container's output
and how the runtime-tools validation suite reads its results. Until now
procd could only relay container output to syslog, so everything a
container wrote was lost to its caller.

ubus carries a single descriptor per message, so a new top-level
stdio-fds.h passes all three as SCM_RIGHTS over a socket pair. procd
receives the socket with the add request and installs the descriptors on
the instance in place of the syslog pipes, falling back to the previous
behaviour when no socket accompanies the request; ujail does the same for
the processes it starts on behalf of exec. The header also carries the
sending helper the uxc side uses.

Signed-off-by: Daniel Golle &lt;daniel@makrotopia.org&gt;
</content>
</entry>
</feed>
