mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
docs(native): correct the end-of-file guidance given to client authors
The how-to told an author writing a client in another language that a queued empty datagram leaves the socket with no events pending, while a closed peer sets POLLHUP. That is false, and a client written to it reproduces the defect the receive path has just had fixed, in their code rather than ours. It also told them to poll with an events mask of zero. The library stopped doing that when the macOS port landed, because a poll that requests nothing registers no filter on Darwin and returns no events for a peer that has in fact closed. A client following the old instruction would never detect a close on macOS at all. The reference page carried both errors and asserted that a zero-byte read is an empty datagram and only that. Both pages now describe what the code does. Request POLLIN. Treat a hang-up as end of file only when FIONREAD reports nothing queued behind the read. Design around the one case neither check can resolve, by never giving a zero-length payload a meaning of its own.
This commit is contained in:
@@ -56,18 +56,42 @@ Neither can happen while the daemon writes exactly one whole line per `sendmsg`
|
||||
and treats a short write as an error, which it does. That is an invariant of
|
||||
two programs, though, not of the socket type.
|
||||
|
||||
## Step 3: Treat a zero-byte read as end of file only when POLLHUP is set
|
||||
## Step 3: Decide end of file from POLLHUP and an empty queue
|
||||
|
||||
**An empty datagram and a closed peer both produce a zero-byte read, and
|
||||
`MSG_EOR` does not tell them apart.** On Linux 6.8, `recvmsg` on an `AF_UNIX`
|
||||
`SOCK_SEQPACKET` socket returns `msg_flags == 0` for a normal message, an empty
|
||||
message and end of file alike, so the flag carries no information.
|
||||
|
||||
`POLLHUP` does discriminate. After a zero-byte read, a queued empty datagram
|
||||
leaves the socket with no events pending, while a closed peer leaves `POLLHUP`
|
||||
set and latched. Poll with an events mask of **zero**, because `POLLHUP` is
|
||||
reported in `revents` whether or not it was requested. The poll costs nothing:
|
||||
it runs only on the zero-byte path and does not block.
|
||||
`POLLHUP` narrows the question and does not answer it. A live peer never sets
|
||||
it, so a zero-byte read without `POLLHUP` is an empty datagram and nothing else.
|
||||
A closed peer does set it, **and it latches while messages are still queued**.
|
||||
Measured on Linux 6.8: a socket holding one empty datagram from a peer that has
|
||||
since closed reports exactly what a drained socket reports, in `revents`, in
|
||||
`FIONREAD`, under `MSG_PEEK` and in the `recvmsg` return alike.
|
||||
|
||||
Poll with an events mask of **`POLLIN`**. Linux reports `POLLHUP` in `revents`
|
||||
whether or not it was requested, but a poll that requests nothing registers no
|
||||
filter on Darwin and returns zero events for a peer that has in fact closed.
|
||||
`POLLIN` costs nothing on either platform, because you mask the result to
|
||||
`POLLHUP` regardless. The poll runs only on the zero-byte path and does not
|
||||
block.
|
||||
|
||||
**When `POLLHUP` is set, ask whether anything is still queued before you call it
|
||||
end of file.** `ioctl(fd, FIONREAD, &n)` reports the bytes the receive queue
|
||||
holds. A non-zero `n` proves a further message is waiting, so the zero-byte read
|
||||
you just took was an empty datagram: deliver it and read on. This is the case
|
||||
that costs a real payload if you get it wrong. A client that sends an empty
|
||||
datagram, then a message, then closes, leaves both queued, and a reader that
|
||||
trusts `POLLHUP` alone discards the message.
|
||||
|
||||
**One case has no answer, and you should design around it rather than solve
|
||||
it.** A zero-length datagram that is the last message before a close is
|
||||
indistinguishable from the close: reading it drains the queue, and `FIONREAD`
|
||||
then reports zero because a zero-length message contributes no bytes. If your
|
||||
protocol gives a zero-length payload a meaning, do not send it as a zero-length
|
||||
socket message. Carry a one-byte discriminator, and keep the zero-byte read for
|
||||
end of file alone.
|
||||
|
||||
Both directions of the mistake are real. Reading an empty datagram as a close
|
||||
lets a peer tear down a live flow by sending nothing, and presents as a
|
||||
|
||||
@@ -249,13 +249,24 @@ non-blocking. A datagram longer than `buf` is truncated and the remainder
|
||||
discarded, which is `SOCK_SEQPACKET` behaviour and is not reported; size `buf`
|
||||
at `max_payload()` and it cannot happen. `EINTR` is retried.
|
||||
|
||||
**`Ok(0)` is an empty datagram and only that.** An empty datagram and a closed
|
||||
peer both produce a zero-byte read, and `MSG_EOR` does not tell them apart. On
|
||||
the zero-byte path only, the library polls the descriptor with an events mask
|
||||
of zero and reads `POLLHUP` from `revents`, returning `EPIPE` for a genuine
|
||||
close and `Ok(0)` otherwise. Without that step a peer could tear down a live
|
||||
flow by sending nothing. The `EPIPE` that `recv` returns means the daemon went
|
||||
away, never that a peer finished.
|
||||
**`Ok(0)` is an empty datagram in every case the socket can distinguish.** An
|
||||
empty datagram and a closed peer both produce a zero-byte read, and `MSG_EOR`
|
||||
does not tell them apart. On the zero-byte path only, the library polls the
|
||||
descriptor with an events mask of `POLLIN` and reads `POLLHUP` from `revents`,
|
||||
and where `POLLHUP` is set it asks `FIONREAD` whether the queue still holds
|
||||
anything. It returns `EPIPE` only when the peer has hung up and nothing is
|
||||
queued behind the read, and `Ok(0)` otherwise. `POLLHUP` alone is not enough,
|
||||
because it latches while messages are still queued: a read that trusted it
|
||||
would report the close early and discard whatever was waiting. The `EPIPE` that
|
||||
`recv` returns means the daemon went away, never that a peer finished.
|
||||
|
||||
**One case is reported as `EPIPE` although it is a datagram.** A zero-length
|
||||
datagram that is the last message before a close is indistinguishable from the
|
||||
close, because reading it drains the queue and `FIONREAD` then reports zero: a
|
||||
zero-length message contributes no bytes. Do not give a zero-length payload a
|
||||
meaning of its own on this API. Carry a one-byte discriminator instead.
|
||||
Separating the two needs a payload that is never zero bytes on the wire, which
|
||||
is a protocol change rather than a receive-path one.
|
||||
|
||||
**`peer_addr()`** and **`local_addr()`** return `FipsAddr`, not
|
||||
`io::Result<FipsAddr>`, unlike their `TcpStream` counterparts. These are field
|
||||
|
||||
Reference in New Issue
Block a user