Skip to content

Evict state minted against a replaced interface - #75

Merged
sbogomolov merged 2 commits into
mainfrom
interface/evict-dependent-state
Aug 10, 2026
Merged

Evict state minted against a replaced interface#75
sbogomolov merged 2 commits into
mainfrom
interface/evict-dependent-state

Conversation

@sbogomolov

Copy link
Copy Markdown
Owner

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

Interface counts replacements, and InterfaceRef pairs a borrowed pointer with
the generation its holder last saw. A holder asks (TakeReplaced()) rather than
being told.

That was the second design. The first added a Reflector::OnInterfaceReplaced
virtual 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 explicit
so 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 separate
acknowledgement to forget.

DIAL proxy

Asymmetric, because a listener binds a source_if address while every upstream is
egress-pinned to target_if. A source replacement drops connections and listeners;
a target replacement drops connections only and leaves the listeners serving, since
Connect reads target_if live 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::OnInterfaceChanged never touched the session table. A session's
reservation 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.

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
sbogomolov force-pushed the interface/evict-dependent-state branch from dafd003 to db0eb86 Compare August 10, 2026 21:54
@sbogomolov sbogomolov self-assigned this Aug 10, 2026
@sbogomolov
sbogomolov merged commit 081063a into main Aug 10, 2026
17 checks passed
@sbogomolov
sbogomolov deleted the interface/evict-dependent-state branch August 10, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant