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