Thread (14 messages) 14 messages, 5 authors, 2026-05-11

Re: [PATCH net 1/2] ip6: vti: Use ip6_tnl.net in vti6_changelink().

From: Maoyi Xie <hidden>
Date: 2026-05-04 05:51:39
Also in: lkml, stable

On 4/30/26, Jakub Kicinski wrote (forwarding AI review):
Because the collision check occurs in the new namespace (dev_net(dev)), but
vti6_update() now modifies the original namespace's hash table (t->net),
could an attacker in the new namespace configure their tunnel to perfectly
match the parameters of an existing victim tunnel in the original namespace?

Since the check in the new namespace finds no collision, it seems it bypasses
the error check. Then vti6_update() prepends the attacker's tunnel
into the original namespace's hash table, which might allow intercepting or
hijacking traffic destined for the victim tunnel.
Confirmed empirically. PoC reproduces on a v7.0 kernel with the
posted 1/2 patch applied.

Setup:
  1. Real init_net root creates a victim tunnel "vti_victim" with
     laddr=fc00::1 raddr=fc00::a in init_net. An attacker tunnel
     "vti_attacker" with different params (laddr=fc00::100
     raddr=fc00::200) also in init_net.
  2. fork() a child that unshare(CLONE_NEWUSER | CLONE_NEWNET) and
     becomes "root" only in its own user_ns.
  3. Real root migrates vti_attacker into the child's netns via
     "ip link set vti_attacker netns <cpid>".
  4. Child issues SIOCCHGTUNNEL on vti_attacker with new params equal
     to vti_victim's (laddr=fc00::1 raddr=fc00::a).

Result on init_net's hash for params=fc00::1/fc00::a:

    [child] SIOCCHGTUNNEL succeeded
    [parent] SIOCGETTUNNEL on init_net's ip6_vti0 with
             params=fc00::1/fc00::a returns name='vti_attacker'

So vti6_locate(init_net, victim_params, 0) now returns the attacker's
tunnel rather than the victim's. The mechanics match the review:

  - vti6_siocdevprivate runs net = dev_net(dev) = child_netns.
  - vti6_locate(child_netns, victim_params) finds nothing.
  - else branch: t = netdev_priv(attacker_dev).
  - vti6_update(t, victim_params) under the 1/2 patch operates on
    t->net = init_net:
      vti6_tnl_unlink(init_net's ip6n, t)   ; t was linked there
      vti6_tnl_change(t, victim_params)
      vti6_tnl_link(init_net's ip6n, t)     ; prepend at head
  - init_net's bucket-for-victim_params chain is now
        attacker (head) -> victim
  - Subsequent matches in init_net resolve to the attacker.

Once an inbound xfrm packet matches victim_params in init_net, the
attacker's tunnel handles rcv/xmit, with t->dev still in the child
netns. So packets destined for the victim are delivered through
the attacker's dev in a netns the attacker fully controls.

Switching vti6_siocdevprivate() to use t->net for the collision
check (or doing the check after vti6_update() under the same lock
that vti6_update is already serialised by) closes the gap, mirroring
what 1/2 already does for vti6_changelink and vti6_update.

Happy to send a follow-up patch if you would prefer me to take it
on, or to wait for v2 of the series. Whichever works for you.

PoC source and the run output above are in poc_vti6_hijack.c and
poc_log.txt, attached.

Best regards,
Maoyi
Nanyang Technological University
https://maoyixie.com/

Attachments

Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help