Evict state minted against a replaced interface - #75
Merged
Conversation
A recreated interface invalidates every bind and egress pin taken against its old kernel object even when it comes back with the same addresses, so DialProxy's address comparison saw nothing to do and kept serving listeners and connections that could no longer carry traffic. Interface now counts replacements and InterfaceRef pairs a borrowed pointer with the generation its holder last saw, so a holder asks rather than being told. Application writes the counter mid-reconcile and holders read it from the existing broadcast afterwards: an integer compare carries none of the ordering a callback would have had to respect, and there is no second notification path to keep in step with the first. The eviction is asymmetric. A listener binds a source_if address while every upstream is egress-pinned to target_if, so a source replacement drops connections and listeners while a target replacement drops connections only -- the next connect pins the fresh interface itself. A detached capture or a moved index is a replacement; memberships gone from an attached capture is a failed re-join on a live interface and must not evict.
A session's reservation is bound to the target's source address and its response registration captures on the target's socket, so both die with that interface -- yet OnInterfaceChanged never touched the session table. A target that was re-addressed left every in-flight session holding a port nothing would arrive on until it expired, with no recreation involved. A replacement drops them wholesale, since they all sit on the same leg. Short of that, a session goes when the target no longer has the address it reserved, which is the address responders reply to. A source-side change leaves them alone: nothing in a session is pinned to that leg. The reserved address is now recorded on the session, since PortReservation keeps only the fd and the port.
sbogomolov
force-pushed
the
interface/evict-dependent-state
branch
from
August 10, 2026 21:54
dafd003 to
db0eb86
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Interface-recreation recovery repaired the capture and its group memberships.
This drops the state that cannot be repaired: bound sockets and egress pins minted
against the interface's old kernel object, which are dead however the replacement
resolves — including when it comes back with the same addresses, where the
address-comparison predicates saw nothing to do.
How a holder finds out
Interfacecounts replacements, andInterfaceRefpairs a borrowed pointer withthe generation its holder last saw. A holder asks (
TakeReplaced()) rather thanbeing told.
That was the second design. The first added a
Reflector::OnInterfaceReplacedvirtual fired from inside the reconcile loop, which meant two notifications with
different rules about what a handler may read — the existing broadcast runs after
the loop precisely because handlers read live interface state, and interfaces later
in the pass are not reconciled yet. Pulling removes the difference instead of
documenting it: Application writes a counter mid-loop, holders read it from the
existing post-loop broadcast, and an integer compare carries none of that ordering.
operator->keeps every ordinary read unchanged;Get()is deliberately explicitso stashing the bare pointer, and losing the tracking with it, has to be a choice.
TakeReplaced()checks and acknowledges in one step, so there is no separateacknowledgement to forget.
DIAL proxy
Asymmetric, because a listener binds a
source_ifaddress while every upstream isegress-pinned to
target_if. A source replacement drops connections and listeners;a target replacement drops connections only and leaves the listeners serving, since
Connectreadstarget_iflive and the next one pins the fresh interface itself.A detached capture or a moved index is a replacement. Memberships gone from a
still-attached capture is a failed re-join on a live interface — it needs repairing,
but evicting over it would throw away working listeners.
Search sessions (a live bug, no recreation required)
SsdpReflector::OnInterfaceChangednever touched the session table. A session'sreservation is bound to the target's source address and its response registration
captures on the target's socket, so a target that was merely re-addressed left every
in-flight session holding a port nothing would arrive on until it expired.
A replacement drops them wholesale, since they all sit on the same leg; short of
that, a session goes when the target no longer has the address it reserved — the
address responders reply to. A source-side change leaves them alone.
Testing
918 unit tests native (Debug, ASan/UBSan), 907 in docker. Each new behaviour was
mutation-checked: suppressing the generation bump fails only the detach test;
dropping listeners on the target leg fails only the target-asymmetry test;
suppressing the session replacement check and the session address predicate each
fail only their own test. Both commits build standalone.