Kai Ole Hartwig
23 min read
By

Linux kernel CVE-2026-31695: A virtual Wi-Fi device points to a device that no longer exists

The virt_wifi driver makes an ordinary network connection look like Wi-Fi and sits on top of an existing network interface to do so. Until the fix, it also registered that interface as its parent in the kernel's device model, without the reference keeping it alive. If the interface is deleted while ethtool is working on the virtual device, the kernel accesses memory that has already been freed. On 2 November, Alexander Popov showed that this leads to root on a Fedora server in its default installation, for any local user who is allowed to create user namespaces. What makes the case instructive is less the single driver than the path to it. A module that no server needs loads itself there on request, and the fix had been available since May when the exploit put the flaw in the spotlight.

TL;DR — 90 seconds

A reference that does not hold on to its target

In the kernel every network device is represented twice. As a struct net_device it belongs to the network stack, and embedded in it is a struct device through which it hangs in the general device model, the same hierarchy as PCI cards or USB devices. The parent field of this structure says which device a device hangs from. Network drivers set it with the SET_NETDEV_DEV() macro; for a real network card it points to the PCI device. Power management uses this reference to wake up the parent device before an access.

virt_wifi is a helper driver that, according to Kconfig, makes Ethernet connections appear as Wi-Fi connections through a special rtnetlink device. You create a link of type virt_wifi on an existing device, the so-called lower device. The driver registers itself as the receiver of that device's packets and links itself to it as an upper device. Until the fix it also called SET_NETDEV_DEV(dev, &priv->lowerdev->dev) in virt_wifi_newlink(), registering the lower device as its parent in the device model as well.

That is exactly where the bug lay. The reference in dev.parent did not keep the lower device alive. When the lower device is deleted, netdev_run_todo() tears down the virt_wifi device attached to it in the same pass and frees the lower device. If an ethtool command is running on the virt_wifi device at that moment, for example a feature change via the netlink interface, ethnl_ops_begin() first calls pm_runtime_get_sync(dev->dev.parent). Only afterwards does the function check whether the device is being unregistered. The kernel therefore works with a struct device whose memory has already been freed and may have been reallocated. Popov found the spot with a customised syzkaller, and KASAN reported a slab-use-after-free in __pm_runtime_resume().

Why an unprivileged user can reach this point at all is explained by two mechanisms that are each intended. Creating a link requires CAP_NET_ADMIN, but the kernel checks this capability against the respective network namespace. Anyone who creates their own user namespace is root inside it and may create devices in an associated network namespace without holding any privilege on the host. According to Popov, that is enough to trigger the bug. The second mechanism is automatic module loading. If rtnetlink finds no driver for the requested link type, the kernel calls request_module("rtnl-link-%s"), and virt_wifi registers for exactly that name via MODULE_ALIAS_RTNL_LINK("virt_wifi"). There is no further permission check in this path, which I verified in the current source of net/core/rtnetlink.c. A driver nobody needs on a server is thus loaded as soon as someone asks for it in their own namespace.

The bug has been in the kernel since commit d43c65b05b84, titled “ethtool: runtime-resume netdev parent in ethnl_ops_begin”, and according to OSV it affects all versions from 5.15. That commit added the wake-up of the parent device in ethnl_ops_begin(), and only then did virt_wifi's unprotected reference become dangerous. The flaw therefore arose from the interplay of two changes, each of which looks reasonable on its own.

Changes in the fix commit

Popov's fix commit 789b06f9f39c is called “wifi: virt_wifi: remove SET_NETDEV_DEV to avoid use-after-free” and is as short as the title promises. It removes the call to SET_NETDEV_DEV() from virt_wifi_newlink() in drivers/net/wireless/virtual/virt_wifi.c. A virt_wifi device then no longer has a parent device, ethnl_ops_begin() finds nothing in dev.parent and skips the wake-up. The connection to the lower device remains through the upper-device link and the packet handler, which the driver explicitly removes during teardown.

Popov had first proposed a different approach. His first patch moved the check for an unregistered device in ethnl_ops_begin() ahead of the call to pm_runtime_get_sync(). The maintainers did not want to touch the shared ethtool path and chose the change in the driver. In the current mainline source the order in ethnl_ops_begin() is still as before, wake-up first, then the check. I conclude from this that the fix removes the cause only for this one driver. Another driver that registers a parent which can disappear before it would reopen the same class of bug. Popov himself still considers the changed order in ethtool a sensible defence.

From your own namespace to root on the host

An attack needs three things: a kernel without the fix, virt_wifi as a loadable module and permission for unprivileged users to create user namespaces. Popov lists CONFIG_USER_NS=y, CONFIG_VETH=m and CONFIG_VIRT_WIFI=m for his setup, and his target was Fedora 44 Server with kernel 6.19.10 in its default installation. Out of the box, you are affected if your distribution ships the driver as a module and does not restrict user namespaces. For Amazon Linux, ALAS lists the default kernel and kernel6.18 on Amazon Linux 2023 as affected. kernel6.12 on Amazon Linux 2023 and all Amazon Linux 2 kernels are listed as not affected; ALAS gives no reason.

Conceptually the attack works like this. The attacker creates their own user and network namespace, creates a pair of virtual Ethernet devices inside it and puts a virt_wifi device on top of one of them. They then delete the lower device and, at the same moment, send an ethtool command to the virt_wifi device. To make the race reliable, Popov increased contention on the lock the ethtool path waits for and filled the freed memory with his own data in the meantime. When the kernel follows the stale pointer, it reads a forged struct device. The rest was done by a chain that defeats kernel ASLR through a side channel, bypasses SMEP and SMAP and finally redirects the path the kernel uses to start helper programs for module loading. SELinux initially stopped the naive last step on Fedora, after which Popov disabled it inside the chain. The slab hardening options CONFIG_RANDOM_KMALLOC_CACHES and CONFIG_SLAB_BUCKETS did not help here, because Popov filled the memory through network buffers that these protections do not cover.

The realistic damage is root on the host, and on a Kubernetes node that means escaping the container. Anyone who runs code in a pod, for example through a flaw in a web application, then gets everything the node can see: the secrets of all pods on that node, the kubelet's credentials and, on AWS, the instance role. The precondition remains that the attacker already runs code on the system. How quickly such a flaw can be attached to a remote flaw was just shown by PHP2Shell, which attackers chained with the kernel flaw Fragnesia to get root on firewalls.

You are really affected if foreign or network-reachable code runs on a host with an affected kernel and all three preconditions are met. That applies to Kubernetes nodes with pods running without seccomp, to build runners that execute third-party pipelines and to multi-user systems. You can stand down, on the other hand, for hosts whose kernel falls in the affected range but where no process can create user namespaces. That includes every pod under the Pod Security Standard “restricted”. It runs with the default seccomp profile, can therefore call neither unshare nor clone with new namespaces and so never reaches the bug. You are equally safe if your kernel does not contain virt_wifi as a module at all. The container image plays no role, because the bug lives in the host kernel.

CVEAffectedFixPreconditionCVSS
CVE-2026-31695Linux 5.15 before 5.15.203, 5.16 before 6.1.168, 6.2 before 6.6.134, 6.7 before 6.12.81, 6.13 before 6.18.22, 6.19 before 6.19.12; Amazon Linux 2023 kernel and kernel6.18Commit 789b06f9f39c; 5.15.203 / 6.1.168 / 6.6.134 / 6.12.81 / 6.18.22 / 6.19.12; ALAS2023-2026-1681 and -1746Local code execution, unprivileged user namespaces, CONFIG_VIRT_WIFI=m7.8 (high)

The exploitation picture is straightforward. Popov described his exploit in detail on 2 November, and according to his article it reaches root on Fedora 44 Server. No attacks in the wild have been reported, and the flaw was not in the CISA KEV catalog on 4 November. Its EPSS score on the same day was 0.00126, about 0.13 percent, and is based on the period before the public exploit. According to press reports Red Hat rates the flaw as low, while Amazon and kernel.org rate it high at 7.8. For your host, what ultimately counts is less the rating than whether the module and user namespaces are available there.

Kernel fix and distribution packages

The only complete mitigation is a kernel with commit 789b06f9f39c. Upstream, according to OSV, it is included in 5.15.203, 6.1.168, 6.6.134, 6.12.81, 6.18.22 and 6.19.12. Only a reboot activates the new kernel; until then the old code keeps running on the host. I found no livepatch for this CVE for Amazon Linux.

On Amazon Linux 2023, Amazon fixed the default kernel on 9 May with ALAS2023-2026-1681; the x86_64 version is kernel-6.1.168-202.320.amzn2023. The advisory gives dnf update --advisory ALAS2023-2026-1681 --releasever 2023.11.20260509 for this. For kernel6.18, ALAS2023-2026-1746 followed on 26 May with kernel6.18-6.18.25-52.107.amzn2023, installed with dnf update --advisory ALAS2023-2026-1746 --releasever 2023.11.20260526. The --releasever parameter is not a detail. Amazon Linux 2023 pins a host to a fixed repository version, and a host on an older version only finds the new package with the version given. On EKS nodes, instead of updating via dnf, you replace the AMI with one that has a patched kernel and roll the nodes. For Amazon Linux 2, ALAS lists all kernels as not affected.

In Wolfi and other container base images there is nothing to do, because they do not ship a kernel. A rebuilt image does not close the flaw, and a scanner that only checks images does not see it at all. The check therefore has to start at the host. The fix brings hardly any behaviour changes. A virt_wifi device no longer has a parent device afterwards, which in my view only matters for tools that evaluate the device hierarchy under /sys. In normal server operation you will not notice it.

Hardening beyond the patch

No measure in the container image works against a kernel flaw, but anything that cuts off the path to the faulty code or makes exploitation harder does. I therefore sort the measures into three classes. The first takes the driver away from the attacker, the second takes away the privileges with which the attacker reaches the driver in their own namespace, and the third raises the cost of the techniques that turn the memory bug into root. The first two close this specific path completely, the third has a broader but weaker effect.

Blocking the virt_wifi module

The cheapest option is not to let the driver load at all. According to the modprobe.d(5) man page, blacklist virt_wifi makes modprobe ignore the module's internal aliases, and automatic loading runs through exactly such an alias, rtnl-link-virt_wifi. install virt_wifi /bin/false is more thorough, because modprobe then runs the given command instead of loading, even when called directly with the module name. Both belong in a file under /etc/modprobe.d/, on Kubernetes nodes in the bootstrap configuration or a custom AMI. The effort is small, since this driver has no job on an ordinary server. The limit is just as clear. If the module is already loaded or built into the kernel, the entry does not help, and it only protects against this one driver.

No module loading after boot

Popov himself recommends the broader variant. According to the kernel documentation, with kernel.modules_disabled=1 modules can be neither loaded nor unloaded, and the toggle cannot be reset until the next reboot. Popov suggests a systemd unit of type oneshot that sets the value after multi-user.target, once all required modules are loaded. This removes the whole class of flaws in rarely used drivers that only become reachable through automatic loading, virt_wifi included. According to Popov, the same lock also prevents the technique his exploit uses to get root in the end.

On Kubernetes nodes the price is higher than on a single web server. CNI plugins, kube-proxy and storage drivers often load netfilter, IPVS or filesystem modules on demand, and after the lock that fails. You therefore need to work out beforehand which modules a node needs in operation, load them at boot and introduce new features with a reboot from then on. I derive this from how the toggle works; I do not know of a ready-made module list for EKS nodes. Popov considers the weakest variant, setting kernel.modprobe to an empty value, insufficient, because the old path can be restored.

User namespaces for unprivileged users

The attack needs CAP_NET_ADMIN in its own network namespace, and an unprivileged user only gets that through their own user namespace. According to the kernel documentation, the sysctl user.max_user_namespaces sets how many user namespaces any user in the current namespace may create. With a value of 0 this option disappears on the entire host, and with it the entry point for this exploit. The measure reaches far beyond this CVE, because user namespaces make network, netfilter and filesystem code reachable for unprivileged users that is otherwise only open to root.

The operational cost lies with everything that uses user namespaces legitimately. That includes rootless Podman and rootless BuildKit on build runners, the sandbox of Chromium-based browsers in test jobs and pods with hostUsers: false, which Kubernetes starts in their own user namespaces. On a Kubernetes node that starts containers exclusively through containerd as root, the restriction usually goes unnoticed, while on a GitLab Runner with rootless builds it shows up immediately. I derive this from how these tools work, so I would roll the setting out on a test node first.

seccomp with the RuntimeDefault profile for every pod

On Kubernetes nodes the path can also be cut more selectively, for containers rather than for the whole host. containerd's default profile only allows unshare, setns, clone3 and mount to processes with CAP_SYS_ADMIN, and clone only without the flags for new namespaces, including CLONE_NEWUSER and CLONE_NEWNET. A process in the container can therefore create neither a user namespace nor a network namespace and never gets near virt_wifi_newlink(). I checked this in contrib/seccomp/seccomp_default.go in the containerd source; Docker's default profile behaves the same way.

The trap is the Kubernetes default, because without a setting pods run as Unconfined, without any seccomp at all. Two places fix this. In the kubelet configuration, seccompDefault: true applies the RuntimeDefault profile to all pods that do not specify one themselves. The Pod Security Standard “restricted”, enforced via the label pod-security.kubernetes.io/enforce: restricted on the namespace, additionally requires seccompProfile.type set to RuntimeDefault or Localhost, drops all capabilities except NET_BIND_SERVICE and requires allowPrivilegeEscalation: false and runAsNonRoot: true. Where pods need CAP_SYS_ADMIN or run privileged, such as CSI drivers or CNI agents, the protection does not apply, and these pods belong on a short, justified exception list. An admission policy with Kyverno or as a ValidatingAdmissionPolicy keeps that list short. How Pod Security, namespaces and exceptions fit together cleanly for TYPO3 is covered in the series TYPO3 on Kubernetes under IT-Grundschutz.

Raising the cost of the exploit itself

The third class targets the technique rather than the access. Popov names the boot parameter init_on_free=1, which fills freed memory with zeros immediately. In his tests this turned the silent use-after-free into a kernel warning, but the privilege escalation still succeeded. According to Popov, Page Table Isolation makes reading kernel addresses harder, although other ways exist, and strong control-flow integrity would block the code reuse his chain depends on. His tool kernel-hardening-checker checks which of these options your kernel has, based on the kernel configuration, boot parameters and sysctls. On a distribution kernel you can only set boot parameters and sysctls yourself; CFI and slab hardening depend on the configuration the distribution builds with. I therefore see this class as a complement that raises the price of the next exploit but replaces neither of the first two.

What does not help here

A new container image, an updated Wolfi package or an image scan changes nothing, because the faulty code runs in the host kernel. runAsNonRoot and a read-only root filesystem do not protect on their own either, because the attack is built for an unprivileged user from the start, and it finds a writable directory such as /tmp in almost every pod. A NetworkPolicy does not help, because the attack runs locally and only uses netlink in its own namespace. SELinux stopped Popov's first attempt on Fedora, but the finished chain disables it beforehand. According to Popov, the slab hardening options CONFIG_RANDOM_KMALLOC_CACHES and CONFIG_SLAB_BUCKETS prevented nothing in this case. And blocking the veth module, which Popov used as the lower device, is not an option on Kubernetes nodes, because the CNI needs it for pod networking.

Detection and verification

You can find out whether a host is affected in three steps. First compare the running kernel from uname -r with the fixed versions, and on Amazon Linux 2023 also the installed packages from rpm -q kernel kernel6.18 with the versions in the ALAS advisories. An installed but not yet booted package does not protect you. Then check whether the driver is present at all: modinfo virt_wifi shows the module or reports that it is missing, and lsmod shows whether it is loaded. Finally, sysctl user.max_user_namespaces shows whether user namespaces are allowed, and /proc/sys/kernel/modules_disabled whether modules may still be loaded. On Kubernetes there is the additional question of which pods run without seccomp, meaning those without a seccompProfile in their securityContext, as long as seccompDefault is not active in the kubelet.

There are no documented indicators from real attacks. I derive three traces from the mechanism. A loaded virt_wifi module on a server without Wi-Fi testing is suspicious in itself, because apart from an attack or a test nobody loads it there. An audit rule on the system calls init_module and finit_module logs every module load, including automatic ones. Kernel messages with __pm_runtime_resume or ethnl_ops_begin in the call trace fit a failed attempt, and with init_on_free Popov observed a warning from kernel/workqueue.c. On Kubernetes nodes, processes in containers that unexpectedly create namespaces also stand out, which runtime tools such as Falco or Tetragon can report.

If you suspect a compromise, take the node out of service with kubectl cordon and kubectl drain, secure the logs and replace it with a new node running a patched kernel instead of repairing it. Then rotate everything the node could see: the secrets of the pods that ran there, the kubelet certificates and the credentials of the instance role.

Recommendation for operators

Act now if foreign or network-reachable code runs on a host with an affected kernel and unprivileged user namespaces are allowed. This mainly concerns Kubernetes nodes on Amazon Linux 2023 with kernel or kernel6.18 before the versions above, where pods run without seccomp, and build runners for third-party pipelines. Install the kernel there today and reboot, or roll the nodes with a new AMI. Until then, block virt_wifi via modprobe.d, which costs practically nothing.

Monitoring is enough if the host runs Amazon Linux 2 or kernel6.12 on Amazon Linux 2023, the module is missing or user namespaces are already disabled. Even then the kernel belongs in the next maintenance window, because configurations change and the public exploit serves others as a template.

That this fix has been available since May and is only getting attention now shows where the real gap in operations lies. Kernel updates should be automated like any other update. Renovate or Dependabot propose new AMI versions, node images and the Helm charts of your node management as merge requests, the pipeline rolls them out to a staging cluster first, and Karpenter or Kured then replace or reboot the nodes in an orderly way. The kernel from the May advisory would then have been in the cluster within hours, not only when the exploit appeared in November. A cooldown for routine updates protects against compromised releases, and security updates are exempt from it. If a new kernel breaks something, for example because a module is missing after module loading has been locked, that shows up on staging and not on the production node. Anyone who updates nodes by hand tends to postpone the reboot, during holidays by weeks, and later catches up with a big jump across several kernel versions, which is far riskier than many small ones.

Frequently asked questions about CVE-2026-31695

Am I affected on Amazon Linux 2?+

According to the Amazon Linux Security Center, no: all Amazon Linux 2 kernels are listed as not affected. On Amazon Linux 2023, kernel and kernel6.18 are affected and fixed with ALAS2023-2026-1681 and -1746, while kernel6.12 is listed as not affected.

Does an up-to-date container image protect me?+

No. The bug lives in the host kernel, and container images such as Wolfi's do not ship a kernel. You have to update the kernel on the node and reboot, or replace the node with a new AMI.

Is blocking virt_wifi enough?+

For this specific flaw, yes, as long as the module is not already loaded or built into the kernel. It still does not replace the kernel fix, and against the next flaw in another driver only the broader lock on module loading or on user namespaces helps.

Why does a flaw from May matter now?+

Because on 2 November Alexander Popov showed that it reliably leads to root and described his approach in detail. The low EPSS score dates from before that. If you have not installed the kernel from May yet, do it now.

Will user.max_user_namespaces=0 break my cluster?+

On a node that starts containers only through containerd as root, usually not. Rootless builds with Podman or BuildKit, browser sandboxes in test jobs and pods with hostUsers: false do need user namespaces, though, so try the setting on a test node first.

Conclusion

CVE-2026-31695 is a small bug with a big lesson. A reference between two objects that does not secure the lifetime of its target becomes a use-after-free as soon as a second path reads it at the wrong moment, and here that path arrived through a change in ethtool that was harmless on its own. For operations, the second lesson weighs more. The bug only became reachable because a server loads a driver it never needs, on behalf of a user who is allowed to play root in their own namespace. You close flaws of this kind for good by keeping the kernel's attack surface small: modules only where they are needed, user namespaces only where they are wanted, and seccomp for every container. And because the fix came half a year before the exploit, every operator with automated kernel updates was long protected when the headlines appeared.

Sources

I check your Kubernetes nodes for vulnerable kernels, lock down unused modules and user namespaces and enforce seccomp for every pod.

Kernel and node audits for EKS and k3s, Pod Security “restricted” with admission policies, automated AMI and kernel updates with Renovate and orderly node rollouts.

Platform operations instead of advice on paper: I continuously check, patch and harden your infrastructure.

About the author

Photo of Kai Ole Hartwig.

Kai Ole Hartwig

Freelance DevSecOps consultant · OnlyOle Consulting

Programming since 2002 – self-taught, set up my own business with KO-Web in 2012. Over 100 projects, with a focus on security, performance, automation and quality. Today freelance: DevSecOps consulting, training and software development.