Re: [PATCH net] amt: fix tunnel list corruption on device stop

Simon Horman <[email protected]>
Newsgroups org.kernel.vger.stable,org.kernel.vger.linux-kernel,org.kernel.vger.netdev
Message-ID <[email protected]>
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/#/patchset/[email protected]?part=1
---
amt: fix tunnel list corruption on device stop

This changes amt_dev_stop() to unlink tunnels under amt->lock using
list_del_init(), adds a list_empty() ownership check to
amt_tunnel_expire(), clears sk_user_data before synchronize_net(), and
switches the per-tunnel GC work to disable_delayed_work_sync().

A few questions on the changelog and on the new drain loop below.

First, about the two justifications given in the changelog:

>     1. Quiesce RX: clear sk_user_data and call synchronize_net() to ensure
>        no RCU readers are traversing tunnel_list. This makes list_del_init()
>        safe (it is not RCU-reader-safe unlike list_del_rcu()).

[Severity: Medium]
Is this claim accurate for all readers of amt->tunnel_list?

amt_dev_xmit() traverses the same list in relay mode:

drivers/net/amt.c:amt_dev_xmit() {
	...
	list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
		hash = amt_group_hash(tunnel, &group);
	...
}

That reader is not reached through amt->sk / sk_user_data, so clearing
sk_user_data and calling synchronize_net() does not cover it.

>     4. Use disable_delayed_work_sync() instead of cancel_delayed_work_sync()
>        to prevent amt_update_handler() from rearming the GC timer in a rare
>        race where a packet arrives before the socket is fully released.

Can this window still exist after the change in point 1?

amt_update_handler() and amt_request_handler() are only called from
amt_rcv(), which resolves amt via rcu_dereference_sk_user_data(sk) inside
rcu_read_lock_bh(). Once sk_user_data is NULLed and synchronize_net() has
returned, amt_rcv() can no longer find amt, so the packet-driven
mod_delayed_work(amt_wq, &tunnel->gc_wq, ...) in amt_update_handler()
cannot run any more.

So point 1 says no RCU reader can be traversing tunnel_list, while point 4
describes a packet reaching amt_update_handler(), which can only reach that
mod_delayed_work() from inside list_for_each_entry_rcu() over
amt->tunnel_list. Could the changelog be made consistent about which
readers the quiesce actually covers, given that this is the stated reason
for dropping the RCU-safe removal primitive and the patch is tagged for
stable?

> diff --git a/drivers/net/amt.c b/drivers/net/amt.c
> index bddc24e1856d..a5db02d81291 100644
> --- a/drivers/net/amt.c
> +++ b/drivers/net/amt.c
> @@ -1348,6 +1348,11 @@ static void amt_tunnel_expire(struct work_struct *work)
>  	struct amt_dev *amt = tunnel->amt;
>  
>  	spin_lock_bh(&amt->lock);
> +	/* amt_dev_stop() marks tunnels it owns with list_del_init(). */
> +	if (list_empty(&tunnel->list)) {
> +		spin_unlock_bh(&amt->lock);
> +		return;
> +	}
>  	rcu_read_lock();
>  	list_del_rcu(&tunnel->list);
>  	amt->nr_tunnels--;

[Severity: High]
This isn't a bug introduced by this patch, but does the new list_empty()
guard cover the case where the GC work is rearmed while it is already
running?

amt_update_handler() rearms the work under rcu_read_lock_bh() only, without
amt->lock:

drivers/net/amt.c:amt_update_handler() {
	list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
		if (tunnel->ip4 == saddr) {
			if ((nonce == tunnel->nonce &&
			     response_mac == tunnel->mac)) {
				mod_delayed_work(amt_wq, &tunnel->gc_wq,
						 msecs_to_jiffies(amt_gmi(amt))
								  * 3);
	...
}

Once process_one_work() has cleared the work's PENDING bit,
mod_delayed_work() arms the delayed timer again, and amt_tunnel_expire()
then proceeds to list_del_rcu() + kfree_rcu(tunnel, rcu) with
tunnel->gc_wq.timer still armed. The rearm delay is amt_gmi() * 3, which is
far longer than the grace period behind kfree_rcu(), so the timer_list
inside the freed tunnel stays linked in the timer wheel.

The drain loop in amt_dev_stop() does not help here, because the first
callback has already removed the tunnel from amt->tunnel_list, so
disable_delayed_work_sync() is never called for it.

And if the work does run again on still-mapped memory, the new guard cannot
detect it either: list_del_rcu() leaves entry->next unchanged, so
list_empty(&tunnel->list) is false and a second list_del_rcu() runs with
prev == LIST_POISON2, which is the same lib/list_debug.c BUG this patch is
fixing.

> @@ -3097,13 +3104,21 @@ static int amt_dev_open(struct net_device *dev)
>  	amt->req_cnt = 0;
>  	WRITE_ONCE(amt->remote_ip, 0);
>  
> -	list_for_each_entry_safe(tunnel, tmp, &amt->tunnel_list, list) {
> -		list_del_rcu(&tunnel->list);
> +	spin_lock_bh(&amt->lock);
> +	while (!list_empty(&amt->tunnel_list)) {
> +		tunnel = list_first_entry(&amt->tunnel_list,
> +					  struct amt_tunnel_list, list);
> +		list_del_init(&tunnel->list);
>  		amt->nr_tunnels--;

[Severity: High]
Can this wedge a concurrent amt_dev_xmit()?

list_del_init() points the node at itself:

include/linux/list.h:list_del_init() {
	__list_del_entry(entry);
	INIT_LIST_HEAD(entry);
}

amt->tunnel_list is published with list_add_tail_rcu() in
amt_request_handler() and read locklessly in amt_dev_xmit():

drivers/net/amt.c:amt_dev_xmit() {
	...
	list_for_each_entry_rcu(tunnel, &amt->tunnel_list, list) {
	...
}

A reader parked on the node being unlinked gets itself back from
list_next_entry_rcu() and never reaches the list head, so it spins forever
with BH disabled inside an RCU read-side section. That also stalls the
grace period behind the kfree_rcu(tunnel, rcu) calls in the same loop.

Is this TX reader quiesced anywhere? amt_link_setup() sets:

drivers/net/amt.c:amt_link_setup() {
	dev->priv_flags		|= IFF_NO_QUEUE;
	dev->lltx		= true;
	...
}

With the noqueue qdisc, noqueue_init() leaves qdisc->enqueue == NULL, so
dev_deactivate_queue() never sets *sync_needed and dev_deactivate_many()
skips its synchronize_net() for this device. And __dev_direct_xmit(), used
by AF_PACKET PACKET_QDISC_BYPASS and xsk generic xmit, tests liveness
before disabling BH:

net/core/dev.c:__dev_direct_xmit() {
	if (unlikely(!netif_running(dev) ||
		     !netif_carrier_ok(dev)))
		goto drop;

	skb = validate_xmit_skb_list(skb, dev, &again);
	...
	local_bh_disable();
	...
}

so a sender can pass the check, be preempted across __dev_close_many() and
amt_dev_stop()'s synchronize_net(), and only then enter amt_dev_xmit().

The previous list_del_rcu() poisoned only entry->prev and left entry->next
intact, so a racing reader still terminated. Would keeping list_del_rcu()
here (and using a different marker for the ownership test in
amt_tunnel_expire()) avoid this?

> -		cancel_delayed_work_sync(&tunnel->gc_wq);
> +		spin_unlock_bh(&amt->lock);
> +
> +		disable_delayed_work_sync(&tunnel->gc_wq);
>  		amt_clear_groups(tunnel);
>  		kfree_rcu(tunnel, rcu);

[Severity: High]
This is a pre-existing issue, but the works nested under the tunnel are
still only cancelled with the non-waiting cancel_delayed_work() while the
tunnel-level work is now upgraded to disable_delayed_work_sync().

amt_clear_groups() -> amt_del_group() does:

drivers/net/amt.c:amt_del_group() {
	if (cancel_delayed_work(&gnode->group_timer))
		dev_put(amt->dev);
	hlist_del_rcu(&gnode->node);
	...
	kfree_rcu(gnode, rcu);
}

and amt_destroy_source() does the same for the source timer:

drivers/net/amt.c:amt_destroy_source() {
	cancel_delayed_work(&snode->source_timer);
	hlist_del_init_rcu(&snode->node);
	...
}

cancel_delayed_work() returns false once process_one_work() has cleared
PENDING, and it is never waited for. The callbacks dereference the objects
and take tunnel->lock before entering any RCU read-side section:

drivers/net/amt.c:amt_group_work() {
	struct amt_tunnel_list *tunnel = gnode->tunnel_list;
	struct amt_dev *amt = gnode->amt;
	...
	spin_lock_bh(&tunnel->lock);
	...
}

drivers/net/amt.c:amt_source_work() {
	struct amt_group_node *gnode = snode->gnode;
	struct amt_dev *amt = gnode->amt;
	...
	tunnel = gnode->tunnel_list;
	spin_lock_bh(&tunnel->lock);
	rcu_read_lock();
	...
}

A worker preempted between PENDING being cleared and spin_lock_bh() is in
plain preemptible process context, not an RCU reader, so the grace periods
behind kfree_rcu(gnode, rcu) and kfree_rcu(tunnel, rcu) can complete. When
it resumes it takes spin_lock_bh() on freed memory, reads gnode fields from
freed memory, and can call dev_put(amt->dev) derived from that state.

Would it make sense to use the sync cancel for the group and source timers
too, so the cancel-before-free ordering is complete for everything nested
under the tunnel?

>  	}
> +	spin_unlock_bh(&amt->lock);
>  
>  	return 0;
>  }
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.