* [PATCH v2] sctp: fix race between addr_wq timer handler and backlog draining
@ 2026-11-09 2:42 Binbin Deng
2026-11-11 8:44 ` netdev-bot+sashiko
0 siblings, 1 reply; 2+ messages in thread
From: Binbin Deng @ 2026-11-09 2:42 UTC (permalink / raw)
To: marcelo.leitner, lucien.xin, davem, edumazet, kuba, pabeni, horms
Cc: linux-sctp, netdev, linux-kernel, Binbin Deng
sctp_addr_wq_timeout_handler() walks net->sctp.auto_asconf_splist and,
holding only bh_lock_sock(), calls sctp_asconf_mgmt() on every
auto-asconf socket. Unlike every other asynchronous SCTP entry point
(sctp_rcv() queuing packets to the backlog, and the timer handlers in
sctp_sm_side_effect()), it does not check sock_owned_by_user() to defer
its work while a user context owns the socket.
While a user thread owns the socket (e.g. recvmsg() blocked in a copy
to user, or release_sock() draining the backlog), sk_lock.owned is set
but the socket spinlock has been dropped, so bh_lock_sock() succeeds
and the timer enters
sctp_asconf_mgmt() -> sctp_send_asconf_add/del_ip() -> sctp_do_sm()
concurrently with the backlog drain path
release_sock() -> __release_sock() -> sk_backlog_rcv()
-> sctp_assoc_bh_rcv() -> sctp_do_sm()
Two state machines then run on the same association with no common
serialization and mutate shared state concurrently: the outqueue
control chunk list, transport->packet.chunk_list and packet->auth, and
the transport/dst lifetime.
The bug was found by code review of this timer entry point against the
sock_owned_by_user() avoidance used by all other SCTP asynchronous
entries, and confirmed with a reproducer that races an RTM_DELADDR
address event against a user-owned auto-asconf socket. It triggers the
following, observed with KASAN and refcount debugging:
- rcuref "imbalanced put()" from dst_release() in
sctp_transport_route() racing the timer's transport routing,
- a GPF in sctp_packet_transmit() dereferencing a chunk pointer
corrupted by concurrent packet/chunk list use, and
- associations dying silently (ETIMEDOUT, GET_PEER_ADDRS failure).
Defer the work like the other entries do: if the socket is owned by
the user, stop processing, leave the addr_waitq entry queued and
re-arm addr_wq_timer to retry after SCTP_ADDRESS_TICK_DELAY. The
retry is merged with the existing IPv6 DAD retry into a single
mod_timer() outside the loops. Sockets processed earlier for the same
address event will see a duplicate ASCONF on retry; the peer reports
that in an ASCONF-ACK error indication, which the association
tolerates.
Fixes: 9f7d653b67ae ("sctp: Add Auto-ASCONF support (core).")
Signed-off-by: Binbin Deng <18983559317@163.com>
---
net/sctp/protocol.c | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
diff --git a/net/sctp/protocol.c b/net/sctp/protocol.c
index 27c26e12f95d..267dc9b7a0e7 100644
--- a/net/sctp/protocol.c
+++ b/net/sctp/protocol.c
@@ -603,6 +603,7 @@ static void sctp_addr_wq_timeout_handler(struct timer_list *t)
struct net *net = timer_container_of(net, t, sctp.addr_wq_timer);
struct sctp_sockaddr_entry *addrw, *temp;
struct sctp_sock *sp;
+ bool retry = false;
spin_lock_bh(&net->sctp.addr_wq_lock);
@@ -624,15 +625,12 @@ static void sctp_addr_wq_timeout_handler(struct timer_list *t)
in6 = (struct in6_addr *)&addrw->a.v6.sin6_addr;
if (ipv6_chk_addr(net, in6, NULL, 0) == 0 &&
addrw->state == SCTP_ADDR_NEW) {
- unsigned long timeo_val;
pr_debug("%s: this is on DAD, trying %d sec "
"later\n", __func__,
SCTP_ADDRESS_TICK_DELAY);
- timeo_val = jiffies;
- timeo_val += msecs_to_jiffies(SCTP_ADDRESS_TICK_DELAY);
- mod_timer(&net->sctp.addr_wq_timer, timeo_val);
+ retry = true;
break;
}
}
@@ -645,16 +643,27 @@ static void sctp_addr_wq_timeout_handler(struct timer_list *t)
if (!sctp_is_ep_boundall(sk))
continue;
bh_lock_sock(sk);
+ if (sock_owned_by_user(sk)) {
+ bh_unlock_sock(sk);
+ retry = true;
+ break;
+ }
if (sctp_asconf_mgmt(sp, addrw) < 0)
pr_debug("%s: sctp_asconf_mgmt failed\n", __func__);
bh_unlock_sock(sk);
}
+ if (retry)
+ break;
#if IS_ENABLED(CONFIG_IPV6)
free_next:
#endif
list_del(&addrw->list);
kfree(addrw);
}
+
+ if (retry)
+ mod_timer(&net->sctp.addr_wq_timer,
+ jiffies + msecs_to_jiffies(SCTP_ADDRESS_TICK_DELAY));
spin_unlock_bh(&net->sctp.addr_wq_lock);
}
--
2.43.0
^ permalink raw reply [flat|nested] 2+ messages in thread* Re: [PATCH v2] sctp: fix race between addr_wq timer handler and backlog draining
2026-11-09 2:42 [PATCH v2] sctp: fix race between addr_wq timer handler and backlog draining Binbin Deng
@ 2026-11-11 8:44 ` netdev-bot+sashiko
0 siblings, 0 replies; 2+ messages in thread
From: netdev-bot+sashiko @ 2026-11-11 8:44 UTC (permalink / raw)
To: 18983559317
Cc: marcelo.leitner, lucien.xin, davem, edumazet, kuba, pabeni,
horms, linux-sctp, netdev, linux-kernel
Thank you for your contribution! Sashiko AI review found 5 potential
issue(s) to consider.
Critical: 0 · High: 1 · Medium: 3 · Low: 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).
- [High] Kernel memory can grow without limit through repeated DEL_IP
ASCONF chunks.
- [Medium] The new early exit breaks an invariant that
sctp_addr_wq_mgmt() depends on.
- [Medium] One socket can now stall address-event processing for the
whole netns.
- [Medium] The commit message says that sockets processed earlier 'will
see a duplicate ASCONF on retry; the peer reports that in an ASCONF-…
- [Low] The new sock_owned_by_user() deferral branch in
sctp_addr_wq_timeout_handler() (net/sctp/protocol.c) sets retry=true…
--
sctp: fix race between addr_wq timer handler and backlog draining
This patch makes sctp_addr_wq_timeout_handler() check sock_owned_by_user()
on each auto-asconf socket before it calls sctp_asconf_mgmt(). If the
socket is owned, processing stops and the addr_waitq entry stays queued.
The addr_wq_timer is then re-armed to retry after SCTP_ADDRESS_TICK_DELAY,
in a single mod_timer() shared with the existing IPv6 DAD retry.
> Sockets processed earlier for the same
> address event will see a duplicate ASCONF on retry; the peer reports
> that in an ASCONF-ACK error indication, which the association
> tolerates.
[Severity: Medium]
Is this description accurate for both event types?
For an SCTP_ADDR_NEW entry, the first pass through
sctp_send_asconf_add_ip() has already added the address with
sctp_add_bind_addr(..., SCTP_ADDR_NEW). On retry the lookup finds it and
skips the association, so no duplicate is sent:
net/sctp/socket.c:sctp_send_asconf_add_ip() {
...
if (sctp_assoc_lookup_laddr(asoc, addr))
break;
...
if (i < addrcnt)
continue;
...
}
For an SCTP_ADDR_DEL entry, the first pass in sctp_send_asconf_del_ip()
only marks the bind address:
if (sctp_cmp_addr_exact(&saddr->a, laddr))
saddr->state = SCTP_ADDR_DEL;
The entry stays valid until the ASCONF-ACK arrives. sctp_bind_addr_match()
only tests ->valid, so the !sctp_assoc_lookup_laddr() check still passes.
Each retry builds another DEL_IP chunk and queues it on addip_chunk_list
behind the outstanding ASCONF. That is one new chunk per retry, not a
single duplicate.
Could the commit message describe what actually happens on retry?
> diff --git a/net/sctp/protocol.c b/net/sctp/protocol.c
> index 27c26e12f95d8..267dc9b7a0e73 100644
> --- a/net/sctp/protocol.c
> +++ b/net/sctp/protocol.c
[ ... ]
> @@ -645,16 +643,27 @@ static void sctp_addr_wq_timeout_handler(struct timer_list *t)
> if (!sctp_is_ep_boundall(sk))
> continue;
> bh_lock_sock(sk);
> + if (sock_owned_by_user(sk)) {
> + bh_unlock_sock(sk);
> + retry = true;
> + break;
> + }
[Severity: Medium]
Can a single owned socket stall address event processing for the whole
netns here?
The first user-owned boundall socket in net->sctp.auto_asconf_splist
breaks out of both loops. No later socket gets processed, and neither does
any later addr_waitq entry, including events for unrelated addresses.
Each retry starts again at the head of both lists. Every socket ahead of
the owned one therefore gets sctp_asconf_mgmt() again on every tick.
User context can hold the socket lock for an unbounded time. For example,
sctp_recvmsg() holds lock_sock() across skb_copy_datagram_msg(). A copy
that faults on a FUSE or userfaultfd backed page keeps sk_lock.owned set
for as long as the user wants.
The other async entry points defer per socket or per association: the
sctp_rcv() backlog queueing and the timer handlers in sm_sideeffect.c. One
busy socket does not block the others there.
Would it make sense to skip only this socket and track which sockets
still need the event, instead of halting all processing?
[Severity: Low]
This isn't a bug, but the sibling deferral sites in
net/sctp/sm_sideeffect.c log before they reschedule. One example is
sctp_generate_heartbeat_event():
if (sock_owned_by_user(sk)) {
pr_debug("%s: sock is busy\n", __func__);
The DAD retry in this same handler also logs. Should this branch have a
similar pr_debug() so repeated deferrals show up in dynamic debug?
> if (sctp_asconf_mgmt(sp, addrw) < 0)
> pr_debug("%s: sctp_asconf_mgmt failed\n", __func__);
> bh_unlock_sock(sk);
> }
> + if (retry)
> + break;
[Severity: Medium]
Does leaving a partly delivered addrw on addr_waitq break the offset logic
in sctp_addr_wq_mgmt()?
By this point sctp_asconf_mgmt() has run for every boundall socket ahead
of the owned one, but the entry stays queued after addr_wq_lock is
dropped. sctp_addr_wq_mgmt() assumes no socket has seen a queued entry
yet:
net/sctp/protocol.c:sctp_addr_wq_mgmt() {
...
/* Offsets existing events in addr_wq */
addrw = sctp_addr_wq_lookup(net, addr);
if (addrw) {
if (addrw->state != cmd) {
...
list_del(&addrw->list);
kfree(addrw);
}
spin_unlock_bh(&net->sctp.addr_wq_lock);
return;
}
...
}
Take a partly delivered SCTP_ADDR_NEW entry, and suppose RTM_DELADDR or
NETDEV_DOWN for the same address arrives before the retry. Both events are
dropped. The earlier sockets' associations have already sent ADD_IP for
the address. They keep it in asoc->base.bind_addr after it is gone, and no
DEL_IP is ever sent.
The reverse happens with a partly delivered SCTP_ADDR_DEL entry if the
address comes back before the retry. The earlier sockets' associations
remove the address when the ASCONF-ACK arrives and never re-add it.
Before this patch, the only path that left an entry queued was the IPv6
DAD break. That break runs before the socket loop, so no socket had seen
the entry yet.
> #if IS_ENABLED(CONFIG_IPV6)
> free_next:
> #endif
> list_del(&addrw->list);
> kfree(addrw);
> }
> +
> + if (retry)
> + mod_timer(&net->sctp.addr_wq_timer,
> + jiffies + msecs_to_jiffies(SCTP_ADDRESS_TICK_DELAY));
[Severity: High]
Can this retry grow kernel memory without bound for an SCTP_ADDR_DEL
entry?
The retry has no limit and keeps no record of which sockets were already
handled. Every SCTP_ADDRESS_TICK_DELAY (500 ms), the timer replays the
same DEL addrw on every boundall socket ahead of the owned one:
sctp_addr_wq_timeout_handler()
sctp_asconf_mgmt()
sctp_send_asconf_del_ip()
sctp_assoc_lookup_laddr() <- still matches, ->valid is 1
sctp_find_unmatch_addr() <- still returns another address
sctp_make_asconf_update_ip() <- new skb and chunk, GFP_ATOMIC
sctp_send_asconf()
While asoc->addip_last_asconf is set, sctp_send_asconf() appends the new
chunk with no length limit:
if (asoc->addip_last_asconf) {
list_add_tail(&chunk->list, &asoc->addip_chunk_list);
goto out;
}
Each retry also calls sctp_transport_route() on every transport of each
association. These allocations happen in timer softirq context and are
not charged to any memcg.
The growth only stops when the owned socket is released or the ASCONF-ACK
arrives. A peer that drops only ASCONF-ACKs keeps the association alive,
because heartbeat ACKs and data SACKs reset overall_error_count. So
sctp_sf_t4_timer_expire() never reaches max_retrans.
A local unprivileged user with user and net namespaces can set this up:
- enable net.sctp.addip_enable in their own netns
- flap an address on a dummy interface
- filter ASCONF-ACKs
- keep a socket owned through a stalled user copy
Before this patch, one address event produced exactly one DEL ASCONF per
association.
> spin_unlock_bh(&net->sctp.addr_wq_lock);
> }
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261109024219.347985-1-18983559317%40163.com
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-11-11 8:44 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-09 2:42 [PATCH v2] sctp: fix race between addr_wq timer handler and backlog draining Binbin Deng
2026-11-11 8:44 ` netdev-bot+sashiko
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®