From: Rongguang Wei <clementwei90@163.com>
To: Willem de Bruijn <willemdebruijn.kernel@gmail.com>,
netdev@vger.kernel.org
Cc: jasowangio@gmail.com, andrew+netdev@lunn.ch, davem@davemloft.net,
kuba@kernel.org, Rongguang Wei <weirongguang@kylinos.cn>
Subject: Re: [PATCH net v8 1/2] tun: keep a kernel copy of the socket filter program
Date: Thu, 29 Oct 2026 09:46:23 +0800 [thread overview]
Message-ID: <5f3ce099-1074-4416-b758-276aa9112bc3@163.com> (raw)
In-Reply-To: <willemdebruijn.kernel.32a846b2c565b@gmail.com>
Willem de Bruijn wrote:
> Rongguang Wei wrote:
>> From: Rongguang Wei <weirongguang@kylinos.cn>
>>
>> TUNATTACHFILTER copies only the sock_fprog header, so tun->fprog.filter
>> stays a pointer into the address space of the process that issued the
>> ioctl. tun_attach() reads it again whenever a queue is attached to the
>> persistent device later on.
>>
>> Rebuilding the filter from that pointer is not reliable. With the inverted
>> check below, a failure of the attach was not fatal: -EFAULT for an unmapped
>> address, -EINVAL for bytes that are not a valid classic BPF program, -EPERM
>> for a locked filter and -ENOMEM all ended up in "err = 0" for a new tfile,
>> so the queue was attached without a filter. And when the read succeeded,
>> the early return meant the queue was never published at all.
>>
>> Keep the program in the kernel instead: tun->fprog_kern holds the
>> instructions, and each queue gets its own program built from it with the
>> new sk_attach_filter_kern(), the kernel memory counterpart of
>> sk_attach_filter(). The copy is freed when the filter is detached or
>> replaced and with the device, and tun->fprog is left untouched so
>> TUNGETFILTER keeps its uapi behaviour.
>>
>> Also fix the inverted check, which made tun_attach() return early when
>> sk_attach_filter_kern() succeeded instead of when it failed. Neither change
>> works on its own: with only the kernel copy, every re-attach returns 0
>> without publishing the queue; with only the check fixed, a re-attach that
>> used to succeed without installing a filter would start to fail.
>>
>> sk_attach_filter_kern() builds the program with bpf_prog_create(), which
>> does not keep an original program, so SO_GET_FILTER returns -EACCES and
>> sock_diag omits the filter for sockets that use it. tun sockets are not
>> exposed as file descriptors, so this is not user visible.
>>
>> The header and the instructions are checked in tun_copy_filter() before
>> anything is replaced: a TUNATTACHFILTER that the classic verifier rejects
>> now fails early and leaves the previously attached filter in place. The
>> instructions are verified with the new bpf_prog_validate_classic(), which
>> runs bpf_check_basics_ok() and bpf_check_classic() on the copied program.
>> That does not cover the s16 check in bpf_convert_filter() or the runtime
>> selection in bpf_prog_select_runtime(), so a program can still be refused
>> when it is built for a queue. Such a late failure does not replace the
>> saved program: the copy is committed to tun->fprog and tun->fprog_kern
>> only after tun_attach_filter() installed it on every queue. A queue that
>> already took the copy is rolled back, which leaves it without a filter,
>> because the program it ran before was already released; restoring it
>> would need an extra reference per queue.
>>
>> The inverted check was discovered by manual code inspection first.
>>
>> Fixes: 54f968d6efdb ("tuntap: move socket to tun_file")
>> Link: https://lore.kernel.org/netdev/179309578401.3134.11519975030981154705@kernel.org/
>> Signed-off-by: Rongguang Wei <weirongguang@kylinos.cn>
>> ---
>> drivers/net/tun.c | 125 +++++++++++++++++++++++++++++++++++++----
>> include/linux/filter.h | 2 +
>> net/core/filter.c | 67 ++++++++++++++++++++++
>> 3 files changed, 182 insertions(+), 12 deletions(-)
>>
>> diff --git a/drivers/net/tun.c b/drivers/net/tun.c
>> index 242899f7fd071..13ce62aa0f142 100644
>> --- a/drivers/net/tun.c
>> +++ b/drivers/net/tun.c
>> @@ -197,6 +197,7 @@ struct tun_struct {
>> int sndbuf;
>> struct tap_filter txflt;
>> struct sock_fprog fprog;
>> + struct sock_fprog_kern fprog_kern;
>> /* protected by rtnl lock */
>> bool filter_attached;
>> u32 msg_enable;
>> @@ -722,12 +723,67 @@ static void tun_force_wake_queue(struct tun_struct *tun,
>> spin_unlock_bh(&tfile->tx_ring.consumer_lock);
>> }
>>
>> +/* Copy the filter that @argp points at into the kernel, so that it can be
>> + * installed again later, independent of the ioctl caller's address space.
>> + * @fprog_kern receives the copy, which belongs to the caller until it is
>> + * installed or freed, and @fprog receives the header, with the user pointer
>> + * kept for TUNGETFILTER.
>> + */
>> +static int tun_copy_filter(struct sock_fprog __user *argp,
>> + struct sock_fprog *fprog,
>> + struct sock_fprog_kern *fprog_kern)
>> +{
>> + struct sock_fprog fp;
>> + struct sock_filter *insns;
>> + int err;
>> +
>> + if (copy_from_user(&fp, argp, sizeof(fp)))
>> + return -EFAULT;
>> +
>> + if (!fp.len || fp.len > BPF_MAXINSNS)
>> + return -EINVAL;
>> +
>> + insns = kmalloc_array(fp.len, sizeof(struct sock_filter),
>> + GFP_KERNEL_ACCOUNT);
>> + if (!insns)
>> + return -ENOMEM;
>> +
>> + if (copy_from_user(insns, fp.filter,
>> + fp.len * sizeof(struct sock_filter))) {
>> + kfree(insns);
>> + return -EFAULT;
>> + }
>> +
>> + /* The instructions are only verified when a program is built from
>> + * them, which happens when the filter is attached to a socket. Run
>> + * the classic verifier here as well, so that a bad program is
>> + * refused before it is saved and before any queue is touched.
>> + * bpf_prog_create() can still refuse the program later, when it is
>> + * converted to eBPF or when no runtime can be selected for it; the
>> + * attach path returns that error, and the saved program is not
>> + * replaced.
>> + */
>> + err = bpf_prog_validate_classic(insns, fp.len);
>> + if (err) {
>> + kfree(insns);
>> + return err;
>> + }
>> +
>> + fprog->len = fp.len;
>> + fprog->filter = fp.filter;
>> + fprog_kern->len = fp.len;
>> + fprog_kern->filter = insns;
>> +
>> + return 0;
>> +}
>> +
>> static int tun_attach(struct tun_struct *tun, struct file *file,
>> bool skip_filter, bool napi, bool napi_frags,
>> bool publish_tun)
>> {
>> struct tun_file *tfile = file->private_data;
>> struct net_device *dev = tun->dev;
>> + bool rollback_filter = false;
>> int err;
>>
>> err = security_tun_dev_attach(tfile->socket.sk, tun->security);
>> @@ -752,10 +808,11 @@ static int tun_attach(struct tun_struct *tun, struct file *file,
>> /* Re-attach the filter to persist device */
>> if (!skip_filter && (tun->filter_attached == true)) {
>> lock_sock(tfile->socket.sk);
>> - err = sk_attach_filter(&tun->fprog, tfile->socket.sk);
>> + err = sk_attach_filter_kern(&tun->fprog_kern, tfile->socket.sk);
>> release_sock(tfile->socket.sk);
>> - if (!err)
>> + if (err)
>> goto out;
>> + rollback_filter = true;
>> }
>>
>> if (!tfile->detached &&
>> @@ -817,6 +874,17 @@ static int tun_attach(struct tun_struct *tun, struct file *file,
>> WRITE_ONCE(tun->numqueues, tun->numqueues + 1);
>> tun_set_real_num_queues(tun);
>> out:
>> + if (err && rollback_filter) {
>> + /* The filter was installed before the rest of the attach
>> + * ran, so drop it again if a later step failed. Any
>> + * program it replaced is already gone: this leaves the
>> + * queue without a filter rather than restoring the
>> + * previous one.
>> + */
>> + lock_sock(tfile->socket.sk);
>> + sk_detach_filter(tfile->socket.sk);
>> + release_sock(tfile->socket.sk);
>> + }
>> return err;
>> }
>>
>> @@ -2400,6 +2468,7 @@ static void tun_free_netdev(struct net_device *dev)
>> security_tun_dev_free_security(tun->security);
>> __tun_set_ebpf(tun, &tun->steering_prog, NULL);
>> __tun_set_ebpf(tun, &tun->filter_prog, NULL);
>> + kfree(tun->fprog_kern.filter);
>> }
>>
>> static void tun_setup(struct net_device *dev)
>> @@ -3064,10 +3133,14 @@ static void tun_detach_filter(struct tun_struct *tun, int n)
>> release_sock(tfile->socket.sk);
>> }
>>
>> + kfree(tun->fprog_kern.filter);
>> + tun->fprog_kern.filter = NULL;
>> + tun->fprog_kern.len = 0;
>> tun->filter_attached = false;
>> }
>>
>> -static int tun_attach_filter(struct tun_struct *tun)
>> +static int tun_attach_filter(struct tun_struct *tun,
>> + struct sock_fprog_kern *fprog_kern)
>> {
>> int i, ret = 0;
>> struct tun_file *tfile;
>> @@ -3075,15 +3148,29 @@ static int tun_attach_filter(struct tun_struct *tun)
>> for (i = 0; i < tun->numqueues; i++) {
>> tfile = rtnl_dereference(tun->tfiles[i]);
>> lock_sock(tfile->socket.sk);
>> - ret = sk_attach_filter(&tun->fprog, tfile->socket.sk);
>> + ret = sk_attach_filter_kern(fprog_kern, tfile->socket.sk);
>> release_sock(tfile->socket.sk);
>> - if (ret) {
>> - tun_detach_filter(tun, i);
>> - return ret;
>> - }
>> + if (ret)
>> + goto out_detach;
>> }
>>
>> tun->filter_attached = true;
>> + return 0;
>> +
>> +out_detach:
>> + /* Drop the copies already installed on queues 0..i-1, which leaves
>> + * them without a filter: their previous sk_filter, if any, was
>> + * already replaced and released. The caller does not replace the
>> + * saved program when this fails, so tun->fprog, tun->fprog_kern and
>> + * filter_attached keep their previous values.
>> + */
>> + while (i--) {
>> + tfile = rtnl_dereference(tun->tfiles[i]);
>> + lock_sock(tfile->socket.sk);
>> + sk_detach_filter(tfile->socket.sk);
>> + release_sock(tfile->socket.sk);
>> + }
>> +
>> return ret;
>> }
>>
>> @@ -3418,17 +3505,31 @@ static long __tun_chr_ioctl(struct file *file, unsigned int cmd,
>> tun_set_sndbuf(tun);
>> break;
>>
>> - case TUNATTACHFILTER:
>> + case TUNATTACHFILTER: {
>> + struct sock_fprog_kern fprog_kern;
>> + struct sock_fprog fprog;
>> +
>> /* Can be set only for TAPs */
>> ret = -EINVAL;
>> if ((tun->flags & TUN_TYPE_MASK) != IFF_TAP)
>> break;
>> - ret = -EFAULT;
>> - if (copy_from_user(&tun->fprog, argp, sizeof(tun->fprog)))
>> +
>> + ret = tun_copy_filter(argp, &fprog, &fprog_kern);
>> + if (ret)
>> + break;
>> +
>> + ret = tun_attach_filter(tun, &fprog_kern);
>> + if (ret) {
>> + kfree(fprog_kern.filter);
>> break;
>> + }
>>
>> - ret = tun_attach_filter(tun);
>> + /* Install the copy only after every queue accepted it. */
>> + kfree(tun->fprog_kern.filter);
>> + tun->fprog_kern = fprog_kern;
>> + tun->fprog = fprog;
>
> struct sock_fprog has padding between @len and @filter (6 bytes on
> 64-bit). Because @fprog is uninitialized on the stack and
> tun_copy_filter() only writes its two fields, `tun->fprog = fprog` can
> copy uninitialized kernel stack bytes into tun->fprog, which
> TUNGETFILTER then leaks to userspace via copy_to_user(...,
> sizeof(tun->fprog)) when CONFIG_INIT_STACK_ALL_ZERO is not enabled
> (KMSAN kernel-infoleak).
>
> Zero-initialize @fprog (e.g. `struct sock_fprog fprog = {};` or
> memset() in tun_copy_filter()).
Thanks Willem.
In v9, the zero-initializes the caller's header in tun_copy_filter() before the
two fields are assigned:
memset(fprog, 0, sizeof(*fprog));
so the padding that TUNGETFILTER copies is zero instead of uninitialized
stack. Everything else about the header is unchanged.
next prev parent reply other threads:[~2026-10-29 1:46 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 9:32 [PATCH net v8 0/2] tun: fix re-attaching the socket filter Rongguang Wei
2026-10-28 9:32 ` [PATCH net v8 1/2] tun: keep a kernel copy of the socket filter program Rongguang Wei
2026-10-28 15:54 ` Willem de Bruijn
2026-10-29 1:46 ` Rongguang Wei [this message]
2026-10-28 9:32 ` [PATCH net v8 2/2] selftests: net: add TAP socket filter attach tests Rongguang Wei
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=5f3ce099-1074-4416-b758-276aa9112bc3@163.com \
--to=clementwei90@163.com \
--cc=andrew+netdev@lunn.ch \
--cc=davem@davemloft.net \
--cc=jasowangio@gmail.com \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=weirongguang@kylinos.cn \
--cc=willemdebruijn.kernel@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox