* [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock
@ 2026-10-11 19:08 Bryam Vargas via B4 Relay
2026-10-12 19:45 ` netdev-bot+sashiko
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Bryam Vargas via B4 Relay @ 2026-10-11 19:08 UTC (permalink / raw)
To: Paolo Abeni, Jakub Kicinski, Thorsten Winkler, Eric Dumazet,
Alexandra Winter, David S. Miller
Cc: Nagamani PV, Hidayath Khan, netdev, Simon Horman, linux-s390,
linux-kernel
From: Bryam Vargas <hexlabsecurity@proton.me>
The HiperSockets receive callbacks change sk_state under bh_lock_sock()
without checking sock_owned_by_user(), so a FIN or SYN|ACK is applied
while a task owns the socket. recvmsg takes no socket lock and sends a
due window update in any state, even after the FIN; one that fails keeps
its credit, and reading the peer's SHT can then return -EFAULT.
Take bh_lock_sock() once in afiucv_hs_rcv() and, while the socket is
owned, defer frames to the socket backlog, handled by a per-socket
sk_backlog_rcv. Data, SHT and WIN frames leave sk_state alone and skip
the backlog while it is empty. Bound each run of the backlog in frames,
refusing a SYN past the bound. Run recvmsg under lock_sock(), with
__iucv_sock_close() on its msglimit path; send a due window update only
while connected, else drop the credit. As recvmsg now holds the lock,
let sendmsg wait for memory without it, and end both its waits with
EPIPE once sending is shut down. Send the FIN and the first SHT past
sk_sndbuf: qeth keeps sent HiperSockets skbs charged to the socket until
TX completion.
Suggested-by: Alexandra Winter <wintera@linux.ibm.com>
Link: https://lore.kernel.org/all/20260724222917.134769-1-hexlabsecurity@proton.me/
Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me>
---
Alexandra, this is stage 1 from the RFC thread [1]: the HiperSockets rx
path and recvmsg() under the socket lock, plus the sendmsg() and
iucv_send_ctrl() changes a locked recvmsg() needs; some of these also
fix bugs net-next has today. Through recvmsg() and sendmsg(), it changes
the z/VM transport too. It's for net-next because recvmsg()'s close goes
through __iucv_sock_close() from your aa5ec571b93f ("net/iucv: Add lock
nesting annotation in iucv_sock_close()"), which isn't in net; that
commit already covers item (a) of the RFC, the split close.
It differs from the plan you OK'd there in three places:
- afiucv_hs_backlog_rcv() is set per socket in iucv_sock_alloc(), not as
iucv_proto.backlog_rcv: sk_alloc() doesn't copy proto->backlog_rcv
(inet, inet6 and phonet do it themselves), so __release_sock() would
call a NULL sk_backlog_rcv on the first deferred frame. tipc, x25 and
vsock set it per socket too.
- Not every frame is deferred. Data, SHT and WIN frames don't touch
sk_state, so they go straight through while the backlog is empty and
wait behind it otherwise: the first data frame of a connection waits
behind its SYN|ACK. Bypassing whenever the socket is connected would
let a frame overtake those that __release_sock() is still draining.
WIN goes with data because afiucv_hs_send() sets it on data frames.
- No sk_add_backlog(): its limit is in bytes, with sk_rmem_alloc counted
in, and nothing resends a frame it drops. With the sk_rcvbuf limit
that __sk_receive_skb() passes, most of a full window of MTU-sized
frames (128 by default) behind a SYN|ACK is dropped, and so is a FIN
behind them; a full receive queue alone costs the FIN. The backlog is
bounded in frames instead, in two budgets: msglimit + 1 for data, SHT
and WIN frames; sk_max_ack_backlog + 2 for the rest (a SYN|ACK and a
FIN, as sk_max_ack_backlog is 0 unless the socket listens). Both
budgets restart only once __release_sock() has run every frame, so a
peer that ignores the window can't keep the owner draining. A SYN past
its budget is refused with SYN|FIN, as a full accept queue refuses it.
The comments on afiucv_hs_defer() and afiucv_hs_backlog_add() say why
the budgets suffice. One gap remains: lowering SO_MSGLIMIT once
connect() has sent its SYN shrinks the data budget below the window
that SYN gave the peer, and a full window behind the SYN|ACK loses its
tail. That takes a setsockopt() after connect() returns without the
SYN|ACK (non-blocking, timed out or interrupted), or one from another
thread while a blocking connect() waits.
Beyond the plan:
The IUCV_CONNECTED test you suggested before the window update, done
here under the lock, leaves the unsent credit in msg_recv; a SHT's
zero-length skb can then push it past msglimit, and recvmsg() takes the
msglimit path (WARN, close, -EFAULT) instead of returning EOF. So when
an update is due and the socket isn't connected, recvmsg() drops the
credit. That also fixes a bug net has today: once data sent but not yet
completed by qeth fills sk_sndbuf, the update fails and sets
IUCV_DISCONN, and reading the peer's SHT can then hit the WARN and
return -EFAULT. The fix sits in the block this patch rewrites, so it's
here rather than in a separate patch for net.
afiucv_hs_backlog_add() keeps sk_add_backlog()'s pfmemalloc check;
otherwise a frame from the reserves would hit the BUG_ON() in
__sk_backlog_rcv() if sk_memalloc_socks() is on when __release_sock()
runs it. The cost: a SYN, SYN|ACK, SYN|FIN or FIN from the reserves that
arrives while a task owns the socket is dropped (a SYN is refused) where
net-next applies it. A WIN frame from the reserves that has to wait is
dropped as well, credit included: net-next applies the credit of a WIN
on a data frame before sk_filter() drops the data. net-next drops data
and SHT frames from the reserves too.
sk_backlog.len saturates at INT_MAX, which msglimit (up to 65535)
MTU-sized frames can exceed. The data budget is the window the socket
grants its peer, and net-next already queues a window's worth in
backlog_skb_q once the receive queue is full.
afiucv_hs_backlog_rcv() drops frames whose socket was closed while they
waited, since by then nothing holds skb->dev. So a SYN that waited for a
listener being closed is dropped, not refused, and the peer's connect()
times out instead of failing with ECONNREFUSED.
With recvmsg() taking the lock, a writer asleep in
sock_alloc_send_pskb() would keep the reader out, and over z/VM, send
memory comes back only once the peer receives, which may wait for this
reader (a cycle I found with a hand-written model of af_iucv's locking
that enumerates interleavings). So sendmsg() now waits for send memory
with the lock released, as it already does for the window. Both waits
end once sending is shut down: sock_alloc_send_pskb() already does, and
the window wait now tests SEND_SHUTDOWN too. sendmsg() then fails with
EPIPE, checked before the connected state, as at its start. In net-next
a send() that races shutdown(SHUT_WR) can put its data on the wire after
the SHT (at the window wait), or wait out SO_SNDTIMEO for a window and
fail with EAGAIN.
In net-next, iucv_send_ctrl() allocates against sk_sndbuf, and a SHT or
FIN it can't allocate is lost without a trace: shutdown() and
afiucv_netdev_event() ignore the error, and close() only skips its wait
for tx completion. On HiperSockets, sent data stays charged to
sk_wmem_alloc until tx completion (qeth_xmit() orphans only non-IQD
skbs), so a shutdown() or close() right after a large send can find the
send buffer full, and the peer never sees EOF. The FIN and the first SHT
now go out past sk_sndbuf with sock_wmalloc(), as TCP forces its one
FIN; a repeated shutdown() still sends its SHT within sk_sndbuf or not
at all, so send memory stays bounded. The iucv_send_ctrl() change fixes
that single-threaded loss (send(), then shutdown() or close()), so it
could go to net on its own, with Fixes: 3881ac441f64 ("af_iucv: add
HiperSockets transport"), if you'd rather. It's here because a
shutdown() during the unlocked memory wait usually finds the send buffer
full, a second way to lose the SHT.
A close() that now gets its FIN out also waits for tx completion
(IUCV_DISCONN_TIMEOUT or SO_LINGER), as any close() with data in flight
does. SYN and WIN keep the sk_sndbuf test: as in net-next,
iucv_send_ctrl() returns -EAGAIN when the send buffer is full and arms
SIGIO for when there is room, and connect() passes the -EAGAIN up. A
window update that fails that way still sets IUCV_DISCONN, and the
credit drop above keeps it from turning into -EFAULT. sock_wmalloc()
doesn't test SEND_SHUTDOWN, so iucv_send_ctrl() no longer clears and
restores it, which also closes a race: afiucv_netdev_event() calls
iucv_send_ctrl() without the socket lock, so in net-next a concurrent
sendmsg() can find SEND_SHUTDOWN cleared.
Tested on s390x under QEMU TCG (no Z here), net-next 8df0638138d3,
KASAN + PROVE_LOCKING + DEBUG_ATOMIC_SLEEP. First over lo, on one CPU,
with real AF_IUCV sockets on the HiperSockets transport. lo can't return
af_iucv's own SYN|ACK to it, so the SYN|ACK is injected raw, as are
frames af_iucv never sends (data past the window, a SYN|FIN flood) and a
frame that has to arrive while __release_sock() is draining. lo orphans
skbs on xmit, so where a test needs send memory to stay charged, a plug
qdisc on lo holds sent frames (as qeth does until tx completion) or a
blocked writer holds a large skb.
I built 33 kernels that each remove or weaken one piece of this patch
and booted each twice, with the same result both times. Every one fails
a test this patch passes. On receive: a FIN applied while recvmsg() owns
the socket; a reader asleep until SO_RCVTIMEO after the FIN; a window
update after the FIN; -EFAULT instead of EOF. In the backlog: lost data;
a SHT or FIN lost there; a SYN refused with room in the accept queue, or
dropped instead of refused; an unbounded backlog; a NULL sk_backlog_rcv
call; the BUG_ON() in __sk_backlog_rcv() and a wrapped sk_backlog.len
(these two need test-only hooks: a pfmemalloc frame, sk_backlog.len set
to INT_MAX - 1). On send: a reader kept out by a writer waiting for
memory; a SHT or FIN lost to a full send buffer; data sent after the
socket's own SHT, or a writer that waits out SO_SNDTIMEO after it;
ECONNRESET for a stream the socket shut down itself; send memory past
sk_sndbuf on every shutdown(); ENOBUFS instead of EAGAIN from connect();
no SIGIO for an O_ASYNC socket when room comes back after that EAGAIN.
Besides these, a lockdep splat and a hang. This patch passes all of
these tests with no KASAN or lockdep report; the only splat is the
existing WARN_ON(1) on the msglimit path, which one test hits on
purpose. Every added line the compiler attributes code to (87) ran at
least once. MSG_OOB, a signal during recvmsg(), SHUT_RD and frames for a
socket that doesn't exist behave as before.
QEMU has no HiperSockets adapter and no z/VM, so I emulated both: the
adapter under the real qeth driver, tx completion included, and CP's
IUCV interface for the z/VM transport. The emulation does what the qeth,
qdio and iucv drivers expect; where they leave a choice, the choice is
mine. I ran the tests on two vCPUs under TCG with -icount, which runs
one vCPU at a time. The z/VM tests and a msglimit test ran with QEMU's
default slice only, the others also with seeded short ones. net-next
fails 7 HiperSockets tests (each a failure listed above) in each of five
runs, and one z/VM test: a writer waits out SO_SNDTIMEO after the peer
quiesces the path. This patch fails no test in those runs, with no KASAN
or lockdep report, and all 33 kernels are told apart from it: two only
by what a test logs; one only by the msglimit test, which runs alone;
and the rest by failing all five runs. Two extra kernels cover the two
changes that had no kernel on lo, each with a test of its own. One keeps
a closed socket's frames; with it, a SYN that waited for a listener
being closed is refused. The other clears and restores SEND_SHUTDOWN
again; with it, a send() after shutdown(SHUT_WR) can succeed while the
device goes down, and put data on the wire after the SHT, as in
net-next.
Two more tests, also under -icount. The freed-device test frees the
device that a closed socket's frames arrived on, while they wait. It
sends 600 SYNs to a listener being closed, and a test-only hook delays
each by 4 ms when __release_sock() runs it, so that unregister_netdev()
and free_netdev() finish while most still wait. This patch drops those
that waited, with no KASAN report; with the same hook and no drop, KASAN
reports a use-after-free in __dev_queue_xmit(). The ingress-device test
covers the check from your 80230a18c164 ("net/iucv: filter frames in
afiucv_hs_rcv() by ingress device"), since this patch changes
afiucv_hs_rcv() around it. On a second emulated LAN, where another of
the guest's adapters has the user ID of a socket connected on the first
LAN, a frame sent to that socket's names reaches no socket in net-next
or with this patch; with the check removed, it reaches that socket.
The same kernels, rebuilt with HZ=100, also ran under MTTCG, with the
two vCPUs at once (at HZ=1000, TCG falls behind the timer tick, and on
an idle host every boot livelocked). Each kernel got three runs of the
default tests (all HiperSockets), three of the z/VM tests and three of
the msglimit test. In each run of the default tests net-next fails the
same 7, and in each z/VM run the same z/VM test; this patch fails none,
with no KASAN or lockdep report. Of the 33 kernels, 30 fail all three
runs of the default tests and one all three runs of the msglimit test.
The other two pass every test but are told apart in each run of the
default tests by what a test logs: frames applied instead of deferred, a
SYN|FIN flood not dropped.
The two extra kernels' tests ran five times each on both of those
kernels and on this patch, and the two kernels are told apart in all
five: the one that clears and restores SEND_SHUTDOWN fails its test, and
the one that keeps a closed socket's frames passes its test but logs the
SYNs that waited as refused instead of dropped. The freed-device test
got five runs too, with the same result in each. With the hook, this
patch drops the frames with no KASAN report, and the kernel that keeps a
closed socket's frames gets the use-after-free. Without the hook, this
patch drops them all before the device is freed, and that kernel still
gets the use-after-free. The ingress-device test also got five runs,
each with the results it gives under -icount.
Not covered: real hardware. Memory ordering was checked only on the
locking model, exhaustively except for three cases that ran clean to
20,000,000 interleavings without finishing. You offered to run each
revision on real hardware; this one needs it for z/VM, HiperSockets and
SMP. Logs on request.
Still open: afiucv_hs_callback_txnotify(), afiucv_netdev_event()
(Nagamani's "net/iucv: fix races in afiucv_netdev_event()") and the z/VM
callbacks change sk_state without checking whether a task owns the
socket.
This patch and [2] ("net/iucv: take a private, writable frame before
rewriting it in place", sent to net) apply on top of each other in
either order, with offsets only.
Hidayath, Nagamani: sorry for the wait. This is the stage 1 your patches
were held for.
[1] https://lore.kernel.org/all/20260724222917.134769-1-hexlabsecurity@proton.me/
[2] https://lore.kernel.org/all/20261008-b4-disp-bf46f1ba-v2-1-46ff1ea9c40b@proton.me/
---
include/net/iucv/af_iucv.h | 2 +
net/iucv/af_iucv.c | 251 ++++++++++++++++++++++++++++++++++++---------
2 files changed, 206 insertions(+), 47 deletions(-)
diff --git a/include/net/iucv/af_iucv.h b/include/net/iucv/af_iucv.h
index df85d19fbf84..b5caf3e14faf 100644
--- a/include/net/iucv/af_iucv.h
+++ b/include/net/iucv/af_iucv.h
@@ -135,6 +135,8 @@ struct iucv_sock {
atomic_t msg_recv;
atomic_t pendings;
int transport;
+ unsigned int sk_backlog_data;
+ unsigned int sk_backlog_ctrl;
void (*sk_txnotify)(struct sock *sk,
enum iucv_tx_notify n);
};
diff --git a/net/iucv/af_iucv.c b/net/iucv/af_iucv.c
index 5ba875798aa4..1fcb47a08755 100644
--- a/net/iucv/af_iucv.c
+++ b/net/iucv/af_iucv.c
@@ -39,6 +39,9 @@
static char iucv_userid[80];
+static int afiucv_hs_backlog_rcv(struct sock *sk, struct sk_buff *skb);
+static int afiucv_hs_dispatch(struct sock *sk, struct sk_buff *skb);
+
static struct proto iucv_proto = {
.name = "AF_IUCV",
.owner = THIS_MODULE,
@@ -374,26 +377,28 @@ static void iucv_sever_path(struct sock *sk, int with_user_data)
static int iucv_send_ctrl(struct sock *sk, u8 flags)
{
struct iucv_sock *iucv = iucv_sk(sk);
- int err = 0;
int blen;
struct sk_buff *skb;
- u8 shutdown = 0;
+ bool force;
blen = sizeof(struct af_iucv_trans_hdr) +
LL_RESERVED_SPACE(iucv->hs_dev);
- if (sk->sk_shutdown & SEND_SHUTDOWN) {
- /* controlling flags should be sent anyway */
- shutdown = sk->sk_shutdown;
- sk->sk_shutdown &= RCV_SHUTDOWN;
- }
- skb = sock_alloc_send_skb(sk, blen, 1, &err);
- if (skb) {
- skb_reserve(skb, blen);
- err = afiucv_hs_send(NULL, sk, skb, flags);
+ /* Control frames go out after a shutdown too. The FIN and the first SHT
+ * end the stream and may exceed sk_sndbuf, like TCP's FIN; at sk_sndbuf
+ * the others get -EAGAIN and arm SIGIO, as sock_alloc_send_skb() does.
+ */
+ force = flags == AF_IUCV_FLAG_FIN ||
+ (flags == AF_IUCV_FLAG_SHT &&
+ !(sk->sk_shutdown & SEND_SHUTDOWN));
+ if (!force && sk_wmem_alloc_get(sk) >= sk->sk_sndbuf) {
+ sk_set_bit(SOCKWQ_ASYNC_NOSPACE, sk);
+ return -EAGAIN;
}
- if (shutdown)
- sk->sk_shutdown = shutdown;
- return err;
+ skb = sock_wmalloc(sk, blen, 1, sk->sk_allocation);
+ if (!skb)
+ return -ENOBUFS;
+ skb_reserve(skb, blen);
+ return afiucv_hs_send(NULL, sk, skb, flags);
}
/**
@@ -507,6 +512,8 @@ static struct sock *iucv_sock_alloc(struct socket *sock, int proto, gfp_t prio,
iucv->transport = AF_IUCV_TRANS_HIPER;
sk->sk_destruct = iucv_sock_destruct;
+ /* not from iucv_proto: sk_alloc() does not copy proto->backlog_rcv */
+ sk->sk_backlog_rcv = afiucv_hs_backlog_rcv;
sk->sk_sndtimeo = IUCV_CONN_TIMEOUT;
sock_reset_flag(sk, SOCK_ZAPPED);
@@ -812,7 +819,7 @@ static int iucv_sock_listen(struct socket *sock, int backlog)
if (sock->type != SOCK_STREAM && sock->type != SOCK_SEQPACKET)
goto done;
- sk->sk_max_ack_backlog = backlog;
+ WRITE_ONCE(sk->sk_max_ack_backlog, backlog);
sk->sk_ack_backlog = 0;
sk->sk_state = IUCV_LISTEN;
err = 0;
@@ -1024,7 +1031,16 @@ static int iucv_sock_sendmsg(struct socket *sock, struct msghdr *msg,
}
}
skb = sock_alloc_send_pskb(sk, headroom + linear, len - linear,
- noblock, &err, 0);
+ 1, &err, 0);
+ if (!skb && err == -EAGAIN && !noblock) {
+ /* unlocked: over z/VM the room comes back only when the peer
+ * receives, and that may wait for our own recvmsg()
+ */
+ release_sock(sk);
+ skb = sock_alloc_send_pskb(sk, headroom + linear,
+ len - linear, 0, &err, 0);
+ lock_sock(sk);
+ }
if (!skb)
goto out;
if (headroom)
@@ -1038,10 +1054,16 @@ static int iucv_sock_sendmsg(struct socket *sock, struct msghdr *msg,
/* wait if outstanding messages for iucv path has reached */
timeo = sock_sndtimeo(sk, noblock);
- err = iucv_sock_wait(sk, iucv_below_msglim(sk), timeo);
+ err = iucv_sock_wait(sk, iucv_below_msglim(sk) ||
+ (sk->sk_shutdown & SEND_SHUTDOWN), timeo);
if (err)
goto fail;
+ /* return -EPIPE if sending has been shut down meanwhile */
+ if (sk->sk_shutdown & SEND_SHUTDOWN) {
+ err = -EPIPE;
+ goto fail;
+ }
/* return -ECONNRESET if the socket is no longer connected */
if (sk->sk_state != IUCV_CONNECTED) {
err = -ECONNRESET;
@@ -1245,6 +1267,47 @@ static void iucv_process_message_q(struct sock *sk)
}
}
+/*
+ * Like skb_recv_datagram(), but sleep with the socket lock released, so that
+ * release_sock() runs the backlog and a deferred FIN reaches the reader.
+ */
+static struct sk_buff *iucv_sock_recv_skb(struct sock *sk, int flags, int *err)
+{
+ long timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);
+ DEFINE_WAIT_FUNC(wait, woken_wake_function);
+ struct sk_buff *skb;
+
+ add_wait_queue(sk_sleep(sk), &wait);
+ for (;;) {
+ /* receive/dequeue next skb:
+ * the function understands MSG_PEEK and, thus, does not
+ * dequeue skb, only refcount is increased.
+ */
+ skb = skb_recv_datagram(sk, flags | MSG_DONTWAIT, err);
+ if (skb || *err != -EAGAIN)
+ break;
+ if (sk->sk_shutdown & RCV_SHUTDOWN)
+ break;
+ if (!timeo)
+ break;
+ if (sk->sk_state != IUCV_CONNECTED) {
+ *err = -ENOTCONN;
+ break;
+ }
+ if (signal_pending(current)) {
+ *err = sock_intr_errno(timeo);
+ break;
+ }
+ sk_wait_event(sk, &timeo,
+ !skb_queue_empty_lockless(&sk->sk_receive_queue) ||
+ READ_ONCE(sk->sk_err) ||
+ (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) ||
+ sk->sk_state != IUCV_CONNECTED, &wait);
+ }
+ remove_wait_queue(sk_sleep(sk), &wait);
+ return skb;
+}
+
static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
size_t len, int flags)
{
@@ -1256,24 +1319,25 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
int err = 0;
u32 offset;
+ lock_sock(sk);
if ((sk->sk_state == IUCV_DISCONN) &&
skb_queue_empty(&iucv->backlog_skb_q) &&
skb_queue_empty(&sk->sk_receive_queue) &&
- list_empty(&iucv->message_q.list))
- return 0;
+ list_empty(&iucv->message_q.list)) {
+ copied = 0;
+ goto out;
+ }
- if (flags & (MSG_OOB))
- return -EOPNOTSUPP;
+ if (flags & (MSG_OOB)) {
+ err = -EOPNOTSUPP;
+ goto out_err;
+ }
- /* receive/dequeue next skb:
- * the function understands MSG_PEEK and, thus, does not dequeue skb
- * only refcount is increased.
- */
- skb = skb_recv_datagram(sk, flags, &err);
+ skb = iucv_sock_recv_skb(sk, flags, &err);
if (!skb) {
if (sk->sk_shutdown & RCV_SHUTDOWN)
- return 0;
- return err;
+ err = 0;
+ goto out_err;
}
offset = IUCV_SKB_CB(skb)->offset;
@@ -1322,8 +1386,9 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
atomic_inc(&iucv->msg_recv);
if (atomic_read(&iucv->msg_recv) > iucv->msglimit) {
WARN_ON(1);
- iucv_sock_close(sk);
- return -EFAULT;
+ __iucv_sock_close(sk);
+ err = -EFAULT;
+ goto out_err;
}
}
@@ -1350,13 +1415,18 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
}
spin_unlock_bh(&iucv->message_q.lock);
- if (send_win) {
+ if (send_win && sk->sk_state == IUCV_CONNECTED) {
err = iucv_send_ctrl(sk, AF_IUCV_FLAG_WIN);
if (err) {
sk->sk_state = IUCV_DISCONN;
sk->sk_state_change(sk);
}
}
+ /* the peer won't take this credit: drop it, or a SHT's
+ * zero-length skb counts past msglimit
+ */
+ if (send_win && sk->sk_state != IUCV_CONNECTED)
+ atomic_set(&iucv->msg_recv, 0);
}
done:
@@ -1365,7 +1435,8 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
copied = rlen;
if (flags & MSG_PEEK)
skb_unref(skb);
-
+out:
+ release_sock(sk);
return copied;
err_out:
@@ -1373,7 +1444,8 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg,
skb_queue_head(&sk->sk_receive_queue, skb);
else
skb_unref(skb);
-
+out_err:
+ release_sock(sk);
return err;
}
@@ -1549,7 +1621,7 @@ static int iucv_sock_setsockopt(struct socket *sock, int level, int optname,
if (val < 1 || val > U16_MAX)
rc = -EINVAL;
else
- iucv->msglimit = val;
+ WRITE_ONCE(iucv->msglimit, val);
break;
default:
rc = -EINVAL;
@@ -1894,7 +1966,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb)
}
nsk = iucv_sock_alloc(NULL, sk->sk_protocol, GFP_ATOMIC, 0);
- bh_lock_sock(sk);
if ((sk->sk_state != IUCV_LISTEN) ||
sk_acceptq_is_full(sk) ||
!nsk) {
@@ -1904,7 +1975,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb)
err = dev_queue_xmit(skb);
if (nsk)
iucv_sock_kill(nsk);
- bh_unlock_sock(sk);
goto out;
}
@@ -1934,7 +2004,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb)
sk->sk_data_ready(sk);
} else
iucv_sock_kill(nsk);
- bh_unlock_sock(sk);
out:
return NET_RX_SUCCESS;
@@ -1952,11 +2021,9 @@ static int afiucv_hs_callback_synack(struct sock *sk, struct sk_buff *skb)
return NET_RX_SUCCESS;
}
- bh_lock_sock(sk);
iucv->msglimit_peer = iucv_trans_hdr(skb)->window;
sk->sk_state = IUCV_CONNECTED;
sk->sk_state_change(sk);
- bh_unlock_sock(sk);
consume_skb(skb);
return NET_RX_SUCCESS;
}
@@ -1973,10 +2040,8 @@ static int afiucv_hs_callback_synfin(struct sock *sk, struct sk_buff *skb)
return NET_RX_SUCCESS;
}
- bh_lock_sock(sk);
sk->sk_state = IUCV_DISCONN;
sk->sk_state_change(sk);
- bh_unlock_sock(sk);
consume_skb(skb);
return NET_RX_SUCCESS;
}
@@ -1994,12 +2059,10 @@ static int afiucv_hs_callback_fin(struct sock *sk, struct sk_buff *skb)
return NET_RX_SUCCESS;
}
- bh_lock_sock(sk);
if (sk->sk_state == IUCV_CONNECTED) {
sk->sk_state = IUCV_DISCONN;
sk->sk_state_change(sk);
}
- bh_unlock_sock(sk);
consume_skb(skb);
return NET_RX_SUCCESS;
}
@@ -2014,6 +2077,7 @@ static int afiucv_hs_callback_win(struct sock *sk, struct sk_buff *skb)
if (!iucv)
return NET_RX_SUCCESS;
+ /* also runs while the user owns the socket, see afiucv_hs_rcv() */
if (sk->sk_state != IUCV_CONNECTED)
return NET_RX_SUCCESS;
@@ -2034,12 +2098,13 @@ static int afiucv_hs_callback_rx(struct sock *sk, struct sk_buff *skb)
return NET_RX_SUCCESS;
}
+ /* also runs while the user owns the socket, see afiucv_hs_rcv() */
if (sk->sk_state != IUCV_CONNECTED) {
kfree_skb(skb);
return NET_RX_SUCCESS;
}
- if (sk->sk_shutdown & RCV_SHUTDOWN) {
+ if (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) {
kfree_skb(skb);
return NET_RX_SUCCESS;
}
@@ -2066,6 +2131,64 @@ static int afiucv_hs_callback_rx(struct sock *sk, struct sk_buff *skb)
return NET_RX_SUCCESS;
}
+/* data, SHT and WIN frames: they leave sk_state alone */
+static bool afiucv_hs_is_data(u8 flags)
+{
+ return !flags || flags == AF_IUCV_FLAG_SHT || flags == AF_IUCV_FLAG_WIN;
+}
+
+/*
+ * Frames that change sk_state wait while the user owns the socket. Data, SHT
+ * and WIN frames go through, but never past waiting ones: sk_backlog.len stays
+ * set until __release_sock() has run them all. So they wait only behind a FIN,
+ * which voids them, or a SYN|ACK, when we have sent nothing for a WIN to ack.
+ */
+static bool afiucv_hs_defer(struct sock *sk, u8 flags)
+{
+ return !afiucv_hs_is_data(flags) || sk->sk_backlog.len != 0;
+}
+
+/*
+ * Not sk_add_backlog(): its limit is in bytes, and nothing resends a frame it
+ * drops. Count frames instead. Data, SHT and WIN frames get msglimit + 1: a
+ * peer that keeps to the window has at most msglimit data frames and one SHT
+ * waiting (a repeated one carries nothing); WINs need no room (see above). The
+ * rest get sk_max_ack_backlog + 2: a SYN|ACK and a FIN, or a listener's SYNs up
+ * to its accept queue and one more. Both counts restart only once
+ * __release_sock() has run every frame: a flood cannot keep the owner draining.
+ */
+static void afiucv_hs_backlog_add(struct sock *sk, struct sk_buff *skb,
+ u8 flags)
+{
+ struct iucv_sock *iucv = iucv_sk(sk);
+ unsigned int *count, limit;
+
+ if (!sk->sk_backlog.len) {
+ iucv->sk_backlog_data = 0;
+ iucv->sk_backlog_ctrl = 0;
+ }
+ if (afiucv_hs_is_data(flags)) {
+ count = &iucv->sk_backlog_data;
+ limit = READ_ONCE(iucv->msglimit) + 1;
+ } else {
+ count = &iucv->sk_backlog_ctrl;
+ limit = READ_ONCE(sk->sk_max_ack_backlog) + 2;
+ }
+ if (*count < limit &&
+ !(skb_pfmemalloc(skb) && !sock_flag(sk, SOCK_MEMALLOC))) {
+ __sk_add_backlog(sk, skb);
+ /* msglimit 65535 x 64K frames would wrap the int */
+ sk->sk_backlog.len = min_t(u32, sk->sk_backlog.len +
+ skb->truesize, INT_MAX);
+ (*count)++;
+ } else if (flags == AF_IUCV_FLAG_SYN) {
+ afiucv_hs_callback_syn(NULL, skb);
+ } else {
+ sk_drops_inc(sk);
+ kfree_skb_reason(skb, SKB_DROP_REASON_SOCKET_BACKLOG);
+ }
+}
+
/*
* afiucv_hs_rcv() - base function for arriving data through HiperSockets
* transport
@@ -2128,6 +2251,28 @@ static int afiucv_hs_rcv(struct sk_buff *skb, struct net_device *dev,
if (!iucv)
sk = NULL;
+ if (!sk)
+ return afiucv_hs_dispatch(NULL, skb);
+
+ bh_lock_sock(sk);
+ if (!sock_owned_by_user(sk) || !afiucv_hs_defer(sk, trans_hdr->flags))
+ err = afiucv_hs_dispatch(sk, skb);
+ else
+ afiucv_hs_backlog_add(sk, skb, trans_hdr->flags);
+ bh_unlock_sock(sk);
+ sock_put(sk);
+ return err;
+}
+
+/*
+ * @sk is NULL, or locked by the caller: bh_lock_sock() in afiucv_hs_rcv(),
+ * where the user may own it too, or user-owned in afiucv_hs_backlog_rcv().
+ */
+static int afiucv_hs_dispatch(struct sock *sk, struct sk_buff *skb)
+{
+ struct af_iucv_trans_hdr *trans_hdr = iucv_trans_hdr(skb);
+ int err = NET_RX_SUCCESS;
+
/* no sock
how should we send with no sock
1) send without sock no send rc checking?
@@ -2172,12 +2317,24 @@ static int afiucv_hs_rcv(struct sk_buff *skb, struct net_device *dev,
default:
kfree_skb(skb);
}
-
- if (sk)
- sock_put(sk);
return err;
}
+/* run by __release_sock() for each frame that waited; @sk is still owned */
+static int afiucv_hs_backlog_rcv(struct sock *sk, struct sk_buff *skb)
+{
+ /* closed while the frame waited: nothing holds skb->dev any more */
+ if (!iucv_sk(sk)->hs_dev) {
+ kfree_skb(skb);
+ return 0;
+ }
+ /* BH off: the callbacks take message_q.lock with spin_lock() */
+ local_bh_disable();
+ afiucv_hs_dispatch(sk, skb);
+ local_bh_enable();
+ return 0;
+}
+
/*
* afiucv_hs_callback_txnotify() - handle send notifications from HiperSockets
* transport
---
base-commit: 8df0638138d3e0344fd1fb36cf2d1ca1cf5028f0
change-id: 20261011-b4-disp-410518f4-9ef2cab21c17
Best regards,
--
Bryam Vargas <hexlabsecurity@proton.me>
^ permalink raw reply [flat|nested] 6+ messages in thread* Re: [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock 2026-10-11 19:08 [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock Bryam Vargas via B4 Relay @ 2026-10-12 19:45 ` netdev-bot+sashiko 2026-10-13 23:12 ` Hidayath Khan 2026-10-20 1:32 ` Jakub Kicinski 2 siblings, 0 replies; 6+ messages in thread From: netdev-bot+sashiko @ 2026-10-12 19:45 UTC (permalink / raw) To: hexlabsecurity Cc: pabeni, kuba, twinkler, edumazet, wintera, davem, nagamani, hidayath, netdev, horms, linux-s390, linux-kernel Thank you for your contribution! Sashiko AI review found 4 potential issue(s) to consider. Critical: 0 · High: 0 (1) · Medium: 3 · Low: 1 (1) If you believe none of the issues are legitimate, please reply to each issue of Medium severity or higher and explain why the AI is wrong. If any of them are legitimate and you plan to respin, please let patchwork know by including "pw-bot: cr" as a separate line at the end of your reply (one such reply per series is enough). - [Medium] afiucv_hs_backlog_add() takes the data budget from the live READ_ONCE(iucv->msglimit) + 1, on the assumption that this equals the… - [Medium] The comments on afiucv_hs_defer() and afiucv_hs_backlog_add() size both budgets on one claim: data and WIN frames only ever wait… - [Medium] Repeated SHT frames can still push msg_recv past msglimit, so recvmsg() takes WARN_ON(1), __iucv_sock_close() and -EFAULT instead of… - [Low] The commit message describes user-visible bugs that date back to 3881ac441f64 ("af_iucv: add HiperSockets transport"): a FIN or SYN|ACK… Pre-existing issues: - [High] This problem wasn't introduced by this patch, but it is high severity. - [Low] In iucv_sock_recv_skb(), a reader that is asleep when the peer's FIN arrives gets -ENOTCONN, while a recvmsg() that starts after the… -- net/iucv: serialize HiperSockets receive with the socket lock This patch makes afiucv_hs_rcv() take bh_lock_sock() once. While a task owns the socket, frames are deferred to the socket backlog, which is bounded by frame count and drained by a per-socket sk_backlog_rcv. It also runs recvmsg() under lock_sock(), lets sendmsg() wait for send memory without the lock, and sends the FIN and first SHT past sk_sndbuf. Link: https://lore.kernel.org/all/20260724222917.134769-1-hexlabsecurity@proton.me/ > diff --git a/net/iucv/af_iucv.c b/net/iucv/af_iucv.c > index 5ba875798aa41..1fcb47a087551 100644 > --- a/net/iucv/af_iucv.c > +++ b/net/iucv/af_iucv.c [ ... ] > @@ -374,26 +377,28 @@ static void iucv_sever_path(struct sock *sk, int with_user_data) > static int iucv_send_ctrl(struct sock *sk, u8 flags) > { [ ... ] > - skb = sock_alloc_send_skb(sk, blen, 1, &err); > - if (skb) { > - skb_reserve(skb, blen); > - err = afiucv_hs_send(NULL, sk, skb, flags); > + /* Control frames go out after a shutdown too. The FIN and the first SHT > + * end the stream and may exceed sk_sndbuf, like TCP's FIN; at sk_sndbuf > + * the others get -EAGAIN and arm SIGIO, as sock_alloc_send_skb() does. > + */ > + force = flags == AF_IUCV_FLAG_FIN || > + (flags == AF_IUCV_FLAG_SHT && > + !(sk->sk_shutdown & SEND_SHUTDOWN)); [Severity: Low] The only trailers are Suggested-by, Link and Signed-off-by. There is no Fixes: tag. The changelog describes bugs users can see that go back to 3881ac441f64 ("af_iucv: add HiperSockets transport"): - a SHT or FIN lost to a full send buffer - a window update that fails and keeps its credit, so recvmsg() hits WARN_ON(1) and returns -EFAULT The notes below the --- line say this iucv_send_ctrl() change could go to net on its own with Fixes: 3881ac441f64. They also say the credit drop in recvmsg() fixes a bug in net today. If both stay folded into one net-next patch, will stable pick them up? Could the iucv_send_ctrl() fix, and possibly the credit drop, go to net as separate patches with Fixes: tags? [ ... ] > @@ -1245,6 +1267,47 @@ static void iucv_process_message_q(struct sock *sk) > } > } > > +/* > + * Like skb_recv_datagram(), but sleep with the socket lock released, so that > + * release_sock() runs the backlog and a deferred FIN reaches the reader. > + */ > +static struct sk_buff *iucv_sock_recv_skb(struct sock *sk, int flags, int *err) > +{ [ ... ] > + if (!timeo) > + break; > + if (sk->sk_state != IUCV_CONNECTED) { > + *err = -ENOTCONN; > + break; > + } [Severity: Low] This is a pre-existing issue and was not introduced by this patch. A reader that is asleep here when the peer's FIN arrives gets -ENOTCONN. A recvmsg() that starts after the FIN returns 0 through the IUCV_DISCONN shortcut at the top of iucv_sock_recvmsg(). The old path, skb_recv_datagram()->__skb_wait_for_more_packets(), also returned -ENOTCONN in this case, because IUCV_CONNECTED equals TCP_ESTABLISHED. So this keeps the existing behaviour. Should a blocked reader also see EOF here, like a reader that arrives after the FIN? [ ... ] > @@ -1322,8 +1386,9 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > atomic_inc(&iucv->msg_recv); > if (atomic_read(&iucv->msg_recv) > iucv->msglimit) { > WARN_ON(1); > - iucv_sock_close(sk); > - return -EFAULT; > + __iucv_sock_close(sk); > + err = -EFAULT; > + goto out_err; > } > } > > @@ -1350,13 +1415,18 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > } > spin_unlock_bh(&iucv->message_q.lock); > > - if (send_win) { > + if (send_win && sk->sk_state == IUCV_CONNECTED) { > err = iucv_send_ctrl(sk, AF_IUCV_FLAG_WIN); > if (err) { > sk->sk_state = IUCV_DISCONN; > sk->sk_state_change(sk); > } > } > + /* the peer won't take this credit: drop it, or a SHT's > + * zero-length skb counts past msglimit > + */ > + if (send_win && sk->sk_state != IUCV_CONNECTED) > + atomic_set(&iucv->msg_recv, 0); [Severity: Medium] Can repeated SHT frames still drive recvmsg() into the WARN_ON(1) and -EFAULT above? On the peer, every shutdown(SHUT_WR) while connected sends another SHT: iucv_sock_shutdown() ... } else iucv_send_ctrl(sk, AF_IUCV_FLAG_SHT); These SHTs don't use any window, since only sendmsg() increments msg_sent. Here, each one becomes a zero-length skb, and consuming it does atomic_inc(&iucv->msg_recv). msg_recv is only reset through send_win. send_win is only set when backlog_skb_q is empty after the requeue loop, and the new credit drop depends on send_win too. Suppose the peer sends a full window of data, then enough repeated SHTs that the receive queue spills into backlog_skb_q. backlog_skb_q may then stay non-empty while msg_recv keeps climbing. Would the first SHT read then push msg_recv past iucv->msglimit? That would make recvmsg() call __iucv_sock_close() and return -EFAULT instead of EOF. The comment in afiucv_hs_backlog_add() says "a repeated one carries nothing", but such an SHT still seems to count here. [ ... ] > @@ -1894,7 +1966,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb) > } > > nsk = iucv_sock_alloc(NULL, sk->sk_protocol, GFP_ATOMIC, 0); > - bh_lock_sock(sk); > if ((sk->sk_state != IUCV_LISTEN) || > sk_acceptq_is_full(sk) || > !nsk) { > @@ -1904,7 +1975,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb) > err = dev_queue_xmit(skb); > if (nsk) > iucv_sock_kill(nsk); > - bh_unlock_sock(sk); > goto out; > } [Severity: High] This isn't a bug introduced by this patch, but does this refusal path leak nsk? iucv_sock_kill() does nothing unless SOCK_ZAPPED is set: static void iucv_sock_kill(struct sock *sk) { if (!sock_flag(sk, SOCK_ZAPPED) || sk->sk_socket) return; ... } iucv_sock_alloc() clears SOCK_ZAPPED, and only __iucv_sock_close() sets it. None of these paths call __iucv_sock_close(). So each SYN refused here seems to leave a whole iucv_sock linked on iucv_sk_list, still holding its sk_alloc() reference. That covers a listener not in IUCV_LISTEN and a full accept queue. The same applies to the iucv_sock_kill(nsk) after a failed dev_queue_xmit() of the SYN|ACK further down. There, the dev_hold(niucv->hs_dev) reference would leak too, and unregister_netdev() of that device could wait forever. A peer on the HiperSockets LAN can repeat connect() against a listener with a full accept queue, so this can grow without bound. The leaked sockets also lengthen the iucv_sk_list walk in afiucv_hs_rcv() for every frame. With this patch, deferred SYNs reach the same path through afiucv_hs_backlog_rcv()->afiucv_hs_dispatch()->afiucv_hs_callback_syn(). The changelog also names the full-accept-queue refusal as the model for the new refusal past the budget. [ ... ] > @@ -2066,6 +2131,64 @@ static int afiucv_hs_callback_rx(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > +/* data, SHT and WIN frames: they leave sk_state alone */ > +static bool afiucv_hs_is_data(u8 flags) > +{ > + return !flags || flags == AF_IUCV_FLAG_SHT || flags == AF_IUCV_FLAG_WIN; > +} > + > +/* > + * Frames that change sk_state wait while the user owns the socket. Data, SHT > + * and WIN frames go through, but never past waiting ones: sk_backlog.len stays > + * set until __release_sock() has run them all. So they wait only behind a FIN, > + * which voids them, or a SYN|ACK, when we have sent nothing for a WIN to ack. > + */ > +static bool afiucv_hs_defer(struct sock *sk, u8 flags) > +{ > + return !afiucv_hs_is_data(flags) || sk->sk_backlog.len != 0; > +} [Severity: Medium] Does this classification hold up for frames the dispatcher doesn't act on? While the socket is owned, every flags value outside {0, SHT, WIN} is deferred and charged to sk_backlog_ctrl. That includes values that afiucv_hs_dispatch() only frees in its default case. It also includes a SYN|ACK or SYN|FIN for a socket that isn't IUCV_BOUND, which the callbacks just drop. On a socket that isn't listening, the control limit is sk_max_ack_backlog + 2, which is 2. Two such frames in one ownership period would leave no room for a real FIN. The FIN would then go to kfree_skb_reason(skb, SKB_DROP_REASON_SOCKET_BACKLOG) and never be resent. Each deferred stray frame also makes sk_backlog.len non-zero. Later data and pure WIN frames are then deferred too and charged to the msglimit + 1 data budget. With traffic in both directions, msglimit data frames plus two or more pure WINs, could data frames be dropped as well? Before this patch these stray frames were freed right away. Would it make sense to drop unknown flags in afiucv_hs_rcv() before deciding whether to defer? > + > +/* > + * Not sk_add_backlog(): its limit is in bytes, and nothing resends a frame it > + * drops. Count frames instead. Data, SHT and WIN frames get msglimit + 1: a > + * peer that keeps to the window has at most msglimit data frames and one SHT > + * waiting (a repeated one carries nothing); WINs need no room (see above). The > + * rest get sk_max_ack_backlog + 2: a SYN|ACK and a FIN, or a listener's SYNs up > + * to its accept queue and one more. Both counts restart only once > + * __release_sock() has run every frame: a flood cannot keep the owner draining. > + */ > +static void afiucv_hs_backlog_add(struct sock *sk, struct sk_buff *skb, > + u8 flags) > +{ > + struct iucv_sock *iucv = iucv_sk(sk); > + unsigned int *count, limit; > + > + if (!sk->sk_backlog.len) { > + iucv->sk_backlog_data = 0; > + iucv->sk_backlog_ctrl = 0; > + } > + if (afiucv_hs_is_data(flags)) { > + count = &iucv->sk_backlog_data; > + limit = READ_ONCE(iucv->msglimit) + 1; [Severity: Medium] Can this budget end up smaller than the window the peer was granted? The SYN advertises the msglimit that is current at connect() time: afiucv_hs_send() if (flags == AF_IUCV_FLAG_SYN) phs_hdr->window = iucv->msglimit; However, iucv_sock_setsockopt(SO_MSGLIMIT) still accepts the option in IUCV_BOUND. A HiperSockets socket stays in IUCV_BOUND from the moment iucv_sock_connect() sends its SYN until the SYN|ACK arrives. That window is open after: - a non-blocking, timed-out or interrupted connect() - another thread calls setsockopt() while a blocking connect() sleeps in iucv_sock_wait() with the lock released If msglimit is lowered in that window, the peer can still send a full window of the old size. Those frames wait behind the deferred SYN|ACK. Every frame past the new msglimit + 1 is dropped here with SKB_DROP_REASON_SOCKET_BACKLOG and never resent, so stream data is lost without any error. The msg_recv > iucv->msglimit check in iucv_sock_recvmsg() would also use the lowered value. The notes below the --- line call this the one remaining gap, but the code still allows it. Could SO_MSGLIMIT be rejected once a HiperSockets SYN has gone out? Alternatively, could the advertised window be saved and used for this budget? [ ... ] -- Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261011-b4-disp-410518f4-v1-1-753bf3258292%40proton.me ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock 2026-10-11 19:08 [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock Bryam Vargas via B4 Relay 2026-10-12 19:45 ` netdev-bot+sashiko @ 2026-10-13 23:12 ` Hidayath Khan 2026-10-17 18:38 ` Hidayath Khan 2026-10-20 1:32 ` Jakub Kicinski 2 siblings, 1 reply; 6+ messages in thread From: Hidayath Khan @ 2026-10-13 23:12 UTC (permalink / raw) To: hexlabsecurity, Paolo Abeni, Jakub Kicinski, Thorsten Winkler, Eric Dumazet, Alexandra Winter, David S. Miller Cc: Nagamani PV, netdev, Simon Horman, linux-s390, linux-kernel Hi Bryam, Below are the test results for this stage 1 patch on z/VM. Setup: one z/VM guest, two HiperSockets hsuids, two CPUs, af_iucv as a module, and an AF_IUCV socket-locking reproducer. Both kernels have the same config - CONFIG_KASAN=y, CONFIG_PROVE_LOCKING=y and CONFIG_DEBUG_ATOMIC_SLEEP=y - so the only variable is the patch. scenario net-next, patch sk_shutdown fail, 271-299 misses pass, 0 in 9058 cycles shut_rd fail, fail, pass pass, 0 in 29 cycles recvrecv fail, 3658-4139 back. pass, 0 out-of-order in 4194304 words backlog pass pass rxclose fail, 16-17 of 35-38 fail, 14 of 57 spin fail, 2020-2190 ms CPU fail, 2070 ms CPU No KASAN, lockdep or DEBUG_ATOMIC_SLEEP report on either kernel, in dmesg or the journal - so nothing reports here either way, and the patch introduces nothing. Since this patch adds local_bh_disable() around the backlog replay, moves __iucv_sock_close() into recvmsg() under lock_sock() and holds the socket lock across copy_to_user(), that was the result I was most interested in. backlog passed on both - it is a guard rather than a reproducer, and 0 frames lost over roughly 1.1 million ownership windows on net-next confirms it has no discriminating power here, so it says nothing either way. shut_rd is too flaky to lean on: on net-next it failed with 2 misses in 43 cycles and 3 in 35, then passed with 0 in 45. So its single pass with the patch is not evidence. Its window is now narrow enough that 30 s under KASAN is not enough cycles; I will re-run it much longer. sk_shutdown and recvrecv are the solid ones. recvrecv is what I would point at: two readers interleaving inside an unlocked iucv_sock_recvmsg() went backwards 3658, 3896 and 4139 times across the three net-next runs and not once with your patch. rxclose fails on both, on the -ENOTCONN that Sashiko flags as Low: 17 of 37, 17 of 38 and 16 of 35 on net-next against 14 of 57 with your patch. Lower with the patch, but that is one patched run against three, so I would not read much into the difference. 0 lost tails in every run - though the lost-tail check only runs on cycles that reached EOF, so the failing cycles are not data-checked. The test counts any errno other than EINTR and EAGAIN as a wrong termination, which is why -ENOTCONN shows as a failure. The asymmetry is that recvmsg()'s shortcut gives EOF on IUCV_DISCONN with the queues empty, while iucv_sock_recv_skb() gives -ENOTCONN on sk_state != IUCV_CONNECTED. Either the loop returns EOF for IUCV_DISCONN with nothing queued, or afiucv_hs_callback_fin() sets RCV_SHUTDOWN as well - that one is a single line but also changes what callback_rx() drops and what iucv_sock_poll() reports. I have tried neither. Which behaviour do you want? spin fails on both, and by the same margin: 2020, 2150 and 2190 ms of CPU while blocked about 3000 ms on net-next, 2070 ms with your patch. __iucv_sock_wait() still calls prepare_to_wait() once, outside its loop, so after the first schedule_timeout() the task is TASK_RUNNING. Outside your diff, and a different bug from the missed wakeup you fix at the same call site. One-liner; I will send it separately. On KASAN specifically, so an empty result is not read as a gap: there will be no KASAN report from these scenarios even on a KASAN kernel. The reproducer does scan /dev/kmsg for KASAN and KCSAN reports and fails a scenario on any hit, but stage 1's defects are missing locks - they surface as lost data, a lost sk_shutdown bit, a wrong errno or a leaked socket, none of which touch freed or out-of-bounds memory. KASAN is a clean-run control here. KCSAN is what can witness the accesses still left unlocked; note that it generally does not flag volatile accesses, so it may not show sk_state races either. A KCSAN run is next. Sashiko's three budget findings look to me like instances of one thing, and it says so itself on the SO_MSGLIMIT case: stream data is "lost without any error". A frame that does not fit is dropped with nothing but sk_drops_inc() and kfree_skb_reason(). Resizing a budget per instance leaves the next one open. Sashiko's three budget findings look like one thing: a frame that does not fit is dropped with only sk_drops_inc() and kfree_skb_reason(), so resizing a budget per instance leaves the next one open. Would you take a patch on top that makes the loss visible instead - the softirq sets a bit in iucv_sock, and iucv_sock_release_cb() turns it into sk_err = ECONNRESET plus IUCV_DISCONN once the owner lets go? Separately: would it be simpler not to defer a pure WIN at all? afiucv_hs_callback_win() is only an atomic_sub() and iucv_sock_wake_msglim(). Thanks, Hidayath On 12/10/26 12:38 am, Bryam Vargas via B4 Relay wrote: > From: Bryam Vargas <hexlabsecurity@proton.me> > > The HiperSockets receive callbacks change sk_state under bh_lock_sock() > without checking sock_owned_by_user(), so a FIN or SYN|ACK is applied > while a task owns the socket. recvmsg takes no socket lock and sends a > due window update in any state, even after the FIN; one that fails keeps > its credit, and reading the peer's SHT can then return -EFAULT. > > Take bh_lock_sock() once in afiucv_hs_rcv() and, while the socket is > owned, defer frames to the socket backlog, handled by a per-socket > sk_backlog_rcv. Data, SHT and WIN frames leave sk_state alone and skip > the backlog while it is empty. Bound each run of the backlog in frames, > refusing a SYN past the bound. Run recvmsg under lock_sock(), with > __iucv_sock_close() on its msglimit path; send a due window update only > while connected, else drop the credit. As recvmsg now holds the lock, > let sendmsg wait for memory without it, and end both its waits with > EPIPE once sending is shut down. Send the FIN and the first SHT past > sk_sndbuf: qeth keeps sent HiperSockets skbs charged to the socket until > TX completion. > > Suggested-by: Alexandra Winter <wintera@linux.ibm.com> > Link: https://lore.kernel.org/all/20260724222917.134769-1-hexlabsecurity@proton.me/ > Signed-off-by: Bryam Vargas <hexlabsecurity@proton.me> > --- > Alexandra, this is stage 1 from the RFC thread [1]: the HiperSockets rx > path and recvmsg() under the socket lock, plus the sendmsg() and > iucv_send_ctrl() changes a locked recvmsg() needs; some of these also > fix bugs net-next has today. Through recvmsg() and sendmsg(), it changes > the z/VM transport too. It's for net-next because recvmsg()'s close goes > through __iucv_sock_close() from your aa5ec571b93f ("net/iucv: Add lock > nesting annotation in iucv_sock_close()"), which isn't in net; that > commit already covers item (a) of the RFC, the split close. > > It differs from the plan you OK'd there in three places: > > - afiucv_hs_backlog_rcv() is set per socket in iucv_sock_alloc(), not as > iucv_proto.backlog_rcv: sk_alloc() doesn't copy proto->backlog_rcv > (inet, inet6 and phonet do it themselves), so __release_sock() would > call a NULL sk_backlog_rcv on the first deferred frame. tipc, x25 and > vsock set it per socket too. > > - Not every frame is deferred. Data, SHT and WIN frames don't touch > sk_state, so they go straight through while the backlog is empty and > wait behind it otherwise: the first data frame of a connection waits > behind its SYN|ACK. Bypassing whenever the socket is connected would > let a frame overtake those that __release_sock() is still draining. > WIN goes with data because afiucv_hs_send() sets it on data frames. > > - No sk_add_backlog(): its limit is in bytes, with sk_rmem_alloc counted > in, and nothing resends a frame it drops. With the sk_rcvbuf limit > that __sk_receive_skb() passes, most of a full window of MTU-sized > frames (128 by default) behind a SYN|ACK is dropped, and so is a FIN > behind them; a full receive queue alone costs the FIN. The backlog is > bounded in frames instead, in two budgets: msglimit + 1 for data, SHT > and WIN frames; sk_max_ack_backlog + 2 for the rest (a SYN|ACK and a > FIN, as sk_max_ack_backlog is 0 unless the socket listens). Both > budgets restart only once __release_sock() has run every frame, so a > peer that ignores the window can't keep the owner draining. A SYN past > its budget is refused with SYN|FIN, as a full accept queue refuses it. > The comments on afiucv_hs_defer() and afiucv_hs_backlog_add() say why > the budgets suffice. One gap remains: lowering SO_MSGLIMIT once > connect() has sent its SYN shrinks the data budget below the window > that SYN gave the peer, and a full window behind the SYN|ACK loses its > tail. That takes a setsockopt() after connect() returns without the > SYN|ACK (non-blocking, timed out or interrupted), or one from another > thread while a blocking connect() waits. > > Beyond the plan: > > The IUCV_CONNECTED test you suggested before the window update, done > here under the lock, leaves the unsent credit in msg_recv; a SHT's > zero-length skb can then push it past msglimit, and recvmsg() takes the > msglimit path (WARN, close, -EFAULT) instead of returning EOF. So when > an update is due and the socket isn't connected, recvmsg() drops the > credit. That also fixes a bug net has today: once data sent but not yet > completed by qeth fills sk_sndbuf, the update fails and sets > IUCV_DISCONN, and reading the peer's SHT can then hit the WARN and > return -EFAULT. The fix sits in the block this patch rewrites, so it's > here rather than in a separate patch for net. > > afiucv_hs_backlog_add() keeps sk_add_backlog()'s pfmemalloc check; > otherwise a frame from the reserves would hit the BUG_ON() in > __sk_backlog_rcv() if sk_memalloc_socks() is on when __release_sock() > runs it. The cost: a SYN, SYN|ACK, SYN|FIN or FIN from the reserves that > arrives while a task owns the socket is dropped (a SYN is refused) where > net-next applies it. A WIN frame from the reserves that has to wait is > dropped as well, credit included: net-next applies the credit of a WIN > on a data frame before sk_filter() drops the data. net-next drops data > and SHT frames from the reserves too. > > sk_backlog.len saturates at INT_MAX, which msglimit (up to 65535) > MTU-sized frames can exceed. The data budget is the window the socket > grants its peer, and net-next already queues a window's worth in > backlog_skb_q once the receive queue is full. > > afiucv_hs_backlog_rcv() drops frames whose socket was closed while they > waited, since by then nothing holds skb->dev. So a SYN that waited for a > listener being closed is dropped, not refused, and the peer's connect() > times out instead of failing with ECONNREFUSED. > > With recvmsg() taking the lock, a writer asleep in > sock_alloc_send_pskb() would keep the reader out, and over z/VM, send > memory comes back only once the peer receives, which may wait for this > reader (a cycle I found with a hand-written model of af_iucv's locking > that enumerates interleavings). So sendmsg() now waits for send memory > with the lock released, as it already does for the window. Both waits > end once sending is shut down: sock_alloc_send_pskb() already does, and > the window wait now tests SEND_SHUTDOWN too. sendmsg() then fails with > EPIPE, checked before the connected state, as at its start. In net-next > a send() that races shutdown(SHUT_WR) can put its data on the wire after > the SHT (at the window wait), or wait out SO_SNDTIMEO for a window and > fail with EAGAIN. > > In net-next, iucv_send_ctrl() allocates against sk_sndbuf, and a SHT or > FIN it can't allocate is lost without a trace: shutdown() and > afiucv_netdev_event() ignore the error, and close() only skips its wait > for tx completion. On HiperSockets, sent data stays charged to > sk_wmem_alloc until tx completion (qeth_xmit() orphans only non-IQD > skbs), so a shutdown() or close() right after a large send can find the > send buffer full, and the peer never sees EOF. The FIN and the first SHT > now go out past sk_sndbuf with sock_wmalloc(), as TCP forces its one > FIN; a repeated shutdown() still sends its SHT within sk_sndbuf or not > at all, so send memory stays bounded. The iucv_send_ctrl() change fixes > that single-threaded loss (send(), then shutdown() or close()), so it > could go to net on its own, with Fixes: 3881ac441f64 ("af_iucv: add > HiperSockets transport"), if you'd rather. It's here because a > shutdown() during the unlocked memory wait usually finds the send buffer > full, a second way to lose the SHT. > > A close() that now gets its FIN out also waits for tx completion > (IUCV_DISCONN_TIMEOUT or SO_LINGER), as any close() with data in flight > does. SYN and WIN keep the sk_sndbuf test: as in net-next, > iucv_send_ctrl() returns -EAGAIN when the send buffer is full and arms > SIGIO for when there is room, and connect() passes the -EAGAIN up. A > window update that fails that way still sets IUCV_DISCONN, and the > credit drop above keeps it from turning into -EFAULT. sock_wmalloc() > doesn't test SEND_SHUTDOWN, so iucv_send_ctrl() no longer clears and > restores it, which also closes a race: afiucv_netdev_event() calls > iucv_send_ctrl() without the socket lock, so in net-next a concurrent > sendmsg() can find SEND_SHUTDOWN cleared. > > Tested on s390x under QEMU TCG (no Z here), net-next 8df0638138d3, > KASAN + PROVE_LOCKING + DEBUG_ATOMIC_SLEEP. First over lo, on one CPU, > with real AF_IUCV sockets on the HiperSockets transport. lo can't return > af_iucv's own SYN|ACK to it, so the SYN|ACK is injected raw, as are > frames af_iucv never sends (data past the window, a SYN|FIN flood) and a > frame that has to arrive while __release_sock() is draining. lo orphans > skbs on xmit, so where a test needs send memory to stay charged, a plug > qdisc on lo holds sent frames (as qeth does until tx completion) or a > blocked writer holds a large skb. > > I built 33 kernels that each remove or weaken one piece of this patch > and booted each twice, with the same result both times. Every one fails > a test this patch passes. On receive: a FIN applied while recvmsg() owns > the socket; a reader asleep until SO_RCVTIMEO after the FIN; a window > update after the FIN; -EFAULT instead of EOF. In the backlog: lost data; > a SHT or FIN lost there; a SYN refused with room in the accept queue, or > dropped instead of refused; an unbounded backlog; a NULL sk_backlog_rcv > call; the BUG_ON() in __sk_backlog_rcv() and a wrapped sk_backlog.len > (these two need test-only hooks: a pfmemalloc frame, sk_backlog.len set > to INT_MAX - 1). On send: a reader kept out by a writer waiting for > memory; a SHT or FIN lost to a full send buffer; data sent after the > socket's own SHT, or a writer that waits out SO_SNDTIMEO after it; > ECONNRESET for a stream the socket shut down itself; send memory past > sk_sndbuf on every shutdown(); ENOBUFS instead of EAGAIN from connect(); > no SIGIO for an O_ASYNC socket when room comes back after that EAGAIN. > Besides these, a lockdep splat and a hang. This patch passes all of > these tests with no KASAN or lockdep report; the only splat is the > existing WARN_ON(1) on the msglimit path, which one test hits on > purpose. Every added line the compiler attributes code to (87) ran at > least once. MSG_OOB, a signal during recvmsg(), SHUT_RD and frames for a > socket that doesn't exist behave as before. > > QEMU has no HiperSockets adapter and no z/VM, so I emulated both: the > adapter under the real qeth driver, tx completion included, and CP's > IUCV interface for the z/VM transport. The emulation does what the qeth, > qdio and iucv drivers expect; where they leave a choice, the choice is > mine. I ran the tests on two vCPUs under TCG with -icount, which runs > one vCPU at a time. The z/VM tests and a msglimit test ran with QEMU's > default slice only, the others also with seeded short ones. net-next > fails 7 HiperSockets tests (each a failure listed above) in each of five > runs, and one z/VM test: a writer waits out SO_SNDTIMEO after the peer > quiesces the path. This patch fails no test in those runs, with no KASAN > or lockdep report, and all 33 kernels are told apart from it: two only > by what a test logs; one only by the msglimit test, which runs alone; > and the rest by failing all five runs. Two extra kernels cover the two > changes that had no kernel on lo, each with a test of its own. One keeps > a closed socket's frames; with it, a SYN that waited for a listener > being closed is refused. The other clears and restores SEND_SHUTDOWN > again; with it, a send() after shutdown(SHUT_WR) can succeed while the > device goes down, and put data on the wire after the SHT, as in > net-next. > > Two more tests, also under -icount. The freed-device test frees the > device that a closed socket's frames arrived on, while they wait. It > sends 600 SYNs to a listener being closed, and a test-only hook delays > each by 4 ms when __release_sock() runs it, so that unregister_netdev() > and free_netdev() finish while most still wait. This patch drops those > that waited, with no KASAN report; with the same hook and no drop, KASAN > reports a use-after-free in __dev_queue_xmit(). The ingress-device test > covers the check from your 80230a18c164 ("net/iucv: filter frames in > afiucv_hs_rcv() by ingress device"), since this patch changes > afiucv_hs_rcv() around it. On a second emulated LAN, where another of > the guest's adapters has the user ID of a socket connected on the first > LAN, a frame sent to that socket's names reaches no socket in net-next > or with this patch; with the check removed, it reaches that socket. > > The same kernels, rebuilt with HZ=100, also ran under MTTCG, with the > two vCPUs at once (at HZ=1000, TCG falls behind the timer tick, and on > an idle host every boot livelocked). Each kernel got three runs of the > default tests (all HiperSockets), three of the z/VM tests and three of > the msglimit test. In each run of the default tests net-next fails the > same 7, and in each z/VM run the same z/VM test; this patch fails none, > with no KASAN or lockdep report. Of the 33 kernels, 30 fail all three > runs of the default tests and one all three runs of the msglimit test. > The other two pass every test but are told apart in each run of the > default tests by what a test logs: frames applied instead of deferred, a > SYN|FIN flood not dropped. > > The two extra kernels' tests ran five times each on both of those > kernels and on this patch, and the two kernels are told apart in all > five: the one that clears and restores SEND_SHUTDOWN fails its test, and > the one that keeps a closed socket's frames passes its test but logs the > SYNs that waited as refused instead of dropped. The freed-device test > got five runs too, with the same result in each. With the hook, this > patch drops the frames with no KASAN report, and the kernel that keeps a > closed socket's frames gets the use-after-free. Without the hook, this > patch drops them all before the device is freed, and that kernel still > gets the use-after-free. The ingress-device test also got five runs, > each with the results it gives under -icount. > > Not covered: real hardware. Memory ordering was checked only on the > locking model, exhaustively except for three cases that ran clean to > 20,000,000 interleavings without finishing. You offered to run each > revision on real hardware; this one needs it for z/VM, HiperSockets and > SMP. Logs on request. > > Still open: afiucv_hs_callback_txnotify(), afiucv_netdev_event() > (Nagamani's "net/iucv: fix races in afiucv_netdev_event()") and the z/VM > callbacks change sk_state without checking whether a task owns the > socket. > > This patch and [2] ("net/iucv: take a private, writable frame before > rewriting it in place", sent to net) apply on top of each other in > either order, with offsets only. > > Hidayath, Nagamani: sorry for the wait. This is the stage 1 your patches > were held for. > > [1] https://lore.kernel.org/all/20260724222917.134769-1-hexlabsecurity@proton.me/ > [2] https://lore.kernel.org/all/20261008-b4-disp-bf46f1ba-v2-1-46ff1ea9c40b@proton.me/ > --- > include/net/iucv/af_iucv.h | 2 + > net/iucv/af_iucv.c | 251 ++++++++++++++++++++++++++++++++++++--------- > 2 files changed, 206 insertions(+), 47 deletions(-) > > diff --git a/include/net/iucv/af_iucv.h b/include/net/iucv/af_iucv.h > index df85d19fbf84..b5caf3e14faf 100644 > --- a/include/net/iucv/af_iucv.h > +++ b/include/net/iucv/af_iucv.h > @@ -135,6 +135,8 @@ struct iucv_sock { > atomic_t msg_recv; > atomic_t pendings; > int transport; > + unsigned int sk_backlog_data; > + unsigned int sk_backlog_ctrl; > void (*sk_txnotify)(struct sock *sk, > enum iucv_tx_notify n); > }; > diff --git a/net/iucv/af_iucv.c b/net/iucv/af_iucv.c > index 5ba875798aa4..1fcb47a08755 100644 > --- a/net/iucv/af_iucv.c > +++ b/net/iucv/af_iucv.c > @@ -39,6 +39,9 @@ > > static char iucv_userid[80]; > > +static int afiucv_hs_backlog_rcv(struct sock *sk, struct sk_buff *skb); > +static int afiucv_hs_dispatch(struct sock *sk, struct sk_buff *skb); > + > static struct proto iucv_proto = { > .name = "AF_IUCV", > .owner = THIS_MODULE, > @@ -374,26 +377,28 @@ static void iucv_sever_path(struct sock *sk, int with_user_data) > static int iucv_send_ctrl(struct sock *sk, u8 flags) > { > struct iucv_sock *iucv = iucv_sk(sk); > - int err = 0; > int blen; > struct sk_buff *skb; > - u8 shutdown = 0; > + bool force; > > blen = sizeof(struct af_iucv_trans_hdr) + > LL_RESERVED_SPACE(iucv->hs_dev); > - if (sk->sk_shutdown & SEND_SHUTDOWN) { > - /* controlling flags should be sent anyway */ > - shutdown = sk->sk_shutdown; > - sk->sk_shutdown &= RCV_SHUTDOWN; > - } > - skb = sock_alloc_send_skb(sk, blen, 1, &err); > - if (skb) { > - skb_reserve(skb, blen); > - err = afiucv_hs_send(NULL, sk, skb, flags); > + /* Control frames go out after a shutdown too. The FIN and the first SHT > + * end the stream and may exceed sk_sndbuf, like TCP's FIN; at sk_sndbuf > + * the others get -EAGAIN and arm SIGIO, as sock_alloc_send_skb() does. > + */ > + force = flags == AF_IUCV_FLAG_FIN || > + (flags == AF_IUCV_FLAG_SHT && > + !(sk->sk_shutdown & SEND_SHUTDOWN)); > + if (!force && sk_wmem_alloc_get(sk) >= sk->sk_sndbuf) { > + sk_set_bit(SOCKWQ_ASYNC_NOSPACE, sk); > + return -EAGAIN; > } > - if (shutdown) > - sk->sk_shutdown = shutdown; > - return err; > + skb = sock_wmalloc(sk, blen, 1, sk->sk_allocation); > + if (!skb) > + return -ENOBUFS; > + skb_reserve(skb, blen); > + return afiucv_hs_send(NULL, sk, skb, flags); > } > > /** > @@ -507,6 +512,8 @@ static struct sock *iucv_sock_alloc(struct socket *sock, int proto, gfp_t prio, > iucv->transport = AF_IUCV_TRANS_HIPER; > > sk->sk_destruct = iucv_sock_destruct; > + /* not from iucv_proto: sk_alloc() does not copy proto->backlog_rcv */ > + sk->sk_backlog_rcv = afiucv_hs_backlog_rcv; > sk->sk_sndtimeo = IUCV_CONN_TIMEOUT; > > sock_reset_flag(sk, SOCK_ZAPPED); > @@ -812,7 +819,7 @@ static int iucv_sock_listen(struct socket *sock, int backlog) > if (sock->type != SOCK_STREAM && sock->type != SOCK_SEQPACKET) > goto done; > > - sk->sk_max_ack_backlog = backlog; > + WRITE_ONCE(sk->sk_max_ack_backlog, backlog); > sk->sk_ack_backlog = 0; > sk->sk_state = IUCV_LISTEN; > err = 0; > @@ -1024,7 +1031,16 @@ static int iucv_sock_sendmsg(struct socket *sock, struct msghdr *msg, > } > } > skb = sock_alloc_send_pskb(sk, headroom + linear, len - linear, > - noblock, &err, 0); > + 1, &err, 0); > + if (!skb && err == -EAGAIN && !noblock) { > + /* unlocked: over z/VM the room comes back only when the peer > + * receives, and that may wait for our own recvmsg() > + */ > + release_sock(sk); > + skb = sock_alloc_send_pskb(sk, headroom + linear, > + len - linear, 0, &err, 0); > + lock_sock(sk); > + } > if (!skb) > goto out; > if (headroom) > @@ -1038,10 +1054,16 @@ static int iucv_sock_sendmsg(struct socket *sock, struct msghdr *msg, > > /* wait if outstanding messages for iucv path has reached */ > timeo = sock_sndtimeo(sk, noblock); > - err = iucv_sock_wait(sk, iucv_below_msglim(sk), timeo); > + err = iucv_sock_wait(sk, iucv_below_msglim(sk) || > + (sk->sk_shutdown & SEND_SHUTDOWN), timeo); > if (err) > goto fail; > > + /* return -EPIPE if sending has been shut down meanwhile */ > + if (sk->sk_shutdown & SEND_SHUTDOWN) { > + err = -EPIPE; > + goto fail; > + } > /* return -ECONNRESET if the socket is no longer connected */ > if (sk->sk_state != IUCV_CONNECTED) { > err = -ECONNRESET; > @@ -1245,6 +1267,47 @@ static void iucv_process_message_q(struct sock *sk) > } > } > > +/* > + * Like skb_recv_datagram(), but sleep with the socket lock released, so that > + * release_sock() runs the backlog and a deferred FIN reaches the reader. > + */ > +static struct sk_buff *iucv_sock_recv_skb(struct sock *sk, int flags, int *err) > +{ > + long timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT); > + DEFINE_WAIT_FUNC(wait, woken_wake_function); > + struct sk_buff *skb; > + > + add_wait_queue(sk_sleep(sk), &wait); > + for (;;) { > + /* receive/dequeue next skb: > + * the function understands MSG_PEEK and, thus, does not > + * dequeue skb, only refcount is increased. > + */ > + skb = skb_recv_datagram(sk, flags | MSG_DONTWAIT, err); > + if (skb || *err != -EAGAIN) > + break; > + if (sk->sk_shutdown & RCV_SHUTDOWN) > + break; > + if (!timeo) > + break; > + if (sk->sk_state != IUCV_CONNECTED) { > + *err = -ENOTCONN; > + break; > + } > + if (signal_pending(current)) { > + *err = sock_intr_errno(timeo); > + break; > + } > + sk_wait_event(sk, &timeo, > + !skb_queue_empty_lockless(&sk->sk_receive_queue) || > + READ_ONCE(sk->sk_err) || > + (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) || > + sk->sk_state != IUCV_CONNECTED, &wait); > + } > + remove_wait_queue(sk_sleep(sk), &wait); > + return skb; > +} > + > static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > size_t len, int flags) > { > @@ -1256,24 +1319,25 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > int err = 0; > u32 offset; > > + lock_sock(sk); > if ((sk->sk_state == IUCV_DISCONN) && > skb_queue_empty(&iucv->backlog_skb_q) && > skb_queue_empty(&sk->sk_receive_queue) && > - list_empty(&iucv->message_q.list)) > - return 0; > + list_empty(&iucv->message_q.list)) { > + copied = 0; > + goto out; > + } > > - if (flags & (MSG_OOB)) > - return -EOPNOTSUPP; > + if (flags & (MSG_OOB)) { > + err = -EOPNOTSUPP; > + goto out_err; > + } > > - /* receive/dequeue next skb: > - * the function understands MSG_PEEK and, thus, does not dequeue skb > - * only refcount is increased. > - */ > - skb = skb_recv_datagram(sk, flags, &err); > + skb = iucv_sock_recv_skb(sk, flags, &err); > if (!skb) { > if (sk->sk_shutdown & RCV_SHUTDOWN) > - return 0; > - return err; > + err = 0; > + goto out_err; > } > > offset = IUCV_SKB_CB(skb)->offset; > @@ -1322,8 +1386,9 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > atomic_inc(&iucv->msg_recv); > if (atomic_read(&iucv->msg_recv) > iucv->msglimit) { > WARN_ON(1); > - iucv_sock_close(sk); > - return -EFAULT; > + __iucv_sock_close(sk); > + err = -EFAULT; > + goto out_err; > } > } > > @@ -1350,13 +1415,18 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > } > spin_unlock_bh(&iucv->message_q.lock); > > - if (send_win) { > + if (send_win && sk->sk_state == IUCV_CONNECTED) { > err = iucv_send_ctrl(sk, AF_IUCV_FLAG_WIN); > if (err) { > sk->sk_state = IUCV_DISCONN; > sk->sk_state_change(sk); > } > } > + /* the peer won't take this credit: drop it, or a SHT's > + * zero-length skb counts past msglimit > + */ > + if (send_win && sk->sk_state != IUCV_CONNECTED) > + atomic_set(&iucv->msg_recv, 0); > } > > done: > @@ -1365,7 +1435,8 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > copied = rlen; > if (flags & MSG_PEEK) > skb_unref(skb); > - > +out: > + release_sock(sk); > return copied; > > err_out: > @@ -1373,7 +1444,8 @@ static int iucv_sock_recvmsg(struct socket *sock, struct msghdr *msg, > skb_queue_head(&sk->sk_receive_queue, skb); > else > skb_unref(skb); > - > +out_err: > + release_sock(sk); > return err; > } > > @@ -1549,7 +1621,7 @@ static int iucv_sock_setsockopt(struct socket *sock, int level, int optname, > if (val < 1 || val > U16_MAX) > rc = -EINVAL; > else > - iucv->msglimit = val; > + WRITE_ONCE(iucv->msglimit, val); > break; > default: > rc = -EINVAL; > @@ -1894,7 +1966,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb) > } > > nsk = iucv_sock_alloc(NULL, sk->sk_protocol, GFP_ATOMIC, 0); > - bh_lock_sock(sk); > if ((sk->sk_state != IUCV_LISTEN) || > sk_acceptq_is_full(sk) || > !nsk) { > @@ -1904,7 +1975,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb) > err = dev_queue_xmit(skb); > if (nsk) > iucv_sock_kill(nsk); > - bh_unlock_sock(sk); > goto out; > } > > @@ -1934,7 +2004,6 @@ static int afiucv_hs_callback_syn(struct sock *sk, struct sk_buff *skb) > sk->sk_data_ready(sk); > } else > iucv_sock_kill(nsk); > - bh_unlock_sock(sk); > > out: > return NET_RX_SUCCESS; > @@ -1952,11 +2021,9 @@ static int afiucv_hs_callback_synack(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > - bh_lock_sock(sk); > iucv->msglimit_peer = iucv_trans_hdr(skb)->window; > sk->sk_state = IUCV_CONNECTED; > sk->sk_state_change(sk); > - bh_unlock_sock(sk); > consume_skb(skb); > return NET_RX_SUCCESS; > } > @@ -1973,10 +2040,8 @@ static int afiucv_hs_callback_synfin(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > - bh_lock_sock(sk); > sk->sk_state = IUCV_DISCONN; > sk->sk_state_change(sk); > - bh_unlock_sock(sk); > consume_skb(skb); > return NET_RX_SUCCESS; > } > @@ -1994,12 +2059,10 @@ static int afiucv_hs_callback_fin(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > - bh_lock_sock(sk); > if (sk->sk_state == IUCV_CONNECTED) { > sk->sk_state = IUCV_DISCONN; > sk->sk_state_change(sk); > } > - bh_unlock_sock(sk); > consume_skb(skb); > return NET_RX_SUCCESS; > } > @@ -2014,6 +2077,7 @@ static int afiucv_hs_callback_win(struct sock *sk, struct sk_buff *skb) > if (!iucv) > return NET_RX_SUCCESS; > > + /* also runs while the user owns the socket, see afiucv_hs_rcv() */ > if (sk->sk_state != IUCV_CONNECTED) > return NET_RX_SUCCESS; > > @@ -2034,12 +2098,13 @@ static int afiucv_hs_callback_rx(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > + /* also runs while the user owns the socket, see afiucv_hs_rcv() */ > if (sk->sk_state != IUCV_CONNECTED) { > kfree_skb(skb); > return NET_RX_SUCCESS; > } > > - if (sk->sk_shutdown & RCV_SHUTDOWN) { > + if (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) { > kfree_skb(skb); > return NET_RX_SUCCESS; > } > @@ -2066,6 +2131,64 @@ static int afiucv_hs_callback_rx(struct sock *sk, struct sk_buff *skb) > return NET_RX_SUCCESS; > } > > +/* data, SHT and WIN frames: they leave sk_state alone */ > +static bool afiucv_hs_is_data(u8 flags) > +{ > + return !flags || flags == AF_IUCV_FLAG_SHT || flags == AF_IUCV_FLAG_WIN; > +} > + > +/* > + * Frames that change sk_state wait while the user owns the socket. Data, SHT > + * and WIN frames go through, but never past waiting ones: sk_backlog.len stays > + * set until __release_sock() has run them all. So they wait only behind a FIN, > + * which voids them, or a SYN|ACK, when we have sent nothing for a WIN to ack. > + */ > +static bool afiucv_hs_defer(struct sock *sk, u8 flags) > +{ > + return !afiucv_hs_is_data(flags) || sk->sk_backlog.len != 0; > +} > + > +/* > + * Not sk_add_backlog(): its limit is in bytes, and nothing resends a frame it > + * drops. Count frames instead. Data, SHT and WIN frames get msglimit + 1: a > + * peer that keeps to the window has at most msglimit data frames and one SHT > + * waiting (a repeated one carries nothing); WINs need no room (see above). The > + * rest get sk_max_ack_backlog + 2: a SYN|ACK and a FIN, or a listener's SYNs up > + * to its accept queue and one more. Both counts restart only once > + * __release_sock() has run every frame: a flood cannot keep the owner draining. > + */ > +static void afiucv_hs_backlog_add(struct sock *sk, struct sk_buff *skb, > + u8 flags) > +{ > + struct iucv_sock *iucv = iucv_sk(sk); > + unsigned int *count, limit; > + > + if (!sk->sk_backlog.len) { > + iucv->sk_backlog_data = 0; > + iucv->sk_backlog_ctrl = 0; > + } > + if (afiucv_hs_is_data(flags)) { > + count = &iucv->sk_backlog_data; > + limit = READ_ONCE(iucv->msglimit) + 1; > + } else { > + count = &iucv->sk_backlog_ctrl; > + limit = READ_ONCE(sk->sk_max_ack_backlog) + 2; > + } > + if (*count < limit && > + !(skb_pfmemalloc(skb) && !sock_flag(sk, SOCK_MEMALLOC))) { > + __sk_add_backlog(sk, skb); > + /* msglimit 65535 x 64K frames would wrap the int */ > + sk->sk_backlog.len = min_t(u32, sk->sk_backlog.len + > + skb->truesize, INT_MAX); > + (*count)++; > + } else if (flags == AF_IUCV_FLAG_SYN) { > + afiucv_hs_callback_syn(NULL, skb); > + } else { > + sk_drops_inc(sk); > + kfree_skb_reason(skb, SKB_DROP_REASON_SOCKET_BACKLOG); > + } > +} > + > /* > * afiucv_hs_rcv() - base function for arriving data through HiperSockets > * transport > @@ -2128,6 +2251,28 @@ static int afiucv_hs_rcv(struct sk_buff *skb, struct net_device *dev, > if (!iucv) > sk = NULL; > > + if (!sk) > + return afiucv_hs_dispatch(NULL, skb); > + > + bh_lock_sock(sk); > + if (!sock_owned_by_user(sk) || !afiucv_hs_defer(sk, trans_hdr->flags)) > + err = afiucv_hs_dispatch(sk, skb); > + else > + afiucv_hs_backlog_add(sk, skb, trans_hdr->flags); > + bh_unlock_sock(sk); > + sock_put(sk); > + return err; > +} > + > +/* > + * @sk is NULL, or locked by the caller: bh_lock_sock() in afiucv_hs_rcv(), > + * where the user may own it too, or user-owned in afiucv_hs_backlog_rcv(). > + */ > +static int afiucv_hs_dispatch(struct sock *sk, struct sk_buff *skb) > +{ > + struct af_iucv_trans_hdr *trans_hdr = iucv_trans_hdr(skb); > + int err = NET_RX_SUCCESS; > + > /* no sock > how should we send with no sock > 1) send without sock no send rc checking? > @@ -2172,12 +2317,24 @@ static int afiucv_hs_rcv(struct sk_buff *skb, struct net_device *dev, > default: > kfree_skb(skb); > } > - > - if (sk) > - sock_put(sk); > return err; > } > > +/* run by __release_sock() for each frame that waited; @sk is still owned */ > +static int afiucv_hs_backlog_rcv(struct sock *sk, struct sk_buff *skb) > +{ > + /* closed while the frame waited: nothing holds skb->dev any more */ > + if (!iucv_sk(sk)->hs_dev) { > + kfree_skb(skb); > + return 0; > + } > + /* BH off: the callbacks take message_q.lock with spin_lock() */ > + local_bh_disable(); > + afiucv_hs_dispatch(sk, skb); > + local_bh_enable(); > + return 0; > +} > + > /* > * afiucv_hs_callback_txnotify() - handle send notifications from HiperSockets > * transport > > --- > base-commit: 8df0638138d3e0344fd1fb36cf2d1ca1cf5028f0 > change-id: 20261011-b4-disp-410518f4-9ef2cab21c17 > > Best regards, > -- > Bryam Vargas <hexlabsecurity@proton.me> ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock 2026-10-13 23:12 ` Hidayath Khan @ 2026-10-17 18:38 ` Hidayath Khan 2026-10-19 18:01 ` Hidayath Khan 0 siblings, 1 reply; 6+ messages in thread From: Hidayath Khan @ 2026-10-17 18:38 UTC (permalink / raw) To: hexlabsecurity, Paolo Abeni, Jakub Kicinski, Thorsten Winkler, Eric Dumazet, Alexandra Winter, David S. Miller Cc: Nagamani PV, netdev, Simon Horman, linux-s390, linux-kernel On 14/10/26 4:42 am, Hidayath Khan wrote: > Hi Bryam, > > Below are the test results for this stage 1 patch on z/VM. > > Setup: one z/VM guest, two HiperSockets hsuids, two CPUs, af_iucv as a > module, and an AF_IUCV socket-locking reproducer. Both kernels have > the same > config - CONFIG_KASAN=y, CONFIG_PROVE_LOCKING=y and > CONFIG_DEBUG_ATOMIC_SLEEP=y - so the only variable is the patch. > > scenario net-next, patch > sk_shutdown fail, 271-299 misses pass, 0 in 9058 cycles > shut_rd fail, fail, pass pass, 0 in 29 cycles > recvrecv fail, 3658-4139 back. pass, 0 out-of-order in 4194304 > words > backlog pass pass > rxclose fail, 16-17 of 35-38 fail, 14 of 57 > spin fail, 2020-2190 ms CPU fail, 2070 ms CPU > > No KASAN, lockdep or DEBUG_ATOMIC_SLEEP report on either kernel, in > dmesg or > the journal - so nothing reports here either way, and the patch > introduces > nothing. Since this patch adds local_bh_disable() around the backlog > replay, > moves __iucv_sock_close() into recvmsg() under lock_sock() and holds > the socket > lock across copy_to_user(), that was the result I was most interested in. > > backlog passed on both - it is a guard rather than a reproducer, and 0 > frames lost over roughly 1.1 million ownership windows on net-next > confirms > it has no discriminating power here, so it says nothing either way. > > shut_rd is too flaky to lean on: on net-next it failed with 2 misses > in 43 > cycles and 3 in 35, then passed with 0 in 45. So its single pass with the > patch is not evidence. Its window is now narrow enough that 30 s under > KASAN > is not enough cycles; I will re-run it much longer. > > sk_shutdown and recvrecv are the solid ones. recvrecv is what I would > point > at: two readers interleaving inside an unlocked iucv_sock_recvmsg() went > backwards 3658, 3896 and 4139 times across the three net-next runs and > not > once with your patch. > > rxclose fails on both, on the -ENOTCONN that Sashiko flags as Low: 17 > of 37, > 17 of 38 and 16 of 35 on net-next against 14 of 57 with your patch. Lower > with the patch, but that is one patched run against three, so I would not > read much into the difference. 0 lost tails in every run - though the > lost-tail check only runs on cycles that reached EOF, so the failing > cycles > are not data-checked. The test counts any errno other than EINTR and > EAGAIN > as a wrong termination, which is why -ENOTCONN shows as a failure. > > The asymmetry is that recvmsg()'s shortcut gives EOF on IUCV_DISCONN with > the queues empty, while iucv_sock_recv_skb() gives -ENOTCONN on > sk_state != IUCV_CONNECTED. Either the loop returns EOF for IUCV_DISCONN > with nothing queued, or afiucv_hs_callback_fin() sets RCV_SHUTDOWN as > well - > that one is a single line but also changes what callback_rx() drops > and what > iucv_sock_poll() reports. I have tried neither. Which behaviour do you > want? > > spin fails on both, and by the same margin: 2020, 2150 and 2190 ms of CPU > while blocked about 3000 ms on net-next, 2070 ms with your patch. > __iucv_sock_wait() still calls prepare_to_wait() once, outside its > loop, so > after the first schedule_timeout() the task is TASK_RUNNING. Outside your > diff, and a different bug from the missed wakeup you fix at the same call > site. One-liner; I will send it separately. > > On KASAN specifically, so an empty result is not read as a gap: there > will > be no KASAN report from these scenarios even on a KASAN kernel. The > reproducer does scan /dev/kmsg for KASAN and KCSAN reports and fails a > scenario on any hit, but stage 1's defects are missing locks - they > surface > as lost data, a lost sk_shutdown bit, a wrong errno or a leaked > socket, none > of which touch freed or out-of-bounds memory. KASAN is a clean-run > control > here. KCSAN is what can witness the accesses still left unlocked; note > that > it generally does not flag volatile accesses, so it may not show sk_state > races either. A KCSAN run is next. > > Sashiko's three budget findings look to me like instances of one > thing, and > it says so itself on the SO_MSGLIMIT case: stream data is "lost > without any > error". A frame that does not fit is dropped with nothing but > sk_drops_inc() and kfree_skb_reason(). Resizing a budget per instance > leaves > the next one open. > > Sashiko's three budget findings look like one thing: a frame that does > not fit > is dropped with only sk_drops_inc() and kfree_skb_reason(), so resizing a > budget per instance leaves the next one open. Would you take a patch > on top > that makes the loss visible instead - the softirq sets a bit in > iucv_sock, and > iucv_sock_release_cb() turns it into sk_err = ECONNRESET plus > IUCV_DISCONN once > the owner lets go? Separately: would it be simpler not to defer > a pure WIN at all? afiucv_hs_callback_win() is only an atomic_sub() and > iucv_sock_wake_msglim(). > > Thanks, > Hidayath > Hi Bryam, Follow-up to the earlier KASAN + PROVE_LOCKING + DEBUG_ATOMIC_SLEEP results. I ran the same reproducer on the same z/VM guest with a KCSAN build of your patch, interrupt watcher on. KCSAN found two af_iucv data races. Both appeared during the sk_shutdown scenario. The second also appeared once in rxclose scenario. I have the full KCSAN reports from /dev/kmsg now, so the fields below are from them rather than from reading the code. 1) sk->sk_shutdown, 1 byte, same address on both sides. A marked read in afiucv_hs_callback_rx(), inlined into afiucv_hs_dispatch(): if (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) by interrupt on one CPU, against the plain read-modify-write in iucv_sock_shutdown(): sk->sk_shutdown |= how; by task on the other CPU. The report gives the contexts as interrupt and task; that the writer holds lock_sock() is from the source - iucv_sock_shutdown() takes it on entry and that line is on every path to it. The receive side must have come through the !afiucv_hs_defer() bypass rather than the !sock_owned_by_user() arm. The writer holds lock_sock(), so sk_lock.owned is set and slock is free; the reader runs inside bh_lock_sock(), which is slock. For the two to overlap, the task must have finished lock_sock() before the softirq took slock - so sock_owned_by_user() was true when the softirq checked it. On the other arm the softirq holds slock while nobody owns the socket, and a task entering lock_sock() blocks on slock until the callback returns, so it cannot reach the write. That is the window I asked you about in the previous comment, where a frame can also be queued after shutdown(SHUT_RD)'s skb_queue_purge(), and which I could not get to fire functionally. It has a KCSAN report behind it now. Making the write a WRITE_ONCE() would silence KCSAN and fix nothing. The frame-after-purge behaviour looks like an ordering problem. 2) iucv->hs_dev, 8 bytes, same address on both sides. A plain read in afiucv_hs_rcv()'s socket-lookup loop: if (iucv_sk(sk)->hs_dev != dev) by interrupt, with only read_lock(&iucv_sk_list.lock) held, against the plain write in __iucv_sock_close(): iucv->hs_dev = NULL; by task, from an ordinary close() - the stack is iucv_sock_release -> sock_close -> __fput -> sys_close, so the socket lock is held there. The read is the ingress check added by Alexandra's 80230a18c164 and the write was already there, so neither line is from your patches. I do not think it needs locking: the lookup is inherently racy with close, since the socket can be closed between the match and the dispatch however well hs_dev is synchronised. I think both outcomes are harmless - observing NULL early drops a frame that was about to be dropped anyway, and observing a stale pointer gets the frame as far as the callbacks, which drop it on sk_state. So I think the right treatment is READ_ONCE()/WRITE_ONCE() on the two accesses with a comment saying the lookup read is deliberately unsynchronised, rather than anything structural. Tell me if you see it differently. One more thing in the same function. __iucv_sock_close() does dev_put(iucv->hs_dev); iucv->hs_dev = NULL; so between those two statements hs_dev names a device this socket no longer holds a reference to, and the lookup loop can match on it. Clearing before the put removes that window - via a local, since swapping the lines as they stand would dev_put(NULL) and leak the reference: struct net_device *dev = iucv->hs_dev; if (dev) { iucv->hs_dev = NULL; sk->sk_bound_dev_if = 0; dev_put(dev); } The test run also has 500+ KCSAN reports with skb_copy_bits / __might_fault. All of them have afiucv_hs_rcv() -> pskb_may_pull() -> __pskb_pull_tail() in the stack. The conflict is on current->pagefault_disabled not skb data, so I don't think either of your patches is involved. Functionally the KCSAN kernel showed nothing new - the same rxclose and spin failures as previous comment, everything else is clean. Thanks, Hidayath ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock 2026-10-17 18:38 ` Hidayath Khan @ 2026-10-19 18:01 ` Hidayath Khan 0 siblings, 0 replies; 6+ messages in thread From: Hidayath Khan @ 2026-10-19 18:01 UTC (permalink / raw) To: hexlabsecurity, Paolo Abeni, Jakub Kicinski, Thorsten Winkler, Eric Dumazet, Alexandra Winter, David S. Miller Cc: Nagamani PV, netdev, Simon Horman, linux-s390, linux-kernel On 18/10/26 12:08 am, Hidayath Khan wrote: > > On 14/10/26 4:42 am, Hidayath Khan wrote: >> Hi Bryam, >> >> Below are the test results for this stage 1 patch on z/VM. >> >> Setup: one z/VM guest, two HiperSockets hsuids, two CPUs, af_iucv as a >> module, and an AF_IUCV socket-locking reproducer. Both kernels have >> the same >> config - CONFIG_KASAN=y, CONFIG_PROVE_LOCKING=y and >> CONFIG_DEBUG_ATOMIC_SLEEP=y - so the only variable is the patch. >> >> scenario net-next, patch >> sk_shutdown fail, 271-299 misses pass, 0 in 9058 cycles >> shut_rd fail, fail, pass pass, 0 in 29 cycles >> recvrecv fail, 3658-4139 back. pass, 0 out-of-order in >> 4194304 words >> backlog pass pass >> rxclose fail, 16-17 of 35-38 fail, 14 of 57 >> spin fail, 2020-2190 ms CPU fail, 2070 ms CPU >> >> No KASAN, lockdep or DEBUG_ATOMIC_SLEEP report on either kernel, in >> dmesg or >> the journal - so nothing reports here either way, and the patch >> introduces >> nothing. Since this patch adds local_bh_disable() around the backlog >> replay, >> moves __iucv_sock_close() into recvmsg() under lock_sock() and holds >> the socket >> lock across copy_to_user(), that was the result I was most interested >> in. >> >> backlog passed on both - it is a guard rather than a reproducer, and 0 >> frames lost over roughly 1.1 million ownership windows on net-next >> confirms >> it has no discriminating power here, so it says nothing either way. >> >> shut_rd is too flaky to lean on: on net-next it failed with 2 misses >> in 43 >> cycles and 3 in 35, then passed with 0 in 45. So its single pass with >> the >> patch is not evidence. Its window is now narrow enough that 30 s >> under KASAN >> is not enough cycles; I will re-run it much longer. >> >> sk_shutdown and recvrecv are the solid ones. recvrecv is what I would >> point >> at: two readers interleaving inside an unlocked iucv_sock_recvmsg() went >> backwards 3658, 3896 and 4139 times across the three net-next runs >> and not >> once with your patch. >> >> rxclose fails on both, on the -ENOTCONN that Sashiko flags as Low: 17 >> of 37, >> 17 of 38 and 16 of 35 on net-next against 14 of 57 with your patch. >> Lower >> with the patch, but that is one patched run against three, so I would >> not >> read much into the difference. 0 lost tails in every run - though the >> lost-tail check only runs on cycles that reached EOF, so the failing >> cycles >> are not data-checked. The test counts any errno other than EINTR and >> EAGAIN >> as a wrong termination, which is why -ENOTCONN shows as a failure. >> >> The asymmetry is that recvmsg()'s shortcut gives EOF on IUCV_DISCONN >> with >> the queues empty, while iucv_sock_recv_skb() gives -ENOTCONN on >> sk_state != IUCV_CONNECTED. Either the loop returns EOF for IUCV_DISCONN >> with nothing queued, or afiucv_hs_callback_fin() sets RCV_SHUTDOWN as >> well - >> that one is a single line but also changes what callback_rx() drops >> and what >> iucv_sock_poll() reports. I have tried neither. Which behaviour do >> you want? >> >> spin fails on both, and by the same margin: 2020, 2150 and 2190 ms of >> CPU >> while blocked about 3000 ms on net-next, 2070 ms with your patch. >> __iucv_sock_wait() still calls prepare_to_wait() once, outside its >> loop, so >> after the first schedule_timeout() the task is TASK_RUNNING. Outside >> your >> diff, and a different bug from the missed wakeup you fix at the same >> call >> site. One-liner; I will send it separately. >> >> On KASAN specifically, so an empty result is not read as a gap: there >> will >> be no KASAN report from these scenarios even on a KASAN kernel. The >> reproducer does scan /dev/kmsg for KASAN and KCSAN reports and fails a >> scenario on any hit, but stage 1's defects are missing locks - they >> surface >> as lost data, a lost sk_shutdown bit, a wrong errno or a leaked >> socket, none >> of which touch freed or out-of-bounds memory. KASAN is a clean-run >> control >> here. KCSAN is what can witness the accesses still left unlocked; >> note that >> it generally does not flag volatile accesses, so it may not show >> sk_state >> races either. A KCSAN run is next. >> >> Sashiko's three budget findings look to me like instances of one >> thing, and >> it says so itself on the SO_MSGLIMIT case: stream data is "lost >> without any >> error". A frame that does not fit is dropped with nothing but >> sk_drops_inc() and kfree_skb_reason(). Resizing a budget per instance >> leaves >> the next one open. >> >> Sashiko's three budget findings look like one thing: a frame that >> does not fit >> is dropped with only sk_drops_inc() and kfree_skb_reason(), so >> resizing a >> budget per instance leaves the next one open. Would you take a patch >> on top >> that makes the loss visible instead - the softirq sets a bit in >> iucv_sock, and >> iucv_sock_release_cb() turns it into sk_err = ECONNRESET plus >> IUCV_DISCONN once >> the owner lets go? Separately: would it be simpler not to defer >> a pure WIN at all? afiucv_hs_callback_win() is only an atomic_sub() and >> iucv_sock_wake_msglim(). >> >> Thanks, >> Hidayath >> > > Hi Bryam, > > Follow-up to the earlier KASAN + PROVE_LOCKING + DEBUG_ATOMIC_SLEEP > results. > > I ran the same reproducer on the same z/VM guest with a KCSAN build of > your > patch, interrupt watcher on. KCSAN found two af_iucv data races. Both > appeared during the sk_shutdown scenario. The second also appeared > once in > rxclose scenario. I have the full KCSAN reports from /dev/kmsg now, so > the fields below are from them rather than from reading the code. > > 1) sk->sk_shutdown, 1 byte, same address on both sides. > > A marked read in afiucv_hs_callback_rx(), inlined into > afiucv_hs_dispatch(): > > if (READ_ONCE(sk->sk_shutdown) & RCV_SHUTDOWN) > > by interrupt on one CPU, against the plain read-modify-write in > iucv_sock_shutdown(): > > sk->sk_shutdown |= how; > > by task on the other CPU. The report gives the contexts as interrupt > and task; > that the writer holds lock_sock() is from the source - > iucv_sock_shutdown() > takes it on entry and that line is on every path to it. > > The receive side must have come through the !afiucv_hs_defer() bypass > rather > than the !sock_owned_by_user() arm. The writer holds lock_sock(), so > sk_lock.owned is set and slock is free; the reader runs inside > bh_lock_sock(), > which is slock. For the two to overlap, the task must have finished > lock_sock() before the softirq took slock - so sock_owned_by_user() > was true > when the softirq checked it. On the other arm the softirq holds slock > while > nobody owns the socket, and a task entering lock_sock() blocks on > slock until > the callback returns, so it cannot reach the write. > > That is the window I asked you about in the previous comment, where a > frame > can also be queued after shutdown(SHUT_RD)'s skb_queue_purge(), and > which I > could not get to fire functionally. It has a KCSAN report behind it now. > > Making the write a WRITE_ONCE() would silence KCSAN and fix nothing. The > frame-after-purge behaviour looks like an ordering problem. > > 2) iucv->hs_dev, 8 bytes, same address on both sides. > > A plain read in afiucv_hs_rcv()'s socket-lookup loop: > > if (iucv_sk(sk)->hs_dev != dev) > > by interrupt, with only read_lock(&iucv_sk_list.lock) held, against > the plain > write in __iucv_sock_close(): > > iucv->hs_dev = NULL; > > by task, from an ordinary close() - the stack is iucv_sock_release -> > sock_close -> __fput -> sys_close, so the socket lock is held there. > > The read is the ingress check added by Alexandra's 80230a18c164 and the > write was already there, so neither line is from your patches. > I do not think it needs locking: the lookup is inherently racy with > close, > since the socket can be closed between the match and the dispatch however > well hs_dev is synchronised. I think both outcomes are harmless - > observing > NULL early drops a frame that was about to be dropped anyway, and > observing a > stale pointer gets the frame as far as the callbacks, which drop it on > sk_state. So I think the right treatment is READ_ONCE()/WRITE_ONCE() > on the > two accesses with a comment saying the lookup read is deliberately > unsynchronised, rather than anything structural. Tell me if you see it > differently. > > One more thing in the same function. __iucv_sock_close() does > > dev_put(iucv->hs_dev); > iucv->hs_dev = NULL; > > so between those two statements hs_dev names a device this socket no > longer > holds a reference to, and the lookup loop can match on it. Clearing > before > the put removes that window - via a local, since swapping the lines as > they > stand would dev_put(NULL) and leak the reference: > > struct net_device *dev = iucv->hs_dev; > > if (dev) { > iucv->hs_dev = NULL; > sk->sk_bound_dev_if = 0; > dev_put(dev); > } > > The test run also has 500+ KCSAN reports with skb_copy_bits / > __might_fault. > All of them have > afiucv_hs_rcv() -> pskb_may_pull() -> __pskb_pull_tail() in the stack. > The conflict is on current->pagefault_disabled not skb data, so I > don't think > either of your patches is involved. > > Functionally the KCSAN kernel showed nothing new - the same rxclose > and spin > failures as previous comment, everything else is clean. > > Thanks, > Hidayath Hi Bryam, Two more things since my last note: I ran the KCSAN build on plain net-next as well, so the two kernels can be compared directly, and I gave every scenario 120 s instead of 30 s. Same z/VM guest and same config as before. Only the new results are below, so this is shorter than the last one. Longer runs, same conclusion as before sk_shutdown net-next 565 misses / 7281 cycles patch 0 / 12771 recvrecv net-next 3917 backwards reads patch 0 / 4 Mi words shut_rd net-next 32 misses / 212 cycles patch 0 / 150 rxclose net-next 51 of 906 patch 40 of 1766 shut_rd is the one worth an update, since I said last time its single pass was not evidence. With more cycles net-next now fails it consistently - 32 misses in 212 cycles - and the patched kernel is still at zero. So I am happier leaning on it now. rxclose is still the -ENOTCONN, but with more cycles the two rates separate a little more clearly: 5.6% of cycles on net-next against 2.3% with your patch. The KCSAN picture, now with a baseline Last time I said "both races predate your patch" was read off the source rather than measured. The net-next run settles it, and it also shows the reports changing in kind rather than only in number. On net-next, 29 reports name af_iucv on both sides of the BUG line: 19 x iucv_sock_recvmsg / iucv_sock_shutdown 6 x afiucv_hs_rcv / iucv_send_ctrl 2 x iucv_send_ctrl / iucv_sock_shutdown 2 x afiucv_hs_rcv / iucv_sock_shutdown With your patch applied, none of those four pairs appear at all. What is left is 2 reports, and they are the two I sent you last time - the sk_shutdown bypass and iucv->hs_dev. So the recvmsg-without-a-lock races go away, which is the main thing the patch set out to do. The hs_dev pair did not come up in the net-next run itself. That run was dominated by the recvmsg pair, so I would not read anything into its absence, and the two accesses are the same in both trees. A blocked reader: the -ENOTCONN question, now with a number I added a scenario for this, since nothing was covering it directly. A reader blocks on an empty queue, nothing is ever sent, the peer closes, and the test measures the wakeup and the return value separately. Half the cycles have a task holding the socket lock when the FIN arrives, so the frame goes through the backlog. The wakeup side looks good: 14151 cycles, mean 0 ms, worst 21 ms with the socket owned and 29 ms without, against a 200 ms limit, zero late and zero stuck. So iucv_sock_recv_skb() is doing its job on that count. The return value is -ENOTCONN in 14151 of 14151 cycles. Never 0. On net-next it is 13627 of 13627, also never 0, with the wakeup equally prompt. So this is the same asymmetry I asked you about last time, but it is not an edge case - it is every blocked reader, on both kernels. rxclose only sees it in 2.3% of cycles because its reader goes back into recvmsg and hits the shortcut at the top; a reader that was already parked when the FIN arrived gets -ENOTCONN every time. Nothing new from me on which of the two fixes to take - still your call, and I am happy to test either. I am only sending the number because it moves this from a corner case to the ordinary "block until the peer closes" pattern, so it may deserve a bit more weight than Low. New: SO_MSGLIMIT in IUCV_BOUND can lose stream data This one only applies to your patch, since net-next has no budget - the scenario passes there over 2613 in-window cycles. The comment above afiucv_hs_backlog_add() describes the assumption: a peer that keeps to the window has at most msglimit data frames and one SHT waiting The peer does keep to the window. What changes is the local msglimit, after the window has already been advertised. iucv_sock_setsockopt() accepts SO_MSGLIMIT in IUCV_OPEN and IUCV_BOUND, and a HiperSockets socket stays in IUCV_BOUND from the SYN until the SYN|ACK arrives - iucv_sock_connect() sends the SYN and then waits, and IUCV_CONNECTED is only set in afiucv_hs_callback_synack(). The SYN carries if (flags == AF_IUCV_FLAG_SYN) phs_hdr->window = iucv->msglimit; and the peer keeps that as msglimit_peer. So a setsockopt() inside that window leaves the peer with credit for the old msglimit while the budget is computed as READ_ONCE(iucv->msglimit) + 1 from the new one. What the test does: raise SO_MSGLIMIT to 64, issue a non-blocking connect(), lower it to 2, then have the peer send 62 sequenced messages while a second thread holds the socket lock, so the frames are deferred instead of taking the bypass. Whether a cycle really landed in the window is measured rather than assumed, since setsockopt() returns 0 for SO_MSGLIMIT only in IUCV_OPEN and IUCV_BOUND. 6297 cycles, 5947 landed in IUCV_BOUND 1 cycle lost one 256-byte message, which never arrived even after a 12000 ms grace period 0 cycles were merely slow I also enabled the skb:kfree_skb tracepoint filtered to SKB_DROP_REASON_SOCKET_BACKLOG, and it recorded exactly one event for that run. One drop, one lost message. To be fair about the rate: that is about once in 370000 messages, so the window is narrow. This is the same silent-loss shape as the budget findings I mentioned last time, just with a path into it from userspace rather than from the peer. Two questions, both yours to decide. Is the budget meant to follow the current msglimit, or the window that was actually advertised? msglimit_peer already holds the number the peer was granted. And would it make sense to refuse SO_MSGLIMIT once the SYN has gone out? One unrelated thing, for information only While running the device-teardown scenario, KCSAN also reported afiucv_netdev_event() walking iucv_sk_list without read_lock(&iucv_sk_list.lock): BUG: KCSAN: data-race in afiucv_netdev_event / iucv_sock_alloc write (marked) to 0x...3c8 of 8 bytes by task: iucv_sock_alloc+0x374 -> hlist_add_head, list.h:1093 (inlined by) iucv_sock_link, af_iucv.c:321 read to 0x...3c8 of 8 bytes by task: afiucv_netdev_event+0x5c -> af_iucv.c:2397, sk_for_each() value changed: 0x0000000082781068 -> 0x000000008923a868 The list pointer changed value while the walk was in progress. That function is untouched by your patch and reads the same on net-next, so it is outside this discussion. Mentioning it only so it does not look like something your patch left open. Thanks, Hidayath > ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock 2026-10-11 19:08 [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock Bryam Vargas via B4 Relay 2026-10-12 19:45 ` netdev-bot+sashiko 2026-10-13 23:12 ` Hidayath Khan @ 2026-10-20 1:32 ` Jakub Kicinski 2 siblings, 0 replies; 6+ messages in thread From: Jakub Kicinski @ 2026-10-20 1:32 UTC (permalink / raw) To: Bryam Vargas via B4 Relay Cc: hexlabsecurity, Paolo Abeni, Thorsten Winkler, Eric Dumazet, Alexandra Winter, David S. Miller, Nagamani PV, Hidayath Khan, netdev, Simon Horman, linux-s390, linux-kernel On Sun, 11 Oct 2026 14:08:17 -0500 Bryam Vargas via B4 Relay wrote: > +static int afiucv_hs_dispatch(struct sock *sk, struct sk_buff *skb); Please reorder the code to avoid forward decl, please respond to AI reviewer's feedback. Please trim the slop in the commit msg and notes. -- pw-bot: cr ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-10-20 1:32 UTC | newest] Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed) -- links below jump to the message on this page -- 2026-10-11 19:08 [PATCH net-next] net/iucv: serialize HiperSockets receive with the socket lock Bryam Vargas via B4 Relay 2026-10-12 19:45 ` netdev-bot+sashiko 2026-10-13 23:12 ` Hidayath Khan 2026-10-17 18:38 ` Hidayath Khan 2026-10-19 18:01 ` Hidayath Khan 2026-10-20 1:32 ` Jakub Kicinski
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®