Skip to content

fix: nonblocking delegated fds and correct error logging - #14

Merged
kaniini merged 2 commits into
mainfrom
fix/nonblocking-delegated-fds
Aug 12, 2026
Merged

kaniini merged 2 commits into
mainfrom
fix/nonblocking-delegated-fds

Conversation

@kaniini

@kaniini kaniini commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Two fixes that surfaced while embedding this library in Protect's daemon, which serves object capabilities in-process rather than from a CLI. Both are places where the library assumed it was the capsudo binary.

A caller may delegate a descriptor it also drives with an async runtime,
so it arrives here non-blocking -- and `dup` shares the open file
description, so the sender-side pump inherits that whether it wants to or
not.

The pumps assumed otherwise. `write_all_blocking` treated `EAGAIN` as
fatal, so the first time a peer wrote faster than the far end read, the
writer gave up and dropped the rest of the stream with no error reported
anywhere. The reader had a milder version: a readiness report is not a
guarantee, and a bare `EAGAIN` after `poll` was taken for a broken
descriptor and closed the stream.

Now `EAGAIN` means wait and retry, which is what a blocking descriptor
would have done for us. The writer waits for `POLLOUT`, treating a hangup
as `EPIPE` so it cannot wait forever on a descriptor that will never
drain; the reader goes back to waiting.

Blocking threads stay: caller descriptors may be regular files or ttys,
which epoll cannot watch. This only makes them indifferent to the mode of
the descriptor they were handed.

The test delegates a non-blocking socket as stdout and drains it late, so
the buffer is full while the daemon still has output to deliver. Without
the fix it receives exactly one socket buffer of a four megabyte payload.
…ting

The client printed a daemon's `Error` message to the process's stderr and
then dropped it. For the `capsudo` binary that happened to be right; for
anything embedding this library it is not. Protect's daemon serves object
capabilities in-process, so a refused session printed a bare line outside
its logging, unattributed to the capability that produced it, and the
caller never saw the text at all.

`SessionOutcome::Failed` now carries the message alongside the exit code.
It is kept distinct from `Exited` because the two mean different things:
one is a program that ran and failed, the other a program that never ran.
`run_client`, which reports an exit status, turns it into the new
`CoreError::Refused` for the same reason.

Printing is the binary's job, and it still does it.

Also stops printing on unexpected message types: a peer is free to send
messages a given version does not know about, and saying so on stderr is
noise, not diagnosis.

Note for callers: `SessionOutcome` gained a variant, so exhaustive
matches need an arm.
@kaniini
kaniini requested review from azenla and bleggett August 12, 2026 23:12
@kaniini
kaniini merged commit 2ac6e90 into main Aug 12, 2026
12 checks passed
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