Re: [PATCH net] sctp: hold socket lock when dumping endpoints in sctp_diag
Xin Long <[email protected]> Mon, 15 Jun 2026 14:24:34 -0400
| Newsgroups | org.kernel.vger.linux-sctp,org.kernel.vger.netdev |
|---|---|
| Message-ID | <CADvbK_e062WLNVy+BbuNTNoJGBvQBR7PHp_BmxLwwSGq4O9_dw@mail.gmail.com> |
On Mon, Jun 15, 2026 at 7:04 AM Simon Horman <[email protected]> wrote: > > This is an AI-generated review of your patch. The human sending this > email has considered the AI review valid, or at least plausible. > Full review at: https://netdev-ai.bots.linux.dev/sashiko/ > --- > sctp: hold socket lock when dumping endpoints in sctp_diag > > This change reworks SCTP_DIAG endpoint dumping so the socket lock is held > across inet_sctp_diag_fill() to keep the bind address list stable, and > moves position tracking into sctp_for_each_endpoint() which now drops the > bucket read_lock_bh() before invoking the callback. > > A few questions on the new iterator and dump callback below. > > > diff --git a/net/sctp/diag.c b/net/sctp/diag.c > > index d758f5c3e06e..9108272ca527 100644 > > --- a/net/sctp/diag.c > > +++ b/net/sctp/diag.c > > @@ -373,42 +374,36 @@ static int sctp_ep_dump(struct sctp_endpoint *ep, void *p) > > struct sk_buff *skb = commp->skb; > > struct netlink_callback *cb = commp->cb; > > const struct inet_diag_req_v2 *r = commp->r; > > - struct net *net = sock_net(skb->sk); > > struct inet_sock *inet = inet_sk(sk); > > int err = 0; > > > > - if (!net_eq(sock_net(sk), net)) > > + lock_sock(sk); > > + if (sctp_sstate(sk, CLOSED)) > > goto out; > > [Low] > The new unconditional skip on sctp_sstate(sk, CLOSED) does not appear to > be described in the changelog. > > SCTP_SS_CLOSED maps to TCP_CLOSE, so a userspace caller that asks for > TCPF_CLOSE in idiag_states will silently receive nothing from the > endpoint hash for entries in that state. > > Most hashed endpoints are LISTENING (sctp_hash_endpoint() is called from > sctp_listen_start() right after inet_sk_set_state(sk, SCTP_SS_LISTENING)) > so the practical effect is small, but is the intent to filter teardown > windows (in which case ep->base.dead might be a more direct check) or to > honour the user-requested idiag_states bitmask? Either way, would it be > worth calling this behaviour change out in the commit message? > > > > > - if (cb->args[4] < cb->args[1]) > > - goto next; > > - > > - if (!(r->idiag_states & TCPF_LISTEN) && !list_empty(&ep->asocs)) > > - goto next; > > + if ((r->idiag_states & ~(TCPF_LISTEN | TCPF_CLOSE)) && > > + !list_empty(&ep->asocs)) > > + goto out; > > [ ... ] > > > @@ -479,41 +474,40 @@ static void sctp_diag_dump(struct sk_buff *skb, struct netlink_callback *cb, > > .r = r, > > .net_admin = netlink_net_capable(cb->skb, CAP_NET_ADMIN), > > }; > > - int pos = cb->args[2]; > > + int pos; > > > > /* eps hashtable dumps > > * args: > > * 0 : if it will traversal listen sock > > * 1 : to record the sock pos of this time's traversal > > - * 4 : to work as a temporary variable to traversal list > > */ > > if (cb->args[0] == 0) { > > - if (!(idiag_states & TCPF_LISTEN)) > > - goto skip; > > - if (sctp_for_each_endpoint(sctp_ep_dump, &commp)) > > - goto done; > > -skip: > > + if (idiag_states & TCPF_LISTEN) { > > + pos = cb->args[1]; > > + if (sctp_for_each_endpoint(sctp_ep_dump, net, &pos, > > + &commp)) { > > + cb->args[1] = pos; > > + return; > > + } > > + } > > cb->args[0] = 1; > > cb->args[1] = 0; > > - cb->args[4] = 0; > > } > > > > + if (!(idiag_states & ~(TCPF_LISTEN | TCPF_CLOSE))) > > + return; > > + > > /* asocs by transport hashtable dump > > * args: > > * 1 : to record the assoc pos of this time's traversal > > * 2 : to record the transport pos of this time's traversal > > * 3 : to mark if we have dumped the ep info of the current asoc > > * 4 : to work as a temporary variable to traversal list > > - * 5 : to save the sk we get from travelsing the tsp list. > > */ > > - if (!(idiag_states & ~(TCPF_LISTEN | TCPF_CLOSE))) > > - goto done; > > - > > + pos = cb->args[2]; > > sctp_transport_traverse_process(sctp_sock_filter, sctp_sock_dump, > > net, &pos, &commp); > > cb->args[2] = pos; > > - > > -done: > > cb->args[1] = cb->args[4]; > > cb->args[4] = 0; > > } > > [Low] > The commit message states that args[4] tracking is removed, but the > asocs-phase comment block still lists: > > * 4 : to work as a temporary variable to traversal list > > and the trailing epilogue still does: > > cb->args[1] = cb->args[4]; > cb->args[4] = 0; > > Are those leftovers from the eps-phase rewrite, or are they still needed > by the asocs phase? If args[4] is genuinely gone, should the comment > and the epilogue be cleaned up too? > > The previous comment also listed: > > * 5 : to save the sk we get from travelsing the tsp list. > > which is dropped here without further mention. Is args[5] confirmed > unused on the asocs path? > > > diff --git a/net/sctp/socket.c b/net/sctp/socket.c > > index 66e12fb0c646..1ed405dedc01 100644 > > --- a/net/sctp/socket.c > > +++ b/net/sctp/socket.c > > @@ -5369,24 +5369,44 @@ struct sctp_transport *sctp_transport_get_idx(struct net *net, > > } > > > > int sctp_for_each_endpoint(int (*cb)(struct sctp_endpoint *, void *), > > - void *p) { > > - int err = 0; > > - int hash = 0; > > - struct sctp_endpoint *ep; > > + struct net *net, int *pos, void *p) { > > + int err, hash = 0, idx = 0, start; > > struct sctp_hashbucket *head; > > + struct sctp_endpoint *ep; > > + struct sock *sk; > > > > for (head = sctp_ep_hashtable; hash < sctp_ep_hashsize; > > hash++, head++) { > > + start = idx; > > +again: > > + sk = NULL; > > read_lock_bh(&head->lock); > > sctp_for_each_hentry(ep, &head->chain) { > > - err = cb(ep, p); > > - if (err) > > + if (sock_net(ep->base.sk) != net) > > + continue; > > + if (idx++ >= *pos) { > > + sk = ep->base.sk; > > + sock_hold(sk); > > break; > > + } > > } > > read_unlock_bh(&head->lock); > > + > > + if (sk) { > > + err = cb(ep, p); > > [Critical] > Can ep be freed before this cb(ep, p) call? > > The bucket read_lock_bh() is dropped above, and only sock_hold(sk) is > taken. In sctp_endpoint_init() the reference direction is > ep -> sk: > > refcount_set(&ep->base.refcnt, 1); > ... > sock_hold(sk); > > so taking sock_hold(sk) does not pin ep. > > A concurrent close(2) on the same socket runs: > > sctp_close() > -> release_sock() /* lock_sock dropped here */ > -> sk_common_release() > -> sctp_destroy_sock() > -> sctp_endpoint_free() > -> sctp_unhash_endpoint() > -> sctp_endpoint_put() /* refcount may reach 0 */ > -> call_rcu(&ep->rcu, sctp_endpoint_destroy_rcu) > > The destroy callback then does sctp_sk(sk)->ep = NULL; sock_put(sk); > kfree(ep). > > Between read_unlock_bh() and cb(ep, p), this iterator is no longer in > any RCU read-side section, and sctp_ep_dump() then calls lock_sock(sk) > which may sleep. That allows an RCU grace period to elapse before the > callback dereferences ep at: > > struct sock *sk = ep->base.sk; > ... > if ((r->idiag_states & ~(TCPF_LISTEN | TCPF_CLOSE)) && > !list_empty(&ep->asocs)) > > The sibling helpers in the same file pin the endpoint explicitly to > avoid this: > > sctp_transport_traverse_process() > if (sctp_endpoint_hold(ep)) { /* asoc can be peeled off */ > ret = cb(ep, tsp, p); > ... > sctp_endpoint_put(ep); > } > > Should sctp_for_each_endpoint() take sctp_endpoint_hold(ep) inside the > bucket read_lock_bh() (while ep is still hashed and live) and a matching > sctp_endpoint_put(ep) after cb returns? > > [Medium] > Can this restart pattern miss or duplicate endpoints when the bucket is > modified concurrently? > > After processing one ep the loop does: > > idx = start; > goto again; > > which re-walks the bucket from the head and uses the running counter > idx compared against *pos to skip already-emitted entries. > > __sctp_hash_endpoint() inserts at the hlist head and > __sctp_unhash_endpoint() can delete arbitrary nodes, both under the > bucket write_lock that this iterator releases between walks. A > concurrent listen() on a sibling endpoint shifts every existing entry's > index by +1, and a concurrent close() before the cursor shifts indices > by -1. > > Under that pattern, can SCTP_DIAG consumers observe duplicated > endpoints (the previously-emitted entry now sits at the new *pos) or > miss live endpoints (the next un-emitted entry is shifted past the > cursor)? The pre-patch sweep walked each bucket once with read_lock_bh > held throughout, so this consistency window did not exist. > > [Low] > For a bucket that contains N matching endpoints, does this restart > pattern walk the bucket O(N^2) times? > > For a bucket {A,B,C} starting from *pos=0 the goto-again loop performs > 1+2+3 traversals to dump three entries plus a final 3-entry walk that > finds nothing and terminates with sk == NULL, which is nine list > iterations to dump three endpoints. The pre-patch code walked each > bucket exactly once because the callback ran while still holding > read_lock_bh(). > > The commit message describes the change as simplifying endpoint > traversal; could the description also note the new per-bucket scan > cost, or could a cursor that survives unlock/relock be used instead? > > > + if (err) { > > + sock_put(sk); > > + return err; > > + } > > + sock_put(sk); > > + (*pos)++; > > + > > + idx = start; > > + goto again; > > + } > > } > > > > - return err; > > + return 0; > > } > > EXPORT_SYMBOL_GPL(sctp_for_each_endpoint); Low: #1, #2, #5, not really issues, but worth mentioning about it in changelog. Critical: #3, not valid. socket refcnt can't be 0 when traversing the chain under read_lock_bh(). But it seems better to hold ep instead sk, and also to check ep->base.dead instead of sk_state CLOSED. Medium: #4, not valid. it's completely okay to dump duplicate or skip socks because of concurrent close() and listen() in diag. will post v2 with some improvements mentioned above. Thanks.