Cached at:
07/07/26, 10:15 AM
# 19+ Years Hidden, $80,000+ Rewarded: Reporting a Linux Kernel Zero-Day for Google kernelCTF: CVE-2026-43456 | セキュリティブログ | 脆弱性診断(セキュリティ診断)のGMOサイバーセキュリティ byイエラエ
Source: https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/
19+ Years Hidden, $80,000+ Rewarded: Reporting a Linux Kernel Zero-Day for Google kernelCTF: CVE-2026-43456公開日:2026\.07\.07 更新日:2026\.07\.07
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 (https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/#14b04b21-3e6d-4084-9553-3631be7b9d89), 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 (https://gmo-cybersecurity.com/blog/19-years-hidden-80000-rewarded-reporting-a-linux-kernel-zero-day-for-google-kernelctf/#ced0678e-5dad-42dd-8f18-3e5da5f29b1d)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\_clone`to`0`- This prevents unprivileged users from obtaining`CAP\_NET\_ADMIN`\. As a trade\-off, features such as Rootless Docker are likely to become unavailable\.
- 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`