NetBSD Problem Report #60782
From www@netbsd.org Thu Sep 24 14:59:47 2026
Return-Path: <www@netbsd.org>
Received: from mail.netbsd.org (mail.netbsd.org [199.233.217.200])
(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)
key-exchange X25519 server-signature RSA-PSS (2048 bits)
client-signature RSA-PSS (2048 bits))
(Client CN "mail.netbsd.org", Issuer "YR1" (not verified))
by mollari.NetBSD.org (Postfix) with ESMTPS id AE9BA1A923B
for <gnats-bugs@gnats.NetBSD.org>; Thu, 24 Sep 2026 14:59:47 +0000 (UTC)
Message-Id: <20260924145946.5A7821A923C@mollari.NetBSD.org>
Date: Thu, 24 Sep 2026 14:59:46 +0000 (UTC)
From: campbell+netbsd@mumble.net
Reply-To: campbell+netbsd@mumble.net
To: gnats-bugs@NetBSD.org
Subject: SIGIO says socket ready for send before send does
X-Send-Pr-Version: www-1.0
X-From4GNATS: "campbell+netbsd@mumble.net via gnats" <gnats-admin@NetBSD.org>
>Number: 60782
>Category: kern
>Synopsis: SIGIO says socket ready for send before send does
>Confidential: no
>Severity: serious
>Priority: medium
>Responsible: kern-bug-people
>State: open
>Class: sw-bug
>Submitter-Id: net
>Arrival-Date: Thu Sep 24 15:00:00 +0000 2026
>Last-Modified: Thu Sep 24 22:35:01 +0000 2026
>Originator: Taylor R Campbell
>Release: current, 11, 10, ...
>Organization:
The NetBSD Foun*** I/O available
>Environment:
>Description:
Consider a local socket pair,connecting processes A and B,
whose buffer is currently full in the direction from A (sender)
to B (receiver).
If process A has subscribed to SIGIO on one socket, and process
B receives a single byte on the peer socket, the system will
send SIGIO with si_code=POLL_OUT si_band=POLLOUT to process A.
But if process A then tries to send anything, even a single
byte, it will block, because the default low water mark of the
socket is >>1, and send will block until the space available in
the buffer is at least the low water mark.
Perhaps we should avoid sending SIGIO notifications claiming
that the socket is writable when it is not, in fact, writable.
>How-To-Repeat:
#ifdef __linux__
#define _GNU_SOURCE /* F_SETSIG */
#endif
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
struct context {
sigset_t mask;
pthread_barrier_t bar;
int s[2];
};
static void
on_sigio(int signo)
{
fprintf(stderr, "[signal] %d (%s)\n", signo, strsignal(signo));
}
static void *
start_receiver(void *cookie)
{
struct context *C = cookie;
int lowat, sndbuf, rcvbuf, nreadable;
socklen_t optlen;
char *buf, *p;
size_t resid, n;
ssize_t nrcvd;
/*
* Block SIGIO in this thread -- only the main thread will
* process SIGIO.
*/
if (pthread_sigmask(SIG_BLOCK, &C->mask, NULL) == -1)
err(EXIT_FAILURE, "pthread_sigmask(SIG_BLOCK)");
/*
* Get the sender's low water mark. This is the minimum space
* that must be free in order to unblock write operations by
* the sender.
*/
optlen = sizeof(lowat);
if (getsockopt(C->s[0], SOL_SOCKET, SO_SNDLOWAT, &lowat, &optlen) ==
-1)
err(EXIT_FAILURE, "getsockopt(SOL_SOCKET, SO_SNDLOWAT)");
/*
* Get the sender's send buffer size and the receiver's receive
* buffer size just out of curiosity.
*/
optlen = sizeof(sndbuf);
if (getsockopt(C->s[0], SOL_SOCKET, SO_SNDBUF, &sndbuf, &optlen) ==
-1)
err(EXIT_FAILURE, "getsockopt(SOL_SOCKET, SO_SNDBUF)");
optlen = sizeof(rcvbuf);
if (getsockopt(C->s[1], SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen) ==
-1)
err(EXIT_FAILURE, "getsockopt(SOL_SOCKET, SO_RCVBUF)");
fprintf(stderr, "[thread] lowat=%d sndbuf=%d rcvbuf=%d\n",
lowat, sndbuf, rcvbuf);
/*
* Wait for the main thread to be ready.
*/
(void)pthread_barrier_wait(&C->bar);
/*
* Find exactly how many bytes are readable,
*/
if (ioctl(C->s[1], FIONREAD, &nreadable) == -1)
err(EXIT_FAILURE, "ioctl(FIONREAD)");
fprintf(stderr, "[thread] FIONREAD %d\n", nreadable);
if (nreadable <= 0)
errx(EXIT_FAILURE, "invalid FIONREAD");
resid = nreadable;
/*
* Allocate a buffer to free enough space up to the low water
* mark.
*/
buf = malloc(resid);
if (buf == NULL)
err(EXIT_FAILURE, "malloc");
p = buf;
/*
* Receive a single byte. This is not enough to unblock the
* sender, but it may be enough to deliver SIGIO -- oops.
*/
fprintf(stderr, "[thread] reading 1\n");
nrcvd = recv(C->s[1], p, 1, /*flags*/0);
if (nrcvd == -1)
err(EXIT_FAILURE, "recv 1");
fprintf(stderr, "[thread] read %zd\n", nrcvd);
p += (size_t)nrcvd;
resid -= (size_t)nrcvd;
/*
* After a delay, receive up to the low water mark if it's
* above 1. This is enough to unblock the sender.
*/
if (lowat > 1) {
sleep(1);
fprintf(stderr, "[thread] slept\n");
while ((size_t)nreadable - resid < (size_t)lowat) {
n = lowat - (nreadable - resid);
fprintf(stderr, "[thread] reading %zu\n", n);
nrcvd = recv(C->s[1], p, n, /*flags*/0);
if (nrcvd == -1)
err(EXIT_FAILURE, "recv %zu",
lowat - (nreadable - resid));
if (nrcvd == 0)
errx(EXIT_FAILURE, "eof");
fprintf(stderr, "[thread] read %zd\n", nrcvd);
resid -= (size_t)nrcvd;
}
}
fprintf(stderr, "[thread] read to low water mark\n");
/*
* After a delay, receive the rest.
*/
sleep(1);
fprintf(stderr, "[thread] slept again\n");
while (resid) {
n = resid;
if (n > 32768)
n = 32768;
fprintf(stderr, "[thread] reading %zu\n", n);
nrcvd = recv(C->s[1], p, n, /*flags*/0);
if (nrcvd == -1)
err(EXIT_FAILURE, "recv %zu", nrcvd);
if (nrcvd == 0)
errx(EXIT_FAILURE, "eof");
fprintf(stderr, "[thread] read %zd\n", nrcvd);
resid -= (size_t)nrcvd;
usleep(100000);
}
return NULL;
}
int
main(void)
{
char buf[PIPE_BUF] = {0};
struct context ctx, *C = &ctx;
int flags;
sigset_t omask;
pthread_t t;
int signo;
siginfo_t si;
ssize_t nsent;
int error, status = 0;
/*
* Create a socket pair to test.
*/
if (socketpair(AF_LOCAL, SOCK_STREAM, 0, C->s) == -1)
err(EXIT_FAILURE, "socketpair");
/*
* Block SIGIO; we will process it with sigtimedwait.
*/
if (sigemptyset(&C->mask) == -1)
err(EXIT_FAILURE, "sigemptyset");
if (sigaddset(&C->mask, SIGIO) == -1)
err(EXIT_FAILURE, "sigaddset");
if (pthread_sigmask(SIG_BLOCK, &C->mask, &omask) == -1)
err(EXIT_FAILURE, "pthread_sigmask(SIG_BLOCK)");
/*
* Subscribe to SIGIO from the socket and set up nonblocking
* I/O for convenience.
*
* Set up a signal handler that does nothing just so the
* signals aren't discarded before sigtimedwait() can get to
* them, because of SIGIO's default disposition to ignore.
*/
if (signal(SIGIO, &on_sigio) == SIG_ERR)
err(EXIT_FAILURE, "signal(SIGIO)");
if (fcntl(C->s[0], F_SETOWN, getpid()) == -1)
err(EXIT_FAILURE, "fcntl(F_SETOWN)");
#ifdef __linux__
/*
* Request SIGIO with siginfo_t populated (si_code = POLL_*,
* si_fd, si_band).
*/
if (fcntl(C->s[0], F_SETSIG, SIGIO) == -1)
err(EXIT_FAILURE, "fcntl(F_GETSIG, SIGIO)");
#endif
flags = fcntl(C->s[0], F_GETFL);
if (flags == -1)
err(EXIT_FAILURE, "fcntl(F_GETFL)");
if (fcntl(C->s[0], F_SETFL, flags|O_ASYNC|O_NONBLOCK) == -1)
err(EXIT_FAILURE, "fcntl(F_SETFL)");
/*
* Fill the socket's buffer and spawn a thread to consume it
* from the peer socket C->s[1].
*/
while (send(C->s[0], buf, sizeof(buf), 0) != -1)
continue;
error = pthread_barrier_init(&C->bar, NULL, 2);
if (error) {
errno = error;
err(EXIT_FAILURE, "pthread_barrier_init");
}
error = pthread_create(&t, NULL, &start_receiver, C);
if (error) {
errno = error;
err(EXIT_FAILURE, "pthread_create");
}
/*
* Wait for the thread to be ready.
*/
(void)pthread_barrier_wait(&C->bar);
/*
* Wait until we receive SIGIO.
*/
signo = sigtimedwait(&C->mask, &si, &(const struct timespec){3,0});
if (signo == -1)
err(EXIT_FAILURE, "sigtimedwait");
printf("signo=%d (%s) code=%d band=0x%lx\n", signo, strsignal(signo),
si.si_code, si.si_band);
fflush(stdout);
if (si.si_code != POLL_OUT || (si.si_band & (POLLOUT|POLLWRNORM)) == 0)
errx(EXIT_FAILURE, "unexpected SIGIO code and band");
/*
* Now that we received SIGIO, we should be ready to write at
* least a single byte, right?
*/
fprintf(stderr, "trying to send\n");
nsent = send(C->s[0], buf, 1, 0);
if (nsent == -1) {
warn("SIGIO said ready but send didn't");
status = 1;
} else {
fprintf(stderr, "sent %zd\n", nsent);
}
/*
* Wait for the thread to definitely receive up to the low
* water mark, and then try sending a byte again.
*/
sleep(2);
fprintf(stderr, "trying to send again\n");
nsent = send(C->s[0], buf, 1, 0);
if (nsent == -1)
err(EXIT_FAILURE, "SIGIO said ready but send didn't");
else
fprintf(stderr, "sent %zd\n", nsent);
error = pthread_join(t, NULL);
if (error) {
errno = error;
err(EXIT_FAILURE, "pthread_join");
}
error = pthread_barrier_destroy(&C->bar);
if (error) {
errno = error;
err(EXIT_FAILURE, "pthread_barrier_destroy");
}
return status;
}
>Fix:
Yes, please!
>Audit-Trail:
From: mlelstv@serpens.de (Michael van Elst)
To: gnats-bugs@netbsd.org
Cc:
Subject: Re: kern/60782: SIGIO says socket ready for send before send does
Date: Thu, 24 Sep 2026 21:17:18 -0000 (UTC)
gnats-admin@NetBSD.org ("campbell+netbsd@mumble.net via gnats") writes:
>>Number: 60782
>>Category: kern
>>Synopsis: SIGIO says socket ready for send before send does
>>Confidential: no
>>Severity: serious
>>Priority: medium
>>Responsible: kern-bug-people
>>State: open
>>Class: sw-bug
>>Submitter-Id: net
>>Arrival-Date: Thu Sep 24 15:00:00 +0000 2026
>>Originator: Taylor R Campbell
>>Release: current, 11, 10, ...
>>Organization:
>The NetBSD Foun*** I/O available
>>Environment:
>>Description:
> Consider a local socket pair,connecting processes A and B,
> whose buffer is currently full in the direction from A (sender)
> to B (receiver).
> If process A has subscribed to SIGIO on one socket, and process
> B receives a single byte on the peer socket, the system will
> send SIGIO with si_code=POLL_OUT si_band=POLLOUT to process A.
> But if process A then tries to send anything, even a single
> byte, it will block, because the default low water mark of the
> socket is >>1, and send will block until the space available in
> the buffer is at least the low water mark.
> Perhaps we should avoid sending SIGIO notifications claiming
> that the socket is writable when it is not, in fact, writable.
In unp_rcvd the receiver updates two hiwater marks for the sender:
mbmax is increased for the memory used by mbufs consumed by the reader:
snd->sb_mbmax += unp->unp_mbcnt - rcv->sb_mbcnt;
unp->unp_mbcnt = rcv->sb_mbcnt;
hiwat is increased for the number of bytes consumed by the reader:
newhiwat = snd->sb_hiwat + unp->unp_cc - rcv->sb_cc;
(void)chgsbsize(so2->so_uidinfo,
&snd->sb_hiwat, newhiwat, RLIM_INFINITY);
unp->unp_cc = rcv->sb_cc;
The sender blocks when it tries to write more than there is free space
in the buffer. But the free space calculation takes both hiwater marks
into account in sbspace():
if (sb->sb_hiwat <= sb->sb_cc || sb->sb_mbmax <= sb->sb_mbcnt)
return 0;
For the test program, the hiwat value doesn't matter. cc on the sender
side is 0 as previous writes are already in the receive buffer and
hiwat has been bumped by the read to 1. But the mbuf highwater value
stays at 0 until the first mbuf has been consumed.
The first mbuf in the chain contains a header. That's why on amd64 with
MSIZE=512 you need to read at least 400 bytes (MHLEN) to free the first mbuf.
Sending SIGIO and waking up a blocked writer is done at the same time,
but a blocked writer just blocks again without noticable issue.
poll(2) checks sbspace(sb) >= sb->sb_lowat. So it signals a writable
descriptor at a different time. If sbspace(sb) > 0 a send smaller
than sb_lowat may even succeed when, according to poll(2) it's not
yet possible.
Maybe changing the test in sbspace to:
if (sb->sb_hiwat <= sb->sb_cc)
return 0;
if (sb->sb_mbmax <= sb->sb_mbcnt)
return 1;
already helps.
From: Taylor R Campbell <riastradh@NetBSD.org>
To: mlelstv@serpens.de (Michael van Elst)
Cc: gnats-bugs@NetBSD.org, netbsd-bugs@NetBSD.org
Subject: Re: kern/60782: SIGIO says socket ready for send before send does
Date: Thu, 24 Sep 2026 22:31:40 +0000
> Date: Thu, 24 Sep 2026 21:17:18 -0000 (UTC)
> From: mlelstv@serpens.de (Michael van Elst)
>
> Sending SIGIO and waking up a blocked writer is done at the same time,
> but a blocked writer just blocks again without noticable issue.
I think the basic issue is that the wakeup path in recv(2) that leads
to SIGIO is simply not conditional on anything about the buffer state.
I bet nobody noticed this because it doesn't cause an actual return
from poll -- just another pass through polscan or whatever, which
might hurt performance but doesn't hurt correctness.
Perhaps sorwakeup/sowwakeup should just check soreadable/sowritable
before calling sowakeup.
> poll(2) checks sbspace(sb) >= sb->sb_lowat. So it signals a writable
> descriptor at a different time. If sbspace(sb) > 0 a send smaller
> than sb_lowat may even succeed when, according to poll(2) it's not
> yet possible.
>
>
> Maybe changing the test in sbspace to:
>
> if (sb->sb_hiwat <= sb->sb_cc)
> return 0;
> if (sb->sb_mbmax <= sb->sb_mbcnt)
> return 1;
>
> already helps.
Why would changing sbspace affect this? I think it is not used
anywhere in the decision of whether to send SIGIO or not. (But maybe
I missed something -- I only took a cursory skim at this part while
lifting the SIGIO rock and finding a whole colony of bugs under it!)
(Contact us)
$NetBSD: query-full-pr,v 1.51 2026/08/10 02:28:17 riastradh Exp $
$NetBSD: gnats_config.sh,v 1.10 2026/05/13 22:00:09 riastradh Exp $
Copyright © 1994-2026
The NetBSD Foundation, Inc. ALL RIGHTS RESERVED.