Describe the bug
In syncTrafficControl, when a TrafficControl's returnPort spec is updated, the controller is supposed to release the stale cached return port and set up the new one. Instead, it releases the new port name, which does not exist yet in portToTCBindings, so the release is a silent no-op, and the genuinely stale old port is never released.
pkg/agent/controller/trafficcontrol/controller.go:838-857 (buggy return port block)
if tc.Spec.ReturnPort != nil {
// Get name of the return port.
returnPortName := c.getPortName(tc.Spec.ReturnPort)
// If the name is different from the cached name in the TrafficControl state, it could be caused by the return
// port update of the TrafficControl or the creation of the TrafficControl.
if returnPortName != tcState.returnPortName {
if tcState.returnPortName != "" {
// If the stale return port name cached in TrafficControl state is not empty, release the stale return port
// from the TrafficControl.
if err = c.releaseTrafficControlPort(returnPortName, tcName, true); err != nil {
return err
}
}
// Get or create the return port.
if _, err = c.getOrCreateTrafficControlPort(tc.Spec.ReturnPort, returnPortName, tcName, true); err != nil {
return err
}
// Update return port name in state.
tcState.returnPortName = returnPortName
}
}
The comment explicitly says "release the stale return port", but the code passes returnPortName (the newly computed name) instead of tcState.returnPortName (the actual stale name).
pkg/agent/controller/trafficcontrol/controller.go:864-873 (correct target port block, for comparison)
if targetPortName != tcState.targetPortName {
if tcState.targetPortName != "" {
if err = c.releaseTrafficControlPort(tcState.targetPortName, tcName, false); err != nil {
return err
}
}
...
pkg/agent/controller/trafficcontrol/controller.go:778-803 (releaseTrafficControlPort)
func (c *Controller) releaseTrafficControlPort(portName, tcName string, isReturnPort bool) error {
c.ovsPortUpdateMutex.Lock()
defer c.ovsPortUpdateMutex.Unlock()
portBinding, exists := c.portToTCBindings[portName]
if !exists {
klog.InfoS("Port used by TrafficControl has been deleted", "port", portName, "TrafficControl", tcName)
return nil
}
...
Since the new return port hasn't been created yet at this point, portToTCBindings[returnPortName] doesn't exist, so the function just logs and returns nil — nothing is released. The old return port's portToTCBindings entry keeps tcName in its trafficControls set forever, since nothing ever removes it, so its OVS port and (for internal ports) host interface are never cleaned up, even after the TrafficControl is later deleted (the deletion path in uninstallTrafficControl releases using tcState.returnPortName, which by then has already been overwritten with the new name).
To Reproduce
- Create a
TrafficControl with a returnPort referring to an OVS internal port, e.g. port-a.
- Update the
TrafficControl to change returnPort to a different port, e.g. port-b.
- Observe the agent log:
Port used by TrafficControl has been deleted is logged for port-b (the new port, not yet created) instead of any cleanup log for port-a.
- Check OVS (
ovs-vsctl show) and the host netdevs: port-a's OVS port and interface remain, even though it is no longer referenced by any TrafficControl and would never be cleaned up by a later delete of this TrafficControl (which now releases port-b instead).
Expected
Updating returnPort releases the actual stale port (port-a) and its underlying OVS port/interface are deleted once unused.
Actual behavior
The stale return port is never released; an OVS port (and host netdev, for internal ports) is leaked on every returnPort update, and the leaked port can never be released later since the state no longer contains its name.
Versions:
- Antrea version: built from
main (HEAD 5b9b27e1, 2026-07-19). Identified by source-code review against the latest main; re-verified the code is unchanged at this HEAD.
Additional context
The bug has existed since TrafficControl support was introduced in #3487 (commit 4e6060072) and has never been touched since. The fix is a one-line change at controller.go:847, releasing tcState.returnPortName instead of returnPortName, mirroring the correct targetPort handling at controller.go:868.
This requires an operator to update spec.returnPort on an existing TrafficControl object (there is no admission webhook or CRD-level immutability constraint preventing this), rather than surfacing on initial creation.
I'd be happy to send a small PR for this. Please feel free to assign it to me.
Describe the bug
In
syncTrafficControl, when aTrafficControl'sreturnPortspec is updated, the controller is supposed to release the stale cached return port and set up the new one. Instead, it releases the new port name, which does not exist yet inportToTCBindings, so the release is a silent no-op, and the genuinely stale old port is never released.pkg/agent/controller/trafficcontrol/controller.go:838-857 (buggy return port block)
The comment explicitly says "release the stale return port", but the code passes
returnPortName(the newly computed name) instead oftcState.returnPortName(the actual stale name).pkg/agent/controller/trafficcontrol/controller.go:864-873 (correct target port block, for comparison)
pkg/agent/controller/trafficcontrol/controller.go:778-803 (releaseTrafficControlPort)
Since the new return port hasn't been created yet at this point,
portToTCBindings[returnPortName]doesn't exist, so the function just logs and returns nil — nothing is released. The old return port'sportToTCBindingsentry keepstcNamein itstrafficControlsset forever, since nothing ever removes it, so its OVS port and (for internal ports) host interface are never cleaned up, even after theTrafficControlis later deleted (the deletion path inuninstallTrafficControlreleases usingtcState.returnPortName, which by then has already been overwritten with the new name).To Reproduce
TrafficControlwith areturnPortreferring to an OVS internal port, e.g.port-a.TrafficControlto changereturnPortto a different port, e.g.port-b.Port used by TrafficControl has been deletedis logged forport-b(the new port, not yet created) instead of any cleanup log forport-a.ovs-vsctl show) and the host netdevs:port-a's OVS port and interface remain, even though it is no longer referenced by anyTrafficControland would never be cleaned up by a later delete of thisTrafficControl(which now releasesport-binstead).Expected
Updating
returnPortreleases the actual stale port (port-a) and its underlying OVS port/interface are deleted once unused.Actual behavior
The stale return port is never released; an OVS port (and host netdev, for internal ports) is leaked on every
returnPortupdate, and the leaked port can never be released later since the state no longer contains its name.Versions:
main(HEAD5b9b27e1, 2026-07-19). Identified by source-code review against the latestmain; re-verified the code is unchanged at this HEAD.Additional context
The bug has existed since
TrafficControlsupport was introduced in #3487 (commit4e6060072) and has never been touched since. The fix is a one-line change at controller.go:847, releasingtcState.returnPortNameinstead ofreturnPortName, mirroring the correcttargetPorthandling at controller.go:868.This requires an operator to update
spec.returnPorton an existingTrafficControlobject (there is no admission webhook or CRD-level immutability constraint preventing this), rather than surfacing on initial creation.I'd be happy to send a small PR for this. Please feel free to assign it to me.