
19+ Years Hidden, $80,000+ Rewarded: Reporting a Linux Kernel Zero-Day for Google kernelCTF: CVE-2026-43456
We are Yuki Koike, Senior Executive Officer/CTO, and Kota Toda, a part-time member of the Advanced Analysis Division.
The vulnerability CVE-2026-43456, which the two of us had reported to the Linux kernel, has now been fixed and is ready for public disclosure. We would therefore like to introduce the details in this blog post.
The most notable characteristic of this vulnerability is that the code at the root cause was merged into the Linux kernel in 20071, and for nearly 19 years it went unnoticed as an exploitable vulnerability.
In addition, due to the characteristics of this vulnerability, an exploit using it can complete reliably within one second with a success rate of over 99%.
In light of this, we submitted this vulnerability to Google’s bug bounty event kernelCTF2 and received a reward of more than $80,000.

In this blog post, we explain the root cause of CVE-2026-43456 and how we achieved privilege escalation.
Conditions and Impact Scope
- Impacted versions: Linux 2.6.24 through 6.12.77
- Subsystem:
net/bonding - Cause: type confusion
- Introducing commit:
1284cd3a2b740d0118458d2ea470a1e5bc19b187 - Fix commit:
950803f7254721c1c15858fbbfae3deaaeeecb11 - Triggering requires
CAP_NET_ADMIN
As can be seen from the affected Linux kernel versions, this vulnerability had an extremely broad impact.
Mitigation
This vulnerability was fixed in March 2026, and it is expected to be fixed in the latest versions of major Linux distributions. Therefore, the most effective mitigation is to update to the latest version.
However, some distributions or versions may still not have been fixed. If you are using such an environment, or if you are unable to update for some reason, you can disable the vulnerability by taking either of the following mitigation measures:
- Set
/proc/sys/kernel/unprivileged_userns_cloneto0- This prevents unprivileged users from obtaining
CAP_NET_ADMIN. As a trade-off, features such as Rootless Docker are likely to become unavailable.
- This prevents unprivileged users from obtaining
- Disable the bonding feature
- Since the vulnerability exists in the network feature called bonding, disabling this feature eliminates the impact.
- In most distributions, bonding is provided as a module, so it can be removed with
rmmod. Environments where bonding is enabled by default are rare in the first place.
In other words, as with CopyFail, the following can be used as a mitigation:
echo "install bonding /bin/false" > /etc/modprobe.d/disable-bonding.conf
rmmod bonding 2>/dev/null
Vulnerability Details
Background Knowledge
Before explaining the vulnerability in detail, we will first introduce some background knowledge.
skb
In the Linux kernel network stack, packets are handled as struct sk_buff objects, hereafter referred to as skb.
skb->head skb->data skb->tail skb->end, skb_shinfo(skb)
| | | |
v v v v
+----------------+-----------------+------------------+------------------+
| headroom | packet data | tail room | skb_shared_info |
+----------------+-----------------+------------------+------------------+
skb->head points to the beginning of the allocated buffer, skb->data points to the beginning of the current packet data, and skb->tail points to the end of the current packet data.skb->data - skb->head is the headroom.
In addition, struct skb_shared_info is placed at the end of the skb buffer.struct skb_shared_info::flags stores the state of the skb, such as whether zerocopy is enabled.
Bonding
Bonding is a Linux networking feature that makes it possible to treat multiple network interfaces as a single interface. The interface that is created is called a bond device, and the lower-level interfaces belonging to the bond are called slave devices.
Root Cause
This vulnerability is what is commonly called a type confusion vulnerability.
The following is the code that actually creates a bond device, and the vulnerability is caused by the single line marked with ★:
static void bond_setup_by_slave(struct net_device *bond_dev,
struct net_device *slave_dev)
{
bool was_up = !!(bond_dev->flags & IFF_UP);
dev_close(bond_dev);
bond_dev->header_ops = slave_dev->header_ops; ★
bond_dev->type = slave_dev->type;
bond_dev->hard_header_len = slave_dev->hard_header_len;
bond_dev->needed_headroom = slave_dev->needed_headroom;
bond_dev->addr_len = slave_dev->addr_len;
header_ops is a function pointer table that groups together functions for handling packet headers of a certain protocol.
A bond device is designed and implemented so that it can transparently handle lower-level slave devices. Therefore, “the processing that a bond performs on headers == the processing that the lower-level device performs on headers,” and at first glance, it seems reasonable to reuse the functions as-is.
However, some of these functions proceed by referencing and modifying the device’s private storage area.
The storage area held by a bond device has a different type from the storage area held by a slave device and is not compatible with it. As a result, this one line alone already creates a situation where type confusion can occur.
Let’s also confirm in the code that these areas are in fact incompatible and can cause problems.
First, on the bond device side, an area of sizeof(struct bonding) bytes is allocated and assigned to dev->priv, the storage area mentioned above.
struct rtnl_link_ops bond_link_ops __read_mostly = {
.kind = "bond",
.priv_size = sizeof(struct bonding),
.setup = bond_setup,
.maxtype = IFLA_BOND_MAX,
dev = kvzalloc(struct_size(dev, priv, sizeof_priv),
GFP_KERNEL_ACCOUNT | __GFP_RETRY_MAYFAIL);
On the other hand, in GRE, the protocol we used in the exploit, dev->priv is used as follows.
static inline void *netdev_priv(const struct net_device *dev)
{
return (void *)dev->priv;
}
struct ip_tunnel *t = netdev_priv(dev);
struct bonding and struct ip_tunnel look like the following structures, and it is obvious that they are not compatible:
struct bonding {
struct net_device *dev; /* first - useful for panic debug */
struct slave __rcu *curr_active_slave;
struct slave __rcu *current_arp_slave;
struct slave __rcu *primary_slave;
struct bond_up_slave __rcu *usable_slaves;
struct bond_up_slave __rcu *all_slaves;
...
struct ip_tunnel {
struct ip_tunnel __rcu *next;
struct hlist_node hash_node;
struct net_device *dev;
netdevice_tracker dev_tracker;
struct net *net; /* netns for packet i/o */
unsigned long err_time; /* Time when the last ICMP error
* arrived */
...
As a result, when a GRE device is connected as a slave device and GRE header processing is performed against the bond device, it can lead to memory corruption and similar issues.
Privilege Escalation Using the Vulnerability
In kernelCTF, participants are required to disclose all details of their exploit methodology for educational purposes, and the methodology of the exploit we submitted has also become public knowledge.
For that reason, while we will avoid disclosing the complete details here, we would still like to briefly explain the contents.
Although the root cause was a simple one-line issue, abusing it to build a stable exploit requires a complex series of steps, as described below, and there are many things that must be carefully considered.
Also, by looking at the following, we can understand why such a simple and seemingly very dangerous vulnerability remained undiscovered for 19 years.
Step 1: KASLR Leak
Like userland, the Linux kernel has a feature called KASLR that randomizes addresses.
Therefore, in order to execute an exploit stably, it is necessary to identify the randomized address. This process is called a leak.
This time, we used a protocol called IP6GRE to perform the leak.
As mentioned earlier, this vulnerability causes type confusion in dev->priv. For a bond, dev->priv is struct bonding; for IP6GRE, dev->priv is struct ip6_tnl.
struct bonding::recv_probe is located at offset 0x38 in the bonding structure.
On the other hand, from the IP6GRE side, struct ip6_tnl::parms.laddr is also read from offset 0x38.
Because this is a field that stores the IPv6 source address, this value is included in received packets.
/* offset | size */ type = struct ip6_tnl {
/* 0x0000 | 0x0008 */ struct ip6_tnl *next;
...
/* 0x0034 | 0x0004 */ __u32 flags;
/* 0x0038 | 0x0010 */ struct in6_addr {
/* 0x0038 | 0x0010 */ union {
/* 0x0010 */ __u8 u6_addr8[16];
/* 0x0010 */ __be16 u6_addr16[8];
/* 0x0010 */ __be32 u6_addr32[4];
/* total size (bytes): 16 */
} in6_u;
/* total size (bytes): 16 */
} laddr;
/* offset | size */ type = struct bonding {
/* 0x0000 | 0x0008 */ struct net_device *dev;
...
/* 0x0038 | 0x0008 */ int (*recv_probe)(const struct sk_buff *, struct bonding *, struct slave *);
As shown above, recv_probe is a function pointer, and from the following code, we can see that it actually contains the address of bond_rcv_validate:
if (bond->params.arp_interval) {
queue_delayed_work(bond->wq, &bond->arp_work, 0);
bond->recv_probe = bond_rcv_validate;
}
Since bond_rcv_validate is a function that exists in the kernel, leaking it allows us to calculate the kernel base address.
Step 2: Arbitrary Code Execution
Since Step 1 allows us to bypass KASLR, the next objective is to corrupt memory and set the Instruction Pointer to an arbitrary value, that is, to achieve arbitrary code execution.
Step 2.1: Rewriting Flags
To state the conclusion first, for arbitrary code execution, we use GRE, the IPv4 version, instead of IP6GRE.
Specifically, by setting uarg->callback shown in the following code to an arbitrary value, we cause a function call to an invalid address.
static inline struct ubuf_info *skb_zcopy(struct sk_buff *skb)
{
bool is_zcopy = skb && skb_shinfo(skb)->flags & SKBFL_ZEROCOPY_ENABLE;
return is_zcopy ? skb_uarg(skb) : NULL;
}
static inline void skb_zcopy_clear(struct sk_buff *skb, bool success)
{
struct ubuf_info *uarg = skb_zcopy(skb);
if (uarg) // If non-NULL, callback is called
uarg->callback(skb, uarg, success);
}
By improperly rewriting the value of skb_shinfo(skb)->flags, we make a non-NULL value be returned when uarg should have been returned as NULL, thereby achieving an invalid function call.
This may sound a bit abrupt, but rewriting skb_shinfo(skb)->flags is made possible by causing type confusion with greh->flags in the following GRE header_ops function:
static int ipgre_header(struct sk_buff *skb, struct net_device *dev,
unsigned short type,
const void *daddr, const void *saddr, unsigned int len)
{
struct ip_tunnel *t = netdev_priv(dev);
struct gre_base_hdr *greh;
struct iphdr *iph;
...
iph = skb_push(skb, t->hlen + sizeof(*iph));
greh = (struct gre_base_hdr *)(iph + 1);
greh->flags = gre_tnl_flags_to_gre_flags(t->parms.o_flags);
When type confusion is occurring, t points to struct bonding.
At this point, in GRE, t->hlen >= sizeof(*greh) should hold, but in struct bonding, t->hlen == 0.
As a result, skb_push() should move skb->data back by sizeof(struct iphdr) + sizeof(struct gre_base_hdr), but instead it only moves skb->data back by sizeof(struct iphdr).
After that, since greh is set to iph + 1, greh becomes the original value of skb->data. In other words, a buffer overflow occurs at this point.
As mentioned in the background section, skb->data normally points to the beginning of the skb packet data.
However, under specific conditions, the beginning of skb_shared_info overlaps with this location. Specifically, this happens when there is no unused buffer remaining.
With the help of the fact that greh and skb_shared_info have the following structures, under this condition, a write to greh->flags becomes a write to skb_shared_info->flags.
/* offset | size */ type = struct gre_base_hdr {
/* 0x0000 | 0x0002 */ __be16 flags;
...
/* offset | size */ type = struct skb_shared_info {
/* 0x0000 | 0x0001 */ __u8 flags;
...
Incidentally, the value used when writing greh->flags, gre_tnl_flags_to_gre_flags(t->parms.o_flags), is always fixed at 0x7ff. Therefore, this does not affect exploit stability.
The value used when writing greh->flags, t->parms.o_flags, is also read from bonding due to type confusion.t->parms.o_flags is at offset 0x6e, which corresponds to the sixth byte of struct bonding::bond_list.next.
Since this is a kernel pointer, those two bytes are always 0xff 0xff.
As a result, the value after GRE flags conversion is as follows:
gre_tnl_flags_to_gre_flags(0xffff) = 0x07ff
Here, the flag indicating that this skb is zerocopy is set.
SKBFL_ZEROCOPY_ENABLE = BIT(0)
In other words, struct skb_shared_info::flags of an skb that was not originally zerocopy is improperly rewritten, and in later paths it is treated as if it were a zerocopy skb.
Step 2.2: Adjusting skb->data
Earlier, we stated that under certain conditions, skb->data and the beginning of skb_shared_info overlap.
Because the exploit uses memory corruption that occurs under this condition, it is necessary to devise a way to satisfy this condition.
Since skb buffers are allocated aligned to the page size, struct skb_shared_info is a structure that always comes at the end of a page.
The following code allocates an skb buffer.
hlen = LL_RESERVED_SPACE(dev);
tlen = dev->needed_tailroom;
linear = __virtio16_to_cpu(vio_le(), vnet_hdr.hdr_len);
linear = max(linear, min_t(int, len, dev->hard_header_len));
skb = packet_alloc_skb(sk, hlen + tlen, hlen, len, linear,
msg->msg_flags & MSG_DONTWAIT, &err);
For example, when LL_RESERVED_SPACE(dev) is 0x3ec0, the skb buffer becomes exactly 0x4000, and the offset of struct skb_shared_info becomes 0x3ec0.
skb_shinfo(skb) = skb->head + (0x4000 - sizeof(struct skb_shared_info))
= skb->head + 0x3ec0
Also, after sending a packet with len == 0, skb->data also comes to offset 0x3ec0, because there is no packet buffer and it overlaps with skb_shared_info.
Therefore, satisfying the condition can be restated as needing to create a bond device for which LL_RESERVED_SPACE(dev) becomes 0x3ec0, and then sending a packet with len == 0.
LL_RESERVED_SPACE is defined as follows.
#define LL_RESERVED_SPACE(dev) \
((((dev)->hard_header_len + READ_ONCE((dev)->needed_headroom)) \
& ~(HH_DATA_MOD - 1)) + HH_DATA_MOD)
Here, we chose to adjust LL_RESERVED_SPACE by increasing bond->needed_headroom.
Specifically, we create 329 GRE devices and chain them as follows. GRE allows devices to be chained.
if0 <- if1 <- if2 <- ... <- if328
The first eight are GRE with FOU encapsulation, and the rest are plain GRE.
When GRE devices are chained, ip_tunnel_bind_dev() adds the current tunnel->hlen and the lower-level device’s tdev->hard_header_len / tdev->needed_headroom.
int hlen = LL_MAX_HEADER;
int t_hlen = tunnel->hlen + sizeof(struct iphdr);
if (tdev)
hlen = tdev->hard_header_len + tdev->needed_headroom;
dev->needed_headroom = t_hlen + hlen;
The required sizes are as follows.
plain GRE: tunnel->hlen = 0x4, t_hlen = 0x18, dev->hard_header_len = 0x18
FOU GRE: tunnel->hlen = 0xc, t_hlen = 0x20, dev->hard_header_len = 0x20
LL_MAX_HEADER = 0x80
The first eight FOU GRE devices bring the value to 0x260. The needed_headroom of if8 becomes 0x298.
After that, when the remaining 320 plain GRE devices are connected, the needed_headroom of N328 becomes 0x3e98.
When the final GRE device is enslaved to the bond, the bond copies that value.
bond->needed_headroom = 0x3e98
bond->hard_header_len = 0x18
Through the above operations, the bond’s LL_RESERVED_SPACE becomes the intended value, 0x3ec0.
LL_RESERVED_SPACE = align_down(0x3eb0, 0x10) + 0x10 = 0x3ec0
This is why the vulnerability went undiscovered for 19 years.
Unless LL_RESERVED_SPACE is set precisely to a specific value through a very carefully designed device chain, the aforementioned situation where skb->data overlaps with the beginning of skb_shared_info does not occur.
Even when they do not overlap, the buffer overflow itself still occurs. However, due to the fact that the skb is aligned to the page size, the write only occurs into an unused memory region. As a result, it has practically no side effects and does not cause a crash, nor is it detected by memory corruption detection mechanisms such as KASAN.
In fact, we initially came across this vulnerability by chance through a syzkaller crash, and we were only able to discover it by analyzing that crash in depth. This is also why some parts of the explanation above may have seemed somewhat abrupt.
Closing
In this blog post, we explained the details of the vulnerability we discovered.
We hope you were able to get a sense of the essential complexity hidden behind its apparently simple appearance, a complexity befitting a vulnerability that remained undiscovered for 19 years.
As mentioned earlier, we used syzkaller to discover it. Although we tuned its configuration, we did not apply any special modifications such as changing its code, and we were surprised by its ability to uncover a vulnerability hidden in such a deep location.
As described above, there was also a large distance between the code location where the crash occurred, namely the callback invocation, and the code location that was the root cause. As a result, the process of identifying the root cause, or RCA; Root Cause Analysis, was extremely difficult.
In fact, AI helped us a great deal during RCA.
This was in the first half of 2025, but even at that point, the knowledge that frontier models had about OSS such as the Linux kernel was remarkable. Looking back, we feel that this was an early example of the now increasingly common pattern of discovering vulnerabilities together with AI.
That said, if asked whether AI today could identify this vulnerability entirely on its own, even now in 2026, after seeing its remarkable evolution and tremendous capabilities, we remain somewhat skeptical.
While we hope that future advances in AI will bring about a world where complex vulnerabilities like this can be discovered automatically, until that future arrives, we humans intend to continue devoting ourselves to finding and eliminating zero-day vulnerabilities.
- commit: 1284cd3a2b740d0118458d2ea470a1e5bc19b187 ↩︎
- One of the events held by Google as part of its bug bounty program, Google VRP. By demonstrating privilege escalation against the Linux kernel using an exploit for a vulnerability, participants can receive a reward depending on the difficulty of the configuration. For this vulnerability, we successfully demonstrated exploitation under the conditions of high stability, zero-day status, and a custom mitigation. Google’s purpose in running this initiative is to observe what kinds of vulnerabilities and exploits outstanding researchers use to compromise targets, and to use that knowledge as research material for making exploitation more difficult. Ref: https://google.github.io/security-research/kernelctf/rules.html ↩︎
