Re: [PATCH v1 net-next] af_unix: Unlink scc_entry in unix_del_edge().

Kuniyuki Iwashima <[email protected]>
Newsgroups gmane.linux.network
Message-ID <CAAVpQUACUFcdUV_TURigYp=-wDamvN2PBQ+c+njYxAyDvmq8cg@mail.gmail.com>
On Thu, Aug 6, 2026 at 10:29 AM Jakub Kicinski <[email protected]> wrote:
>
> This is an AI-generated review of your patch. The human sending this
> email says "I meant to send this yesterday, damn. This is completely
> separate right?"
> ---
> af_unix: Unlink scc_entry in unix_del_edge().
>
> A vertex whose out_degree drops to zero is moved to fpl->vertices and later
> freed by unix_free_vertices(), but it stayed linked in the circular
> scc_entry list built by __unix_walk_scc().  This adds a
> list_del(&vertex->scc_entry) so a following unix_walk_scc_fast() no longer
> walks into the freed vertex.  Reported-by: Kyle Zeng <[email protected]>,
> Fixes: 4090fa373f0e ("af_unix: Replace garbage collection algorithm.").
>
> Two questions below, one about the changelog wording and one about the
> cyclic SCC accounting that the described remnant vertex feeds into.
>
> > af_unix: Unlink scc_entry in unix_del_edge().
> >
> > Kyle Zeng reported that GC could free a dead SCC partially.
> [ ... ]
> > However, X's SCC forces the next GC to call unix_walk_scc_fast(),
> > and it iterates over A through B's scc_entry.
>
> [Severity: Low]
> Could the changelog name what "iterates over A" means in practice?  A's
> vertex has already been kfree()d by unix_free_vertices() at that point, so
> the fast walk reads and writes freed memory:
>
> net/unix/garbage.c:unix_scc_dead() {
>         list_for_each_entry_reverse(vertex, scc, scc_entry) {
>                 /* Don't restart DFS from this vertex. */
>                 list_move_tail(&vertex->entry, &unix_visited_vertices);
>         ...
> }
>
> net/unix/garbage.c:unix_collect_skb() {
>         list_for_each_entry_reverse(vertex, scc, scc_entry) {
>                 ...
>                 edge = list_first_entry(&vertex->edges, typeof(*edge), vertex_entry);
>         ...
> }
>
> The summary says "GC could free a dead SCC partially", and the words
> use-after-free or list corruption do not appear anywhere in the message.
> Since the trigger is ordinary unprivileged sendmsg(SCM_RIGHTS) plus close()
> on AF_UNIX sockets, would it help stable triage to say explicitly that the
> pre-patch behaviour is a use-after-free on the freed unix_vertex?
>
> > This itself is not a problem since the next GC run will judge
> > B as dead as well and free it finally.
> >
> >        X -.   A <.> B -.
> >        ^--'         ^--'
>
> > diff --git a/net/unix/garbage.c b/net/unix/garbage.c
> > index 0783555e25266..9fcaaf55cba5d 100644
> > --- a/net/unix/garbage.c
> > +++ b/net/unix/garbage.c
> > @@ -186,6 +186,7 @@ static void unix_del_edge(struct scm_fp_list *fpl, struct unix_edge *edge)
> >       if (!vertex->out_degree) {
> >               edge->predecessor->vertex = NULL;
> >               list_move_tail(&vertex->entry, &fpl->vertices);
> > +             list_del(&vertex->scc_entry);
> >       }
> >  }
>
> [Severity: High]
> This isn't a bug introduced by this patch, but the surviving remnant vertex
> that the changelog accepts as harmless looks like it unbalances
> unix_graph_cyclic_sccs.
>
> __unix_walk_scc() only counts SCCs that are still alive:
>
> net/unix/garbage.c:__unix_walk_scc() {
>         if (unix_scc_dead(&scc, false)) {
>                 unix_collect_skb(&scc, hitlist);
>         } else {
>                 ...
>                 if (unix_scc_cyclic(&scc))
>                         cyclic_sccs++;
>         }
> }
>
> so the {A, B} SCC that the full walk judges dead contributes 0.  But the
> fast walk decrements once for every ring it finds dead:
>
> net/unix/garbage.c:unix_walk_scc_fast() {
>         vertex = list_first_entry(&unix_unvisited_vertices, typeof(*vertex), entry);
>         list_add(&scc, &vertex->scc_entry);
>
>         if (unix_scc_dead(&scc, true)) {
>                 cyclic_sccs--;
>                 unix_collect_skb(&scc, hitlist);
>         }
>         ...
>         WRITE_ONCE(unix_graph_cyclic_sccs, cyclic_sccs);
>         WRITE_ONCE(unix_graph_state,
>                    cyclic_sccs ? UNIX_GRAPH_CYCLIC : UNIX_GRAPH_NOT_CYCLIC);
> }
>
> Using the exact scenario from the changelog, with X's live cyclic SCC plus
> {A, B}: the full walk collects {A, B} and stores unix_graph_cyclic_sccs = 1
> for X.  Because the purge runs with fpl->dead == true, unix_del_edge()
> deliberately skips unix_update_graph(), so B survives as a self-linked
> singleton ring back on unix_unvisited_vertices and the state stays
> UNIX_GRAPH_CYCLIC.
>
> The next GC then takes the fast walk, finds B's singleton ring dead and does
> cyclic_sccs-- on a group that was never counted, taking the counter 1 -> 0.
>
> Can that flip unix_graph_state to UNIX_GRAPH_NOT_CYCLIC while X's cyclic SCC
> is still present?  After that, both entry points bail out:
>
> net/unix/garbage.c:unix_gc() {
>         if (unix_graph_state == UNIX_GRAPH_NOT_CYCLIC) {
>                 spin_unlock(&unix_gc_lock);
>                 goto skip_gc;
>         }
> }
>
> net/unix/garbage.c:unix_schedule_gc() {
>         if (READ_ONCE(unix_graph_state) == UNIX_GRAPH_NOT_CYCLIC)
>                 return;
> }
>
> Closing X's fd does not release the socket while it is inflight, so nothing
> calls unix_del_edges()/unix_update_graph() to re-arm the state.  Does that
> leak X's struct sock, its skb and the pinned struct file until some
> unrelated sender happens to pass an AF_UNIX fd to an inflight receiver?
>
> And if two such remnant rings are found in one fast walk while the counter
> is 1, does the second cyclic_sccs-- wrap the unsigned long to ULONG_MAX?
> The state would then stay UNIX_GRAPH_CYCLIC, so unix_walk_scc() is never
> selected again to recompute the counter, and this makes every fd-passing
> send take the synchronous path:
>
> net/unix/garbage.c:unix_schedule_gc() {
>         if (user && READ_ONCE(unix_graph_cyclic_sccs))
>                 flush_work(&unix_gc_work);
> }
>
> The counter and the unconditional decrement predate this patch (58b47c713711
> and e29c7a4cec86), so this is not a regression from the one-line addition,
> but should the accounting also be corrected for the remnant case?

Good point, looks like Claude is now better than Gemini :)

I'll change unix_walk_scc_fast() like below.

if (unix_scc_dead(&scc, true)) {
        unix_collect_skb(&scc, hitlist);
        if (list_empty(&scc))
                cyclic_sccs--;
}

Thanks !
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.