# CVE-2026-43456 — Variant Root Cause Analysis (ip6gre alternate trigger)

## Summary

This variant stage found a **distinct alternate trigger** of CVE-2026-43456's
bonding `header_ops` type-confusion kernel denial-of-service — but **not a
bypass** of the upstream fix. The original reproduction exercised the IPv4 GRE
sink `ipgre_header()` (`net/ipv4/ip_gre.c`, reading `struct ip_tunnel`). It
attempted to also exercise the IPv6 GRE sink `ip6gre_header()`
(`net/ipv6/ip6_gre.c`, reading `struct ip6_tnl`) but **never reached it**,
because its ip6gre probe created the tunnel *with* a remote address
(`remote fd00::2`); `ip6gre_tunnel_init()` only assigns `ip6gre_header_ops`
when `ipv6_addr_any(&tunnel->parms.raddr)` (NBMA / no-remote mode), so
`ip6gre_header_ops` was never assigned and `ip6gre_header()` was never invoked
(zero `ip6gre_header` printks in the repro log).

This variant creates the ip6gre tunnel with **no remote** (`ip link add ip6gre1
type ip6gre local fd00::1`), which assigns `ip6gre_header_ops`, enslaves it to
an active-backup bond, and triggers `dev_hard_header(bond)` via an `AF_PACKET`
`SOCK_DGRAM` send. On the **vulnerable** kernel (Linux 7.0.0-rc2, commit
`e3f5e0f22`) this reaches the type-confused `ip6gre_header(bond1)` sink:
`netdev_priv(bond1)` is `struct bonding` but is read as `struct ip6_tnl`. With
the confused `ip6_tnl.hlen` populated to a sign-bit-set value, `needed = hlen +
sizeof(ipv6hdr)` overflows to a negative `int`, the (unsigned) headroom test is
satisfied, and `pskb_expand_head()` hits `BUG_ON(nhead < 0)` → **kernel panic**
— the same DoS sink as the original, reached via a different sink function,
file, and confused struct. On the **fixed** kernel (same bzImage with
`bonding.ko` rebuilt after applying fix `950803f7`), `bond_header_ops` delegates
`ip6gre_header` to the active slave `ip6gre1` device (`dev=ip6gre1`,
`netdev_priv` = correct `struct ip6_tnl`, `hlen=4`), so **no crash** occurs and
the VM prints `RESULT: NOT VULNERABLE`. The fix is generic and covers the
ip6gre sink, so this is an **alternate trigger, not a bypass**.

## Fix Coverage / Assumptions

**Fix:** upstream commit `950803f7254721c1c15858fbbfae3deaaeeecb11`
(`bonding: fix type confusion in bond_setup_by_slave()`), in
`drivers/net/bonding/bond_main.c`.

**Invariant the fix relies on:** the type confusion is exploited through
`header_ops->create` (and `->parse`), invoked by `dev_hard_header()` on the
bond. The fix replaces `bond_dev->header_ops` with a static wrapper,
`bond_header_ops`, whose `bond_header_create()` / `bond_header_parse()` delegate
to the **active slave's own device** (`slave->dev`), so the slave's header
callback's `netdev_priv(dev)` receives the slave's correct private struct.

**Code paths the fix explicitly covers:**
- `bond_setup_by_slave()` — the *only* writer of `bond_dev->header_ops` for the
  non-Ethernet case (verified by grep across `drivers/net/bonding/`). It now
  sets `bond_dev->header_ops = slave_dev->header_ops ? &bond_header_ops : NULL`.
- `dev_hard_header()` → `ops->create` (wrapped) and `dev_parse_header()` →
  `ops->parse` (wrapped), for *any* non-Ethernet slave whose `header_ops` has a
  `.create` — including `ipgre_header` and `ip6gre_header`.

**What the fix does NOT wrap (and why it is still safe):**
`struct header_ops` has six callbacks (`.create`, `.parse`, `.cache`,
`.cache_update`, `.validate`, `.parse_protocol`). `bond_header_ops` wraps only
`.create` and `.parse`. The other four are left NULL on the bond. This is safe
because the fix *replaces* (not merges) `header_ops`: the slave's
`.cache`/`.cache_update`/`.validate`/`.parse_protocol` are simply not present on
the bond and are never invoked through it; all in-kernel callers are
NULL-guarded (`dev_parse_header_protocol`, `neigh_hh_init`,
`dev_validate_header` all check for NULL). The only tunnel `header_ops` that
dereference `netdev_priv()` in a type-confusing way are the GRE ones
(`ipgre_header`, `ip6gre_header`), both `.create` sinks, both covered. The
`ip_tunnel_header_ops` family (ipip/sit/vti/ip6_tunnel/xfrm) has only
`.parse_protocol` (which does *not* dereference `netdev_priv`) and no `.create`,
so those slaves are not type-confusion triggers at all.

**Assumption about active slave:** the wrapper uses
`bond->curr_active_slave`. In active-backup mode (used here and by the CVE) this
is the one up slave. If `curr_active_slave` is NULL, the wrapper returns 0 (no
header built) — a functional no-op, not a crash.

## Variant / Alternate Trigger

**Path:** IPv6 GRE (no remote) enslaved to an active-backup bond →
`dev_hard_header(bond)` → `ip6gre_header(bond)`.

**Exact entry point / commands (run inside the QEMU VM as PID 1):**
```
ip link set lo up
ip -6 addr add fd00::1/128 dev lo
ip link add dummy0 type dummy ; ip link set dummy0 up
ip link add ip6gre1 type ip6gre local fd00::1      # NO remote -> ip6gre_header_ops assigned
ip link add bond1 type bond mode active-backup
ip link set ip6gre1 master bond1                   # bond_setup_by_slave copies header_ops
ip link set ip6gre1 up ; ip link set bond1 up
insmod populate_hlen6.ko                           # writes 0x961a63cc to netdev_priv(bond1).ip6_tnl.hlen
# AF_PACKET SOCK_DGRAM sendto on bond1:
sendto(<af_packet SOCK_DGRAM>, ..., bond1)
```

**Code path involved (files / functions):**
- `drivers/net/bonding/bond_main.c` :: `bond_setup_by_slave()` (root cause;
  copies `slave_dev->header_ops` onto `bond_dev` on the vulnerable kernel).
- `net/ipv6/ip6_gre.c` :: `ip6gre_header()` (the **distinct variant sink**,
  lines 1365-1407) — `struct ip6_tnl *t = netdev_priv(dev); needed = t->hlen +
  sizeof(*ipv6h);` then `pskb_expand_head()`.
- `net/ipv6/ip6_gre.c` :: `ip6gre_tunnel_init()` (lines ~1525-1533) — assigns
  `dev->header_ops = &ip6gre_header_ops` only when
  `ipv6_addr_any(&tunnel->parms.raddr)`.
- `net/core/skbuff.c` :: `pskb_expand_head()` — `BUG_ON(nhead < 0)` (the shared
  DoS sink, line 2306).
- `net/packet/af_packet.c` :: `packet_snd()` → `dev_hard_header()` (the entry
  that drives `dev_hard_header(bond)`).

This is materially distinct from the original repro's sink
(`net/ipv4/ip_gre.c:ipgre_header`, `struct ip_tunnel`, `hlen` offset 160,
`sizeof(iphdr)=20`): here the sink is `ip6gre_header`, the confused struct is
`struct ip6_tnl`, the confused `hlen` is at offset **264** inside `struct
bonding`, and the added constant is `sizeof(ipv6hdr)=40`.

## Impact

- **Package / component:** Linux kernel bonding driver
  (`drivers/net/bonding/bond_main.c`) interacting with `net/ipv6/ip6_gre.c`
  (`ip6gre_header`) and `net/core/skbuff.c` (`pskb_expand_head`).
- **Affected versions (as tested):** vulnerable at mainline **7.0.0-rc2**
  (commit `e3f5e0f22cfc…`, parent of the upstream fix). Fixed by
  `950803f7254721c1c15858fbbfae3deaaeeecb11` (and its stable backports
  `6ac890f1d60a`, `95597d11dc8b`, `9baf26a91565`).
- **Risk level:** Medium (CVE severity). **Consequences:** type confusion /
  invalid memory interpretation; a kernel `BUG()`/Oops/panic (local denial of
service) when the type-confused `t->hlen` has its sign bit set. Trust boundary:
a local user with `CAP_NET_ADMIN` (in the init namespace or a network
namespace) issues network configuration that the bonding driver mis-handles,
crashing the kernel for all users on the host.

## Impact Parity

- **Disclosed / claimed maximum impact (parent CVE):** local kernel
  denial-of-service (`BUG_ON`/panic via `pskb_expand_head` reached through
  `dev_hard_header` → a GRE `*_header` type-confused callback).
- **Reproduced impact from this variant run:** local kernel
  denial-of-service — `kernel BUG at net/core/skbuff.c:2306!`,
  `Oops: invalid opcode`, `RIP: 0010:pskb_expand_head+0x59c/0x6d0`,
  `Call Trace: ip6gre_header+0x14a/0x430 [ip6_gre]`, `Kernel panic - not
  syncing: Fatal exception`.
- **Parity:** **full** — the variant reaches the identical `BUG_ON(nhead < 0)`
  DoS sink in `pskb_expand_head()` as the original ipgre path, via the distinct
  `ip6gre_header` sink, on the same vulnerable kernel.
- **Not demonstrated:** no read/write primitive or code execution was
  demonstrated; only the disclosed DoS (kernel panic). The confused `hlen` is
  read-only exploited to drive a negative `needed` into `pskb_expand_head`.

## Root Cause

`bond_setup_by_slave()` copies a non-Ethernet slave's `header_ops` verbatim
onto the bond net device:

```c
bond_dev->header_ops = slave_dev->header_ops;   // vulnerable
```

When the network stack later calls `dev_hard_header(bond_dev)` (e.g. via
`packet_sendmsg` → `packet_snd` → `dev_hard_header`), the slave's
header-create callback runs with `dev = bond_dev`. That callback dereferences
`netdev_priv(dev)` expecting the slave's private struct, but for a bond device
`netdev_priv()` returns `struct bonding`. The bonding memory is therefore
reinterpreted as the tunnel struct — a classic type confusion. For ip6gre,
`ip6gre_header()` computes `needed = t->hlen + sizeof(*ipv6h)`; when the
confused `t->hlen` (the `int` at `offsetof(struct ip6_tnl, hlen)` = offset 264
inside `struct bonding`) has its sign bit set, `needed` overflows to a negative
`int`. Because `skb_headroom()` returns `unsigned int`, the test
`skb_headroom(skb) < needed` promotes the negative `needed` to a huge unsigned
value and is satisfied, so `pskb_expand_head()` is called with a negative
`nhead` and hits `BUG_ON(nhead < 0)`, panicking the kernel.

The same root cause underlies both the original ipgre trigger and this ip6gre
variant; the only differences are the slave tunnel type, the sink function/file,
the confused private struct, the confused-field offset (160 vs 264), and the
header-size constant (20 vs 40). The shared DoS sink is
`pskb_expand_head()`'s `BUG_ON(nhead < 0)`.

**Fix commit:** `950803f7254721c1c15858fbbfae3deaaeeecb11`. The fix is
**generic** — `bond_header_create()` delegates *whatever* the active slave's
`header_ops->create` is to the slave's own device, so `netdev_priv()` always
receives the slave's correct private struct. It does not hardcode `ipgre`, so
it covers `ip6gre_header` identically.

## Reproduction Steps

1. **Script:** `bundle/vuln_variant/reproduction_steps.sh` (idempotent; reuses
   the kernel/rootfs cache built by the repro stage).
2. **What it does:**
   - Verifies the prepared cache (`bzImage`, vulnerable/fixed `bonding.ko`,
     module stage, rootfs base) built by the repro stage.
   - Builds `populate_hlen6.ko` against the cached kernel tree (writes
     `0x961a63cc` into `netdev_priv(bond1).ip6_tnl.hlen`).
   - Compiles a static in-VM `/init` (`bond_variant_init`) that sets up
     ip6gre(no-remote) → bond and fires `AF_PACKET SOCK_DGRAM` on `bond1`.
   - Builds two rootfs images that differ **only** in `bonding.ko` (vulnerable
     vs fixed) — a clean A/B negative control on the identical kernel image and
     userspace.
   - Boots each in QEMU and analyzes the serial logs.
3. **Expected evidence (and what was observed):**
   - **Vulnerable:** `ip6gre_header: dev=bond1 hlen=-1776655412 needed=-1776655372
     headroom=288` → `kernel BUG at net/core/skbuff.c:2306!` → `Oops: invalid
     opcode` → `RIP: 0010:pskb_expand_head+0x59c/0x6d0` → `Call Trace:
     ip6gre_header+0x14a/0x430 [ip6_gre]` → `Kernel panic`. The init never
     reaches its `RESULT:` line (it crashed first). **Observed.**
   - **Fixed:** `ip6gre_header: dev=ip6gre1 hlen=4 needed=44 headroom=288`
     (delegated to the slave device) → `RESULT: NOT VULNERABLE`; no crash
     markers. **Observed.**
   - The script exits **1** (variant reproduced on the vulnerable kernel only;
     the fix covers it on the fixed kernel → not a bypass). Exit 0 would mean a
     true bypass (variant reproduced on the fixed kernel), which did **not**
     happen.

## Evidence

- **Logs:**
  - `bundle/logs/qemu_var_vuln.log` — vulnerable kernel ip6gre run (crash).
  - `bundle/logs/qemu_var_fixed.log` — fixed kernel ip6gre run (no crash).
  - `bundle/logs/vuln_variant_repro.log` — script driver log with the A/B
    analysis lines.
- **Key excerpts (vulnerable):**
  ```
  [   12.258698] ip6_gre: CVE-2026-43456 ip6gre_header: dev=bond1 hlen=0 needed=40 headroom=288
  [   12.853212] CVE-2026-43456 VAR(ip6gre): bond1 priv=ffff888103b4c9c0 ip6_tnl.hlen offset=264 old=0x00000000(0) new=0x961a63cc(-1776655412)
  [   13.239107] ip6_gre: CVE-2026-43456 ip6gre_header: dev=bond1 hlen=-1776655412 needed=-1776655372 headroom=288
  [   13.240854] kernel BUG at net/core/skbuff.c:2306!
  [   13.241626] Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
  [   13.244640] RIP: 0010:pskb_expand_head+0x59c/0x6d0
  [   13.249474]  ip6gre_header+0x14a/0x430 [ip6_gre]
  [   13.263177] Kernel panic - not syncing: Fatal exception
  ```
  The `dev=bond1` (not `dev=ip6gre1`) and the read of `ip6_tnl.hlen` at offset
  264 inside `struct bonding` prove the type confusion; the negative `needed`
  drives `pskb_expand_head()` into `BUG_ON(nhead < 0)`.
- **Key excerpts (fixed):**
  ```
  [   12.058548] ip6_gre: CVE-2026-43456 ip6gre_header: dev=ip6gre1 hlen=4 needed=44 headroom=288
  [   12.644487] CVE-2026-43456 VAR(ip6gre): bond1 priv=ffff888103a9c9c0 ip6_tnl.hlen offset=264 old=0x00000000(0) new=0x961a63cc(-1776655412)
  [init] RESULT: NOT VULNERABLE (no kernel crash; fixed bond_header_ops used the slave ip6gre1 device)
  ```
  `bond_header_ops` delegated `ip6gre_header` to the slave `ip6gre1` device
  (`dev=ip6gre1`, correct `struct ip6_tnl`, `hlen=4`); the `populate_hlen6`
  write to `bond1`'s private data was harmless (never read as `ip6_tnl`). No
  crash.
- **Environment:** QEMU `qemu-system-x86_64` (TCG, 4 vCPU, 4 GB), Linux
  7.0.0-rc2 (commit `e3f5e0f22`, `#1 SMP PREEMPT_DYNAMIC`, KASAN generic,
  `oops=panic`). Identical bzImage for both runs; only `bonding.ko` swapped
  (vulnerable = no `bond_header_ops`, fixed = has `bond_header_ops` from
  `950803f7`).
- **Structured artifacts:** `bundle/vuln_variant/variant_manifest.json`,
  `bundle/vuln_variant/validation_verdict.json`,
  `bundle/vuln_variant/runtime_manifest.json`,
  `bundle/vuln_variant/root_cause_equivalence.json`,
  `bundle/vuln_variant/source_identity.json`,
  `bundle/vuln_variant/patch_analysis.md`.

## Recommendations / Next Steps

- **No additional security fix is required.** The upstream `bond_header_ops`
  wrapper is generic: it delegates the active slave's `header_ops->create` to
  the slave's own device, so `netdev_priv()` receives the correct struct
  (`struct ip_tunnel` for ipgre, `struct ip6_tnl` for ip6gre). The ip6gre
  alternate sink is covered; the variant does **not** reproduce on the fixed
  kernel. The fix is complete for the disclosed class of
  `header_ops->create`-based type confusion from non-Ethernet slaves.
- **Ruled-out candidates (no fix gap):**
  - ipip / sit / ip_vti / ip6_vti / ip6_tunnel / xfrm slaves use
    `ip_tunnel_header_ops = { .parse_protocol = ip_tunnel_parse_protocol }`,
    which has **no `.create`** and whose `.parse_protocol` does **not**
    dereference `netdev_priv()`. Not type-confusion triggers; the fix also
    NULLs `parse_protocol` on the bond.
  - `.cache` / `.cache_update` / `.validate` / `.parse_protocol` of non-Ethernet
    slaves: the GRE tunnel `header_ops` do not define these, and the fix
    NULLs them on the bond regardless; all callers are NULL-safe.
- **Advisory (functional, not security):** because `bond_header_ops` wraps only
  `.create` and `.parse`, a non-Ethernet bond loses `.cache`/`.cache_update`/
  `.validate`/`.parse_protocol` (header caching, validation). If full
  functional parity for non-Ethernet bonds is desired, those callbacks could be
  wrapped analogously (`bond_header_cache` delegating to the active slave's
  `.cache` with the slave device). This is independent of CVE-2026-43456 and
  does not affect the type-confusion fix.
- **Coding-stage guidance:** when shipping the fix, ensure the full
  `bond_header_ops` wrapper (create + parse) is present and that
  `bond_setup_by_slave()` substitutes `&bond_header_ops` for the slave's
  `header_ops`. No change is needed to `ip6_gre.c` or `ip_gre.c`.

## Additional Notes

- **Bypass vs alternate trigger:** This is an **alternate trigger** (distinct
  sink `ip6gre_header` / `struct ip6_tnl`), **not a bypass** — the fix covers
  it on the fixed kernel. The original repro missed this sink only because of a
  misconfiguration (it used a remote address on the ip6gre tunnel, which
  prevents `ip6gre_header_ops` from being assigned).
- **Idempotency:** `bundle/vuln_variant/reproduction_steps.sh` was run three
  times end-to-end; each run rebuilt the rootfs images, booted both kernels,
  and produced identical analysis (`VARIANT_ON_VULN=true`,
  `VARIANT_ON_FIXED=false`, `FIX_COVERS=true`, exit 1). The script reuses all
  cached build artifacts (bzImage, vulnerable/fixed `bonding.ko`, module stage,
  rootfs base, `populate_hlen6.ko`) and only recompiles/refreshes the proof
  artifacts (`/init`, `populate_hlen6.ko`, rootfs images) when missing.
- **Limitations:** The confused `hlen` value (`0x961a63cc`) is written by the
  out-of-tree `populate_hlen6.ko` helper to emulate a layout where the bonding
  memory overlapping `ip6_tnl.hlen` (offset 264) holds a sign-bit-set value —
  exactly analogous to the repro stage's `populate_hlen.ko` for the ipgre case.
  This preserves the real `AF_PACKET → dev_hard_header → ip6gre_header →
  pskb_expand_head` boundary; only the confused-field value is set. The DoS is
  a privileged-local-user (`CAP_NET_ADMIN`) kernel crash, matching the CVE's
  Medium severity.
