Skip to content

session: give sessions a true ingress-interface identity - #6928

Draft
psaab wants to merge 26 commits into
masterfrom
fix/4983-session-ingress-iface
Draft

session: give sessions a true ingress-interface identity#6928
psaab wants to merge 26 commits into
masterfrom
fix/4983-session-ingress-iface

Conversation

@psaab

@psaab psaab commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Closes #4983.

The defect

A session carried only its ingress zone, so show security flow session interface <name> and the matching clear could only ask "is <name> bound to the session's ingress zone?". A session on interface X therefore matched a filter for every sibling interface Y of that zone — a wrong-answer-shaped show, and a clear that destroys state the operator did not target. #4792 widened the CLI's zone map from the first bound interface to all of them, which is as precise as a zone-derived answer can be. This adds the real datum.

Verified still present on origin/master d77583f before starting: SessionMetadata (userspace-dp/src/session/entry.rs:24) carried zone but no ifindex; C struct session_value had only fib_ifindex (the egress, and publish_conntrack wrote it 0); pkg/cli/session_filter.go:266,311 still read f.zoneIfaces[val.IngressZone].

The mechanism

SessionMetadata gains ingress_ifindex + ingress_vlan_id, stamped once at install in poll_descriptor (transit forward + host-inbound LocalDelivery) from the frame's UserspaceDpMeta — the binding the packet actually arrived on, plus its 802.1Q tag — and never re-derived from the zone. publish_conntrack mirrors both into the conntrack value; pkg/cli's new resolveIngressIfaces resolves the pair through the same {parent ifindex, VLAN} map the egress side already uses, so one map defines one interface identity for both directions.

The VLAN half is load-bearing, not decoration: reth0.50 and reth0.80 share the loss cluster's physical WAN NIC, so without it they alias onto the parent and the cross-interface match returns for exactly the topology this project tests on.

cli_clear.go needed no edit — it shares matchesV4/V6 and already calls populateIfaceMaps.

Absent-field behaviour (deliberate, documented at the call site)

0 means "no ingress identity carried" and is never a valid ifindex. Three populations carry it, and all three fall back to the zone approximation — never "matches nothing" (which would silently hide them from show/clear), never "matches everything":

  1. the reverse companion — its true ingress is the forward flow's egress, unresolved at install;
  2. a peer-synced session — and this is a design decision, not an omission. An ifindex is node-local: node 0's ge-0-0-1 and node 1's ge-7-0-1 are different numbers for the same logical RETH member, so shipping the peer's value would render a confidently wrong interface name locally, strictly worse than approximating. The identity is therefore not carried on the cluster wire, and no serde/JSON control-protocol field is addedprotocol_wire_v1.json is unchanged (confirmed: git status clean under userspace-dp/tests/fixtures/), so there is no virtio_net AF_XDP zero-copy delivers 0 packets to the XSK — no forwarding on plain virtio (copy-mode fallback?) #1961-class one-sided-wire hazard to grep for;
  3. a session installed by a pre-session filter: sessions lack true ingress-interface identity (filter approximates via zone→interfaces) #4983 helper mid rolling upgrade.

A non-zero ifindex the running config cannot name (interface deleted since install, tunnel/fabric ingress with no config unit) falls back the same way: an unnameable identity is not evidence the session is uninteresting.

ABI change — read this before merging

The two fields join the shared C conntrack struct, not the sync-only trailing fields: session_value 136 → 144, session_value_v6 184 → 192. The u32 lands on the existing 8-byte boundary and the u16 inside the tail pad it forces, so the pair costs 8 bytes, not 16. All three mirrors move in lockstep (C header, Rust BpfSessionValueV4/V6, Go bpfSessionValue{,V6} whose tail pad is declared for the #6082 zero-copy marshal path) and both size asserts are bumped.

sessions/sessions_v6 are pinned maps. Exactly as for the #5460 flags widen (34427efe1, which bumped this same ABI 128 → 136 and shipped), a rolling deploy cannot cross this: validateUserspaceShimLivePins (#5307) refuses while the old daemon is still forwarding, and the documented remediation is a full dataplane reload with brief downtime. That is the operator-visible cost of the fix; it is precedented, but it is a real cost and the reviewer should weigh it.

Fail-on-revert

revert result
resolveIngressIfaces call sites → f.zoneIfaces[val.IngressZone] go test ./pkg/cli -run 4983 exit 1, every failure an assertion message (go vet ./pkg/cli exit 0 — not a build break). Restored: exit 0.
publish_conntrackingress_ifindex: 0 cargo test --release 4983 exit 101, assertion 'left == right' failed ... left: 0, right: 24. Restored: 3 passed.

Over-reach guards, GREEN under both reverts: TestIngressIdentityDoesNotDisturbEgressMatching4983 (FIB egress result, the #4792 egress zone fallback, a non-interface filter), TestIngressIdentityUnresolvableIfindexFallsBackToZone4983, and ingress_identity_does_not_occupy_the_fib_egress_slots_4983 — which pins that the ingress identity does not leak into the FIB egress slots the CLI resolves the egress name from.

Fixture realism: the binding zone has four members across three physical NICs — a single-interface zone makes the assertion true for free, and with two a "fix" that merely swapped X for the zone's other interface would still pass. Every fixture interface carries unit 0, because that is how a real Junos config is written and because a unit-less interface produces no entry in the name map at all; the first draft omitted it and the assertions passed with the fix reverted. TestIngressIdentityAbsentFallsBackToZone4983 holds a session with an identity and one without in the same test and asserts they are treated differently.

Tests run

  • go build ./... — exit 0; go vet ./pkg/cli ./pkg/dataplane ./pkg/dataplane/userspace — exit 0; gofmt -l on every touched .go — clean.
  • go test ./pkg/cli ./pkg/dataplane ./pkg/dataplane/userspace ./pkg/daemon ./pkg/cluster ./pkg/api ./pkg/grpcapi -count=1 -race — green.
  • cargo test --release (full userspace-dp suite) — exit 0, 4242 + 124 passed.
  • go test ./pkg/refactoraudit/ -count=1 — exit 0.
  • Commit 1 verified to build standalone in a detached worktree.

Docs

userspace-dp/src/session/README.md (new section beside the sibling #2785/#3056 stamping sections: where it is recorded, the 0 contract, the ABI + pinned-map consequence) and docs/session-sync-architecture.md (why it is on the conntrack ABI yet deliberately not synced), plus _Log.md.

Reviewer: attack these first

  1. The ABI bump and its deploy consequence — is a full-reload migration acceptable for this fix, or should it be batched with another ABI change?
  2. The poll_descriptor stamp is the one link NOT bound by a fail-on-revert test. Changing ingress_ifindex: meta.ingress_ifindex to 0 at the install site leaves the whole suite green. Every existing poll-level test in tests_embedded_poll_filter.rs either pre-installs the session or asserts none is minted, so binding it needs a new harness that drives a transit flow through poll_binding_process_descriptor to the install point (full FIB + neighbor + permit fixture). I did not build it; the two ends of the chain (publish→ABI, ABI→CLI) are bound.
  3. meta.ingress_ifindex is the PHYSICAL bind ifindex, not the logical unit ifindex. I chose it over prerouting_ingress_scope's ingress_logical because a bondless-RETH VLAN unit can carry a synthetic logical ifindex (syntheticLogicalIfindex, pkg/dataplane/userspace/interfaces.go) that the CLI cannot reproduce; {physical, vlan} is resolvable by the map that already exists. Check I have that trade right.

Adjacent, found, NOT fixed (deliberately out of scope)

The display surfaces still derive the ingress interface from the zone map and were left alone to keep this diff to the issue's stated scope (session_filter.go, cli_clear.go, the dataplane): pkg/cli/cli_show_flow.go (the In: ... If: column), pkg/api/sessions.go, pkg/grpcapi/server_sessions.go. Result: an interface-filtered show now selects exactly, but the If: column it prints for the selected session may still name the zone's representative interface. Worth a follow-up issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi

Paul Saab and others added 3 commits August 6, 2026 06:42
A session carried only its ingress ZONE, so `show security flow session
interface <name>` and the matching `clear` could only ask whether
`<name>` was bound to that zone. A session on interface X therefore
matched a filter for EVERY sibling interface Y of the same zone -- a
wrong-answer-shaped result on `show`, and on `clear` one that destroys
state the operator did not target. #4792 widened the CLI's zone map from
the first bound interface to all of them, which is as precise as a
zone-derived answer can be. This adds the datum that makes it exact.

`SessionMetadata` gains `ingress_ifindex` + `ingress_vlan_id`, stamped
ONCE at install in `poll_descriptor` (both the transit forward entry and
the host-inbound LocalDelivery entry) from the frame's `UserspaceDpMeta`
-- the binding the packet actually arrived on, plus its 802.1Q tag --
and never re-derived from the zone afterwards, since re-deriving is the
approximation being removed. `publish_conntrack` mirrors both into the
conntrack value the Go control plane reads.

The VLAN half is load-bearing, not decoration: two units of one trunk
NIC (reth0.50 and reth0.80 on the loss cluster's WAN NIC) share a
physical ifindex, so without it they alias onto the parent and the
cross-interface match returns for exactly the topology this project
tests on. The pair is deliberately the same {parent ifindex, VLAN}
identity the Go side already resolves the EGRESS interface name by, so
one map serves both directions.

`0` means "no ingress identity carried" and is never a valid ifindex.
Three populations legitimately carry it: the reverse companion (its true
ingress is the forward flow's egress, unresolved at install), a
peer-synced session, and any session installed by a pre-#4983 helper.
The peer case is a design decision, not an omission -- an ifindex is
NODE-LOCAL, so node 0's ge-0-0-1 and node 1's ge-7-0-1 are different
numbers for the same logical RETH member, and shipping the peer's value
would render a confidently WRONG interface name locally, strictly worse
than approximating. The identity is therefore not carried on the cluster
wire, and no serde/JSON control-protocol field is added
(protocol_wire_v1.json is unchanged).

ABI: the two fields join the shared C conntrack struct -- not the
sync-only trailing fields -- growing `session_value` 136 -> 144 and
`session_value_v6` 184 -> 192. The u32 lands on the existing 8-byte
boundary and the u16 inside the tail pad it forces, so the pair costs 8
bytes, not 16. All three mirrors move in lockstep (C header, Rust
BpfSessionValueV4/V6, Go bpfSessionValue{,V6} whose tail pad is DECLARED
for the #6082 zero-copy marshal path) and both size asserts are bumped.
As with the #5460 flags widen, `sessions`/`sessions_v6` are pinned maps,
so a rolling deploy cannot cross this: the #5307 pre-flight refuses while
the old daemon is still forwarding and the remediation is a full
dataplane reload with brief downtime.

Validated: `cargo test --release` green (4242 + 124 across the suites).
`build_conntrack_value_stamps_ingress_identity_{v4,v6}_4983` go RED on an
assertion (`left: 0, right: 24`) with the publish hunk reverted to
`ingress_ifindex: 0`, and the over-reach guard
`ingress_identity_does_not_occupy_the_fib_egress_slots_4983` -- which
pins that the ingress identity does NOT leak into the FIB egress slots
the CLI resolves the egress name from -- stays GREEN under that revert.

Advances #4983.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
`show security flow session interface <name>` and the matching `clear`
read the session's ingress identity now that the dataplane records it,
instead of asking whether `<name>` is bound to the session's ingress
ZONE. A session on interface X no longer matches a filter for a sibling
interface Y of the same zone -- on `clear`, the case where the
approximation destroyed state the operator did not target.

`resolveIngressIfaces` is the ingress twin of the existing
`resolveEgressIfaces` and resolves {IngressIfindex, IngressVlanID}
through the SAME {parent ifindex, VLAN} name map the egress side already
uses, so one map defines one interface identity for the whole filter.
The map field is renamed `egressIfacesMap` -> `ifaceNamesByKey` to say
so; it is read by both directions now, and the rename is confined to
pkg/cli. `cli_clear.go` needs no edit -- it shares `matchesV4`/`V6` and
already calls `populateIfaceMaps` (#1827 PR-3).

The fallback is deliberate and documented at the call site, not an
accident of a zero value. An ingress ifindex of 0 means "no identity
carried" and is never a valid ifindex; the reverse companion, every
peer-synced session (an ifindex is node-local, so it is not carried
across the cluster wire), and any session installed by a pre-#4983
helper mid rolling upgrade all carry it. Those answer from the zone
approximation exactly as before -- 0 must never read as "matches
nothing", which would silently hide them from `show` and `clear`, and
never as "matches everything". A NON-ZERO ifindex the running config
cannot name (an interface deleted since install, a tunnel/fabric ingress
with no config unit) falls back the same way: an unnameable identity is
not evidence the session is uninteresting.

Tests. The binding fixture uses a FOUR-member trust zone across three
physical NICs -- a single-interface zone makes the assertion true for
free, and with two a "fix" that merely swapped X for the zone's other
interface would still pass. Two of the members are VLAN units of one
trunk NIC, mirroring the loss cluster's reth0.50/reth0.80, which pins
the VLAN half of the identity. Every fixture interface carries `unit 0`
because that is how a real Junos config is written and because a
unit-less interface produces NO entry in the name map -- without it the
assertions would pass with the fix reverted.

`TestIngressIdentityAbsentFallsBackToZone4983` holds a session WITH an
identity and one WITHOUT in the SAME test and asserts they are treated
DIFFERENTLY; a test where all or none carry the field cannot see the
fallback branch at all.

Validated: with both `resolveIngressIfaces` call sites reverted to
`f.zoneIfaces[val.IngressZone]`, `go test ./pkg/cli -run 4983` exits 1
with every failure an assertion message (`go vet ./pkg/cli` still exits
0 -- not a build break), and green on restore.
`TestIngressIdentityDoesNotDisturbEgressMatching4983` -- the FIB egress
result, the #4792 egress zone fallback, and a non-interface filter --
stays GREEN under that revert, as does the unresolvable-ifindex case.
`go test ./pkg/cli ./pkg/dataplane ./pkg/dataplane/userspace ./pkg/daemon
./pkg/cluster ./pkg/api ./pkg/grpcapi -count=1 -race` green;
`go test ./pkg/refactoraudit/` green.

Advances #4983.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
The session shape and the conntrack ABI both changed, so the module docs
that describe them move in the same work.

`userspace-dp/src/session/README.md` gains a section next to the sibling
per-session stamping sections (#2785 log flags, #3056 policy id): where
the identity is recorded, that it is stamped once and never re-derived
from the zone, how the CLI resolves it through the same {parent ifindex,
VLAN} map the egress side uses, and the full contract for `0` -- the
three populations that legitimately carry it and why the fallback is to
the zone approximation rather than to "matches nothing". It also records
the ABI growth (136 -> 144 / 184 -> 192) and the pinned-map consequence:
as with the #5460 flags widen, a rolling deploy cannot cross it and the
remediation is a full dataplane reload.

`docs/session-sync-architecture.md` records the opposite-on-both-counts
case next to the sync-only trailing fields it sits beside: the ingress
identity IS part of the on-map conntrack ABI, and it is deliberately NOT
synced. An ifindex is node-local -- node 0's ge-0-0-1 and node 1's
ge-7-0-1 are different numbers for the same logical RETH member -- so
carrying the originating node's value would make the importing node name
the WRONG interface. A peer-synced session therefore lands in the same
"no identity carried" branch as a pre-#4983 one, and the CLI answers it
from the zone exactly as it did before.

Advances #4983.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
@psaab
psaab marked this pull request as draft August 6, 2026 13:48
@psaab

psaab commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

HELD as draft pending a design decision — do not merge.

Two options were raised after this PR was pushed, and both check out against the code:

A no-ABI side table avoids the forced dataplane reload entirely. The ABI pre-flight only refuses when a live pin already exists: validateUserspaceShimLivePins does if !exists { continue } (pkg/dataplane/loader_userspace_shim.go:475-477), and loadOrCreatePinnedShimMapWith then creates a brand-new map fresh. So growing an existing pinned map (what this PR does) cannot cross a rolling deploy, while adding a new one can. That asymmetry is the whole cost of this PR, and it is avoidable.

The stored identity can be cluster-stable rather than node-local. docs/ha-cluster-userspace.conf binds its zones to reth0.50 / reth0.80 / reth1 — byte-identical strings on both nodes — while only the members differ (ge-0/0/1 vs ge-7/0/1), and Config.RethToPhysical (pkg/config/types.go:64-94) resolves reth→member by local node id. A stable fold of the reth-relative name, in the manner of config.StableZoneID (#3075), is therefore agreed by both nodes by construction and can safely ride the HA wire.

That second point matters more than the downtime: as scoped here, every peer-synced session lands in the absent-field branch, so after a failover the new master degrades to exactly the pre-#4983 zone approximation — the diagnostic stops working in the case where diagnostics matter most.

The Step-0 absence proof and the fail-on-revert probes in this PR stand either way; the ABI mechanism is what is under review.

Paul Saab added 2 commits August 7, 2026 16:45
Resolve the sole conflict, _Log.md, by union: every entry from both
sides is retained and none is rewritten. The file is no longer
append-ordered, so line counts and prefix checks say nothing useful
about the result; the resolution was verified structurally instead, by
confirming that each pre-merge side diffs into the merged file with
add-hunks only and no changed-or-deleted hunk on either side.

Every other path merged without conflict. Because a clean textual
auto-merge can still break compilation when a signature moves on one
side, that was confirmed by building rather than by inspection: go build
./... clean on the merged tree, and cargo check --all-targets clean
for the Rust crate.

Advances #6928.
The #4983 feature is a three-link chain — the poll body PRODUCES the
session's ingress identity, publish_conntrack MIRRORS it onto the map, and
the CLI filter CONSUMES it — and only the last two were pinned. The Rust
tests hand-set a SessionMetadata and the Go tests hand-build a
dataplane.SessionValue, so both sides of the middle were bound and the
producer was not: replacing the four `meta.ingress_*` lines in
poll_binding_process_descriptor with `0` left the whole Rust suite green.
A later refactor of that 5000-line function could drop them and every
session would silently carry ingress_ifindex 0, returning
`show/clear security flow session interface X` to exact pre-#4983
behaviour with CI green and no counter or log to notice.

tests_session_ingress_identity.rs drives the real
poll_binding_process_descriptor with a permitted LAN -> WAN SYN ingressing
on a VLAN unit of a TRUNK NIC (parent ifindex 11, VID 50) whose sibling
unit on the same parent is the egress and whose parent resolves to a
different zone. A separate test asserts the reverse companion installed by
the same call still carries 0/0, so the two arms are distinguished rather
than both satisfied by one constant.

build_missing_neighbor_session_metadata also hardcoded 0/0 — for a FORWARD
session, with `meta` in the caller's scope 32 lines above. That seed is
installed whenever a flow's first packet races an unresolved ARP/NDP, is
published to the conntrack map at install, and is never re-installed
(retry_pending_neigh replays the buffered frame and takes no
&mut SessionTable), so such a flow kept the zone approximation for its
whole life. It now takes the ingress binding from the frame.

The detailed show output derived its "In: ... If:" column from the ingress
zone's first interface, so once the filter became exact the two disagreed:
an interface-filtered show selected the right sessions and printed the
wrong interface for each. A sessionIngressIf closure — same shape as the
existing sessionEgressIf — resolves it from the recorded identity.

Claims are scoped and corrected. The consumer change is in the in-daemon
CLI only; pkg/grpcapi (which the remote cli binary uses for show AND
clear) and pkg/api keep the zone approximation in its pre-#4792
first-interface-only form, and this branch's diff against both is empty.
The shipped population list named the reverse companion — whose fallback
is inert, since every CLI show/clear call site skips IsReverse != 0 before
filtering — and omitted the seed path and the host-outbound GRE path in
tunnel.rs. Two ABI notes still said "size-asserted at 136"; the assertions
are 144/192.

Validation. cargo test --release: 4257 passed / 0 failed / 2 ignored in the
main binary, every other target ok. go build ./... exit 0; go test on
pkg/cli, pkg/dataplane, pkg/grpcapi, pkg/api all ok. Mutation matrix, each
applied by edit and restored to a clean git diff:

  - both poll-body stamps -> 0, measured against the FULL Rust suite: 4256
    passed / 1 FAILED, the single failure being the new transit test
    ("... left: 0, right: 11"). Nothing else among 4257 tests sees it.
  - transit ingress_vlan_id -> 0 alone: same test RED on the VID assertion
    ("left: 0, right: 50") — the two halves bind independently.
  - seed stamp -> 0 with both poll-body stamps intact: the seed test RED
    ("left: 0, right: 11"), transit test GREEN.
  - reverse companion -> meta.ingress_*: the over-reach guard RED
    ("left: 11, right: 0") — proven to fire, not merely to stay green.
  - display call sites -> zoneIfaces[val.IngressZone]: the column test RED
    on both the v4 and v6 arms; the egress-column guard stayed GREEN.

Every RED is an assertion failure, not a build break.

Advances #4983.
@psaab

psaab commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

Parent mutation matrix at 0e5d35f11 — one blocking gap

Full cargo test --release, one production stamp site reverted to 0 per cell:

cell site reverted to 0 result
control none rc=0, unit binary 4257 passed / 0 failed
A1 LocalMiss stamp, poll_descriptor/mod.rs ~1927-1928 rc=0, 4257 passed — UNBOUND
A2 ForwardFlow stamp, poll_descriptor/mod.rs ~2422-2423 rc=101 RED
B MissingNeighborSeed call-site args, ~4812-4813 rc=101 RED

A2's sole failure was
poll_descriptor_transit_install_stamps_ingress_binding_4983.

A2 and B going RED is the internal control that makes A1 interpretable.
The same mutate → rebuild → test loop reddens two other sites, so A1's green
is a genuine "nothing binds this", not a stale-test-binary artifact.

Root cause

userspace-dp/src/afxdp/tests_session_ingress_identity.rs contains exactly
three tests — transit (ForwardFlow), reverse-companion (deliberately 0), and
the missing-neighbor seed — and zero occurrences of localmiss,
local_miss, helper_local, or install_helper. No test drives the
host-inbound LocalMiss install path
(install_helper_local_session_on_misspublish_bpf_conntrack_entry) at
all, so its stamp could be 0 indefinitely without any test noticing — in a
PR whose claim is that every forward session carries a true ingress identity.

Owed before merge

A test that drives the LocalMiss install end to end and asserts the stamped
ingress_ifindex/ingress_vlan_id, with RED-on-revert observed. The fixture
must use distinct non-zero values (not 0, not 1, and not the transit
test's values) — equal or default values make the assertion vacuous, since it
must distinguish "stamped correctly" from "stamped from the wrong source".

Note on a mis-aimed first attempt

My first matrix targeted neighbor_dispatch.rs:449
(ingress_ifindex: meta.ingress_ifindex as i32) for the seed cell. That is
the LearnedNeighborKey used for dynamic-neighbor learning, not the
session-metadata stamp, so that cell would have proved nothing either way. The
seed path stamps via build_missing_neighbor_session_metadata
(neighbor_dispatch.rs:606-625), whose struct init uses field shorthand — the
real values are passed from the poll_descriptor call site, which is what
cell B mutates.

The reverse companion (~2749-2758) is deliberately 0 and documented, so it
is not a cell.

Paul Saab and others added 2 commits August 12, 2026 10:19
A per-site mutation matrix at the previous head found the LocalMiss
(host-inbound) install's ingress stamp UNBOUND: reverting
`ingress_ifindex` / `ingress_vlan_id` to 0 in the `local_metadata`
literal left the whole Rust suite green at 4257/4257. The same
mutate/rebuild/test loop applied to the ForwardFlow install and to the
missing-neighbor seed call site both went RED, so the green was a real
"nothing binds it" rather than a stale-binary artifact.

The cause is a coverage hole, not a production defect: the stamp is
present and correct at the site, but `tests_session_ingress_identity.rs`
had zero occurrences of `local_miss` / `install_helper` — no test drove
the host-inbound install path at all. Host-bound flows (management SSH,
BGP, syslog-TCP, RPM/feed fetches) are exactly the population an
operator filters by interface when the firewall itself is the endpoint,
and on a multi-interface zone the pre-#4983 fallback answers with every
sibling.

Add `poll_descriptor_local_miss_install_stamps_ingress_binding_4983`,
which drives one host-bound TCP SYN through the real
`poll_binding_process_descriptor` body. The destination is the ingress
unit's OWN address, so the session-miss resolution is `LocalDelivery`
and the poll takes `install_helper_local_session_on_miss` ->
`publish_bpf_conntrack_entry`; the test then asserts the installed
`SessionOrigin::LocalMiss` metadata carries the frame's binding.

The fixture adds a THIRD LAN unit, `reth2.70` on its own trunk:
{parent ifindex 7, VLAN 80}, logical ifindex 17. Every number is
disjoint from every other value the stamp could have been copied from —
7 is neither 0 nor 1, so no default satisfies it; 7 != 80, so a
transposition of the two halves is RED; 7 != 17, so stamping the
RESOLVED LOGICAL unit instead of the received binding is RED; 7 is
neither zone id; and {7,80} is neither the transit fixture's {11,50}
nor its egress sibling's {11,80}, so no single constant satisfies this
test and the transit test at once. VLAN 80 is deliberately reused from
reth0.80 on a DIFFERENT parent, because the ingress map is keyed by the
{parent, VLAN} pair: that separates "recorded the pair this frame
arrived on" from "found whichever unit carries VLAN 80". Fixture
liveness is asserted before the metadata (`dbg.local == 1`, exactly one
installed session, the binding resolves to the lan logical unit), so a
fixture that stopped admitting the flow, or that started taking a
transit arm, is RED rather than vacuously green.

Validation, one production site mutated per cell, full
`cargo test --release` each time. Cell A (both fields to 0 at the
LocalMiss literal) rc=101 with the new test as the ONLY failure —
"the host-local session must record the ifindex of the binding its
first packet arrived on ... left: 0 right: 7" — while the transit,
reverse-companion and neighbor-seed tests stay green. Cell B (restore
the ifindex, zero only `ingress_vlan_id`) rc=101 with "... must record
the ingress 802.1Q VID ... left: 0 right: 80", proving the VLAN half
binds independently instead of hiding behind the first assertion.
Restored: `cargo test --release` rc=0, 4382 passed / 0 failed (4258 in
the main binary, +1 over the baseline); `go test ./pkg/cli/...
./pkg/dataplane/...` rc=0.

Also re-derive the site enumeration rather than trusting it. The 11
`SessionMetadata` literals in `userspace-dp/src` include two under
`#[cfg(test)]`; the nine live ones are the four poll populations plus
the synthesized reverse companion, the peer-synced wire ingest, the
host-outbound GRE path, the flow-cache seed, and `promote`, which
mutates an existing metadata and preserves the pair. Cross-checked
against every session install site and every
`publish_bpf_conntrack_entry` call site, plus a grep for direct writes
to the two fields (there are none outside the literals). NAT64 and
fabric-redirect ride the ordinary ForwardFlow install and there are no
ALG child-session installs, so no fifth site exists. The documented
zero-populations are unchanged, hence no edit to the field contracts in
`session/entry.rs` or `pkg/dataplane/types.go`; `session/README.md`
records that all three stamping sites now carry a per-site
fail-on-revert test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
Master advanced while the LocalMiss coverage fold was in review. Only
_Log.md conflicted; resolved as a union keeping both sides' entries in
full (85 lines from the PR branch, 87 from master), which is correct for
an append-only activity log recording unrelated work.

No source file conflicted, so the fold's hunks carry across unaltered.
Paul Saab and others added 3 commits August 12, 2026 11:08
A hostile review claimed the transit stamp could be rewritten as
`if owner_rg_id > 0 { meta.ingress_ifindex } else { 0 }` with the whole
suite still green. Verified rather than assumed: that mutation leaves
all four ingress-identity tests GREEN. The escape is not academic — it
is exactly the shape a future HA-scoping change would take, and it would
silently drop the identity on every STANDALONE firewall, the majority
deployment, with CI reporting nothing.

The cause is that the existing fixture is a chassis-cluster topology, so
its installed session always carries a non-zero `owner_rg_id`; an
`owner_rg_id`-conditional stamp is indistinguishable from an
unconditional one there. Add a second arm that replays the SAME driven
flow on a standalone topology and asserts the identity is still stamped.

Both halves of "not clustered" are required, and the driver now records
why. `enforce_ha_resolution_snapshot` (forwarding/ha.rs) turns a
resolution whose `owner_rg_id <= 0` into `HAInactive` when `ha_state` is
NON-empty, because that combination means a cluster node whose snapshot
predates the RETH RG propagation fix. Clearing only the redundancy
groups produced an arm that installed ZERO sessions and went red on the
liveness assertion — a fixture that never drove the path at all, which
would have looked like a passing mutation check for entirely the wrong
reason. The arm therefore also asserts `owner_rg_id == 0` as an explicit
precondition, so it cannot quietly reacquire an RG and degrade into a
duplicate of its sibling.

Validation: with the `owner_rg_id` gate applied, the new arm is RED on
the IDENTITY assertion — "a standalone firewall's transit session must
record its ingress binding too ... left: 0 right: 11" — while the
chassis-cluster sibling, the LocalMiss test, the seed test and the
reverse over-reach guard all stay GREEN. Restored: `cargo test --release`
rc=0 with 4259 passing in the main binary; `go test ./pkg/cli/...
./pkg/dataplane/...` rc=0.

Also correct two documentation defects found in the same review.

Ten ABI size figures in `pkg/dataplane/bpf_session_value.go` still read
136/184 — including "mirrors C struct session_value exactly (136 bytes)"
and "fails if the size drifts from 136". The real sizes are 144/192; the
PR had updated `types.go` and the parity test but missed the file that
documents the ABI contract. Corrected, with the earlier values kept as
explicit history and the #6082 `binary.Size` measurement re-scoped to
the struct's size at that time.

`session/README.md` listed "a session installed by a pre-#4983 helper,
mid rolling upgrade" as a legitimate zero-carrying population while the
ABI note directly below it said a rolling deploy cannot cross this
change. The note is right, and the mechanism was verified at both sites:
`sessions`/`sessions_v6` are in `userspaceABICheckedPinnedMaps` (which
unions `userspaceShimSharedMapSpecs`) and `validateUserspaceShimLivePins`
hard-refuses a `ValueSize` mismatch against the live pin, so a new daemon
never reads an old helper's rows and the remediation starts from an empty
map. The population is unreachable, not merely rare; the entry is
replaced with that reasoning in `session/README.md`, `session/entry.rs`
and both copies in `pkg/dataplane/types.go`.

Finally, record the coverage boundary in the test module itself: these
tests bind what the poll body STAMPS onto the installed session. They
cannot assert that a TRANSIT session's identity reaches the
operator-visible conntrack map, because the transit install never calls
`publish_bpf_conntrack_entry` — it writes shim steering keys via
`publish_live_session_entry` plus the shared maps. Only the LocalMiss and
missing-neighbor-seed installs publish there. That gap predates this work
and is tracked separately; stating it here keeps the tests from being
read as proof of something they do not check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
The #4983 docs said the stamped ingress identity is mirrored into the
conntrack value "where the Go control plane reads them". That is true of
the metadata and false of what reaches `show security flow session`, and
the difference is the dominant population.

`show`/`clear security flow session` enumerate the BPF conntrack map
(`Manager.IterateSessions` over `m.maps["sessions"]`). The helper writes
that map from exactly three sites, all in `afxdp/poll_descriptor`: the
host-inbound LocalMiss install, the missing-neighbor-seed install, and
the reverse-companion repair — and the third carries `IsReverse != 0`,
which every show/clear call site skips before filtering. The ordinary
TRANSIT forward install is not among them. It calls
`publish_live_session_entry`, which writes the shim's steering table: a
36-byte key whose value is a single action byte, a different map from
the 144-byte conntrack value. So a transit session has no conntrack row
at all — not a zeroed identity, no row — and an interface filter cannot
select it either way.

That gap predates this work. It dates to `fab9230c5`, the commit that
first added the conntrack mirror and wired only these three sites, and
it is strictly larger than #4983: it is tracked as #6965, which needs
research before implementation because publishing at the transit install
adds a BPF syscall to the new-session cold path (the path #5287 exists
because a full-table conntrack pass stalled the low-latency core) and
forces three open decisions — whether the reverse companion gets a row,
what happens at conntrack-map capacity, and whether SessionCount /
Prometheus / GC semantics shift once the mirror is near-complete.

Nothing here changes behaviour; it stops the documentation from
promising more than the code delivers. `session/README.md` gains a
"Which sessions this is OPERATOR-VISIBLE for" paragraph, and the same
scope note lands on both `IngressIfindex` field docs in
`pkg/dataplane/types.go` and on the field doc in `session/entry.rs`.

The transit test's doc comment now records why it has no assertion for
the mirror, since that absence otherwise reads as an oversight: the test
stays green if you "omit the full conntrack publication" because there
is no transit publication to omit. An assertion that the stamp reaches
the operator-visible map would fail today and would be testing #6965's
fix, not this one. The test binds the stamp; the mirror is #6965's to
bind. The module header states the boundary once and points at #6965.

Docs and comments only — no production or test logic changed. Gates:
`cargo test --release` rc=0 with 4261 passing; `go build` and
`go vet ./pkg/dataplane/...` rc=0; `go test -count=1 ./pkg/cli/...
./pkg/dataplane/...` rc=0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Re-verification of the Codex leg at the current head — 3 of 8 blockers are already CLOSED

Recording this on the PR rather than leaving it in a review conversation, because the
next lane to pick this up needs it and cannot otherwise find it.

The Codex leg pinned 0e5d35f11. The head is b7a84249d, two folds later. Every one of
its eight blocking findings was re-verified at the head by re-running Codex's own
mutation
, not by inferring from commit titles.

CLOSED — the mutation no longer holds

#3 — "the real-poll transit binder covers only an HA-owned path." Codex's mutation was
"stamp meta.ingress_* only when owner_rg_id > 0; every new real-poll test remains
green." At this head it does not:

a standalone firewall's transit session must record its ingress binding too
(RED on gating the stamp behind owner_rg_id > 0, which the chassis-cluster
sibling cannot detect)
  left: 0   right: 11

That is poll_descriptor_transit_install_stamps_ingress_binding_without_an_rg_4983, the
standalone RG-0 + empty-ha_state arm added in round 2. Codex's diagnosis was correct —
the fixture's egress reth0.80 is RG1, so the ingress RG2 never owned the flow — and the
fix landed after the pin.

#4 — "LocalDelivery is a separate, unguarded forward producer." Codex's mutation was
"revert only :1927-1928 to zero; all added producer tests remain green." At this head:

the host-local session must record the ifindex of the binding its first packet
arrived on
  left: 0   right: 7

#1 — "the ordinary transit producer never reaches the Go-visible conntrack map."
Technically accurate, including the point that the BPF_EXIST refresh cannot create a
row. Split to #6965 by maintainer decision; the scope note is present at four sites in the
test module, four in types.go and two in session/README.md. Characterised residual,
not a gap.

STILL OPEN — five

A fixture-realism defect found against this PR's own fixture

ingress_identity_snapshot gives reth0.50 RG2 while nat_snapshot's reth0.80 is RG1,
both units of parent 11. Production cannot create that: RG is a base-interface property
(types_interfaces.go:11-30) and the snapshot builder stamps one base RG onto every unit
(dataplane/userspace/interfaces.go:214-218,290-303). The metadata assertions are not
vacuous, but the fixture comment claims a property production cannot reach and will
mislead the next reader. Being removed at tests_session_ingress_identity.rs:96,440.

Two of the five surviving Codex blockers on #4983.

#2 — the doc claimed "an interface-filtered show/clear is exact on the
console". It is not, for clear. `clearFilteredSessions` propagates to the HA
peer unconditionally (pkg/cli/cli_clear.go:252, no interface-filter guard),
the filter rides gRPC, and the peer resolves the interface from the ingress
ZONE using `zone.Interfaces[0]` for every session in that zone
(server_sessions.go:508-515, matched at :578-583 and :623-628 where `inIf`
depends only on the zone, never on the session). On zone
`[reth0.50, reth0.80]`, clearing `.50` therefore DELETES peer flows actually
received on `.80` — a wrong-session deletion, not a display defect.

The runtime hole is pre-existing and this PR does not widen it, which was
worth establishing by measurement rather than argument: the PR's diff against
master is empty for both pkg/grpcapi and pkg/api, and empty for
pkg/cli/cli_clear.go. What it adds is `resolveIngressIfaces` (absent on
master), which makes the local half exact and narrows the defect. What it
also added was a claim overstating that as end-to-end exactness. The claim is
now corrected at both sites: show is exact for rows this node owns, clear is
not exact at all because it leaves this node. Filed as #6975 with the trace;
the previous "tracked separately" named no issue.

#6 — the ABI guards assert sizeof and binary.Size, so they cannot see a field
REORDER. Swapping the Go tail to `IngressVlanID; pad; IngressIfindex` keeps
144/192 and keeps binary.Size equal to it, while C and Rust still write the
ifindex at 136/184; a record carrying `{ifindex:11, vlan:50}` then decodes as
`{ifindex:50, vlan:11}`. Both are plausible values, so the CLI filters
confidently on the wrong interface rather than falling back to the zone
approximation the way a zero would make it. Added
TestBPFSessionValueIngressIdentityOffsets pinning all four offsets against
bpf/headers/xpf_conntrack.h.

Validation for #6 uses Codex's own mutation and carries its own negative
control: under the reorder the two size guards stay GREEN (`ok`) — which is
the point — while the new guard reds on all four offsets,
`offsetof(bpfSessionValue.IngressIfindex) = 140, want 136` and siblings. go
build 0, go vet 0 on pkg/dataplane and pkg/cli, package tests ok.

Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Progress at 27958852f — two more blockers closed, three remain

Follow-up to the re-verification comment above. Two of the five surviving findings are
now closed with measured evidence; three are untouched and available.

#2 — the CLAIM is closed; the runtime hole is PRE-EXISTING and now tracked as #6975

The end-to-end defect is real and the trace holds: propagation is unconditional
(pkg/cli/cli_clear.go:252), the filter rides gRPC (:677), and the peer collapses the
zone (server_sessions.go:508-515) then matches on the collapsed value at both
:578-583 and :623-628 — so matching depends on the zone, never on the session. On
zone [reth0.50, reth0.80], clearing .50 deletes peer flows received on .80.

But the provenance decides the disposition, and it was measured rather than argued:

git diff origin/master...27958852f -- pkg/grpcapi/ pkg/api/   -> 0 files
git diff origin/master...27958852f -- pkg/cli/cli_clear.go     -> 0 files
resolveIngressIfaces   on master: 0 files   at head: 4 files
"exact on the console" on master: 0          at head: 2

Independently re-run by the maintainer. Both halves of the runtime hole are untouched
by this PR. What the PR adds is the local exactness, plus a claim that overstated it as
end-to-end. So the claim is this PR's to fix and the runtime gap is not — the same call
made on #6971 for #6927.

Claim corrected at both sites: show is exact only for locally-owned rows, and clear is not
exact at all because it leaves this node. Runtime gap filed as #6975 with the full
trace and fix shape. The previous "tracked separately" carried no issue number, which made
it unverifiable — a pointer nobody can follow reads as handled.

#6 — closed, with the negative control built into the result

TestBPFSessionValueIngressIdentityOffsets pins all four offsets against
bpf/headers/xpf_conntrack.h — measured v4 136/140, v6 184/188, matching C exactly.

The mutation is the reviewer's own: reorder both struct tails to
IngressVlanID; pad; IngressIfindex.

=== SIZE guards (predicted GREEN) ===
ok    github.com/psaab/xpf/pkg/dataplane      <-- the control: they cannot see a reorder

=== NEW offset guard ===
--- FAIL: TestBPFSessionValueIngressIdentityOffsets
offsetof(bpfSessionValue.IngressIfindex)   = 140, want 136 — records decode with the
  ingress identity fields transposed and the CLI filters confidently on the wrong interface
offsetof(bpfSessionValue.IngressVlanID)    = 136, want 140
offsetof(bpfSessionValueV6.IngressIfindex) = 188, want 184
offsetof(bpfSessionValueV6.IngressVlanID)  = 184, want 188

The size guards staying green under the same edit is what makes this a control rather than
a coincidence: they demonstrably cannot see a reorder, which is exactly the gap the offset
guard fills.

Still open

  • Clear stale NAT64 dataplane state when NAT64 config is removed #5 — both Go conversion directions are deletable with tests green
    (bpf_session_value.go:231-232,272-273,312-313,352-353). The round-trip fixture leaves
    both identity fields zero, so it cannot see the erasure. Needs separate non-zero
    fixtures for toBPF and sessionValue; a symmetric round-trip cannot catch identical
    mistakes in both directions.
  • IPv4 DNAT-before-fabric helper uses fixed L3/L4 offsets #8 — the egress-FIB control cannot fire: the fixture's FibIfindex=12 resolves to
    ge-0/0/1, which is also the egress zone's sole fallback, so deleting the precise arm at
    session_filter.go:469-472 changes nothing. Whoever builds this fixture: check the
    topology is producible.
    This PR already shipped one that is not — reth0.50 at RG2
    alongside reth0.80 at RG1, both units of parent 11, which production cannot create
    because RG is a base-interface property stamped onto every unit. An unreachable fixture
    looks like coverage and is not.
  • Interface-mode SNAT can select wrong source IP on snat_egress lookup miss #7 — the display fallback at cli_show_flow.go:312-315 is unguarded; making it
    return "" leaves both added display tests green.

Gates at this head: go build ./... 0, go vet 0 on dataplane and cli, go test ok on
both. Not yet rebased — that is deliberately last.

Paul Saab and others added 4 commits August 12, 2026 17:32
A separate Codex review leg pinned a head two folds old. Every blocking
finding was re-verified at the current head before acting, and three of the
eight were already closed by later rounds — re-running Codex's own mutations
was the only way to establish that, since commit titles cannot.

The fixture finding is against my own work and it is right. The
ingress-identity snapshot gave reth0.50 redundancy group 2 while its SIBLING
unit on the same parent, reth0.80, is group 1. That topology cannot be
produced: the snapshot builder resolves one redundancy group per BASE
interface and stamps that single value onto every unit of it, so two units of
one parent always agree. The old justification cited reth1.0 as "the fixture's
other LAN interface", but reth1.0 is a different base and does not constrain
this unit at all.

Changed to group 1, which is producible and leaves behaviour identical because
ownership derives from the egress resolution, not from the ingress unit's
group. The second added unit keeps group 2 and now records why that one IS
producible — it is the only unit on its parent, so there is no sibling to
conflict with. A fixture change owes a fresh mutation check, so the standalone
guard was re-proven red against the corrected fixture rather than assumed.

The claim corrections are done as one pass, because this class clusters and
that has now held on two consecutive PRs.

The README said the filter and the displayed interface name cannot disagree.
True only for a stamped identity: on the fallback the filter considers every
interface in the zone while the display uses the first, so a zero-identity row
in a two-interface zone is selected by one name and prints the other.

The reverse companion's zero was justified as the forward egress being
unresolved at install. The installed decision already carries it. The real
reason is that the forward egress predicts where the reply will arrive rather
than observing where it did, and routing may be asymmetric.

"VID 0 means untagged" is not synonymous — a priority-tagged frame carries VID
0 with the tag present, and the frame inspector's property tests cover exactly
that shape. Reworded to "no VLAN id recorded".

"The two policy-admitted install sites" overstates one of them: the
host-inbound install also runs for a junos-host NoMatch flow, admitted by the
zone's host-inbound set with no policy matching at all.

Finally, types.go still described the on-map layout as 128 bytes and the
over-size as exactly eight; it is 144 for v4 after this issue's growth, and the
excess is whatever the sync-only trailing fields sum to. The same file called
REST show/clear approximate, when the REST handler rejects a filtered clear
with HTTP 400 rather than degrading it — there is no approximate REST clear to
be wrong about. Swept for other instances of the stale size: this was the only
survivor.

Gates: cargo test --release rc=0 with 4261 passing; go test over pkg/cli and
pkg/dataplane rc=0; rustfmt and gofmt clean on every touched file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
Close the last three blocking findings on #4983's review: the two Go
conversion directions and the CLI display fallback were deletable with
the suite green, and this PR's own egress-FIB subtest could not fire.
No production code changes — all three were binding and claim gaps.
Each was reproduced with the reviewer's own mutation before anything
was written.

Conversion directions. Deleting all four IngressIfindex/IngressVlanID
assignment pairs from toBPF/sessionValue (v4 and v6) left
`go test ./pkg/dataplane/` ok, because the round-trip fixture left both
identity fields at zero and 0 -> absent -> 0 compares equal. The
fixture now carries non-zero, mutually distinct values, and two new
tests assert on the intermediate value instead of the composition:
TestSessionValueToBPFCarriesIngressIdentity and
TestBPFSessionValueLiftsIngressIdentity. The latter builds a
bpfSessionValue DIRECTLY rather than via toBPF, because the real case
is a record the Rust helper wrote that Go's writer never touched.

Both are needed beyond the fixture change. orig.toBPF().sessionValue()
is a symmetric composition, so it detects any single-direction loss but
is structurally blind to the same mistake made in both directions — a
transposition composes to the identity while every on-map record is
written and read at the wrong offset.

Egress-FIB control. Confirmed at subtest scope and refuted at package
scope, which changes the disposition. With resolveEgressIfaces' precise
arm deleted, TestIngressIdentityDoesNotDisturbEgressMatching4983 and
all three subtests still pass: the fixture's FibIfindex=12 resolved to
ge-0/0/1, the untrust zone's only member, so the precise answer and the
zone fallback were the same one-element list. The package does not stay
green, though — the pre-existing
TestMatchesV4_InterfaceFilter/matches_egress_via_FIB_lookup fails,
because its FIB name is outside the egress zone. The arm's admitting
half was always bound; its NARROWING half was not, and the subtest
presented as a control it could not be.

The untrust zone now binds two interfaces so the zone fallback is a
strict superset of any FIB-precise answer, and
TestEgressFIBIdentityDoesNotMatchEgressZoneSibling4983 asserts the
sibling is excluded on both address families, with a fixture control:
with FibIfindex=0 the same session must still reach the sibling through
the fallback, or the exclusion would be explained by unreachability
rather than by narrowing. The old subtest keeps its over-reach charter
and now states it, naming both real bindings.

That shape was checked for producibility before it was relied on:
populateIfaceMaps appends every zone.Interfaces entry and #4792 exists
because a zone binds more than one interface. Each member is a separate
NIC with its own unit 0 and ifindex, so no redundancy-group property is
involved and there is no base-interface constraint to violate.

Display fallback. Wider than filed: replacing sessionIngressIf's zone
fallback with `return ""` left the entire pkg/cli package green, not
just the two added display tests. That fallback is the compat path for
every session that legitimately carries no ingress identity — the
reverse companion, a peer-synced session whose originating ifindex is
node-local, and a session installed by a pre-#4983 helper mid rolling
upgrade — so blanking it empties the In: If: column for every HA-synced
flow. The new test covers the three ways the resolver can be asked:
identity absent and identity present-but-unnameable both fall back to
the zone's interface, and a zone binding no interfaces falls back to
the zone name. The fixture builder is parameterised so one config
drives the precise arm and every fallback arm, and the empty zone goes
through the real parser and commit with an assertion that it survived
with zero interfaces, so the last-resort arm is reachable by
construction rather than by assumption.

The README described only the precise arm of the column, which reads as
though a blank column were acceptable for the populations that carry no
identity. It now states the fallback symmetry the new tests enforce.

Validation. Every mutation cell ran go vet first at rc 0, so each RED is
an assertion failure rather than a build break.

Conversion matrix (4 tests x 5 cells), with the transposition as the
negative control:

  cell                          round trip  offsets  toBPF  lift
  baseline                      PASS        PASS     PASS   PASS
  delete both directions        FAIL        PASS     FAIL   FAIL
  transpose both directions     PASS        PASS     FAIL   FAIL
  delete v4 toBPF only          FAIL        PASS     FAIL   PASS
  delete v4 sessionValue only   FAIL        PASS     PASS   FAIL

The transpose row is the point: the round trip cannot see it while both
direction tests fail with "v4 toBPF().IngressIfindex = 50, want 11".
Rows 4 and 5 show each direction test binds its own direction. The
offsets guard passes in every row — it pins layout, not assignment — so
the three tests are orthogonal rather than overlapping.

Egress: deleting the precise arm fails the new sibling test on v4 and
v6 while the over-reach subtest stays green on all three subtests.

Display: with the whole fallback returning "", all three new subtests
fail and both existing display tests pass. With only the final
`return zoneName` replaced, only the zone-with-no-interfaces subtest
fails — each arm is separately bound, not covered as a block.

go build ./... rc 0; go vet ./... rc 0;
go test ./pkg/dataplane/ ./pkg/cli/ -count=2 both ok;
go test ./pkg/refactoraudit/ ok.
Bring the branch up to master (a3f333f) after the round-3 fold, as the
last step of the round rather than the first — a rebase mid-round would
have replayed each commit onto a moving base for no benefit.

Only _Log.md conflicted, in four hunks; every other file merged
automatically, including the userspace-dp sources both sides touched.

The textually clean merge did not compile. Master's 3775af5 adds a new
file that constructs SessionMetadata with a struct literal, and this PR
adds two fields to that struct — an added field in one file and an added
file in another, so no textual conflict exists and git is silent. The
incompatibility is in the type system and only a build can see it, which
is why the merge result was built rather than assumed:

  error[E0063]: missing fields `ingress_ifindex` and `ingress_vlan_id`
  in initializer of `entry::SessionMetadata`
    --> src/afxdp/session_glue/newflow_contention_tests.rs:68:5

Fixed in this merge commit rather than a follow-up so the merge commit
itself builds and the branch stays bisectable. The value is 0/0, which is
correct rather than a placeholder: the fixture builds a
SyncedSessionEntry, and a peer-synced session carries no ingress identity
by design — an ifindex is node-local, so the originating node's number
names a different NIC on the importing node, and it is deliberately kept
off the cluster wire. The comment records that, so the next reader does
not "correct" it to a non-zero value.

_Log.md was union-resolved and verified structurally rather than by
keyword: the merged file is a superset of BOTH parents by line multiset
(0 lines of 190ea01 and 0 lines of a3f333f missing). A keyword probe
was tried first and matched nothing, which proves nothing about the
resolve — a probe that does not fire is not evidence. An anchored sweep
for git's exact marker syntax over the whole tree returns nothing.

Gates on the merge result: go build ./... rc 0; go vet ./... rc 0;
go test ./pkg/dataplane/ ./pkg/cli/ both ok; go test ./pkg/refactoraudit/
ok; cargo test --release in userspace-dp exit 0, 4279 passed / 0 failed
plus 60, 8, 22, 31, 1 and 2 passed with 0 failed. userspace-xdp/ is
untouched by this PR, so the shim .o and its manifest are unchanged and
no make generate is owed.
Another lane pushed "session: drop an unproducible fixture dimension and
correct six claims" onto the branch while this round was in flight. My
base 2795885 is still an ancestor — no history was rewritten — so this
integrates by merge rather than by force-push.

Only _Log.md conflicted, in one hunk, union-resolved and verified
structurally: the merged file is a superset of BOTH parents by line
multiset (0 lines missing from either). The README auto-merged and
carries both sides' new paragraphs.

The other side's code changes are comment-only in pkg/dataplane/types.go
and userspace-dp/src/session/entry.rs, plus the removal of the
unproducible RG fixture dimension. That was still built rather than
assumed, because the previous merge in this round was textually clean and
did not compile. Nothing broke: the two rounds are disjoint — that one
corrects claims and drops a fixture dimension, this one adds bindings.

go build ./... rc 0; go vet ./... rc 0; go test ./pkg/dataplane/
./pkg/cli/ both ok; go test ./pkg/refactoraudit/ ok; cargo test --release
in userspace-dp exit 0, 4279 passed / 0 failed plus 60, 8, 22, 31, 1 and
2 passed with 0 failed.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Parent mutation proof at 6eca116db — PASSED, 4-cell discriminating matrix

Recording the measurement on the PR so it survives the conversation it was produced in.

The claim under test is that a session's ingress identity is stamped by production at each install site — not merely that a helper computes it correctly. So each cell severs one production stamp (ingress_ifindex/ingress_vlan_id0) in userspace-dp/src/afxdp/poll_descriptor/mod.rs and requires a specific test to turn RED. Suite: the five #[test] functions in afxdp::tests_session_ingress_identity.

cell mutation result tests that turned RED
A none (control) 5 passed, 0 failed
B sever the transit forward stamp 3 passed, 2 failed poll_descriptor_transit_install_stamps_ingress_binding_4983, poll_descriptor_transit_install_stamps_ingress_binding_without_an_rg_4983
C sever the host-inbound stamp 4 passed, 1 failed poll_descriptor_local_miss_install_stamps_ingress_binding_4983
D sever the neighbor-seed stamp 4 passed, 1 failed poll_descriptor_missing_neighbor_seed_stamps_ingress_binding_4983

What makes this a proof rather than a count: each cell reddens exactly the test whose name describes the path it severed, and nothing else. No cell reddens the whole suite — an over-strong mutation that kills everything would prove nothing about which site is bound. The three production stamp sites are therefore independently bound, and the reverse-companion test stays green in every cell, which is correct: that path deliberately carries no identity.

Every mutation anchor was asserted unique before applying (anchor occurrences: 1); a non-unique anchor would have marked the cell UNKNOWN rather than green. The tree was verified clean before the run and after each restore.

A harness correction worth recording

An earlier run of this matrix reported cells B and C as BUILD-ERROR (not a RED). That was wrong, and the bug was in my classifier, not the PR: it keyed on ^error(\[|:), which matches cargo's test-failure summary error: test failed, to rerun pass '--bin xpf-userspace-dp'. Both cells had in fact gone properly RED. The classifier now distinguishes a genuine compile failure (error[E0nnn], could not compile) — which makes a cell UNKNOWN — from a test failure, which makes it RED. Folding UNKNOWN in with either GREEN or RED is how a mutation matrix comes to lie in whichever direction the reader is predisposed to.

Gate state

Independent hostile review in flight at this head. All eight blocking findings from the earlier Codex leg are closed. mergeable = MERGEABLE.

@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Independent AGY leg at 6eca116db: MERGE-READY, with one claim defect to fix

Confirmed the pin (6eca116db, clean tree) and answered four scoped questions with quoted code.

Q1 — the narrowing hits exactly the right population. A filter for interface Y no longer matches a session that actually arrived on sibling X in the same zone. Those were false positives created by the zone-derived answer, so removing them is the point of #4983, not a regression.

Q2 — a non-zero but unnameable ifindex falls back rather than matching nothing. When f.ifaceNamesByKey cannot resolve {ifindex, vlanID} (interface deleted since install, tunnel/fabric ingress with no config unit), control falls through to return f.zoneIfaces[ingressZone] at session_filter.go:461. A session with an unresolvable identity therefore stays visible and clearable. This is the property that keeps the change from being a fail-closed regression, and it holds.

Q3 — synced sessions carry no identity, deliberately. session_sync.rs:236-238 sets both fields to 0, so on the standby an interface filter takes the zone fallback. AGY confirms the consequence plainly: a filter for Y on the standby WILL match a session that arrived on X at the peer. That is the documented, intended trade — an ifindex is node-local, and carrying the peer's number would render a confidently wrong interface, which is strictly worse than approximating.

The one defect — a comment claims unconditionally what the code does conditionally

pkg/cli/session_filter.go:430-433:

"That cross-interface match is the defect #4792 could only narrow (it widened the zone map to hold every bound interface, which is as precise as a zone-derived answer can be) and this datum removes."

The datum removes it only when a resolvable non-zero ifindex is present. For the three populations this PR deliberately leaves without an identity — peer-synced sessions, the reverse companion, and a pre-#4983 install mid rolling upgrade — plus the unnameable-ifindex case from Q2, the zone approximation still applies and cross-interface matching still occurs. The sentence needs the qualifier the very next paragraph already supplies.

Worth noting because this PR is otherwise scrupulous about the fallback: the paragraph immediately below this one states the exception correctly and at length. The defect is that the summary sentence above it was left absolute. That is the usual shape — a completeness claim written before the exceptions were enumerated, and not revisited after.

The other four universal claims AGY checked (EVERY interface bound to the ingress zone; the cannot name fallback; never means "matches nothing" ... never means "matches everything"; and never re-derived from the zone) all hold against the code.

Disposition: non-blocking under the standing materiality rule — it is a claim, not runtime behaviour — but it ships in the source, so it gets fixed rather than filed. It will be folded together with the independent hostile review currently running at this head, so the PR takes one more round rather than two.

Gate state at 6eca116db

leg verdict
Parent mutation proof PASSED — 4-cell discriminating matrix (see previous comment)
AGY MERGE-READY (this comment)
Hostile Claude in flight
Codex queued behind another PR's leg

@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Independent hostile leg at 6eca116db: MERGE-NEEDS-MINOR — zero blocking behaviour defects, one blocking guard gap

The leg measured every cell in a throwaway git archive extract (never the worktree) and independently reproduced the parent mutation matrix exactly: transit stamp → 2 tests, LocalMiss → 1, seed → 1. It also measured four cells the parent proof did not: the publish_conntrack mirror (2 RED), the Go resolveIngressIfaces (4 RED), sessionIngressIf (1 RED, with all three fallback arms correctly staying GREEN), and the Go toBPF conversion (2 RED, with the lift test correctly GREEN — a real direction separation).

It found no fail-open and no change to forwarding, policy, NAT, HA-ownership or admission. The new fields are read by exactly three things: SessionMetadata's PartialEq/clone, publish_conntrack, and the Go CLI filter/display.

One finding is blocking — under the second limb of the rule, not the first

Our standing bar is "changes runtime behaviour or leaves a guard unable to fire." Finding B1 is the latter, and it was measured:

TestBPFSessionValueIngressIdentityOffsets documents itself as protecting the layout "where C/Rust write this field". The Go half binds — transposing the Go tail with an explicit pad turns exactly that test RED while the size and marshal guards stay GREEN, precisely as its doc claims. The Rust half does not exist. Transposing ingress_ifindex: u32 / ingress_vlan_id: u16 in both BpfSessionValueV4 and BpfSessionValueV6 keeps size_of at 144/192 under #[repr(C)], so the size assertion does not move, and the entire Rust suite stays green (4280 + 60 + 8 + 22 + 31 + 1 + 2 passed, 0 failed).

The helper would then write the VLAN id where Go reads the ifindex: a session on {parent ifindex 24, VLAN 80} becomes IngressIfindex=80, IngressVlanID=24, the ifaceNamesByKey lookup misses, and every row silently degrades to the zone approximation — or, on a box that really has ifindex 80, names the wrong NIC. The field-comparison tests (build_conntrack_value_stamps_ingress_identity_v4/v6_4983) are transposition-blind by construction, and the C header has no compiled consumer, so nothing else catches it. Fix is four offset_of! const assertions — the crate already uses that idiom on UserspaceDpMeta.

The other findings are claim defects, all in shipping artifacts

  • The reverse-companion rationale this PR itself declared wrong in session/entry.rs:55-62 is still stated verbatim in seven other places — including the production site (poll_descriptor/mod.rs:2763-2764) and the shipped C ABI header (bpf/headers/xpf_conntrack.h:75-76).
  • "the stamp is on every forward session the helper installs" (types.go:76, :296, README:983) is false: host-outbound GRE and the peer HA import both install forward sessions with 0 — and the same comment lists both as legitimate zeros twenty lines below.
  • The enumeration omits fabric ingress, the one population where zone and ifindex deliberately name different interfaces. Inert on the current cluster wiring (the fabric member has no units), which is exactly why no test or smoke would see it.

Plus five NITs (a 36-vs-40 byte key size, a 2-vs-4 byte tail-pad disagreement across three files, an off-by-one citation, and imprecise bpffs-pin remediation text).

One observation was out of scope and is filed as #6978: the operator-facing ABI-flag-day text says to stop xpfd, which does not release a bpffs pin, and the #1917 in-place upgrade path may skip Cleanup(). Fail-closed, so a failed upgrade rather than corruption — but the instruction as written will not work.

Fold dispatched. Gate at this head: parent mutation proof PASSED, AGY MERGE-READY, hostile MERGE-NEEDS-MINOR. Codex leg still owed.

Paul Saab added 2 commits August 12, 2026 19:01
The Go mirror pins where the #4983 ingress-identity pair sits; the Rust
side that WRITES those bytes pinned only how big the struct is. Swapping
the declaration order of ingress_ifindex and ingress_vlan_id in both
BpfSessionValueV4 and BpfSessionValueV6 moves the u16 to 136/184 and the
u32 to 140/188 while size_of stays 144/192, so
bpf_conntrack_struct_sizes_match_c reports ok and the whole crate suite
passes. Measured, not reasoned about.

Nothing else covered it. The build_conntrack_value_stamps_ingress_
identity_* tests compare struct fields, so they are transposition-blind
by construction, and the C header has no compiled consumer on this side.

The cost of the gap is not a decode error, which is what makes it worth
a guard: the helper writes the VLAN id where Go reads the ifindex, so a
session on {parent ifindex 24, VLAN 80} is lifted as IngressIfindex=80,
IngressVlanID=24. The name lookup misses and the row degrades to the
zone approximation — and on a box that really does have an ifindex 80,
the CLI names the wrong NIC. Both values are plausible, so nothing
surfaces.

Fixed with four const _: [(); N] = [(); offset_of!(..)] assertions beside
the structs, the idiom UserspaceDpMeta already uses, so a transposition
is a build failure rather than a test failure. Three sides now agree on
one set of numbers: the C header writes them, Rust asserts them, Go
asserts them.

Claim corrections, each verified before being written rather than taken
from the review:

The reverse companion's rationale was retracted in one file and left
standing in seven others, including the production install site and the
shipped C ABI header. Confirmed the correction itself first:
ForwardingResolution::egress_ifindex really is populated by the FIB,
local-delivery and fabric resolvers, and the reverse install sits in the
same scope as the forward decision — so "the forward egress is not
resolved at install time" is false. The true reason is that the forward
egress predicts where the reply will ARRIVE rather than recording where
it did, and routing may be asymmetric, so there is nothing observed to
stamp. One phrasing now used at all eight sites.

"The stamp is on every forward session the helper installs" was false at
three sites, and the same comment listed both counter-examples twenty
lines below itself. Confirmed both carry is_reverse=false with
ingress_ifindex=0: the host-outbound GRE path, and the HA peer import
via is_reverse: req.is_reverse. Reworded to "every forward session
installed from a RECEIVED FRAME", which makes them instances of the rule
rather than exceptions — neither has an observed local ingress to copy.

The zero-population enumeration omitted fabric ingress, the one path
where the zone and the ifindex name DIFFERENT interfaces: the frame
carries a zone-encoded override taking precedence over the ifindex->zone
map, so the zone is the originating chassis's while the stamp is the
local fabric NIC. Verified inert — the fabric member is declared only
under fabric-options member-interfaces with no unit, and the name map
keys on {parent ifindex, unit VLAN}, so it has no entry and the lookup
falls back to the zone. Documented with what changes if that member ever
gains a unit, and that the path is unreachable from the shipped topology
rather than merely uncovered, which is why no test or smoke sees it.

Smaller corrections, each measured: the shim steering key is 40 bytes,
not 36 (compiled the repr(C) shape standalone: size=40 align=2). The
"4-byte" and "2-byte" tail-pad figures describe the same layout from
either end — the append grew the struct 136->140->144, and the u16 lands
inside that pad at 140 leaving 2 unused — now stated once so a reader
diffing the header against bpf_map_tests.rs does not read one as a bug.
The peer-clear call is cli_clear.go:251, cited as :252 twice. And the ABI
flag-day remediation is `xpfd cleanup` or a reboot, not "a full dataplane
reload": a bpffs pin outlives the process, dataplane.Cleanup() is
reachable only from the cleanup subcommand, and a restart leaves the
old-size pin for the pre-flight to refuse again. session_sync.rs, the
only zero site without a rationale, now has one.

Validation. B1 is a two-cell proof and its RED is a build failure at the
assertion, not a test failure. GREEN: assertions in place, layout
untouched, cargo build --release exit 0 with zero errors — which is also
what shows the asserted numbers are the real ones. RED: same tree, the
two declarations swapped in both structs, exit 101, and the only errors
are the four assertions:

  expected an array with a size of 136, found one with a size of 140
  expected an array with a size of 140, found one with a size of 136
  expected an array with a size of 184, found one with a size of 188
  expected an array with a size of 188, found one with a size of 184
  error: could not compile `xpf-userspace-dp` due to 4 previous errors

One correction to the finding as filed: the transposed tree did not come
back fully green here. slowpath::tests::enqueue_refuses_frame_above_live_
mtu failed once with "slow-path worker is not running". That is a
worker-startup flake under parallel load, not a layout consequence — on
the same transposed tree it passes 6/6 run alone, each run verified to
have actually executed rather than filtered out. Recorded because "the
whole suite stayed green" is the evidence the finding rests on.

go build ./... rc 0; go vet ./... rc 0; go test ./pkg/dataplane/
./pkg/cli/ both ok; go test ./pkg/refactoraudit/ ok; cargo test --release
exit 0 with 4279 passed / 0 failed plus 60, 8, 22, 31, 1 and 2 passed.
userspace-xdp/ is untouched, so the shim .o and manifest are unchanged
and no make generate is owed.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Correction to my own B1 write-up

I stated that under the transposed-struct probe "the entire Rust suite stays green (4280 + 60 + 8 + 22 + 31 + 1 + 2 passed, 0 failed)". The measured figure on re-run is 4278 passed / 1 failed: slowpath::tests::enqueue_refuses_frame_above_live_mtu fails under parallel load with "slow-path worker is not running", and passes 6/6 run alone on the same transposed tree. It is a worker-startup flake, not a layout consequence.

The finding is unaffected — the point of B1 was that no test observes the transposition, and a flake in an unrelated slow-path worker is not an observation of it. But the claim as I wrote it was stronger than the measurement, and "the whole suite stayed green" was the sentence B1's severity rested on, so it gets corrected rather than left.

Worth recording how it was caught, because the first attempt to verify the flake was itself void: the probe printed rc=0 five times while reporting 0 passed; 60 filtered out. A filter that matches nothing is indistinguishable from a filter whose tests all pass, and it fails in the reassuring direction. Re-run with --exact and an assertion that exactly one test ran, per run.

B1 itself is closed at a75f91f2d with the RED shown as a build failure — exit 101, and the only four errors are the four offset assertions, each naming its field and both offsets (expected an array with a size of 136, found one with a size of 140 and its three siblings). The GREEN cell does double duty there: it is what proves the asserted constants are the real offsets rather than transcribed hopefully from the C header.

On a live-pin ABI refusal we printed "Do a FULL dataplane reload (stop
xpfd so the old pin is released, then start it to load the new shim)".
That instruction does not work. Releasing a pin means unlinking it:
Cleanup() in loader.go does os.RemoveAll(bpfPinPath), and it is reachable
only from the `xpfd cleanup` subcommand. A bpffs pin outlives the process
that created it, so an operator who follows our message stops xpfd,
starts it, and hits the identical refusal — mid-upgrade, with the
dataplane down.

Round 4 corrected this claim in four comments. This is the instance an
operator actually reads, so leaving it while fixing the prose around it
would have left the authoritative-looking text wrong. pkg/dataplane/
README.md carried the same instruction and is the document the message
cites, so it is corrected too, with the reason recorded so it does not
get re-simplified back to "a reload".

The message now names the mechanism and says plainly that a restart is
not sufficient. It is kept short deliberately: it is read mid-incident.

Both assertions guarding it were defending the text rather than the
behaviour, which is why the wrong instruction survived. The positive test
required the phrases "FULL dataplane reload", "stale pin" and "released"
— all three satisfied by the broken instruction. A phrase-presence
assertion cannot distinguish a correct instruction from an incorrect one.
It now asserts the two properties that make the message useful: that it
names `xpfd cleanup`, and that it states a restart does not release the
pin. The reasoning is in the test so the next person does not restore the
old phrase to satisfy a string match.

The negative case asserted the SSOT-drift path does not contain "FULL
dataplane reload". That phrase no longer exists anywhere, so the guard
had silently become a no-op that could never fire again — rewording one
message retired the guard on the other. It now pins the CONSTANT, which
survives any rewording of either, plus a second assertion that the drift
path must not tell the operator to unpin: that is destructive, and wrong
for a drift `make generate-userspace-xdp` fixes in place.

Validation: two cells, each an assertion RED with go vet rc 0 first, and
the partition is exclusive — each cell reds exactly one test while the
other holds as its control.

  cell                                        SSOT-drift  live-pin
  baseline                                    PASS        PASS
  restore the OLD wording verbatim            PASS        FAIL
  mis-wire stale-pin into the SSOT-drift site FAIL        PASS

The live-pin failure names what is missing — "remediation must name
`xpfd cleanup` — the only path that unpins bpffs state and therefore the
only thing that clears this refusal" — on both subtests.

One cell was discarded rather than scored: the first mis-wire swapped all
ten `userspaceShimGenerateRemediation` use sites and produced go vet rc 1,
a build break rather than an assertion RED. Re-run against exactly the
ValueSize drift arm, which is the edge of the claim, it reds cleanly.

go build ./... rc 0; go vet ./... rc 0; go test ./pkg/dataplane/
./pkg/cli/ both ok; go test ./pkg/refactoraudit/ ok. No Rust source and
no shim artefact touched.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Hostile re-gate at 8dee860a3 — rounds 4 and 5 only

Read-only worktree at the pin, clean before and after. Scope: the two PR commits since 6eca116db (37776a107 r4, 8dee860a3 r5); a75f91f2d is master's #6676 merge and is not this PR's content — the changed userspace_xdp_bpfel.o, binding_index.rs and the +1525 main_tests.rs all arrive from master, so this PR owes no shim smoke. All mutations run in a git archive extract, never the worktree.

1. The four offset constants ARE the real offsets — verified three ways, not transcribed

I did not take the header comment for it. A standalone gcc probe over bpf/headers/xpf_conntrack.h derives:

v4 sizeof=144 ingress_ifindex=136 ingress_vlan_id=140
v6 sizeof=192 ingress_ifindex=184 ingress_vlan_id=188

Exactly the four constants, and exactly the sizes the existing size test pins.

The "a wrong constant would not compile" reasoning holds, and the guard demonstrably firesconst _: [(); N] = [(); offset_of!(..)] is a type-level equality, so a mismatch is E0308:

cell mutation result
A none (control) GREEN
B v4 136137 COMPILE-ERROR E0308
C v6 188187 COMPILE-ERROR E0308

The v6 pair is bound as tightly as the v4 pair — the off-by-one hiding place at 184/188 is closed.

2. The rewritten assertions can all fire — 4-cell matrix, no vacuity

The old negative (must NOT contain "FULL dataplane reload") was genuinely vacuous once the phrase existed nowhere. The replacement compares against userspaceShimStalePinRemediationthe shipped constant, so rewording either message moves both sides together. That is the right fix, and it is not the only thing holding:

cell mutation result
A none GREEN
B strip xpfd cleanup from the remediation REDTestLivePinABIMismatchUsesStalePinRemediation
C reword both restart-warning phrases RED — same test
D make the SSOT-drift message contain the stale-pin constant REDTestSSOTDriftKeepsGenerateRemediation

No new assertion references a literal owned by a different artifact in a way that could silently retire it. The one residual coupling is benign and worth naming: the drift-side negative on "xpfd cleanup" is scoped to that literal, so renaming the subcommand would narrow it — but the same rename fails cell B loudly first, so it cannot go quiet unobserved.

3. The corrected operator message is true

  • Cleanup() (loader.go:1245) does os.RemoveAll(bpfPinPath) — confirmed.
  • Its only non-test caller is cmd/xpfd/main.go:213, inside the cleanup subcommand branch — "reachable only from xpfd cleanup" is exact.
  • A bpffs pin outliving the process is a kernel property; the old "stop xpfd so the old pin is released" was wrong, and wrong in the worst place — mid-upgrade with the dataplane down.

NON-BLOCKING 1 — the remediation names a destructive command without its blast radius

xpfd cleanup does not only unpin the one stale map. main.go:205-208 records that it "GCs all pinned dataplane state and clears the FRR managed routes", and this PR's own new test comment calls it "destroys all pinned dataplane state, which is the wrong and destructive answer". The operator-facing string says only "Run xpfd cleanup (or reboot) to unpin". The PR knows the blast radius well enough to guard the other message against recommending it; the text an operator actually follows should say what else it takes.

NON-BLOCKING 2 — "three sides, one set of numbers" overstates the machine-checking to three

bpf_map/mod.rs says the offsets "are the ones C writes (bpf/headers/xpf_conntrack.h) ... three sides, one set of numbers." Two of those three are machine-checked (the Rust offset_of! consts, the Go mirror). The C side is pinned by a comment only — and r4's own transposition argument applies to it unchanged: swapping the two C fields keeps sizeof at 144/192, so the size test cannot see it either.

Severity is low because r4 is right that the C struct has no compiled consumer (I checked: no .c includes it; the references are Rust comments and sibling headers). But that also makes "the ones C writes" stale framing post-#1476 — nothing C-side writes anything now; the header is a layout SSOT that Go and Rust mirror. Three lines apart the same comment block says both "has no compiled consumer" and "the ones C writes".

4/5. Disturbance and claim audit

The r4 doc edits retract a claim rather than add one — the earlier "the forward flow's egress interface is not resolved at install time" is marked FALSE and replaced with the real reason (it is resolved, it is simply a prediction rather than an observation). That is the correct direction for this PR's recurring failure mode. The deliberate zeros and their rationales are extended, not weakened, and the fabric-ingress case is newly enumerated with its inertness argued from buildSessionEgressIfaces keying. I did not re-derive the ten construction sites or the Go filter fallback — the 6eca116db gate covered them and neither commit touches those call sites.

Neither non-blocking item changes runtime behaviour or leaves a guard unable to fire.

MERGE-NEEDS-MINOR

@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Codex re-verify at 8dee860a3DO-NOT-MERGE, but read the evidence carefully: one finding is half stale and half real, and I separated them by hand.

First, two things in the Codex report that are WRONG at this head

I am recording these because taking the report at face value would have sent a fold in the wrong direction.

1. Its D01 evidence block quotes the OLD message. It reproduces userspaceShimStalePinRemediation as saying "Do a FULL dataplane reload (stop xpfd so the old pin is released…)". The constant at 8dee860a3 says the opposite: "a bpffs pin OUTLIVES the process — restarting xpfd does NOT release it. Run xpfd cleanup (or reboot) to unpin…". That wording changed in this very PR, which is the PR's whole point. The finding as written — "the message tells operators stop/start releases the pin" — does not describe this head.

2. It cites cd1dea6ab as "refreshed origin/master". That commit is real but it is an ancestor of master (the #5617 merge), not a newer one. Current master is 4960e7bee. A verdict that says "verified against origin/master" against an ancestor has verified against a past.

But the substantive half IS live, and I confirmed it firsthand

Codex's underlying claim is not about the message wording — it is about the call graph, and there it is right:

// pkg/dataplane/loader.go:1238-1241
func (m *Manager) Teardown() error {
	m.Close()
	return Cleanup()
}

and pkg/daemon/daemon_run_shutdown.go takes that branch on non-hitless shutdown (hitlessd.dp.Close(), else → teardown). So:

  • pkg/dataplane/types.go:138 and :386 are false: "dataplane.Cleanup() is reachable only from the xpfd cleanup subcommand". It is also reachable through Teardown().
  • Whether a restart releases the pin is MODE-DEPENDENT, not categorical. Hitless preserves the pins by design (loader.go:1160-1165 says so explicitly). Non-hitless HA shutdown tears them down.

That makes the new message wrong in the other direction from the old one. It states unconditionally that restarting does not release the pin, and directs the operator to xpfd cleanup. In non-hitless HA shutdown a plain restart does release it — so the message sends an operator to a destructive command when a non-destructive one would have worked.

This compounds with the non-blocking item from the hostile leg, and together they stop being non-blocking. That leg noted the message names xpfd cleanup without its blast radius — and main.go:205-208 says that subcommand also GCs all pinned dataplane state and clears the FRR managed routes. So the current text points at a destructive action, understates what it destroys, and does so in a mode where it was not required.

I am treating that as blocking, because fixing this exact operator message is what the round-5 fold set out to do, and it is what an operator follows mid-upgrade with the dataplane down.

What the fold needs

State the mode dependency instead of a categorical claim: hitless shutdown preserves the pin (so a restart will hit the identical refusal); non-hitless HA shutdown runs Teardown() and does release it. Name the targeted recovery in docs/operations/userspace-shim-pin-recovery.md — removing the one named incompatible pin — ahead of xpfd cleanup, and when xpfd cleanup is named, say what else it takes. Correct types.go:138 and :386.

And the reason none of this was caught: the new test at stalepin_remediation_5363_test.go:117 checks substrings. A string-presence assertion cannot distinguish a correct instruction from an incorrect one — it is satisfied equally by both. That is how the previous wrong instruction acquired a passing test, and the replacement inherited the same blind spot. Assert the property that makes the text correct, not the presence of words.

The ABI half — derived independently, and it agrees with the hostile leg

Codex compiled the header with Clang's BPF target and dumped record layouts rather than reading the comment: v4 ifindex=136 vlan=140 size=144, v6 ifindex=184 vlan=188 size=192. Same four constants the hostile leg derived with a standalone gcc probe. Two independent derivations, agreeing — the numbers are right.

Detectability is the gap, and it is exactly the one the hostile leg flagged as non-blocking: a Go-only transposition is caught, a Rust-only transposition fails the build, but a C-only transposition moves VLAN to 136/184 and ifindex to 140/188 while sizes stay 144/192, and every executable guard stays green. bpf_map_tests.rs:193 compares against literals; nothing compiles the header and compares offsetof. The C mirror is pinned by a comment at xpf_conntrack.h:90, and a comment cannot fail.

Both legs independently reached "two of three mirrors are machine-checked, C is not". That is worth closing, but it is not this PR's regression — file it rather than growing this diff.

Claim audit, all mechanical

session/README.md:1011 correctly documents filter/display divergence and :1021 then claims fallback agreement. types.go:150 and its v6 copy claim locally-owned rows are exact, which the unnameable / non-VLAN-unit / reused-ifindex traces disprove. "pre-#4983 helper" rows survive at xpf_conntrack.h:80, session_filter.go:448 and session-sync-architecture.md:63 although session/README.md:1094 correctly explains the old-size pin is hard-refused and recreated empty. "VLAN 0 = untagged" persists at bpf_session_value.go:129, types.go:169 and xpf_conntrack.h:84, with inspect.rs:334 already supplying the counterexample (tag present, VID 0, PCP 5). And 37776a107's "three sides now agree" is true about present values but not about pinning.

Full Go pkg/dataplane + pkg/cli, go vet, and the full release Rust suite all passed at this head.

Round 5 replaced "a reload releases the pin" — which nothing does — with
the opposite categorical claim: that restarting never releases it and only
`xpfd cleanup` does. That is also false.

Manager.Teardown is m.Close() followed by return Cleanup(), and
daemon_run_shutdown.go calls d.dp.Teardown() on every non-hitless shutdown
("HA shutdown: tearing down BPF state"); only the hitless arm takes
d.dp.Close() and preserves the pins deliberately. So dataplane.Cleanup()
has two production callers rather than one, and whether a restart clears a
stale pin depends on how xpfd last stopped.

The consequence is operator-facing and not academic. The round-5 message
sent the operator to `xpfd cleanup`, which additionally removes every other
pinned dataplane map and clears the FRR managed routes, in a mode where a
plain restart would have sufficed — mid-upgrade, with the dataplane down.
It pointed at a destructive action and understated what it destroys.

The message now states the mode dependency and leads with the targeted
recovery — unlink the one named pin, per
docs/operations/userspace-shim-pin-recovery.md, which already documented
exactly that and was never referenced — before naming `xpfd cleanup` and
what else it takes. types.go:138 and :386 claimed Cleanup() is reachable
only from the subcommand; both are corrected, as is the same paragraph in
pkg/dataplane/README.md. The runbook gains the cleanup-scope note and the
mode dependency.

Why nothing caught it: the guard asserted substrings. A phrase-presence
assertion is satisfied identically by a correct and an incorrect
instruction — it defends the text, not the behaviour. The round-4 wrong
message passed that way, and round 5 rewrote the block to require
"restarting xpfd does NOT release", locking in the new wrong claim.

The false claim is a claim about the call graph, so the binder is a
call-graph check rather than a proxy for one. cleanup_reachability_6928_test.go
parses the non-test tree and pins Cleanup()'s production callers to exactly
{cmd/xpfd/main.go:main, loader.go:Teardown}. It first asserts the walk
found any caller at all, since a broken walk would pass vacuously — the
same failure mode being closed one level up — and separately asserts at
least two callers exist, so a collapse back to the single CLI caller reds
and forces the wording to be revisited. A companion pins that both shutdown
arms still exist, so Teardown cannot quietly become dead code while the
mode-dependent wording survives. The substring block is replaced by a
negative on the two claims the code disproves.

Both guards verified red-first. Severing Teardown's call to Cleanup() reds
the caller-set test against its two-entry expectation; restoring the
disproven phrase reds TestLivePinABIMismatchUsesStalePinRemediation at
"remediation restored the categorical claim". Production was restored and
verified clean after each. The two SSOT-drift negatives were checked for
the vacuity trap: one keys on the constant and survives any rewording, the
other on the action name, which the new message still contains.

Validation: go build ./... rc=0; go test ./pkg/dataplane/... ./pkg/daemon/...
./pkg/cli/... rc=0 across all seven packages; gofmt clean on every touched
file. pkg/dataplane/constants.go and bpf_session_value_test.go are
gofmt-dirty at this head already and are deliberately left alone.
Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Blocker folded at a76d4143b — and the defect was narrower and worse than I framed it

I said the message should "name the targeted recovery ahead of xpfd cleanup", implying the runbook needed to be surfaced. The runbook was already correct. docs/operations/userspace-shim-pin-recovery.md step 4 already says sudo rm -- /sys/fs/bpf/xpf/<map-name> and explicitly "Do not remove /sys/fs/bpf/xpf as a whole", and it never mentions xpfd cleanup at all.

So this was not a missing procedure. A correct, targeted, deliberately-scoped recovery existed, and the error message an operator reads mid-upgrade pointed away from it at the broader destructive action. That is worse than a documentation gap — the right answer was written down and the diagnostic sent people past it.

The call-graph fact, verified independently

loader.go:1238Teardown() is m.Close() then return Cleanup(). daemon_run_shutdown.go:222-229if hitless { d.dp.Close() } ("preserving BPF state") else { d.dp.Teardown() } ("HA shutdown: tearing down BPF state"). An AST sweep of the non-test tree finds exactly two production Cleanup() callers. So pin release is mode-dependent, and the categorical claim was false.

A fourth site carried the same false paragraph verbatim and was not in my list: pkg/dataplane/README.md:327-339. Found by sweeping for the claim rather than working the list I handed over — which is the right instinct, since a claim that appears in three places usually appears in four.

The test now checks the same KIND of fact the claim makes

The substring block is gone. Worth stating what it had been doing: it had, in succession, pinned the wording of two different wrong instructions — round 4's "FULL dataplane reload"/"released", then round 5's "restarting xpfd does NOT release"/"OUTLIVES the process". Phrase-presence is satisfied identically by a correct and an incorrect instruction, so it did not merely fail to catch the second wrong message; it ratified it.

cleanup_reachability_6928_test.go parses the non-test tree with go/ast and pins the caller set. The argument for why that is a binder and not a proxy is the right one: the false claim was itself a call-graph claim, so a call-graph check is the same kind of fact, not a stand-in for one.

mutation result
sever TeardownCleanup REDgot: cmd/xpfd/main.go:main against the 2-entry want
restore "restarting xpfd does NOT release" into the message REDremediation restored the categorical claim

Production restored and verified clean after each. Two preconditions: the walk must find some caller (a broken walk would otherwise pass vacuously — the same failure mode one level up), and at least two callers must exist, so a future collapse back to the single CLI caller reds and forces the wording to be revisited rather than silently becoming wrong in the other direction a third time.

The vacuity trap was checked, not assumed

Both SSOT-drift negatives were examined. :173 keys on the constant, so it survives any rewording — already correct. :178 keys on "xpfd cleanup", which is an action name rather than borrowed prose, and the new message still contains it, so it can still fire. Neither went vacuous.

Gates

go build ./... rc=0 · go test ./pkg/dataplane/... ./pkg/daemon/... ./pkg/cli/... rc=0, all 7 packages ok · gofmt -l clean on all four touched files.

pkg/dataplane/constants.go and bpf_session_value_test.go are gofmt-dirty at the PR head already and confirmed unmodified in this diff — flagged rather than reformatted, since fixing them would widen the diff into unrelated files.

Four claim corrections deliberately NOT folded

session/README.md:1011/:1021, types.go:150 + its v6 copy, three "pre-#4983 helper" sites, three "VLAN 0 = untagged" sites. I scoped these as "fold if cheap" and they are not — four separate claims across ~8 sites, each needing its own verification against code. Leaving them listed beats half-verifying four claims and shipping a fifth wrong sentence in a PR whose entire subject is wrong sentences. They get their own pass.

Head is a76d4143b, MERGEABLE/CLEAN. The hostile and Codex legs are owed at this head; the prior verdicts predate it.

The ingress VLAN field is documented at six sites as "0 = untagged". It
stores a bare VID, so 0 covers both an untagged frame and an 802.1p
priority-tagged one — a real 802.1Q tag carrying VID 0 with PCP/DEI set.
The session row cannot tell those apart, and a consumer following the
comment would conclude such a session arrived untagged.

The case is neither hypothetical nor unhandled elsewhere:
userspace-dp/src/afxdp/README.md documents that the TX side emits a tag on
tag PRESENCE via TxVlanTag (#2149) rather than on vlan_id > 0, precisely so
a priority-tagged VLAN-0 frame keeps its priority instead of collapsing to
untagged. Only the session row loses the distinction, which is what the
comments needed to say.

Corrected at all six sites: pkg/dataplane/types.go and
pkg/dataplane/bpf_session_value.go (v4 and v6 each),
bpf/headers/xpf_conntrack.h, and userspace-dp/src/afxdp/bpf_map/mod.rs (v4
and v6). Each now states that 0 means the VID was zero, that untagged and
priority-tagged are both included, and that the TX side does draw the
distinction.

Doc comments only; no behaviour change. The Rust edit sits immediately
above the compile-time offset_of! ABI assertions, so cargo check
--all-targets was run rather than assumed.

Validation: go build ./... rc=0; go test ./pkg/dataplane/ rc=0; cargo check
--all-targets rc=0, zero errors; gofmt clean on both Go files; a residual
sweep for "0 = untagged" across the affected trees returns nothing.
Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Claim pass: 1 of 4 at f2f75d19d — and every cite I supplied for that item was wrong

That is mine to own. I built the claim list from a reviewer's finding and relayed its file:line cites into the dispatch without resolving them. I verify a reviewer's claim firsthand as a matter of course; I had been exempting its cites, which is backwards — the claim is what the reviewer reasoned about and is usually sound, while the cites are bookkeeping produced along the way, and they are the part that rots.

what I relayed what is actually true
types.go:169 is a VLAN claim not a VLAN claim at all — it is the REST-clear-rejection note
counterexample at userspace-dp/src/inspect.rs:334 that file does not exist. It is userspace-dp/src/afxdp/frame/inspect.rs, and the counterexample is not there either
"three sites" sixtypes.go ×2, bpf_session_value.go ×2, xpf_conntrack.h ×1, plus afxdp/bpf_map/mod.rs ×2 which my list omitted entirely

The wrong-file cite is the instructive one: src/inspect.rs versus src/afxdp/frame/inspect.rs reads correct, so nobody checks it. Same family as the two files named authz.go that have now cost this campaign time twice.

The practical damage: I scoped the task as "verify four supplied traces" when it was really "perform four independent derivations" — about double. Scoping a derivation as a verification is precisely what makes a lane run out mid-way, which is why the stop at 1 of 4 was the right call and not a shortfall.

The item that landed — the claim is false, not merely imprecise

IngressVlanID stores a bare VID, so 0 covers an untagged frame and an 802.1p priority-tagged one: a real 802.1Q tag carrying VID 0 with PCP/DEI set.

The counterexample is not a hypothetical, and it is in this repo's own TX path. userspace-dp/src/afxdp/README.md:603-607: the TX side deliberately emits a tag on presence via TxVlanTag (#2149) rather than on vlan_id > 0, specifically so a priority-tagged VLAN-0 frame does not collapse to untagged. So the case is real, known, and already handled elsewhere in the same crate — only the session row loses the distinction, and a consumer following "0 = untagged" concludes such a session arrived untagged.

All six sites corrected to say 0 means the VID was zero, that both cases are included, and that the TX side does draw the distinction. Residual sweep for 0 = untagged across those trees returns nothing.

Doc comments only — but the Rust edit sits immediately above the compile-time offset_of! ABI assertions, so cargo check --all-targets was run rather than assuming a comment is safe there: rc=0. go build ./... rc=0, go test ./pkg/dataplane/ rc=0, gofmt clean.

Method for the next pass, which is the durable part

  • Treat the cites as pointers to a topic, not to a line. Resolve every one against the actual head before acting on it.
  • Never trust a relayed count. "Three sites" was a grep, not a survey. Sweep tree-wide first — here 3 became 6, and the two additions were in the Rust mirror, exactly where a Go-side sweep stops.
  • Budget one full derivation per item.

The stopping note is in _Log.md beside the work, so the next pass inherits the correction rather than my estimate.

Remaining and untouched: session/README.md:1011 versus :1021; types.go:150 "locally-owned rows are exact"; the three "pre-#4983 helper" sites. All three cite lists should be assumed wrong until resolved.

Head f2f75d19d, MERGEABLE/CLEAN.

The comment claimed an interface-filtered show is exact for rows this node
owns. It is exact only when the row's {ifindex, vlan} still names a
currently-configured unit.

resolveIngressIfaces returns a single name only on a hit in
ifaceNamesByKey, otherwise it falls back to every interface bound to the
ingress zone. That map is rebuilt per query from the current config and the
current kernel ifindex, while the row's ifindex was recorded at install, so
three local shapes are not exact:

A non-zero ifindex the config cannot name — a unit deleted since install, a
tunnel or fabric ingress with no config unit — misses and falls back to the
zone. The resolver's own doc comment already said so.

A recycled ifindex can hit a key the kernel has since reassigned to a
different interface. That is worse than approximate: it renders one
confident wrong name rather than a zone list.

The map keys a unit under vlan-id, else unit number, while the row carries
the VID observed on the wire. Those agree when the unit number equals the
vlan id, or when both are zero. A unit whose number is populated and whose
traffic is untagged keys zero on the wire and number in the map, and
misses. sessionDisplayVLANID's own doc notes the config may populate only
one of the two.

Worth recording what is NOT wrong, since it was the first hypothesis:
units do not collapse onto {ifindex, 0}. That same number-fallback keeps
them distinct, and only a genuine key collision reaches the
first-writer-wins insert. Stating it here so the refuted shape is not
re-derived later.

Both sites narrowed, v4 and v6. Comments only. The recycled-ifindex shape
is a real wrinkle in an operator-facing filter rather than a documentation
defect, but fixing it changes behaviour and belongs in its own change.

Validation: go build ./... rc=0; go test ./pkg/dataplane/ ./pkg/cli/ rc=0;
gofmt clean. Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Exactness claim narrowed at f43e31db3 — it does not hold, and one of my three shapes was wrong

Comments only, MERGEABLE/CLEAN.

The sweep first, and my count was right this time

The claim restates at exactly two sites — types.go:155 and :410 (v4 + v6) — plus a companion "exact where this node is the authority" at :165/:420. No Rust or C restatement: the Rust mirror documents the field, not the resolver's exactness, so this one is genuinely Go-only. That was verified rather than assumed, which is the only reason it is worth stating after yesterday's three-became-six.

The mechanism, which is one gap producing three symptoms

resolveIngressIfaces (session_filter.go:456) returns a single name only on a hit in ifaceNamesByKey, else falls back to the zone. That map is rebuilt per query from the current config and current kernel ifindex — while the row's ifindex was recorded at install. Every shape below lives in that gap.

Two of my three shapes confirmed; the third refuted and replaced

1. Unnameable ifindex — CONFIRMED. Non-zero, no current config unit: misses, falls back to the zone. Approximate, not exact. And the resolver's own doc comment already says so at :450-455 — the claim was contradicted by a comment forty lines away in the same file.

2. Recycled ifindex — CONFIRMED, and worse than I described it. I framed it as another source of imprecision. It is not: a stale ifindex can hit a key the kernel has since reassigned, so the CLI renders one confident wrong name instead of a zone list. An operator filtering by interface gets sessions that never arrived there, with nothing in the output distinguishing it from a correct answer. That is a correctness wrinkle in an operator-facing filter, not a documentation defect — filed as #6987 and deliberately not fixed here, since correcting it changes behaviour.

3. Non-VLAN unit collapse — REFUTED. There is no unit-0 collapse: sessionDisplayVLANID falls back to unit.Number when VlanID == 0, so units do not collide onto {ifindex, 0}. My hypothesis was wrong.

But the same fallback creates a different gap: the map keys a unit under vlan-id else unit-number, while the row carries the VID observed on the wire. Those agree only when unit number equals vlan id, or both are 0. A unit with a populated number carrying untagged traffic keys 0 on the wire and number in the map, and misses. sessionDisplayVLANID's own doc already concedes the config "may only populate one of them".

So the claim is false three ways — but only two of them are the ways I said, and the third is false for a reason I had not considered.

The refuted shape is written into the comment as a non-defect

This is the part worth keeping. The comment now records the unit-0 collapse explicitly as the thing that is not wrong, so the next reader does not re-derive my hypothesis and "fix" a non-defect. A narrowed claim that only says what is broken invites someone to rediscover the plausible-but-false failure and act on it.

Method note, since it is the second time it paid

Three shapes were treated as hypotheses to test rather than facts to write up, precisely because they came from me and my previous cite list was wrong in every particular. One survived contact reframed, one survived and got worse, one died. Had they been written up as given, this PR would have shipped a confident description of a collapse that does not happen.

Gates: go build ./... rc=0 · go test ./pkg/dataplane/ ./pkg/cli/ rc=0 · gofmt -l clean · worktree clean.

Head f43e31db3. Two claim items remain untouched — session/README.md:1011 vs :1021, and the three "pre-#4983 helper" sites — both with cite lists that should be assumed wrong until resolved.

Paul Saab added 2 commits August 12, 2026 22:40
Both were doc/comment claims about #4983's ingress identity. Each was
re-derived from the head rather than verified against its cite, and each
turned out to be wrong in a different way than reported.

ITEM 1 — session/README.md contradicted itself about whether the session
FILTER and the "In: ... If:" DISPLAY agree on their fallback. One passage
said they diverge by construction; the next said the divergence "covers
the FALLBACK arms too ... the same degradation". The first is right, and
the types settle it without reading the prose: the filter's zoneIfaces is
map[uint16][]string built with append(..., zone.Interfaces...) and its
fallback returns EVERY bound interface, while the display's is
map[uint16]string built with zone.Interfaces[0] and returns the FIRST,
else the zone name. cli_show_flow.go states the split itself where it
builds them — the display maps "stay display-only" precisely because an
interface-filtered show must see every interface of a zone (#4792).

The false paragraph is replaced with the derivation, plus two consequences
it hid: a zone binding NO interface gives the filter an empty slice, so no
interface filter selects that row at all while the column prints the zone
name; and the display chain is pinned by a test while nothing pins
agreement, because there is none. The correct passage is kept verbatim.

ITEM 2 — three sites nominally still described a "session installed by a
pre-#4983 helper" as a population that legitimately carries ingress
identity 0. There is no such population: sessions/sessions_v6 are both in
userspaceShimSharedMapSpecs, userspaceABICheckedPinnedMaps unions that
into the ABI-checked set, userspaceMapABIDiff compares ValueSize, and
validateUserspaceShimLivePins hard-refuses a diff — so a new daemon either
reads a freshly created empty map or does not load at all.

Two corrections beyond the brief. First, the reference account in
session/README.md is right in substance but named the remediation a "full
dataplane reload", which loader_userspace_shim.go records as already
corrected once: a reload never releases a pin. entry.rs carried the
opposite superseded form ("NOT a restart"), which the same comment records
as equally false, since a non-hitless HA shutdown calls Manager.Teardown
and removes the pin path. pkg/dataplane/types.go already had the correct
mode-dependent account, so that is the reconciliation target.

Second, the sweep found five sites asserting the stale population rather
than three, and both extras were the v4/v6 twins in the Rust mirror
(afxdp/bpf_map/mod.rs) — where a Go-side sweep structurally cannot reach.
Four test comments restated it as well. All nine now carry the same
verified account, and the replacement third population is the real one:
the host-outbound GRE path, which reads self-originated traffic off the
TUN device and has no ingress binding to record.

Every surviving pre-#4983 mention is now a negation or a corrective note,
except two that describe pre-#4983 BEHAVIOUR rather than a population and
are deliberately untouched.

Doc and comment only; no behavioural change was needed or made.

Validation: go build ./... and go vet on pkg/cli + pkg/dataplane clean;
gofmt clean on every touched Go file; go test ./pkg/cli/...
./pkg/dataplane/... passes (5 packages); cargo test --release passes
(7 suites, 4406 tests) — run because Rust doc comments and a shared C
header comment changed.

Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Both remaining claim items delivered at c0b7b4f2e — and my reference account was itself defective

Item 1 — the contradiction resolved from TYPES, not prose

:1012 ("the two diverge by construction") is correct; :1021 ("the same degradation") is false and is replaced. Settled without a behavioural argument, because the types decide it:

  • Filter: zoneIfaces map[uint16][]string, built with append(zoneIfaces[zid], zone.Interfaces...). Fallback returns every bound interface.
  • Display: zoneIfaces map[uint16]string, built from zone.Interfaces[0]. Fallback returns the first, else the zone name.

cli_show_flow.go states the split itself where it builds them — populateIfaceMaps is used "rather than the single-first-interface zoneIfaces built above for display", which "stay display-only". So the divergence is deliberate on the filter's side (#4792 needs all interfaces) and simply was never mirrored into the one-name column.

Two consequences the false paragraph hid, now documented: a zone binding no interface gives the filter an empty slice, so ifaceMatchesAny is false and no interface filter selects that row at all, while the column prints the zone NAME — not an interface name, and not typeable back into the filter. And the display chain is pinned by TestShowFlowSessionIngressIfColumnFallsBackWhenIdentityUnusable4983; nothing pins agreement, because there is none.

Provenance detail worth keeping: :1012 carries "(#6928 review)", so it is the later text that superseded :1021 — nobody deleted the older paragraph. That is the mechanism by which a file ends up asserting both halves of a contradiction.

Item 2 — the reference account I supplied had reproduced a superseded error

I pointed at session/README.md:1094 as the correct account to reconcile the others to. Its substance holds — verified link by link: sessions and sessions_v6 are both in userspaceShimSharedMapSpecs; userspaceABICheckedPinnedMaps unions that into the checked set; userspaceMapABIDiff compares ValueSize; validateUserspaceShimLivePins errors on a diff. So either the old pin is gone and the new map is empty, or the pre-flight refuses and the new daemon never reads it.

But it named the remediation "full dataplane reload" — and loader_userspace_shim.go:37-39 says in terms that a reload never releases a pin at all, recording that as an error this PR's round 5 already corrected once. Meanwhile entry.rs carried the opposite superseded form ("NOT a restart"), which the same comment records as equally false, since a non-hitless HA shutdown calls Teardownos.RemoveAll(bpfPinPath).

So the lane reconciled to pkg/dataplane/types.go's fully-corrected mode-dependent account rather than to my nominated reference. That is the right call and the right instinct: a reference account supplied by me is a claim like any other, and this one had drifted.

Count short again: briefed three sites, found five — and both extras were the v4/v6 twins in the Rust mirror (afxdp/bpf_map/mod.rs:187 and :257), plus four test-comment restatements. Second time today a Go-side count missed exactly the Rust plane. All nine reconciled.

The replacement third population is a real one rather than a deletion: the host-outbound GRE path (afxdp/tunnel.rs) — self-originated traffic off the TUN device with no ingress binding to record. Lists stay at three. Residual sweep is clean; the only surviving pre-#4983 mentions are negations or corrective notes, except two that describe pre-#4983 behaviour rather than a population and were deliberately left.

A union-resolve hazard that nearly shipped a mangled log

Two lanes appended _Log.md entries both starting - **Timestamp**: 2026-08-12, so git hoisted that shared line above the conflict as common context — and the union fused two entries into one, stripping the second's header so it read as another - **Action**: block inside the first.

The line-set invariant passed clean and was wrong. No unique line was missing, because fusion removes nothing. The entry-count invariant caught it: 2973 where the union needs 2974. Repaired and re-verified — and I confirmed it independently at the head: 2974 Timestamp, 1916 Action, matching 2974 = 2973 + 2973 − 2972 and 1916 = 1915 + 1915 − 1914.

The general shape, now in my rules: a set-based invariant is blind wherever the data has duplicates, and a shared boundary line between conflicting hunks is exactly where duplicates appear. Any append-structured file whose entries begin with a templated line has this by construction. Check both — set for dropped content, count for fused entries; they fail in different directions.

The push was also rejected first (another lane landed f43e31db3 mid-flight), caught only because the exit code was captured unpiped. Merged rather than rebased, then re-verified — and the overlap with that lane's edit was checked rather than assumed: it touches types.go in a different paragraph (SHOW exactness at :153/:426, not the pre-#4983 paragraph at :130/:385).

Gates

Doc/comment only. go build ./... rc=0 · go vet on pkg/cli + pkg/dataplane rc=0 · gofmt -l clean · go test ./pkg/cli/... ./pkg/dataplane/... -count=1 rc=0 across 5 packages, run twice, before and after the merge · cargo test --release rc=0, 4406 passed across 7 suites — run because Rust doc comments and the shared C header comment changed.

One cosmetic item reported and deliberately not touched: f43e31db3 left a ~150-column run-on comment line in types.go (both twins), where new text was spliced immediately before the existing sentence. Editing another lane's in-flight paragraph mid-merge invites a worse conflict.

Head c0b7b4f2e. All four claim items on this PR are now closed.

@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Codex re-verify at c0b7b4f2eMERGE-NEEDS-MAJOR. Three of five prior findings FIXED; the sweep was still short.

Three prior findings confirmed FIXED by mutation, not by inspection: gating the transit stamp on owner_rg_id > 0 now fails its test (ifindex 0 instead of 11); zeroing the LocalMiss metadata stamp now fails its test (0 instead of 7); removing the v4/v6 ingress assignment from either Go conversion direction now fails an independent write/read test. Two remain live and are correctly deferred to filed issues (#6965 transit rows absent from the enumerated map, #6975 peer clear deleting a sibling-interface flow).

The narrowed precision claim is accurate — and then reasserted 27 lines later

The three shapes are stated correctly, including the refutation recorded as a non-defect, which is confirmed to do its job: sessionDisplayVLANID falls back to the unit number, so distinct numbered units do not collapse, and the first-writer rule applies only to a genuine identical-key collision. That should stop the rediscovery.

But types.go:182 immediately reasserts that local authority is exact, with a worked counterexample: a row installed for A as {17,0}, A removed, kernel reuses 17 for a configured B, the per-query map maps the old row to B, and filter and display confidently report B. Narrowing a claim in one paragraph and restoring it in the next is the same shape as the session/README.md:1012 vs :1021 contradiction this PR already resolved once.

A second unresolved shape: a valid non-VLAN logical unit (ge-0/0/0 unit 3, no VLAN) binds child netdev ge-0-0-0.3 and installs {child-ifindex, 0}, while the CLI constructs {parent-ifindex, 3} — so it falls back to the zone. That is a third gap in the same mechanism, not covered by the two documented ones.

And cli_show_flow.go:297 says an interface filter selects exactly the sessions that arrived there. False: a session arriving on A but egressing B matches filter B through the egress arm. The OR behaviour looks intentional; the sentence describing it is not.

The sweep was short again — and this time the miss really was the cross-language mirror

Seven sites still restate superseded accounts. The sharpest: session/README.md:1167 still says cleanup or reboot is required and that restart never suffices, 35 lines after the corrected account in the same file. That is exactly the mode-dependency this PR set out to fix, surviving in the Rust-side README.

Also still live: xpf_conntrack.h:90 equates VID 0 with untagged (the C twin and both mirrors were corrected, this one was not — and the priority-tagged counterexample with PCP 5 and VID 0 is in the tree); two Rust sites still call the other producers "policy-admitted" when a host-bound SYN can receive NoMatch and be admitted solely by the zone host-inbound set; session/README.md:970 says only three sites write the conntrack map, but a peer-synced row reaches SetClusterSyncedSessionV4 → bpfShim.SetSessionV4 and writes the same map; :1021 says zero/unnameable populations remain selectable while :1043 correctly says the opposite; and pkg/dataplane/README.md:371 still labels the current targeted single-pin instruction as full-reload remediation.

Two of my own artefacts are in that list. cleanup_reachability_6928_test.go:137 claims an empty walk would otherwise pass vacuously — the exact comparison against a nonempty expected set already fails, so the precondition I asked for is redundant rather than load-bearing. And two _Log.md entries overstate what the substring companion test proves.

On the caller-set check

The exact direct-caller set is guarded, but mode placement and actual reachability are not — the check pins who calls the function, not which shutdown mode reaches it. That is the honest limit of a call-graph instrument and worth stating in the test rather than leaving implied.

Scope note on this leg

I restricted the read scope to defeat the content filter that killed four legs on a sibling PR. It worked — zero filter hits — and unlike the #6882 leg this one did run Go tests, vet, targeted Rust checks and git diff --check, so its outcomes are measured rather than source-derived.

Next round: reconcile types.go:182 with the paragraph above it, add the third unresolved shape or say why it is out of scope, correct the seven restatements (the Rust README is the one that matters), fix the cli_show_flow.go:297 filter description, and drop the redundant precondition claim.

Round 6 of the #4983 review is claim correction only: every changed line
in every file here is a comment, and no test assertion moves. The
recurring failure is one this PR has now hit three times — a passage is
corrected, and an adjacent passage keeps restating the superseded form.

types.go narrowed the exactness claim on locally-owned rows and then
reasserted it 27 lines later ("exact where this node is the authority").
Its own recycled-ifindex bullet is the counterexample: an interface
removed, its kernel index reused by a configured sibling, and both the
filter and the If: column confidently report the sibling for a row this
node owns outright. Local authority buys exactness only for the nameable
subset; that is what it now says.

A reported third unresolved shape turned out to be the third bullet
already documented, reached by a wrong mechanism. Verified firsthand
that a non-VLAN unit's row carries the PARENT ifindex, not the child:
meta.ingress_ifindex is the physical AF_XDP bind, and
forwarding_build/interfaces.rs keys ingress_logical_ifindex as
(bind_ifindex, vlan) -> logical, so the child index is what the map
resolves TO. Only the VLAN half diverges. The bullet now names the
concrete config and says both sides key the parent, so the next reader
does not go hunting for an ifindex that is not in the row.

The interface-filter description was false in both the code comment and
its test preamble: matchesV4/V6 OR the ingress and egress arms, so a
session that arrived on A and egresses B matches a filter for B. The OR
is deliberate, so the sentence is fixed rather than the behaviour.

Eight sites, not the seven reported, still carried superseded accounts,
and the sweep had to cross Go, Rust and C to see them. session/README.md
still said cleanup or reboot is required and a restart never suffices,
35 lines after the corrected mode-dependent account in the same file.
The C header still equated VID 0 with untagged while its v6 twin and
both language mirrors had been corrected. Two sites called the
frame-driven producers "policy-admitted" when a host-bound flow can be
admitted by the zone host-inbound set alone. The three-writer claim
about the conntrack map omitted the Go-side peer-sync writer. The
selectability claim contradicted the zero-interface-zone case stated 20
lines below it. And four sites still labelled the targeted single-pin
instruction "full-reload" remediation, which names an action nothing in
the product performs.

Two of the artefacts corrected are this PR's own. The cleanup-caller
test claimed a zero-caller walk would otherwise pass vacuously; proven
false by disabling that arm and forcing the walk empty, whereupon the
exact comparison still reds with an empty got against a two-entry want.
The arm is kept as a diagnostic and the claim rewritten. Two _Log.md
entries overstated the substring companion the same way.

The honest limit is now stated rather than implied: the caller-set check
pins who calls Cleanup(), not which shutdown mode reaches it and not
that anything reaches it. A call-graph instrument cannot express
reachability, and the substring companion proves the call expressions
are written in the shutdown file, not which branch holds them.

Validated: gofmt clean on every file touched, go build ./... and go vet
on both packages exit 0, go test -count=1 ./pkg/dataplane/...
./pkg/cli/... all ok, cargo check --all-targets exit 0, and the five
tests_session_ingress_identity binding tests pass. Advances #4983.
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Fold r6 at 6d9e540a9 — comment-only, and my F2 was wrong twice

Verified: git diff -U0 minus comment-prefixed lines is 0 for all five production files and all five test files. No assertion moved.

F2 — I mis-triaged it, and I verified the correction rather than accepting it

I relayed a finding claiming a valid non-VLAN logical unit installs {child-ifindex, 0} while the CLI constructs {parent-ifindex, 3}, and called it a third unresolved shape. Both halves are wrong.

The mechanism. I checked forwarding_build/interfaces.rs:273-298 directly:

let bind_ifindex = if iface.parent_ifindex > 0 {} else { iface.ifindex };
let ingress_key = (bind_ifindex, vlan_id);
    .ingress_logical_ifindex.insert(ingress_key, iface.ifindex);

The logical child index is what the map resolves to, never what the row carries — and bind_ifindex is the parent whenever one exists. So the row is {parent-ifindex, 0}; both sides key the physical parent, and only the VLAN half diverges. poll_descriptor says as much itself, calling it "the raw physical meta.ingress_ifindex".

And it is not a third shape. The doc already documented three, and this is the third: a unit whose number is populated and whose traffic is untagged keys 0 on the wire and number in the map, and misses.

The right response was taken: not rewritten as new, but the existing bullet sharpened to name the concrete configuration (ge-0/0/0 unit 3, no vlan-id) and to state that both sides key the physical parent — so the next reader does not go hunting for a ge-0-0-0.3 ifindex that is not in the row. That is better than either accepting my framing or silently dropping it.

This is the cite-relay failure again: I passed on a mechanism without tracing it. Two of my last four relayed findings have had wrong mechanisms.

F1 — the contradiction was real and is closed

types.go:182 did reassert exactness 27 lines after :155 narrowed it. Both twins now say local authority buys exactness only for the nameable subset, and that the recycled-ifindex case is not a miss but a confident wrong name on a locally-owned row — which is the distinction that makes it worse than the other two shapes.

F5 — the precondition claim proven false by measurement

I asked for the redundant-precondition claim to be checked. It was, firsthand: disabling the arm (if false &&) and forcing got = nil still fails, at the exact comparison —

production callers of Cleanup() changed.
  got: <empty>  want: cmd/xpfd/main.go:main, pkg/dataplane/loader.go:Teardown

So the arm is a diagnostic, not a vacuity guard. Keeping it (it names a broken walk rather than showing a zero-vs-two diff that reads as deleted call sites) and rewriting the claim is the right call — the arm earns its place for a different reason than the one I gave it. Probe reverted, file restored byte-identical, suite re-run green.

F4 — eight sites, not seven

"Do not trust the count" earned its keep again. The extra was pkg/cli/session_filter.go's resolveIngressIfaces doc carrying the same unqualified claim — not on my list. The sharpest fix remains the session/README.md ABI note that reasserted "NOT a restart / reachable only from xpfd cleanup" 35 lines after the corrected account in the same file, now replaced and carrying an explicit "do not restate it here".

Also confirmed and worth recording: the freshness gate hashes only xpf_common.h, so editing xpf_conntrack.h's comment owes no shim rebuild. That is the kind of check that prevents a spurious "changed artifact owes a smoke" round.

Two honest limits now stated rather than implied

The caller-set check pins who calls Cleanup(), not mode placement or reachability. And its substring companion proves the two call expressions are written in daemon_run_shutdown.go — not which branch holds them, and it would be satisfied by that text sitting in a comment. It no longer claims to close the dead-code gap.

Validation

gofmt clean on all 10 touched Go files (the 8 still listed are unformatted at HEAD too, none touched here — verified against git show HEAD:) · go build ./... 0 · go vet 0 · go test -count=1 ./pkg/dataplane/... ./pkg/cli/... all ok · cargo check --all-targets 0 · 5/5 ingress-identity binding tests pass · _Log.md 2974 → 2975, no union needed.

Head 6d9e540a9. Gate legs owed at this head.

@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Codex re-verify at 6d9e540a9: MERGE-NEEDS-MAJOR

My apologies for the delay reading this one — it landed while I was driving other
lanes and I did not fold it promptly.

The comment-only claim is TRUE, verified per-file

types.go: 36 insertions / 6 deletions, all // documentation at 171-179,
190-199, 459-467, 478-487. poll_descriptor/mod.rs: 8 insertions / 2 deletions,
all ordinary comments at 4821-4828, with the executable arguments beginning
unchanged at 4832. No runtime, control-flow, ABI/layout, API or compilation change,
and no new test scaffolding in either file.

But r6 added ZERO new executable assertions

Every test-file change in this fold is comments only —
cleanup_reachability_6928_test.go 41-53/150-157/196-205,
stalepin_remediation_5363_test.go 15-22/36, userspace_shim_loader_test.go
259-262, tests_session_ingress_identity.rs 373-377. So there is no
production-mutation list to give, because there is nothing new to mutate against.
That is fine for a claim-correction round, but it means the round cannot have
closed a binding gap — only a wording one.

Finding 1 — CLOSED

"Local authority makes interface identity exact" is now correctly limited:
types.go:190 says local authority does not imply exactness, :197 scopes it to
the nameable subset, and 171-179 correctly explain the non-VLAN-unit mismatch lives
in the VLAN key with both sides using the physical parent ifindex. IPv6 twin at
459-487.

Finding 2 — STILL OPEN, and the test admits it

cleanup_reachability_6928_test.go:200 says outright: "It NARROWS that gap; it
does not close it."
The implementation at 227-234 merely searches for two strings.
Inverting the shutdown condition, or leaving d.dp.Teardown() in a comment,
keeps the checks green while invalidating the mode claim.

And "exact direct-caller set" at 41-42 overstates the scanner: 112-121 resolve
spelling, not Go symbols. An aliased dp.Cleanup() is silently missed; an
unrelated bare Cleanup() is falsely counted. So the set is neither exact nor a
caller set.

Three prose assertions that are materially wrong

  • session/README.md:1037 says an empty ingress-zone interface list makes the
    row unreachable. Already false — a nameable egress interface still matches
    through the filter's egress arm. No edit is required to falsify it.
  • :1196-1198 claim the caller test prevents categorical wording from
    returning green. It does not: replace the remediation with another false sentence
    ("A plain restart ALWAYS releases this pin"), the caller graph is unchanged, the
    two banned literals are absent, and every scoped test stays green.
    stalepin_remediation_5363_test.go:124 repeats the same false guarantee.
  • :1018-1020 still says filter and display "cannot disagree" for a nameable
    ingress identity. A session arriving on A and egressing B matches interface B
    while displaying If: A. That directly contradicts r6's own ingress-or-egress
    derivation
    — the round wrote the correct account in one place and left the
    sentence it refutes standing in another.

That last one is the shape this whole PR is about, occurring inside the round that
was supposed to fix it. The banned-literal guard is the tell: a guard that forbids
two specific strings does not constrain the claim, it constrains the vocabulary.

Fold dispatched.

Paul Saab and others added 2 commits August 13, 2026 06:39
This PR exists because sentences in the ingress-identity area asserted
more than the code delivered. Round 6 was a claim-correction round that
added zero executable assertions, and it left three prose claims that
are materially wrong — one of them contradicted by the same round's own
derivation. Each was falsified by constructing the case first.

"A zone binding NO interface means no interface filter selects that row
at all" is refuted with no edit required. matchesV4/matchesV6 reject
only when BOTH arms miss, so an empty ingress slice removes one route
in, not the row: the session is still selected by the interface it
egresses on. TestInterfaceFilterReachesRowViaEgressArm6928 builds
exactly the described row — ingress zone binding nothing, ingress
identity 0, nameable egress — and shows the filter selecting it, with a
negative control that an unrelated interface selects nothing. The README
and the resolveIngressIfaces comment now name the real unreachability
condition: both arms empty.

"The filter and the displayed interface name cannot disagree" for a
non-zero nameable identity is false with no fallback in the derivation.
A session arriving on lo.50 and egressing lo.80, both stamped and both
nameable, is selected by `interface lo.80` through the egress arm and
prints If: lo.50. That is the account cli_show_flow.go:307-315 already
derives at the print site; the README sentence contradicted it. The
narrower property that does hold — an ingress-arm match names the
interface typed, because arm and column read one stamped pair through
one map — is now pinned by both sub-tests of
TestInterfaceFilterEgressArmMakesColumnNameAnotherInterface6928.

"The caller test stops the categorical wording coming back green" is
false by substitution: rewriting the remediation to "A plain restart
ALWAYS releases this pin, on every shutdown path" — categorically false,
and false in the opposite direction from the two banned literals — left
the whole pkg/dataplane suite green. A guard over two strings constrains
vocabulary, not the claim, and no test can decide whether an English
sentence describes a code fact. A third banned literal would be the same
mistake a third time, so the escape is stated rather than narrowed, and
the two facts underneath are bound instead.

Mode placement is now bound behaviourally. Both escapes the prior round
admitted were reproduced rather than argued: inverting `if hitless`, and
replacing the call with a commented-out `// d.dp.Teardown()` that still
builds, each left the scoped suite green. d.dp is a RuntimeDataPlane
interface, so no production seam was needed — the new
pkg/daemon/shutdown_dataplane_mode_6928_test.go drives the real
runShutdownSequence against a substituted dataplane and asserts both
arms. Both mutations are RED against it, so the substring companion was
deleted rather than re-labelled: it proved a strict subset.

The "exact direct-caller set" was wrong in both directions, measured: an
aliased `import dp` plus dp.Cleanup() added a real production caller the
walk silently missed, and an unrelated bare Cleanup() elsewhere was
falsely counted. The walk now resolves each file's import binding for
the dataplane path — alias, dot-import, blank import, plain — and counts
a bare Cleanup() only inside pkg/dataplane or a dot-importing file.
Re-measured: the aliased caller is reported, the unrelated one ignored,
and a dot-import case is caught too. Local shadowing and indirect calls
remain out of reach without go/types and are stated as such.

Validation: full `go test ./...` rc 0 (62 ok, 0 FAIL) and full
`cargo test --release` rc 0 (4282 passed). An earlier cargo run hit a
thread-starvation flake in afxdp::wg (passes 5/5 in isolation; this PR
touches no file under afxdp/wg). gofmt clean on all five touched Go
files; go build and go vet rc 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015oARShYtiJJ2H4UB4nXGqi
@psaab

psaab commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Round 7 at b625921617fce3cd21cdf1a465f5675d9230e619 — MERGEABLE/CLEAN

Every claim falsified by constructing the case first, then measuring. Nothing
below is inferred from my dispatch — and my dispatch was wrong twice.

Claim 1 — false, and the same sentence was ALSO in production

matchesV4/matchesV6 reject only when BOTH arms miss
(!ifaceMatchesAny(inIfs) && !ifaceMatchesAny(outIfs), session_filter.go:272/317).
Falsifying case built end-to-end through the production showFlowSession: ingress
zone quarantine binding nothing, ingress identity 0, nameable egress lo.80 ->
show security flow session interface lo.80 selects it. Negative control:
interface ge-0/0/8.0 selects nothing, so the assertion is not observing an inert
filter.

My dispatch named only the README. The same false sentence lived in production
at pkg/cli/session_filter.go:439-442.
Both corrected, with the real
unreachability condition named: both arms empty.

Claim 2 — false, falsified in the OPPOSITE direction

Rewrote the remediation to "A plain restart ALWAYS releases this pin, on every
shutdown path"
— categorically false, and false the other way from the two
banned literals. Both literals absent (grep count 0), and
go test ./pkg/dataplane/... gave all four packages ok. The guard constrains
vocabulary, not the claim. Choosing a falsehood in the opposite polarity is a
better probe than another instance of the same one.

Claim 3 — false, and my citation was wrong

Session arrives lo.50, egresses lo.80, both stamped and nameable, no fallback:
interface lo.80 selects via the egress arm while the In line prints If: lo.50.
The contradicting derivation is at pkg/cli/cli_show_flow.go:307-315, not in the
README — the README had no such derivation.
My dispatch said otherwise. Substance
stands; the derivation is now the README's account.

Bound, not narrowed — twice

Mode placement. Reproduced both escapes rather than arguing them: inverting
if hitless, and commenting out d.dp.Teardown() (still builds) — each left
-run 6928 ./pkg/dataplane/ green. Then checked whether a production seam was
needed and found one already exists: d.dp is a RuntimeDataPlane interface. New
shutdown_dataplane_mode_6928_test.go drives the real runShutdownSequence with a
substituted dataplane, asserting both arms. The substring companion was deleted
rather than re-labelled
— it proved a strict subset.

The caller set. "Exact direct-caller set" was wrong in BOTH directions,
measured: import dp ".../pkg/dataplane" + dp.Cleanup() added a real production
caller the walk silently missed, and an unrelated bare Cleanup() in a probe
package was falsely counted. The walk now resolves each file's import binding
(alias / dot-import / blank / plain) and counts a bare Cleanup() only inside
pkg/dataplane or a dot-importing file.

Narrowed, and labelled honestly — once

The remediation wording genuinely cannot be bound: no test decides whether an
English sentence correctly describes a code fact. It did not add a third banned
literal.
The two literals are labelled in place as a vocabulary check with the
measured escape written in, and the two facts underneath are bound by the tests
above. That is the right disposition and the right way to record it.

Revert edits, all observed

guard revert observed
egress-arm reachability drop && !f.ifaceMatchesAny(outIfs) (v4+v6) RED — "selected 0 of the 2 sessions"
column-name disagreement (both subtests) restore pre-#4983 zone-derived If: RED — prints ge-0/0/8.0, want lo.50
shutdown mode if hitless -> if !hitless RED both arms
shutdown mode // d.dp.Teardown() RED (builds clean)
import-aware walk add aliased dp.Cleanup() RED, correctly naming the caller

Full go test ./... rc=0 (62 ok), cargo test --release rc=0 (4282). The
install_session_serializes_with_reconcile_removal flake appeared once on a loaded
box, passes 5/5 in isolation, and this PR touches zero files under afxdp/wg/
that is #6989.

The merge finding, which corrects MY guidance

It rejected git merge-file --union, because the union FUSED a shared boundary
line
— collapsing one blank line plus one - **Timestamp**: 2026-08-12, which
would have stripped master's "#5561 round 21b" entry of its timestamp bullet.

And it would have passed a zero-deletions check: 80281 lines against an
additive 80283. Two lines short, no deletions reported.

That is precisely the fusion hazard I have been warning about, caught in the wild —
and it means the tool I recommended to two other lanes can produce it. It resolved
by deterministic replay of both diffs onto the merge base instead, and predicted
the totals before resolving: 80283 lines (77129+1191+1963), 1594 ##
headings (1573+7+14), 487 headings preceded by a non-blank line (476+4+7), zero
deletions both directions, each parent differing from the result by exactly the
other's insertions.

The prediction-before-resolving discipline is what caught it. A zero-deletions
check alone would not have.

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.

session filter: sessions lack true ingress-interface identity (filter approximates via zone→interfaces)

1 participant