上席執行役員/CTO 小池と高度解析課のアルバイトの戸田です。
この度、我々2名で Linux Kernel に報告していた脆弱性 CVE-2026-43456 が修正され、開示を行ってよい状況になったため、ブログで詳細について紹介できればと思います。
この脆弱性の一番の特徴は、根本の原因となるコードは 2007 年に Linux kernel に取り込まれており(注1)、実に 19 年近くもの間見過ごされ、exploit 可能な脆弱性であることに気づかれていませんでした。
また、この脆弱性の特性により、この脆弱性を用いた exploit は、成功率 99% 以上で 1 秒以内に安定的に実行が完了します。
この性質に鑑み、我々はこの脆弱性を Google のバグバウンティイベント kernelCTF(注2)に提出し、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 が有効になっている環境は稀です。
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;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 になります。
一方、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版)を用います。
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 ではこの条件下で起きるメモリ破壊を利用しているわけなので、この条件を満たすように工夫をする必要があります。
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 の発展によってこのような複雑な脆弱性も自動で見つけられるような未来が訪れることは祈りつつ、そのような未来が訪れるまで、我々人間もゼロデイ脆弱性の発見と解消に邁進していきたいと思います。
(注釈)
1. commit: 1284cd3a2b740d0118458d2ea470a1e5bc19b187 ↩︎
2. Google が自社のバグバウンティ活動 Google VRP の一環で開催しているイベントの1つ。Linux Kernel に対して、脆弱性を用いた exploit により、権限昇格を実証すると設定の難易度に応じた報奨金が得られる。今回の脆弱性では、高安定性・ゼロデイ・独自 mitigation 付きという条件で実証に成功している。Google がこの取り組みを行う目的は、卓越した研究者たちがどのような脆弱性・exploit により攻撃対象を攻略するかを観察し、より exploit を難しくするにはどうしたらよいかを研究するための材料とすることにある。参考: https://google.github.io/security-research/kernelctf/rules.html
