public inbox for netdev@vger.kernel.org
 help / color / mirror / Atom feed
* [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
@ 2026-03-02 21:26 Helen Koike
  2026-03-02 23:08 ` Pablo Neira Ayuso
  0 siblings, 1 reply; 9+ messages in thread
From: Helen Koike @ 2026-03-02 21:26 UTC (permalink / raw)
  To: pablo, fw, phil, netfilter-devel, coreteam, netdev, linux-kernel,
	kernel-dev, koike

struct nf_hook_ops has a pointer to dev, which can be used by
__nf_unregister_net_hook() after it has been freed by tun_chr_close().

Fix it  by calling dev_hold() when saving dev to ops struct.

Reported-by: syzbot+bb9127e278fa198e110c@syzkaller.appspotmail.com
Closes: https://syzkaller.appspot.com/bug?extid=bb9127e278fa198e110c
Signed-off-by: Helen Koike <koike@igalia.com>
---
 net/netfilter/nf_tables_api.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index fd7f7e4e2a43..00b5f900a51d 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -352,6 +352,7 @@ static void nft_netdev_hook_free_ops(struct nft_hook *hook)
 
 	list_for_each_entry_safe(ops, next, &hook->ops_list, list) {
 		list_del(&ops->list);
+		dev_put(ops->dev);
 		kfree(ops);
 	}
 }
@@ -2374,6 +2375,7 @@ static struct nft_hook *nft_netdev_hook_alloc(struct net *net,
 			err = -ENOMEM;
 			goto err_hook_free;
 		}
+		dev_hold(dev);
 		ops->dev = dev;
 		list_add_tail(&ops->list, &hook->ops_list);
 	}
-- 
2.53.0


^ permalink raw reply related	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-02 21:26 [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev Helen Koike
@ 2026-03-02 23:08 ` Pablo Neira Ayuso
  2026-03-03 14:33   ` Helen Koike
  2026-03-04 12:49   ` Phil Sutter
  0 siblings, 2 replies; 9+ messages in thread
From: Pablo Neira Ayuso @ 2026-03-02 23:08 UTC (permalink / raw)
  To: Helen Koike
  Cc: fw, phil, netfilter-devel, coreteam, netdev, linux-kernel,
	kernel-dev

On Mon, Mar 02, 2026 at 06:26:05PM -0300, Helen Koike wrote:
> struct nf_hook_ops has a pointer to dev, which can be used by
> __nf_unregister_net_hook() after it has been freed by tun_chr_close().
> 
> Fix it  by calling dev_hold() when saving dev to ops struct.

Sorry, I don't think this patch works, dev_hold()/dev_put() use
per_cpu area.

The nf_tables_flowtable_event() function used to release the hook, but
now things have changed since there is auto-hook registration.

> Reported-by: syzbot+bb9127e278fa198e110c@syzkaller.appspotmail.com
> Closes: https://syzkaller.appspot.com/bug?extid=bb9127e278fa198e110c
> Signed-off-by: Helen Koike <koike@igalia.com>
> ---
>  net/netfilter/nf_tables_api.c | 2 ++
>  1 file changed, 2 insertions(+)
> 
> diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
> index fd7f7e4e2a43..00b5f900a51d 100644
> --- a/net/netfilter/nf_tables_api.c
> +++ b/net/netfilter/nf_tables_api.c
> @@ -352,6 +352,7 @@ static void nft_netdev_hook_free_ops(struct nft_hook *hook)
>  
>  	list_for_each_entry_safe(ops, next, &hook->ops_list, list) {
>  		list_del(&ops->list);
> +		dev_put(ops->dev);
>  		kfree(ops);
>  	}
>  }
> @@ -2374,6 +2375,7 @@ static struct nft_hook *nft_netdev_hook_alloc(struct net *net,
>  			err = -ENOMEM;
>  			goto err_hook_free;
>  		}
> +		dev_hold(dev);
>  		ops->dev = dev;
>  		list_add_tail(&ops->list, &hook->ops_list);
>  	}
> -- 
> 2.53.0
> 

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-02 23:08 ` Pablo Neira Ayuso
@ 2026-03-03 14:33   ` Helen Koike
  2026-03-04  5:32     ` Florian Westphal
  2026-03-04 12:49   ` Phil Sutter
  1 sibling, 1 reply; 9+ messages in thread
From: Helen Koike @ 2026-03-03 14:33 UTC (permalink / raw)
  To: Pablo Neira Ayuso
  Cc: fw, phil, netfilter-devel, coreteam, netdev, linux-kernel,
	kernel-dev

Hi Pablo,

Thanks for your quick reply, please see my comments below.

On 3/2/26 8:08 PM, Pablo Neira Ayuso wrote:
> On Mon, Mar 02, 2026 at 06:26:05PM -0300, Helen Koike wrote:
>> struct nf_hook_ops has a pointer to dev, which can be used by
>> __nf_unregister_net_hook() after it has been freed by tun_chr_close().
>>
>> Fix it  by calling dev_hold() when saving dev to ops struct.
> 
> Sorry, I don't think this patch works, dev_hold()/dev_put() use
> per_cpu area.

In my understanding, being a percpu_ref shouldn't affect the behavior, 
since after a percpu_ref_kill() it switches to the usual mode with 
shared atomic_t counter.

But if I understood correctly from your comment below, the proper 
solution would be to fix the order that the hooks are released, is my 
understanding correct?

> 
> The nf_tables_flowtable_event() function used to release the hook, but
> now things have changed since there is auto-hook registration.

I'll investigate this further. Thanks a lot for the pointer (and sorry 
for the noise).

Regards,
Helen

> 
>> Reported-by: syzbot+bb9127e278fa198e110c@syzkaller.appspotmail.com
>> Closes: https://syzkaller.appspot.com/bug?extid=bb9127e278fa198e110c
>> Signed-off-by: Helen Koike <koike@igalia.com>
>> ---
>>   net/netfilter/nf_tables_api.c | 2 ++
>>   1 file changed, 2 insertions(+)
>>
>> diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
>> index fd7f7e4e2a43..00b5f900a51d 100644
>> --- a/net/netfilter/nf_tables_api.c
>> +++ b/net/netfilter/nf_tables_api.c
>> @@ -352,6 +352,7 @@ static void nft_netdev_hook_free_ops(struct nft_hook *hook)
>>   
>>   	list_for_each_entry_safe(ops, next, &hook->ops_list, list) {
>>   		list_del(&ops->list);
>> +		dev_put(ops->dev);
>>   		kfree(ops);
>>   	}
>>   }
>> @@ -2374,6 +2375,7 @@ static struct nft_hook *nft_netdev_hook_alloc(struct net *net,
>>   			err = -ENOMEM;
>>   			goto err_hook_free;
>>   		}
>> +		dev_hold(dev);
>>   		ops->dev = dev;
>>   		list_add_tail(&ops->list, &hook->ops_list);
>>   	}
>> -- 
>> 2.53.0
>>


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-03 14:33   ` Helen Koike
@ 2026-03-04  5:32     ` Florian Westphal
  2026-03-04 12:26       ` Phil Sutter
  0 siblings, 1 reply; 9+ messages in thread
From: Florian Westphal @ 2026-03-04  5:32 UTC (permalink / raw)
  To: Helen Koike
  Cc: Pablo Neira Ayuso, phil, netfilter-devel, coreteam, netdev,
	linux-kernel, kernel-dev

Helen Koike <koike@igalia.com> wrote:

Phil, can you please take a look at this?

The reg/unregister logic is ... strange.

> But if I understood correctly from your comment below, the proper 
> solution would be to fix the order that the hooks are released, is my 
> understanding correct?

I don't think its about ordering.  I think the code allows to register
devices multiple times in the same flowtable, but UNREG doesn't handle
that.

static int nft_flowtable_event(unsigned long event, struct net_device *dev,
			       struct nft_flowtable *flowtable, bool changename)
{
	struct nf_hook_ops *ops;
	struct nft_hook *hook;
	bool match;

	list_for_each_entry(hook, &flowtable->hook_list, list) {
		ops = nft_hook_find_ops(hook, dev);
		match = !strncmp(hook->ifname, dev->name, hook->ifnamelen);

		switch (event) {
		case NETDEV_UNREGISTER:
			/* NOP if not found or new name still matching */
			if (!ops || (changename && match))
				continue;

			/* flow_offload_netdev_event() cleans up entries for us. */
			nft_unregister_flowtable_ops(dev_net(dev),
						     flowtable, ops);
			list_del_rcu(&ops->list);
			kfree_rcu(ops, rcu);
			break;
		case NETDEV_REGISTER:
			/* NOP if not matching or already registered */
			if (!match || (changename && ops))
				continue;

And *THIS* looks buggy.
Shouldn't that simply be:
			if (!match || ops)
				continue;

Or can you explain why changename has any relevance here?
changename means dev->name has already been updated.

So, we want to skip a new registration if either:
1. the name doesn't match
2. it matches but its already registered.

In case changename is true, only UNREGISTER: case is
relevant: If its not matching anymore -> unregister.

Still matching?  Keep it.  In that case, we havn't
registered the device again because 'ops' was non-null in
REGISTER case.

		}
		break;

If its allowed to register the same device twice (or more), then the
above 'break' needs to be removed, AND one has to alter UNREGISTER
above to loop until no more ops are found, i.e.
		case NETDEV_UNREGISTER:
			/* NOP if not found or new name still matching */
			if (!ops || (changename && match))
				continue;

			do {
				/* flow_offload_netdev_event() cleans up entries for us. */
				nft_unregister_flowtable_ops(dev_net(dev),
							     flowtable, ops);
				list_del_rcu(&ops->list);
				kfree_rcu(ops, rcu);
				ops = nft_hook_find_ops(hook, dev);
			} while (ops);
			break;

Phil, can you please have a look?  Thanks!

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-04  5:32     ` Florian Westphal
@ 2026-03-04 12:26       ` Phil Sutter
  2026-03-04 13:38         ` Florian Westphal
  0 siblings, 1 reply; 9+ messages in thread
From: Phil Sutter @ 2026-03-04 12:26 UTC (permalink / raw)
  To: Florian Westphal
  Cc: Helen Koike, Pablo Neira Ayuso, netfilter-devel, coreteam, netdev,
	linux-kernel, kernel-dev

Hi,

On Wed, Mar 04, 2026 at 06:32:15AM +0100, Florian Westphal wrote:
> Helen Koike <koike@igalia.com> wrote:
> 
> Phil, can you please take a look at this?
> 
> The reg/unregister logic is ... strange.
> 
> > But if I understood correctly from your comment below, the proper 
> > solution would be to fix the order that the hooks are released, is my 
> > understanding correct?
> 
> I don't think its about ordering.  I think the code allows to register
> devices multiple times in the same flowtable, but UNREG doesn't handle
> that.
> 
> static int nft_flowtable_event(unsigned long event, struct net_device *dev,
> 			       struct nft_flowtable *flowtable, bool changename)
> {
> 	struct nf_hook_ops *ops;
> 	struct nft_hook *hook;
> 	bool match;
> 
> 	list_for_each_entry(hook, &flowtable->hook_list, list) {
> 		ops = nft_hook_find_ops(hook, dev);
> 		match = !strncmp(hook->ifname, dev->name, hook->ifnamelen);
> 
> 		switch (event) {
> 		case NETDEV_UNREGISTER:
> 			/* NOP if not found or new name still matching */
> 			if (!ops || (changename && match))
> 				continue;
> 
> 			/* flow_offload_netdev_event() cleans up entries for us. */
> 			nft_unregister_flowtable_ops(dev_net(dev),
> 						     flowtable, ops);
> 			list_del_rcu(&ops->list);
> 			kfree_rcu(ops, rcu);
> 			break;
> 		case NETDEV_REGISTER:
> 			/* NOP if not matching or already registered */
> 			if (!match || (changename && ops))
> 				continue;
> 
> And *THIS* looks buggy.
> Shouldn't that simply be:
> 			if (!match || ops)
> 				continue;
> 
> Or can you explain why changename has any relevance here?
> changename means dev->name has already been updated.

The changename parameter is true if the event handler was called for a
NETDEV_CHANGENAME event. In that, case, we call nft_flowtable_event()
twice for each flowtable: First with NETDEV_REGISTER event value and
second with NETDEV_UNREGISTER value. If a device is renamed, we will try
to register it with all flowtables having a matching interface spec if
not already registered with. If that succeeds, we try to unregister it
from all flowtables it's registered with if not matching anymore.

In "changename mode" therefore, NETDEV_REGISTER case is skipped if name
matches but device is registered already. NETDEV_UNREGISTER case is
skipped if already registered but name still matches.

In regular mode, i.e. either real NETDEV_REGISTER or NETDEV_UNREGISTER,
the NETDEV_UNREGISTER case is skipped if the device was not registered.
If it was, the name is not relevant, we have to unregister it anyway.
The NETDEV_REGISTER case is skipped if the name does not match. If it
matches, we assume it is not registered already. This is true since
NETDEV_REGISTER notifier runs for each device just once, right?

> So, we want to skip a new registration if either:
> 1. the name doesn't match
> 2. it matches but its already registered.

The "already registered" part should not happen in real NETDEV_REGISTER
event, though: When parsing netdev hooks, non-distinct prefixes are
eliminated so that a given device name will never match more than a
single hook per flowtable. This is done by comparing with min prefix
length in nft_hook_list_find().

> In case changename is true, only UNREGISTER: case is
> relevant: If its not matching anymore -> unregister.
> 
> Still matching?  Keep it.  In that case, we havn't
> registered the device again because 'ops' was non-null in
> REGISTER case.

You're right, the 'changename' check in NETDEV_REGISTER is not needed
because even if not changing names one should skip if already
registered. Actually, this indicates a bug unless handling
NETDEV_CHANGENAME. Maybe add a WARN_ON_ONCE()?

> 		}
> 		break;
> 
> If its allowed to register the same device twice (or more), then the
> above 'break' needs to be removed, AND one has to alter UNREGISTER
> above to loop until no more ops are found, i.e.

A device may register to a single flowtable multiple times only
temporary while it is being renamed: If one hook matches the old name
and another the new name. It is supposed to register to the newly
matching hook first, then unregister from the no longer matching one.

Cheers, Phil

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-02 23:08 ` Pablo Neira Ayuso
  2026-03-03 14:33   ` Helen Koike
@ 2026-03-04 12:49   ` Phil Sutter
  2026-03-04 13:28     ` Florian Westphal
  1 sibling, 1 reply; 9+ messages in thread
From: Phil Sutter @ 2026-03-04 12:49 UTC (permalink / raw)
  To: Pablo Neira Ayuso
  Cc: Helen Koike, fw, netfilter-devel, coreteam, netdev, linux-kernel,
	kernel-dev

On Tue, Mar 03, 2026 at 12:08:56AM +0100, Pablo Neira Ayuso wrote:
> On Mon, Mar 02, 2026 at 06:26:05PM -0300, Helen Koike wrote:
> > struct nf_hook_ops has a pointer to dev, which can be used by
> > __nf_unregister_net_hook() after it has been freed by tun_chr_close().
> > 
> > Fix it  by calling dev_hold() when saving dev to ops struct.
> 
> Sorry, I don't think this patch works, dev_hold()/dev_put() use
> per_cpu area.

Why is this problematic? netdev_refcnt_read() sums the per-cpu variables
up, so it should be fine if we refcount_inc on one CPU and refcount_dec
on another, no?

> The nf_tables_flowtable_event() function used to release the hook, but
> now things have changed since there is auto-hook registration.

But isn't __nf_unregister_net_hook() still called immediately when
handling NETDEV_UNREGISTER event? I guess struct nf_hook_ops::dev may
still be accessed afterwards since ops is RCU-freed. Is Helen's report
inaccurate in that regard?

Looking at netdev_wait_allrefs_any(), I see NETDEV_UNREGISTER
notification before rcu_barrier() call. Does that suffice for our
kfree_rcu() upon NETDEV_UNREGISTER?

If we really risk losing the device pointer while the holding
nf_hook_ops object is still in use, dev_hold/_put should help. But where
to call dev_put() then? AIUI, nft_netdev_hook_free_ops() does not apply
to the NETDEV_UNREGISTER event code path, does it?

Cheers, Phil

> > Reported-by: syzbot+bb9127e278fa198e110c@syzkaller.appspotmail.com
> > Closes: https://syzkaller.appspot.com/bug?extid=bb9127e278fa198e110c
> > Signed-off-by: Helen Koike <koike@igalia.com>
> > ---
> >  net/netfilter/nf_tables_api.c | 2 ++
> >  1 file changed, 2 insertions(+)
> > 
> > diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
> > index fd7f7e4e2a43..00b5f900a51d 100644
> > --- a/net/netfilter/nf_tables_api.c
> > +++ b/net/netfilter/nf_tables_api.c
> > @@ -352,6 +352,7 @@ static void nft_netdev_hook_free_ops(struct nft_hook *hook)
> >  
> >  	list_for_each_entry_safe(ops, next, &hook->ops_list, list) {
> >  		list_del(&ops->list);
> > +		dev_put(ops->dev);
> >  		kfree(ops);
> >  	}
> >  }
> > @@ -2374,6 +2375,7 @@ static struct nft_hook *nft_netdev_hook_alloc(struct net *net,
> >  			err = -ENOMEM;
> >  			goto err_hook_free;
> >  		}
> > +		dev_hold(dev);
> >  		ops->dev = dev;
> >  		list_add_tail(&ops->list, &hook->ops_list);
> >  	}
> > -- 
> > 2.53.0
> > 
> 

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-04 12:49   ` Phil Sutter
@ 2026-03-04 13:28     ` Florian Westphal
  0 siblings, 0 replies; 9+ messages in thread
From: Florian Westphal @ 2026-03-04 13:28 UTC (permalink / raw)
  To: Phil Sutter, Pablo Neira Ayuso, Helen Koike, netfilter-devel,
	coreteam, netdev, linux-kernel, kernel-dev

Phil Sutter <phil@nwl.cc> wrote:
> But isn't __nf_unregister_net_hook() still called immediately when
> handling NETDEV_UNREGISTER event? I guess struct nf_hook_ops::dev may
> still be accessed afterwards since ops is RCU-freed. Is Helen's report
> inaccurate in that regard?

Its a red herring.

The device is registered twice.  But UNREGISTER only removes ONE
instance.

Then, later, when a different device (same name!) invokes netlink handler,
the walk finds the old, free'd net_device.

I hacked UNREGISTER to handle this: no more splat.
I reverted this change and altered REGISTER to never allow
double-register: no splats.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-04 12:26       ` Phil Sutter
@ 2026-03-04 13:38         ` Florian Westphal
  2026-03-04 14:59           ` Helen Koike
  0 siblings, 1 reply; 9+ messages in thread
From: Florian Westphal @ 2026-03-04 13:38 UTC (permalink / raw)
  To: Phil Sutter, Helen Koike, Pablo Neira Ayuso, netfilter-devel,
	coreteam, netdev, linux-kernel, kernel-dev

Phil Sutter <phil@nwl.cc> wrote:
> > And *THIS* looks buggy.
> > Shouldn't that simply be:
> > 			if (!match || ops)
> > 				continue;

FWIW I can't get the reproducer to trigger a splat with this change.
I've fed this to syzbot to double-check.

> You're right, the 'changename' check in NETDEV_REGISTER is not needed
> because even if not changing names one should skip if already
> registered. Actually, this indicates a bug unless handling
> NETDEV_CHANGENAME. Maybe add a WARN_ON_ONCE()?

Well, it does trigger, afaics.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev
  2026-03-04 13:38         ` Florian Westphal
@ 2026-03-04 14:59           ` Helen Koike
  0 siblings, 0 replies; 9+ messages in thread
From: Helen Koike @ 2026-03-04 14:59 UTC (permalink / raw)
  To: Florian Westphal, Phil Sutter, Pablo Neira Ayuso, netfilter-devel,
	coreteam, netdev, linux-kernel, kernel-dev



On 3/4/26 10:38 AM, Florian Westphal wrote:
> Phil Sutter <phil@nwl.cc> wrote:
>>> And *THIS* looks buggy.
>>> Shouldn't that simply be:
>>> 			if (!match || ops)
>>> 				continue;
> 

I tested this change locally (with syz reproducer) and I'm unable to 
trigger the issue anymore. Without this change I always reproduce it.

If you are to send this patch, please add:

Tested-by: Helen Koike <koike@igalia.com>



> FWIW I can't get the reproducer to trigger a splat with this change.
> I've fed this to syzbot to double-check.
> 
>> You're right, the 'changename' check in NETDEV_REGISTER is not needed
>> because even if not changing names one should skip if already
>> registered. Actually, this indicates a bug unless handling
>> NETDEV_CHANGENAME. Maybe add a WARN_ON_ONCE()?
> 
> Well, it does trigger, afaics.


Thanks,
Helen

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-03-04 14:59 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-02 21:26 [PATCH] netfilter: nf_tables: fix use-after-free on ops->dev Helen Koike
2026-03-02 23:08 ` Pablo Neira Ayuso
2026-03-03 14:33   ` Helen Koike
2026-03-04  5:32     ` Florian Westphal
2026-03-04 12:26       ` Phil Sutter
2026-03-04 13:38         ` Florian Westphal
2026-03-04 14:59           ` Helen Koike
2026-03-04 12:49   ` Phil Sutter
2026-03-04 13:28     ` Florian Westphal

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox