Download raw body.
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.
trunk(4): adding a bridge/veb/tpmr member to a trunk panics on teardown