NetBSD-Bugs archive
[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index][Old Index]
kern/60792: fifo: poll reader before writers?
>Number: 60792
>Category: kern
>Synopsis: fifo: poll reader before writers?
>Confidential: no
>Severity: serious
>Priority: medium
>Responsible: kern-bug-people
>State: open
>Class: sw-bug
>Submitter-Id: net
>Arrival-Date: Fri Sep 25 18:45:00 +0000 2026
>Originator: Taylor R Campbell
>Release: current, 11, 10, ...
>Organization:
Fifes and Piccolos GmbH
>Environment:
>Description:
Consider the following operations on a named pipe:
1. rd0 = open("fifo", O_RDONLY|O_NONBLOCK)
2. wr = open("fifo", O_WRONLY); close(wr)
3. rd1 = open("fifo", O_RDONLY|O_NONBLOCK)
If you try to read from rd0 or rd1 after each step, what should
happen? If you poll rd0 or rd1 for POLLIN after each step,
what should you get?
POSIX.1-2024 says:
> POLLIN
> The file descriptor is ready for reading data other than
> high-priority data.
> [...]
> POLLHUP
> A device has been disconnected, or a pipe or FIFO has
> been closed by the last process that had it open for
> writing. Once set, the hangup state of a FIFO shall
> persist until some process opens the FIFO for writing or
> until all read-only file descriptors for the FIFO are
> closed. This event and POLLOUT are mutually-exclusive.
> However, this event and POLLIN, POLLRDNORM, POLLRDBAND,
> or POLLPRI are not mutually-exclusive. This flag is only
> valid in the revents bitmask; it shall be ignored in the
> events member.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/poll.html
Additionally, POSIX.1-2024 says read() should _always_ give a
zero-byte return on a fifo with no writers to report EOF
without blocking, which is the case here:
> When attempting to read from an empty pipe or FIFO:
>
> If no process has the pipe open for writing, read()
> shall return 0 to indicate end-of-file.
https://pubs.opengroup.org/onlinepubs/9799919799/functions/read.html
This suggests POLLIN should be set for all of them, because the
fifo can always be read without blocking (and give EOF). This
is also suggests that POLLHUP should be set after (2), and for
rd0 after (3), but it is unclear whether it should also be set
after (1) or for rd1 after (3).
It would be _much simpler_ if POLLHUP is set in all of these
cases so we don't have to keep state to distinguish them. The
patch I have drafted for PR kern/60789 (read on fifo without
writer may block, https://gnats.NetBSD.org/60789) does that by
just deleting a few lines of code for another purpose.
On the one hand, POSIX.1-2024 says `a pipe or FIFO has been
_closed by the last process that had it open for writing_',
which suggests that POLLHUP should not be set in (1).
On the other hand, how far back do we have to keep the history
of whether a fifo has been open for writing? Surely, once all
fds for a fifo have been closed, its state should be reset.
(Certainly this is the case if the system reboots!) Keeping
state to distinguish rd0 from rd1 is particularly annoying, but
Linux and FreeBSD seem to do that.
Here are the actual results on things that claim to be real
operating systems:
=> NetBSD 11.0:
1. read EOF poll 0 *bzzt*
2. read EOF poll POLLIN|POLLHUP
3. rd0: read EOF poll 0 *bzzt*
rd1: read EOF poll 0 *bzzt*
=> Linux 6.1.0:
1. read EOF poll 0 *bzzt*
2. read EOF poll POLLHUP
3. rd0: read EOF poll POLLHUP
rd1: read EOF poll 0 *bzzt*
=> FreeBSD 13.0:
1. read *block* poll 0
2. read EOF poll POLLIN|POLLHUP
3. rd0: read EOF poll POLLIN|POLLHUP
rd1: read EOF poll 0 *bzzt*
=> macOS (Darwin 25.6.0):
1. read EOF poll 0 *bzzt*
2. read EOF poll 0 *bzzt*
3. rd0: read EOF poll 0 *bzzt*
rd1: read EOF poll 0 *bzzt*
The lines marked *bzzt* are internally inconsistent: poll fails
to report that the fd is readable when a read would return an
immediate result.
With my naive patch for PR 60789 (to apply a change that
FreeBSD made back in 1997), I get:
=> NetBSD patched for PR kern/60789:
1. read EOF poll POLLIN|POLLHUP
2. read EOF poll POLLIN|POLLHUP
3. rd0: read EOF poll POLLIN|POLLHUP
rd1: read EOF poll POLLIN|POLLHUP
That's internally consistent _and_ agrees with POSIX for the
read behaviour. But what about the poll results? Is that the
right thing?
>How-To-Repeat:
Test program:
#include <sys/stat.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <poll.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void
showpoll(const char *title, int events)
{
int n = 0;
printf("%s=0x%x<", title, events);
if (events & POLLIN)
printf("%sPOLLIN", n++ ? "," : "");
if (events & POLLRDNORM)
printf("%sPOLLRDNORM", n++ ? "," : "");
if (events & POLLRDBAND)
printf("%sPOLLRDBAND", n++ ? "," : "");
if (events & POLLOUT)
printf("%sPOLLOUT", n++ ? "," : "");
if (events & POLLWRNORM)
printf("%sPOLLWRNORM", n++ ? "," : "");
if (events & POLLWRBAND)
printf("%sPOLLWRBAND", n++ ? "," : "");
if (events & POLLHUP)
printf("%sPOLLHUP", n++ ? "," : "");
if (events & POLLERR)
printf("%sPOLLERR", n++ ? "," : "");
if (events & POLLNVAL)
printf("%sPOLLNVAL", n++ ? "," : "");
printf(">\n");
}
int
main(void)
{
int rd0, wr, rd1;
struct pollfd pfd[2];
int nfds;
char ch;
ssize_t nread;
int bzzt = 0;
(void)unlink("fifo");
if (mkfifo("fifo", 0600) == -1)
err(EXIT_FAILURE, "mkfifo");
printf("1. open rd0\n");
fflush(stdout);
if ((rd0 = open("fifo", O_RDONLY|O_NONBLOCK)) == -1)
err(EXIT_FAILURE, "open rd0");
memset(&pfd, 0, sizeof(pfd));
pfd[0].fd = rd0;
pfd[0].events = POLLIN;
if ((nfds = poll(pfd, 1, 0)) == -1)
err(EXIT_FAILURE, "poll");
printf("nfds=%d\n", nfds);
showpoll("pfd[0].revents", pfd[0].revents);
fflush(stdout);
if ((nread = read(rd0, &ch, 1)) == -1) {
int error = errno;
warn("read rd0");
if (error == EAGAIN &&
(pfd[0].revents & POLLIN) != 0) {
printf("*bzzt* poll said rd0 readable, why block?\n");
bzzt = 1;
}
} else {
printf("read rd0 returned %zd bytes\n", nread);
if ((pfd[0].revents & POLLIN) == 0) {
printf("*bzzt* poll said rd0 nonreadable, why read?\n");
bzzt = 1;
}
}
printf("2. open and close wr\n");
fflush(stdout);
if ((wr = open("fifo", O_WRONLY)) == -1)
err(EXIT_FAILURE, "open wr");
if (close(wr) == -1)
err(EXIT_FAILURE, "close wr");
memset(&pfd, 0, sizeof(pfd));
pfd[0].fd = rd0;
pfd[0].events = POLLIN;
if ((nfds = poll(pfd, 1, 0)) == -1)
err(EXIT_FAILURE, "poll");
printf("nfds=%d\n", nfds);
showpoll("pfd[0].revents", pfd[0].revents);
fflush(stdout);
if ((nread = read(rd0, &ch, 1)) == -1) {
int error = errno;
warn("read rd0");
if (error == EAGAIN &&
(pfd[0].revents & POLLIN) != 0) {
printf("*bzzt* poll said rd0 readable, why block?\n");
bzzt = 1;
}
} else {
printf("read rd0 returned %zd bytes\n", nread);
if ((pfd[0].revents & POLLIN) == 0) {
printf("*bzzt* poll said rd0 nonreadable, why read?\n");
bzzt = 1;
}
}
printf("3. open rd1\n");
fflush(stdout);
if ((rd1 = open("fifo", O_RDONLY|O_NONBLOCK)) == -1)
err(EXIT_FAILURE, "open rd1");
memset(&pfd, 0, sizeof(pfd));
pfd[0].fd = rd0;
pfd[0].events = POLLIN;
pfd[1].fd = rd1;
pfd[1].events = POLLIN;
if ((nfds = poll(pfd, 2, 0)) == -1)
err(EXIT_FAILURE, "poll");
printf("nfds=%d\n", nfds);
showpoll("pfd[0].revents", pfd[0].revents);
showpoll("pfd[1].revents", pfd[1].revents);
fflush(stdout);
if ((nread = read(rd0, &ch, 1)) == -1) {
int error = errno;
warn("read rd0");
if (error == EAGAIN &&
(pfd[0].revents & POLLIN) != 0) {
printf("*bzzt* poll said rd0 readable, why block?\n");
bzzt = 1;
}
} else {
printf("read rd0 returned %zd bytes\n", nread);
if ((pfd[0].revents & POLLIN) == 0) {
printf("*bzzt* poll said rd0 nonreadable, why read?\n");
bzzt = 1;
}
}
fflush(stdout);
if ((nread = read(rd1, &ch, 1)) == -1) {
int error = errno;
warn("read rd1");
if (error == EAGAIN &&
(pfd[1].revents & POLLIN) != 0) {
printf("*bzzt* poll said rd1 readable, why block?\n");
bzzt = 1;
}
} else {
printf("read rd1 returned %zd bytes\n", nread);
if ((pfd[1].revents & POLLIN) == 0) {
printf("*bzzt* poll said rd1 nonreadable, why read?\n");
bzzt = 1;
}
}
fflush(stdout);
return bzzt | ferror(stdout);
}
Output on various systems:
* NetBSD 11.0
1. open rd0
nfds=0
pfd[0].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
2. open and close wr
nfds=1
pfd[0].revents=0x11<POLLIN,POLLHUP>
read rd0 returned 0 bytes
3. open rd1
nfds=0
pfd[0].revents=0x0<>
pfd[1].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
read rd1 returned 0 bytes
*bzzt* poll said rd1 nonreadable, why read?
* Linux 6.1.0
1. open rd0
nfds=0
pfd[0].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
2. open and close wr
nfds=1
pfd[0].revents=0x10<POLLHUP>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
3. open rd1
nfds=1
pfd[0].revents=0x10<POLLHUP>
pfd[1].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
read rd1 returned 0 bytes
*bzzt* poll said rd1 nonreadable, why read?
* FreeBSD 13.0
1. open rd0
nfds=0
pfd[0].revents=0x0<>
a.out: read rd0: Resource temporarily unavailable
2. open and close wr
nfds=1
pfd[0].revents=0x11<POLLIN,POLLHUP>
read rd0 returned 0 bytes
3. open rd1
nfds=1
pfd[0].revents=0x11<POLLIN,POLLHUP>
pfd[1].revents=0x0<>
read rd0 returned 0 bytes
read rd1 returned 0 bytes
*bzzt* poll said rd1 nonreadable, why read?
* macOS (Darwin 25.6.0)
1. open rd0
nfds=0
pfd[0].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
2. open and close wr
nfds=0
pfd[0].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
3. open rd1
nfds=0
pfd[0].revents=0x0<>
pfd[1].revents=0x0<>
read rd0 returned 0 bytes
*bzzt* poll said rd0 nonreadable, why read?
read rd1 returned 0 bytes
*bzzt* poll said rd1 nonreadable, why read?
>Fix:
# HG changeset patch
# User Taylor R Campbell <riastradh%NetBSD.org@localhost>
# Date 1790351249 0
# Fri Sep 25 15:47:29 2026 +0000
# Branch trunk
# Node ID b83e1108a0db8fc4edec32caa286fe4f8400e346
# Parent 9443bf71b263744642d81ee2b0d3083456da337b
# EXP-Topic riastradh-pr59578-fifoopesigrace
fifo: Fix EOF reporting in various cases.
According to POSIX:
> When attempting to read from an empty pipe or FIFO:
>
> If no process has the pipe open for writing, read() shall
> return 0 to indicate end-of-file.
So:
1. When there are no writers, after open(O_RDONLY|O_NONBLOCK),
blocking reads should immediately report EOF instead of blocking,
and should continue to immediately report EOF on repeated reads.
Previously, they would simply block, because the wrong socket was
initialized with SS_CANTRCVMORE -- though curiously, blocking
reads would consistently report EOF.
2. When the last writer has closed, blocking reads should immediately
report report EOF instead of blocking, and should continue to
immediately report EOF on repeated reads.
Previously, the _first_ such read would report EOF, and
_subsequent_ reads would block because we cleared SS_CANTRCVMORE
on the first read.
The old behaviour dates back to 1990 when the fifo_vnops.c code was
first added to the UCB CSRG BSD code in SCCS (without any commit
message):
https://github.com/robohack/ucb-csrg-bsd/commit/1036021a3155e1972e24dae4115eaf0167c4a3fb
This change simply applies FreeBSD's fifo_vnops.c rev. 1.39 by Bruce
Evans from 1997(!):
https://cgit.freebsd.org/src/commit/?id=80987b7a3b0ba57a094d9b5c42af439d5f94fe8f
commit 80987b7a3b0ba57a094d9b5c42af439d5f94fe8f
Author: Bruce Evans <bde%FreeBSD.org@localhost>
Date: Sat Dec 13 13:49:59 1997 +0000
Fixed EOF handing.
1. SS_CANTRCVMORE was initially set on the wrong socket, so reads
when there has never been a writer on the socket did not return 0.
Note that such reads are only possible if the fifo was opened in
(O_RDONLY | O_NONBLOCK) mode.
2. SS_CANTSENDMORE was initially set on the wrong socket, but this
was harmless because the wrong socket is never sent from and there
is no need to set the flag initially on the right socket (since open
in (O_WRONLY | O_NONBLOCK) mode fails if there is no reader...).
3. SS_CANTRCVMORE was cleared when read() returns. This broke the
case where read() returns 0 - subsequent reads are supposed to
return 0 until a writer appears. There is no need to clear the
flag when read() returns, since it is cleared correctly when a
writer appears.
PR kern/60789: read on fifo without writer may block
diff -r 9443bf71b263 -r b83e1108a0db sys/miscfs/fifofs/fifo_vnops.c
--- a/sys/miscfs/fifofs/fifo_vnops.c Fri Sep 25 15:15:25 2026 +0000
+++ b/sys/miscfs/fifofs/fifo_vnops.c Fri Sep 25 15:47:29 2026 +0000
@@ -179,8 +179,7 @@ fifo_open(void *v)
fip->fi_writers = 0;
fip->fi_rgen = 0;
fip->fi_wgen = 0;
- wso->so_state |= SS_CANTRCVMORE;
- rso->so_state |= SS_CANTSENDMORE;
+ rso->so_state |= SS_CANTRCVMORE;
cv_init(&fip->fi_rcv, "fiford");
cv_init(&fip->fi_wcv, "fifowr");
vp->v_fifoinfo = fip;
@@ -267,7 +266,6 @@ fifo_read(void *v)
struct uio *uio;
struct socket *rso;
int error, sflags;
- size_t startresid;
uio = ap->a_uio;
rso = ap->a_vp->v_fifoinfo->fi_readsock;
@@ -277,15 +275,9 @@ fifo_read(void *v)
#endif
if (uio->uio_resid == 0)
return (0);
- startresid = uio->uio_resid;
VOP_UNLOCK(ap->a_vp);
sflags = (ap->a_ioflag & IO_NDELAY) ? MSG_NBIO : 0;
error = (*rso->so_receive)(rso, NULL, uio, NULL, NULL, &sflags);
- /*
- * Clear EOF indication after first such return.
- */
- if (error == 0 && uio->uio_resid == startresid)
- rso->so_state &= ~SS_CANTRCVMORE;
if (ap->a_ioflag & IO_NDELAY) {
if (error == EWOULDBLOCK &&
ap->a_vp->v_fifoinfo->fi_writers == 0)
diff -r 9443bf71b263 -r b83e1108a0db tests/lib/libc/sys/t_mkfifo.c
--- a/tests/lib/libc/sys/t_mkfifo.c Fri Sep 25 15:15:25 2026 +0000
+++ b/tests/lib/libc/sys/t_mkfifo.c Fri Sep 25 15:47:29 2026 +0000
@@ -604,9 +604,6 @@ ATF_TC_BODY(mkfifo_readeof_block, tc)
RL(mkfifo(path, 0600));
- atf_tc_expect_fail("PR kern/60789:"
- " read on fifo without writer may block");
-
/*
* Open nonblocking so we open immediately, but then switch to
* blocking to test the blocking path.
Home |
Main Index |
Thread Index |
Old Index