Skip to content

file: allow exec to return stdout as a file descriptor - #40

Open
JuliusBairaktaris wants to merge 1 commit into
openwrt:masterfrom
JuliusBairaktaris:file-exec-stream-fd
Open

JuliusBairaktaris wants to merge 1 commit into
openwrt:masterfrom
JuliusBairaktaris:file-exec-stream-fd

Conversation

@JuliusBairaktaris

@JuliusBairaktaris JuliusBairaktaris commented Aug 15, 2026 •

Copy link
Copy Markdown

Adds an optional stream flag to file exec. When it is set, the child's
stdout goes to a memfd and the descriptor is returned over ubus instead of
inline. Today stdout is returned inline, and output over 256 KB fails the call
with no output at all, which rules out returning a backup archive.

The memfd is capped at 16 MB with RLIMIT_FSIZE in the child. If the child
hits the cap, the call still completes, the fd is attached, and code is
128 + the signal number, as in a shell. Without stream nothing changes.

openwrt/cgi-io#6 uses this to run the sysupgrade backup through rpcd. It is a
prerequisite for openwrt/openwrt#24740 and pairs with #38.

Tested end to end through cgi-io#6 on an ipq807x router. I have not tested
hitting the 16 MB cap.

Would you rather an oversized stream fail the call with
UBUS_STATUS_NOT_SUPPORTED instead?

I used AI to help me write the change.

rpc_file_exec captures stdout and stderr into ustreams capped at
RPC_FILE_MAX_SIZE (256 KB); exceeding the cap fails the request with
UBUS_STATUS_NOT_SUPPORTED and no output at all. That cap makes the method
unusable for carrying a config backup archive.

A non-root uhttpd cannot build a LuCI config backup because cgi-io forks
/sbin/sysupgrade --create-backup as a CGI child inheriting uhttpd's uid,
which cannot read /etc/shadow or 0600 configs. Letting rpcd run the
command and hand the output back as a file descriptor keeps those reads
in the root daemon.

The new optional "stream" field (default false) writes the child's stdout
to an anonymous memfd instead of a ustream buffer. When stream is false
or absent the behaviour is byte-identical to before; stderr is still
returned inline and capped as today. The memfd is attached to the deferred
reply and carried to the caller via ubus fd passing; it is the same open
file description opened by root, so the receiver needs no DAC bypass.

The memfd is capped at 16 MB; the kernel enforces the cap via
RLIMIT_FSIZE, killing a child that writes past the limit with SIGXFSZ at
exactly the cap. Exit codes follow the shell convention: a signal-killed
child reports 128 plus the signal number, so this surfaces as 153
(128 + SIGXFSZ) rather than the misleading 0 that WEXITSTATUS() yields
for a signal death or a silently truncated stream.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Copilot AI lite review requested due to automatic review settings August 15, 2026 07:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@JuliusBairaktaris

Copy link
Copy Markdown
Author

@jow- @hauke — review ping.

Adds stream: true to file exec, returning the child's stdout as a file descriptor over SCM_RIGHTS instead of inline in the reply, so callers are not bound by the 256 KB RPC_FILE_MAX_SIZE cap (which errors out rather than truncating). The non-stream path is unchanged.

This is the primitive that lets cgi-io hand back a sysupgrade backup archive without being root (openwrt/cgi-io#6), and so blocks openwrt/openwrt#24558. Best reviewed after #38.

JuliusBairaktaris added a commit to JuliusBairaktaris/luci that referenced this pull request Sep 7, 2026
openwrt/cgi-io#6 moves archive creation to rpcd's file exec method so
that it keeps working when uhttpd runs as a non-root user, and
openwrt/rpcd#40 returns the child's stdout as a file descriptor so the
archive is streamed rather than buffered into the ubus reply. rpcd
matches the requested command against the caller's session ACL, so the
grant names the exact command line.

Depends on openwrt/cgi-io#6 and openwrt/rpcd#40.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
JuliusBairaktaris added a commit to JuliusBairaktaris/luci that referenced this pull request Oct 2, 2026
openwrt/cgi-io#6 moves archive creation to rpcd's file exec method so
that it keeps working when uhttpd runs as a non-root user, and
openwrt/rpcd#40 returns the child's stdout as a file descriptor so the
archive is streamed rather than buffered into the ubus reply. rpcd
matches the requested command against the caller's session ACL, so the
grant names the exact command line.

Depends on openwrt/cgi-io#6 and openwrt/rpcd#40.

Assisted-by: Claude:claude-opus-5
Signed-off-by: Julius Bairaktaris <julius@bairaktaris.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants