
19年以上見過ごされていたLinux kernelのゼロデイ脆弱性を報告した話:CVE-2026-43456
上席執行役員/CTO 小池と高度解析課のアルバイトの戸田です。
この度、我々2名でLinux Kernelに報告していた脆弱性CVE-2026-43456が修正され、開示を行ってよい状況になったため、ブログで詳細について紹介できればと思います。
この脆弱性の一番の特徴は、根本の原因となるコードは2007年にLinux kernelに取り込まれており1、実に19年近くもの間見過ごされ、exploit可能な脆弱性であることに気づかれていませんでした。
また、この脆弱性の特性により、この脆弱性を用いたexploitは、成功率99%以上で1秒以内に安定的に実行が完了します。
この性質に鑑み、我々はこの脆弱性をGoogleのバグバウンティイベントkernelCTF2に提出し、8万ドル以上の報奨金をいただきました。

このブログでは、CVE-2026-43456の根本原因とどのようにして権限昇格を達成したか解説します。
条件と影響範囲
- 影響範囲: Linux 2.6.24から6.12.77
- サブシステム:
net/bonding - 原因: type confusion
- 導入コミット:
1284cd3a2b740d0118458d2ea470a1e5bc19b187 - 修正コミット:
950803f7254721c1c15858fbbfae3deaaeeecb11 - トリガーには
CAP_NET_ADMINが必要
影響範囲にあるLinuxのkernelバージョンから分かるとおり、実に広範囲に影響がある脆弱性でした。
対策
本脆弱性は2026年の3月には修正されている脆弱性であり、メジャーなLinuxディストリビューションの最新バージョンにおいては修正されていることが期待されます。ですので、アップデートによりバージョンを最新のものにすることが一番の対策です。
しかしながら、一部のディストリビューション・バージョンにおいては現在でも修正が行われていない可能性があります。そのような環境を利用している、または、アップデートができない事情がある場合においては、以下の緩和策のいずれかを取ることで無効化が可能です:
- /proc/sys/kernel/unprivileged_userns_clone を0にする
- CAP_NET_ADMINを非特権ユーザーが獲得できないようにするためです。代償として、Rootless Dockerなどが利用できなくなると考えられます。
- Bonding機能を無効にする
- Bondingと呼ばれるネットワーク機能に存在する脆弱性のため、この機能を無効にすると影響はありません。
- ほとんどのディストリビューションにおいてBondingはモジュールとして提供されているため、rmmodにより除外可能ですし、そもそもデフォルトでBondingが有効になっている環境は稀です。
すなわち、CopyFailと同様に、以下で対策できます:
echo "install bonding /bin/false" > /etc/modprobe.d/disable-bonding.conf
rmmod bonding 2>/dev/null
脆弱性詳細
前提知識
脆弱性の詳細を説明する前に、前提となる知識について紹介します。
skb
Linux kernelのnetwork stackでは、packetをstruct sk_buff(以下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は確保されたbufferの先頭、skb->dataは現在のpacket dataの先頭、skb->tailは現在のpacket dataの最後を指します。skb->data - skb->headがheadroomです。
また、skb bufferの末尾にはstruct skb_shared_infoが置かれます。struct skb_shared_info::flagsには、このskbがどのような状態か(zerocopyが有効かなど)が保存されています。
Bonding
bondingは、Linuxのネットワーク機能であり、複数のnetwork interfaceを1つのinterfaceとして扱うことができます。作成されるinterfaceをbondデバイス、bondに所属する下位interfaceをslaveデバイスと呼びます。
根本原因
本脆弱性はいわゆるtype confusionと呼ばれる脆弱性です。
以下はbondデバイスを実際に作成するコードですが、★で示すたった1行によって脆弱性が引き起こされています:
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とは、なんらかのプロトコルのパケットヘッダを扱うための関数群をまとめた関数ポインタテーブルです。
bondデバイスは、下位のslaveデバイスを透過的に扱えるように設計・実装されているため、「bondがヘッダに対して行う処理 = 下位デバイスがヘッダに対して行う処理」であり、関数をそのまま流用して使用することは、一見問題ないように思えます。
しかしながら、これらの関数の一部では、デバイスが持っている記憶領域を参照・変更しながら処理を進めていきます。
bondデバイスが持っている記憶領域は、slaveデバイスが持っている記憶領域とは型が異なり互換性がないため、この1行ですでにtype confusionが起こる状況に陥ってしまっているわけです。
実際に互換性のなく、問題が起こりうることをコード上でも確認してみましょう。
まず、bondデバイス側では、sizeof(struct bonding)バイトの領域を確保して、dev->priv(前述した記憶領域)に代入しています。
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);
一方、例えばexploitにおいて利用したプロトコルGREにおいては、以下のようにdev->privが利用されています。
static inline void *netdev_priv(const struct net_device *dev)
{
return (void *)dev->priv;
}
struct ip_tunnel *t = netdev_priv(dev);
struct bondingとstruct ip_tunnelは以下のような見た目の構造体であり、明らかに互換性がありません:
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 */
...
結果として、slaveデバイスとしてGREをつないだ状態で、bondデバイスに対してGREのヘッダ処理を行うと、メモリ破壊などに繋がってしまいます。
脆弱性を用いた権限昇格
kernelCTFでは後学のためにexploitの手法の詳細をすべて開示することが求められており、今回提出したexploitについても、その手法は公知のものとなっています。
そのため、完全な詳細は伏せることとしつつも、ここでも簡単に内容を解説できればと思います。
根本原因は1行のシンプルなものでしたが、これを悪用して安定したexploitを行うには以下のように、複雑な行程を経る必要があり、たくさん考えないといけないことがあります。
また、なぜ19年もの間、このシンプルかつ非常にまずそうな脆弱性が見つからなかったのかについても、下記を見ることで説明がつきます。
Step 1: KASLR Leak
Linux kernelには、userland同様にアドレスをランダム化する機能(KASLR)が存在します。
そのため、安定してexploitを実行するためには、このランダム化されたアドレスを特定する必要があります。この作業のことをleakと呼びます。
今回はleakを行うために、IP6GREと呼ばれるプロトコルを用いました。
先ほど述べたように、この脆弱性では、dev->privのtype confusionが発生します。bondに対するdev->privはstruct bonding、IP6GREに対するdev->privはstruct ip6_tnlになります。
struct bonding::recv_probeはbonding構造体のオフセット0x38の位置にあります。
一方、IP6GRE側から見たstruct ip6_tnl::parms.laddrも、オフセット0x38から読まれます。
これは、IPv6 source addressを格納するフィールドであるため、この値は受信したパケットに含まれます。
/* 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 *);
上記のとおり、recv_probeは関数ポインタであり、以下のコードから、実際にはbond_rcv_validateのアドレスが含まれると分かります:
if (bond->params.arp_interval) {
queue_delayed_work(bond->wq, &bond->arp_work, 0);
bond->recv_probe = bond_rcv_validate;
}
bond_rcv_validateはカーネルに存在する関数であるため、
これをleakすることにより、カーネルのbase addressが計算できます。
Step 2: 任意のコード実行
Step 1によりKASLRは突破できるので、次にメモリを破壊し、Instruction Pointerを任意の値にすることを目指します
(すなわち、任意のコード実行に繋げます)。
Step 2.1: フラグの書き換え
結論から言うと、任意のコード実行には、IP6GREの代わりに、今度はGRE(IPv4版)を用います。
具体的には、以下のコードで示されている、uarg->callbackを任意の値にすることで、不正なアドレスを関数呼び出しさせます。
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) // 非NULLの場合はcallbackが呼ばれる
uarg->callback(skb, uarg, success);
}
skb_shinfo(skb)->flagsの値を不正に書き換えることにより、uargがNULLで返却されるべきときに、非NULLな値を返却させることで、不正な関数呼び出しを達成します。
天下り的になりますが、このskb_shinfo(skb)->flagsの書き換えは、以下のGREのheader_ops関数において、greh->flagsとのtype confusionを引き起こすことで可能になります:
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);
type confusionが起きているとき、tはstruct bondingを指しています。
このとき、GREではt->hlen >= sizeof(*greh)であるべきところ、struct bondingではt->hlen == 0となっています。
そうすると、skb_push()はsizeof(struct iphdr) + sizeof(struct gre_base_hdr)だけskb->dataを戻す(引き算する)べきところ、sizeof(struct iphdr)だけskb->dataを戻します。
その後、grehがiph + 1とされているため、grehは元のskb->dataの値になります。すなわち、この時点で、buffer overflowが発生しています。
前提知識のセクションで触れた通り、skb->dataは本来は、skbのパケットデータの先頭を指すものです。
しかし特定条件においては、ここにskb_shared_infoの先頭が重なります(具体的には、未使用バッファがもうないとき)。
grehとskb_shared_infoはそれぞれ以下のような構造をしていることも手伝って、この条件下では、greh->flagsへの書き込みが、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;
...
ちなみに、greh->flagsの書き込みに使われる値、gre_tnl_flags_to_gre_flags(t->parms.o_flags)は常に固定で0x7ffになります(したがって、これが安定性に影響することはない)。
greh->flagsの書き込みに使われる値、t->parms.o_flagsもtype confusionによりbondingから読まれます。t->parms.o_flagsはオフセット 0x6eで、struct bonding::bond_list.nextの6バイト目にあたります。
これは、カーネルポインタなので、その2バイトは常に0xff 0xffになります。
結果として、GRE flags変換後の値は以下になります。
gre_tnl_flags_to_gre_flags(0xffff) = 0x07ff
ここで、このskbがzerocopyであるというフラグが立ちます。
SKBFL_ZEROCOPY_ENABLE = BIT(0)
つまり、本来zerocopyではなかったskbのstruct skb_shared_info::flagsが不正に書き換えられ、後続のパスでzerocopyのskbであるかのように扱われます。
Step 2.2: skb->dataの調整
先ほど、特定条件下では、skb->dataとskb_shared_infoの先頭が重なると述べました。
exploitではこの条件下で起きるメモリ破壊を利用しているわけなので、この条件を満たすように工夫をする必要があります。
skbのバッファはページサイズにalignされて確保されるため、struct skb_shared_infoは、ページの末尾に必ず来る構造体です。
以下のコードはskbのバッファを確保するコードです。
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);
例えば LL_RESERVED_SPACE(dev)が0x3ec0の時、skbのバッファは0x4000ちょうどとなり、struct skb_shared_infoのオフセットは0x3ec0になります。
skb_shinfo(skb) = skb->head + (0x4000 - sizeof(struct skb_shared_info))
= skb->head + 0x3ec0
また、len == 0なパケットをsendした後のskb->dataも(パケット用のバッファが存在しないのでskb_shared_infoと重なり)、オフセット0x3ec0に来ます。
したがって、条件を達成するためには、LL_RESERVED_SPACE(dev)が0x3ec0になるようなbondデバイスを作り、len == 0なパケットを送信する必要があると言い換えられます。
LL_RESERVED_SPACEは以下のように定義されます。
#define LL_RESERVED_SPACE(dev) \
((((dev)->hard_header_len + READ_ONCE((dev)->needed_headroom)) \
& ~(HH_DATA_MOD - 1)) + HH_DATA_MOD)
ここでは、bond->needed_headroomを増やす方針で、LL_RESERVED_SPACEの調整を行いました。
具体的には、329個のGREデバイスを作り、次のようにチェインします(GREではデバイスのチェインが可能です)。
if0 <- if1 <- if2 <- ... <- if328
最初の8個はFOU encapsulation付きGRE、残りはplain GREにします。
GREをchainすると、ip_tunnel_bind_dev()が、現在のtunnel->hlenと下位deviceの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;
必要なサイズはこうなります。
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
最初の8個のFOU GREで値が0x260になります。if8のneeded_headroomは0x298になります。
その後、残り320個のplain GREがつながると、N328のneeded_headroomは0x3e98になります。
最後のGREをbondへenslaveすると、bondがその値をコピーします。
bond->needed_headroom = 0x3e98
bond->hard_header_len = 0x18
以上の操作によって、bondのLL_RESERVED_SPACEは狙い通り、0x3ec0となります。
LL_RESERVED_SPACE = align_down(0x3eb0, 0x10) + 0x10 = 0x3ec0
この脆弱性が19年もの間見つからなかった理由はここにあります。
非常に巧妙に設計したデバイスのチェインによって、LL_RESERVED_SPACEを正確に特定の値に設定しない限り、前述したようなskb->dataとskb_shared_infoの先頭が重なる状況は生まれません。
これらが重なっていない場合においても、buffer overflow自体は発生しているのですが、skbがページサイズにalignされている影響で、何にも使われていないメモリ領域に書き込みが発生するだけになるため、副作用は何も生じていないに等しく、クラッシュおろかKASANなどのメモリ破壊を検知する機構にも見つからないのです。
実際我々もこの脆弱性は当初syzkallerのクラッシュを偶然引き当て、それを深くまで解析することによってようやく発見することができました。説明の一部が天下り的になっていたのもこれが影響しています。
終わりに
このブログでは我々が発見した脆弱性の詳細について解説を行いました。
19年もの間見つからないだけはある、見た目のシンプルさの裏に潜む複雑さを皆さんにも感じ取っていただけたのではないかと思います。
前述したとおり、発見にはsyzkallerを利用したのですが、設定のチューニングは行ったものの、特別にコードを改変する等の工夫なく、このような深い場所に潜んでいた脆弱性を見つけ出す性能には驚かされました。
また、上記のように、クラッシュしたコード箇所(callbackの呼び出し)から、根本原因となったコード箇所までには大きな距離があり、根本原因を突き止める作業 (RCA; Root Cause Analysis)は非常に難航しました。
実はRCAにおいては、AIに大いに助けられています。
当時2025年の前半でしたが、その時点でも、Linux KernelのようなOSSに対するフロンティアモデルの知識には目を見張るものがあり、今ではありふれてきている、AIとの二人三脚で脆弱性を発見する事例の先駆けになっていたと、振り返ってみて思います。
しかしながら、「現在であればAIによって、AIのみでこの脆弱性が突き止められるか」と言われると、著しい進化とすさまじい性能を見せつけられた2026年の今でも、少し懐疑的ではあります。
今後、AIの発展によってこのような複雑な脆弱性も自動で見つけられるような未来が訪れることは祈りつつ、
そのような未来が訪れるまで、我々人間もゼロデイ脆弱性の発見と解消に邁進していきたいと思います。
- commit: 1284cd3a2b740d0118458d2ea470a1e5bc19b187 ↩︎
- Googleが自社のバグバウンティ活動Google VRPの一環で開催しているイベントの1つ。Linux Kernelに対して、脆弱性を用いたexploitにより、権限昇格を実証すると設定の難易度に応じた報奨金が得られる。今回の脆弱性では、高安定性・ゼロデイ・独自mitigation付きという条件で実証に成功している。Googleがこの取り組みを行う目的は、卓越した研究者たちがどのような脆弱性・exploitにより攻撃対象を攻略するかを観察し、よりexploitを難しくするにはどうしたらよいかを研究するための材料とすることにある。参考: https://google.github.io/security-research/kernelctf/rules.html ↩︎
