Index | Thread | Search

From:
Tom Smyth <tom.smyth@wirelessconnect.eu>
Subject:
trunk(4): adding a bridge/veb/tpmr member to a trunk panics on teardown
To:
bugs@openbsd.org
Cc:
tech <tech@openbsd.org>
Date:
Wed, 4 Nov 2026 00:13:27 +0000

Download raw body.

Thread
  • Tom Smyth:

    trunk(4): adding a bridge/veb/tpmr member to a trunk panics on teardown

>Synopsis: trunk_port_create() fails to reject an interface that is
already a bridge(4)/veb(4)/tpmr(4) port; destroying the stack
later triggers a NULL ether_port deref (panic on DIAGNOSTIC).
>Category: kernel
>Severity: serious
>Priority: medium
>Class: sw-bug
>Release: OpenBSD 8.0-current (snapshot, GENERIC.MP #125, 2026-11-02)
>Environment:
System      : OpenBSD 8.0
Details     : OpenBSD 8.0-current (GENERIC.MP) #125: Mon Nov  2
     04:56:12 CST 2026
     deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC.MP
Architecture: OpenBSD.amd64
Machine     : amd64 (QEMU/KVM guest, 4 vcpu)

>Description:

ifconfig <trunk> trunkport <if> accepts an interface that is already
enslaved to a bridge(4), veb(4) or tpmr(4).  trunk_port_create()
(sys/net/if_trunk.c) guards against joining a second trunk/aggr by
testing ac_trport, but never tests ether_brport_isset(), so the
bridge-family membership is not detected.  aggr_add_port() checks both
and is not affected.

Both the bridge-family driver and trunk(4) interpose the member's
if_ioctl by saving the previous handler and installing their own:

   tpmr  add:  p_ioctl  = ifp->if_ioctl;      /* original */
               ifp->if_ioctl = tpmr_p_ioctl;  ether_brport_set()
   trunk add:  tp_ioctl = ifp->if_ioctl;      /* == tpmr_p_ioctl */
               ifp->if_ioctl = trunk_port_ioctl;

Only the tpmr-first / trunk-second order is reachable, because
trunk_port_create() sets if_type = IFT_IEEE8023ADLAG, after which the
bridge-family add would fail its IFT_ETHER check.

If the bridge-family interface is then destroyed (or the member is
removed from it) before the trunk is torn down, tpmr_p_dtor() clears
ac_brport and restores if_ioctl, but trunk(4) still holds tp_ioctl
pointing at the now-stale tpmr_p_ioctl.  When the member is finally
detached, trunk_port_destroy() restores that stale pointer and calls
ifnewlladdr(), which re-enters tpmr_p_ioctl() with ac_brport == NULL:

   panic: kernel diagnostic assertion "ep != NULL" failed:
       file ".../sys/net/if_tpmr.c", line 684
       vether101: tpmr_p_ioctl called without an ether_brport set

   db_enter()
   panic()
   tpmr_p_ioctl()        tpmr_p_ioctl+0xe1
   ifnewlladdr()         ifnewlladdr+0x104
   trunk_port_destroy()  trunk_port_destroy+0x2e8
   if_hooks_run()        if_hooks_run+0xdb
   if_deactivate()       if_deactivate+0x34
   ether_ifdetach()      ether_ifdetach+0x21
   vether_clone_destroy()
   if_clone_destroy()
   ifioctl()
   sys_ioctl()
   syscall()

On a non-DIAGNOSTIC (GENERIC) kernel the KASSERT is compiled out and
this is an unchecked NULL pointer dereference in tpmr_p_ioctl().

The operations are all available to root via ioctl(SIOCSTRUNKPORT) /
SIOCIFDESTROY; no special hardware is involved.  bridge(4) and veb(4)
exhibit the same acceptance bug (same missing check); the reverse order
is refused in every case because of the if_type change above.

>How-To-Repeat:

As root:

   ifconfig tpmr0 create
   ifconfig trunk0 create
   ifconfig vether0 create
   ifconfig tpmr0 add vether0
   ifconfig trunk0 trunkport vether0   # should fail EBUSY, but succeeds
   ifconfig tpmr0 destroy              # clears ac_brport; trunk unaware
   ifconfig vether0 destroy            # panic

>Fix:

Make trunk_port_create() reject an interface that is already a
bridge-family port, mirroring aggr_add_port().  trunk_port_create()
already has a local "int error = 0;" and ether_brport_isset() returns
EBUSY when ac_brport is set.

Index: sys/net/if_trunk.c
===================================================================
--- sys/net/if_trunk.c
+++ sys/net/if_trunk.c
@@ -295,6 +295,10 @@
  if (SMR_PTR_GET_LOCKED(&ac0->ac_trport) != NULL)
  return (EBUSY);

+ /* Reject a port already enslaved to a bridge(4)/veb(4)/tpmr(4) */
+ if ((error = ether_brport_isset(ifp)) != 0)
+ return (error);
+
  /* Take MTU from the first member port */
  if (SLIST_EMPTY(&tr->tr_ports)) {
  if (tr->tr_ifflags & IFF_DEBUG)

With the diff applied, the "trunkport" step above returns
"device busy" and the panic no longer occurs.

>Verification:

Built GENERIC.MP from -current with the diff applied and booted it
(OpenBSD 8.0-current GENERIC.MP #0, 2026-11-03).  On the stock #125
kernel the How-To-Repeat sequence panics at the final destroy; on the
patched kernel:

   # ifconfig trunk0 trunkport vether0
   ifconfig: trunk0 vether0: SIOCSTRUNKPORT: Device busy

i.e. the enslave is now refused with EBUSY and the subsequent teardown
no longer panics.  Tested on an isolated amd64 KVM guest.

-- 
Kindest regards,
Tom Smyth.