Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 67 additions & 12 deletions CubeNet/cubevs/dns_reaper.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ func reapDNSState() {
reapDNSQueryTrack(now)
}

// reapDNSLearnedPolicies scans allow_out_v3 and removes expired DNS-learned entries.
// reapDNSLearnedPolicies removes expired DNS-learned allow_out_v3 entries.
//
// The fast path iterates ifindex_to_mvmmeta so Ready-pool TAPs (metadata
// deleted, allow_out flushed) are skipped without walking every HashOfMaps
// outer key. If metadata cannot be loaded, fall back to iterating allow_out_v3
// outers so a transient pin/ENOENT failure cannot stall DNS TTL expiry for
// every subsequent tick.
func reapDNSLearnedPolicies(now uint64) {
allowOut, err := loadPinnedMap(MapNameAllowOutV3)
if err != nil {
Expand All @@ -35,13 +41,46 @@ func reapDNSLearnedPolicies(now uint64) {
}
defer allowOut.Close()

meta, err := loadPinnedMap(MapNameIfindexToMVMMetadata)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding (Low, PLAUSIBLE): the fast path only reaps ifindices present in ifindex_to_mvmmeta; the allow_out_v3 outer scan is now reachable only when the metadata map fails to load, not when an ifindex is simply missing from it.

If the "metadata deleted ⟺ allow_out flushed" invariant is ever violated (crash or failed cleanup between the two steps, partial pool cleanup failure), an allow_out_v3 inner whose ifindex is absent from mvmmeta will never have its expired DNS-learned entries reaped — the previous outer-scan covered every outer key. The fallback covers map-load failures only. Low likelihood given the invariant, but it's a silent robustness regression in exactly the partial-failure modes the old code tolerated. Consider also reaping allow_out outers whose ifindex is not in the metadata set, or documenting/asserting that the invariant is enforced atomically.

enqueueEvent(Event{
Error: err,
Message: "failed to load ifindex_to_mvmmeta map; falling back to allow_out_v3 outer scan",
})
reapDNSLearnedPoliciesFromAllowOutOuter(allowOut, now)
return
}
defer meta.Close()

var (
ifindex uint32
innerMapID uint32
ifindex uint32
mvmMeta mvmMetadata
)
iter := meta.Iterate()
for iter.Next(&ifindex, &mvmMeta) {
Comment thread
FakeLearne marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fast path only reaps ifindices present in ifindex_to_mvmmeta. Any allow_out_v3 outer key whose ifindex lost its metadata (invariant violation, e.g. a partially-cleaned TAP) is never visited here, and the fallback reapDNSLearnedPoliciesFromAllowOutOuter only triggers when the metadata map fails to load, not when a specific key is missing. The previous code iterated allow_out_v3 outers directly and reaped every ifindex with an outer key, so expired DNS-learned entries for metadata-less ifindices now persist until the next startup stale-GC. Worth either iterating the outer when the two maps disagree, or documenting the invariant. Also note this new fast path (and the fallback trigger) has no direct unit test — the kept reapDNSLearnedPoliciesForInnerMap is exercised, but not the mvmmeta iteration.

if err := reapDNSLearnedPoliciesForIfindex(allowOut, ifindex, now); err != nil {
enqueueEvent(Event{
Error: err,
Message: fmt.Sprintf("failed to reap DNS-learned policies, ifindex: %d", ifindex),
})
}
}
if err := iter.Err(); err != nil {
enqueueEvent(Event{
Error: err,
Message: "failed to iterate ifindex_to_mvmmeta map",
})
}
}

func reapDNSLearnedPoliciesFromAllowOutOuter(allowOut *ebpf.Map, now uint64) {
var (
ifindex uint32
value uint32
)
iter := allowOut.Iterate()
for iter.Next(&ifindex, &innerMapID) {
if err := reapDNSLearnedPoliciesForInnerMap(innerMapID, now); err != nil {
for iter.Next(&ifindex, &value) {
if err := reapDNSLearnedPoliciesForIfindex(allowOut, ifindex, now); err != nil {
enqueueEvent(Event{
Error: err,
Message: fmt.Sprintf("failed to reap DNS-learned policies, ifindex: %d", ifindex),
Expand All @@ -51,20 +90,25 @@ func reapDNSLearnedPolicies(now uint64) {
if err := iter.Err(); err != nil {
enqueueEvent(Event{
Error: err,
Message: "failed to iterate allow_out_v3 map",
Message: "failed to iterate allow_out_v3 outer map",
})
return
}
}

// reapDNSLearnedPoliciesForInnerMap deletes expired DNS-learned entries from one allow_out_v3 inner map.
func reapDNSLearnedPoliciesForInnerMap(innerMapID uint32, now uint64) error {
inner, err := ebpf.NewMapFromID(ebpf.MapID(innerMapID))
func reapDNSLearnedPoliciesForIfindex(allowOut *ebpf.Map, ifindex uint32, now uint64) error {
inner, err := acquireInnerMap(allowOut, ifindex, MapNameAllowOutV3, nil)
if err != nil {
return fmt.Errorf("ebpf.NewMapFromID failed: %w, id: %d", err, innerMapID)
if errors.Is(err, ebpf.ErrKeyNotExist) {
return nil
}
return fmt.Errorf("failed to open allow_out_v3 inner map: %w", err)
}
defer inner.Close()
return reapDNSLearnedPoliciesForInner(inner, now)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurrency hazard: the DNS reaper uses a cached inner map that a concurrent TAP destroy / stale-GC can Close out from under it.

acquireInnerMap returns the process-wide cached *ebpf.Map and releases the per-key lock before the caller uses it. Here the reaper then iterates and deletes entries on that map via reapDNSLearnedPoliciesForInner with no lock held. Meanwhile destroyTapDeleteTAPDevicePolicyMapsdeleteCachedInnerAndOuter takes the same per-key lock and Close()s that exact FD. The reaper runs on a 5s ticker goroutine (StartSessionReaper/doReap) and is not serialized against destroy for a given ifindex — the comment in acquireInnerMap ("Callers must not use the returned map across TAP destroy / stale-outer GC for the same ifindex") does not protect this caller.

Failure modes: an iteration on a closed FD returns EBADF (iter.Err() is logged and that tick's reap for the ifindex is silently lost), or worse — if the freed FD number is reused by another map creation in the window, the reaper's inner.Delete targets an unrelated map and can corrupt a live sandbox's allow_out policy.

Suggestion: hold the per-key lock for the duration of the inner iteration (the destroy path already takes the same lock, so it would wait out the reap before closing), or defer the Close in deleteCachedInnerAndOuter until no reader can be in flight.

}

// reapDNSLearnedPoliciesForInner deletes expired DNS-learned entries from one
// allow_out_v3 inner map.
func reapDNSLearnedPoliciesForInner(inner *ebpf.Map, now uint64) error {
var (
key lpmKeyV3
value netPolicyValueV3
Expand All @@ -84,6 +128,17 @@ func reapDNSLearnedPoliciesForInnerMap(innerMapID uint32, now uint64) error {
return nil
}

// reapDNSLearnedPoliciesForInnerMap is kept for tests/callers that still pass a
// map ID. Prefer reapDNSLearnedPoliciesForInner with a cached FD.
func reapDNSLearnedPoliciesForInnerMap(innerMapID uint32, now uint64) error {
inner, err := ebpf.NewMapFromID(ebpf.MapID(innerMapID))
if err != nil {
return fmt.Errorf("ebpf.NewMapFromID failed: %w, id: %d", err, innerMapID)
}
defer inner.Close()
return reapDNSLearnedPoliciesForInner(inner, now)
}

// reapDNSQueryTrack deletes expired pending DNS queries that never got a response.
func reapDNSQueryTrack(now uint64) {
queryTrack, err := loadPinnedMap(MapNameDNSQueryTrack)
Expand Down
28 changes: 4 additions & 24 deletions CubeNet/cubevs/dnspolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,13 @@ func populateDNSAllowInnerMap(inner *ebpf.Map, rules []dnsAllowRule) error {
}

func flushDNSAllowForIfindex(outerMap *ebpf.Map, ifindex uint32) error {
inner, err := lookupInnerMap(outerMap, ifindex)
inner, err := lookupInnerMap(outerMap, ifindex, MapNameDNSAllowV2)
if errors.Is(err, ebpf.ErrKeyNotExist) {
return nil
}
if err != nil {
return err
}
defer inner.Close()

return flushDNSAllowInnerMap(inner)
}

Expand Down Expand Up @@ -177,18 +175,7 @@ func mergePortsIntoDNSValue(v *dnsAllowValue, src []l7PortEntry) {
}

func flushDNSAllowInnerMap(inner *ebpf.Map) error {
var oldKey dnsAllowKey
var oldValue dnsAllowValue
iter := inner.Iterate()
for iter.Next(&oldKey, &oldValue) {
if err := inner.Delete(&oldKey); err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) {
return fmt.Errorf("dns allow delete failed: %w", err)
}
}
if err := iter.Err(); err != nil {
return fmt.Errorf("dns allow iterate failed: %w", err)
}
return nil
return flushInnerEntries[dnsAllowKey, dnsAllowValue](inner)
}

// cleanupDNSAllow clears the sandbox DNS allow inner map while keeping it preallocated.
Expand All @@ -199,15 +186,13 @@ func cleanupDNSAllow(ifindex uint32) error {
}
defer dnsAllow.Close()

inner, err := lookupInnerMap(dnsAllow, ifindex)
inner, err := lookupInnerMap(dnsAllow, ifindex, MapNameDNSAllowV2)
if err != nil {
if errors.Is(err, ebpf.ErrKeyNotExist) {
return nil
}
return err
}
defer inner.Close()

return flushDNSAllowInnerMap(inner)
}

Expand All @@ -226,16 +211,11 @@ func applyDNSAllow(ifindex uint32, rules []dnsAllowRule, replace bool) error {
if len(rules) == 0 {
return flushDNSAllowForIfindex(dnsAllow, ifindex)
}
if err := ensureDNSAllowInnerMap(dnsAllow, ifindex); err != nil {
return err
}

inner, err := lookupInnerMap(dnsAllow, ifindex)
inner, err := acquireInnerMap(dnsAllow, ifindex, MapNameDNSAllowV2, newInnerDNSAllowMap)
if err != nil {
return err
}
defer inner.Close()

if replace {
if err := flushDNSAllowInnerMap(inner); err != nil {
return err
Expand Down
111 changes: 111 additions & 0 deletions CubeNet/cubevs/inner_cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package cubevs

import (
"errors"
"fmt"
"sync"

"github.com/cilium/ebpf"
)

// policyInnerCache keeps open FDs for HashOfMaps inners keyed by outer map name
// and TAP ifindex. Userspace BPF_MAP_LOOKUP_ELEM on HashOfMaps is extremely
// expensive on large hosts; create/apply/reaper paths must reuse cached inners
// instead of looking up the outer map on every call.
//
// FD budget: each live TAP may pin up to one FD per net-policy outer
// (allow_out_v2, deny_out, dns_allow) — about 3 × active-TAP-count on Cubelet,
// plus any Active ifindexes warmed by the DNS reaper. Entries are released only
// on TAP destroy / startup stale-outer GC; there is no size-bounded eviction.
// There is also no BPF-reload generation counter today (no reload path); if
// pinned outers are ever recreated, call clearPolicyInnerCacheForTest-style
// invalidation (or add a generation) before reuse.
type policyInnerKey struct {
mapName string
ifindex uint32
}

var policyInnerMaps sync.Map // policyInnerKey -> *ebpf.Map

// acquireInnerMap returns the inner map for ifindex, creating it when missing
// and newInner is non-nil. The returned map is owned by the process-wide cache;
// callers must not Close it. A nil newInner means "must already exist".
//
// Cubelet completes stale-outer GC synchronously before starting background TAP
// work or serving requests, and its TAP lifecycle prevents concurrent ownership
// of one ifindex. This cache therefore does not add another per-key lock.
func acquireInnerMap(outerMap *ebpf.Map, ifindex uint32, mapName string,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design note: unbounded FD cache with no invalidation hook, and no direct unit tests.

Two things worth addressing before this lands:

  1. FD budget. Every {mapName, ifindex} now pins one open inner-map FD for the life of the TAP (3 maps × active TAPs), and the DNS reaper's newInner=nil acquires additionally warm the cache for any active ifindex that was created before this cache existed. On a large host this adds a steady ~3×-active-TAP-count to Cubelet's FD usage with no eviction other than TAP destroy / startup GC. The Lookup-then-cache path (line 56) makes even read-only consumers pin FDs. A bounded cache (or at least a documented FD-accounting expectation) would make the tradeoff explicit.

  2. No reload invalidation. The cache is process-global and keyed only by {mapName, ifindex} — if the pinned outer maps are ever recreated (a future BPF reload path), acquireInnerMap would keep handing out FDs to maps no longer in the dataplane. The current codebase has no reload path so this is latent, but a generation counter or a clearPolicyInnerCache hook would make it safe.

Also: this is the highest-risk new file in the PR (per-key locking + FD ownership invariants) but has no direct unit tests — the invariants are only exercised indirectly through netpolicy/dnspolicy tests. A focused test for the lock-ordering / cache-vs-outer consistency would be valuable.

newInner func() (*ebpf.Map, error),
) (*ebpf.Map, error) {
key := policyInnerKey{mapName: mapName, ifindex: ifindex}
if cached, ok := policyInnerMaps.Load(key); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH — stale-cache TOCTOU: a reused ifindex can silently lose all allow/deny policy.

The cache-hit path returns the cached inner without verifying the outer HashOfMaps still maps this ifindex to it. The DNS reaper (dns_reaper.go fast path) is a new background reader that populates this cache every 5s via acquireInnerMap(allowOut, ifindex, ..., nil), and LookupLoadOrStore is not atomic with deleteCachedInnerAndOuter's outer.Delete:

  1. Reaper: outerMap.Lookup(X) succeeds → inner = A
  2. Destroy: deleteCachedInnerAndOuter does LoadAndDelete(X) (cache miss) then outer.Delete(X)
  3. Reaper: LoadOrStore(X, A) re-caches an ifindex whose outer key is now gone

Nothing evicts it (destroy already ran its eviction; GC only runs at startup). When the kernel reuses ifindex X for a new TAP, acquireInnerMap cache-hits A and all policy setup writes into a map the dataplane never looks up — the create succeeds but egress policy is silently detached, with no error and no recovery until restart. deleteCachedInnerAndOuter has the mirror window: its LoadAndDelete runs before outer.Delete, so a concurrent acquire landing between them re-populates the cache after eviction.

The comment at lines 34–36 justifies skipping a per-key lock with the "no concurrent ownership of one ifindex" lifecycle invariant — but that invariant covers two creators of the same ifindex, not a background reader racing a destroy, which is precisely what the reaper now is.

Suggested fix: per-key serialization around acquire/release (per-key mutex, or lock-per-key via sync.Map), or in deleteCachedInnerAndOuter re-evict any entry stored between the first LoadAndDelete and outer.Delete; validating the outer on cache-hit works but defeats the Lookup-cost goal.

return cached.(*ebpf.Map), nil
}

var inner *ebpf.Map
err := outerMap.Lookup(&ifindex, &inner)
if err == nil {
actual, loaded := policyInnerMaps.LoadOrStore(key, inner)
if loaded {
_ = inner.Close()
return actual.(*ebpf.Map), nil
}
return inner, nil
}
if !errors.Is(err, ebpf.ErrKeyNotExist) {
return nil, fmt.Errorf("map.Lookup failed: %w, name: %s", err, mapName)
}
if newInner == nil {
return nil, fmt.Errorf("map.Lookup failed: %w, name: %s", ebpf.ErrKeyNotExist, mapName)
}

created, err := newInner()
if err != nil {
return nil, err
}
if err := outerMap.Put(&ifindex, created); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concurrency hazard in the create path: two goroutines that both miss the cache for the same (mapName, ifindex) and both reach newInner() can leave the cache and the outer map pointing at different inner maps.

Interleaving:

  • G1: LookupErrKeyNotExist, G2: LookupErrKeyNotExist
  • G1: created1 = newInner(); outer.Put(ifindex, created1)
  • G2: created2 = newInner(); outer.Put(ifindex, created2) → overwrites G1's entry in the outer map
  • G1: LoadOrStore(key, created1) → stores, returns created1
  • G2: LoadOrStore(key, created2)loaded=true, closes created2, returns created1

End state: the process-wide cache holds created1, while the kernel's HashOfMaps entry (what the eBPF programs see) points to created2. Every subsequent policy write through the cached created1 is invisible to the dataplane, and the divergence persists until releaseCachedInner (TAP destroy). The pre-refactor code re-read the outer map on each call, so it self-healed; the cache removes that.

Worth serializing creation per (mapName, ifindex) (e.g., a per-key mutex around the lookup→create→put), or re-reading the outer value after a LoadOrStore miss and reconciling which inner is actually live in the outer map.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create-and-Put critical section is unsynchronized per key. Two concurrent acquireInnerMap calls for the same (mapName, ifindex) can both miss the cache, both create inners, and both outerMap.Put — the outer ends up referencing one inner while LoadOrStore keeps the other in the cache (closing the loser). Subsequent cache hits then write policy to an inner the outer no longer references (silent policy loss, since the datapath looks up the outer), and the superseded kernel inner leaks.

Separately, a Lookup result cached after a concurrent releaseCachedInner/outer-delete leaves a stale FD in the cache; a later TAP reusing the ifindex will hit the stale inner and write policy to a map not installed in the outer. With the DNS reaper and the new async GC now running concurrently with the create/apply paths, these races are reachable in practice. Consider guarding with per-key locks, and/or re-validating on cache hit that the outer still references the cached inner.

_ = created.Close()
return nil, fmt.Errorf("map.Put failed: %w, name: %s", err, mapName)
}
actual, loaded := policyInnerMaps.LoadOrStore(key, created)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM — creation branch is only safe under an unenforced external invariant.

If two goroutines create the same (mapName, ifindex) concurrently (both newInner non-nil), both miss the Lookup, both Put; the second Put overwrites the outer key with the loser's map, then the loser's LoadOrStore reports loaded and closes its map while the outer still references it, and the cache returns the winner's map which the outer no longer references — policy writes then go to a detached map.

Today the only concurrent background reader (the reaper) passes nil newInner, so this isn't triggerable — but the "no concurrent ownership" invariant is external to this helper and untested. A per-key lock (or only Put the outer when the cache-store wins) makes the helper correct independent of caller discipline. Consider folding this into the fix for the stale-cache TOCTOU at line 41.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create race: if two goroutines both miss the cache and both observe ErrKeyNotExist before either Puts, then G1 Puts I1 and wins LoadOrStore while G2 Puts I2 (overwriting the outer reference) and then loses LoadOrStore and closes I2. The cache returns I1, but the outer — and the kernel datapath — references I2, whose only userspace FD was just closed. Policy applied through the cached I1 is invisible to the datapath and I2 is orphaned. The comment at lines 34-36 documents the "no concurrent ownership" assumption, but nothing here enforces it, and the DNS reaper now runs concurrently on a ticker (though it passes nil newInner). Since the failure mode is silent policy divergence, consider a per-key singleflight or a test that drives two concurrent creators for the same key to pin down the expected behavior.

if loaded {
_ = created.Close()
return actual.(*ebpf.Map), nil
}
return created, nil
}

// releaseCachedInner closes and drops a cached inner FD.
func releaseCachedInner(mapName string, ifindex uint32) {
key := policyInnerKey{mapName: mapName, ifindex: ifindex}
if cached, ok := policyInnerMaps.LoadAndDelete(key); ok {
_ = cached.(*ebpf.Map).Close()
}
}

// deleteCachedInnerAndOuter removes the outer key and its cached userspace FD.
func deleteCachedInnerAndOuter(outer *ebpf.Map, mapName string, ifindex uint32) error {
key := policyInnerKey{mapName: mapName, ifindex: ifindex}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding (Medium, PLAUSIBLE): use-after-close on a shared cached FD — the 5s DNS reaper can hold a pointer to an inner that deleteCachedInnerAndOuter has just closed.

deleteCachedInnerAndOuter does LoadAndDelete + Close() on the cached inner FD, while the reaper (dns_reaper.goacquireInnerMap(allowOut, ifindex, MapNameAllowOutV3, nil) → iterate) fetches the same cache entry and uses it without holding any lock. The exclusivity reasoning in the acquireInnerMap doc comment (line 32–36: "TAP lifecycle prevents concurrent ownership of one ifindex") holds for the create/apply path, but the background reaper is a separate goroutine sharing the same cache entries: its tick can snapshot an ifindex, then TAP destroy closes the cached FD, then the reaper iterates/updates the closed map → EBADF; in the worst case the FD number is reused and the reaper touches an unrelated kernel object.

Pre-PR each reaper pass opened its own FD via ebpf.NewMapFromID and closed it locally, so this race did not exist. Impact is bounded (a destroyed TAP's entries are moot), but this is a new concurrency hazard on shared mutable state and will fire under the create/destroy churn this PR targets. Consider per-key refcounting (or a per-key mutex spanning Load→use→release, with the destroy path taking the same lock), or having the reaper recover from EBADF by re-acquiring the outer.

var toClose *ebpf.Map
if cached, ok := policyInnerMaps.LoadAndDelete(key); ok {
toClose = cached.(*ebpf.Map)
}
err := outer.Delete(&ifindex)
if toClose != nil {
_ = toClose.Close()
}
if err != nil && !errors.Is(err, ebpf.ErrKeyNotExist) {
return err
}
return nil
}

func clearPolicyInnerCacheForTest() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clearPolicyInnerCacheForTest is defined but never called anywhere in the PR, so the process-global policyInnerMaps cache leaks across tests in the cubevs package. The migration tests reuse ifindex = 42 (six tests) and 43, all with the same map names (MapNameDNSAllowV2 / MapNameAllowOutV3), and each test creates a fresh outer map. With the old lookupInnerMap(outerMap, ifindex) the inner was resolved from that test's own outer and every test was isolated; now acquireInnerMap/lookupInnerMap return the cached inner FD for (name, ifindex) from whichever test ran first — an FD belonging to a different, already-closed outer. The removed defer dest.Close() calls in migration_test.go masked this: writes and the subsequent dest.Lookup assertions both go through the stale cached FD, so the tests still pass but now validate a detached map the outer under test never references. Please wire t.Cleanup(clearPolicyInnerCacheForTest) into the cache-touching tests (or clear in TestMain), and consider adding the outer map's ID to the cache key so an entry can never be served for a different outer.

policyInnerMaps.Range(func(k, v any) bool {
policyInnerMaps.Delete(k)
if m, ok := v.(*ebpf.Map); ok && m != nil {
_ = m.Close()
}
return true
})
}
6 changes: 2 additions & 4 deletions CubeNet/cubevs/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,10 @@ func migrateAllowOutInnerMap(current *ebpf.Map, ifindex uint32, sourceName strin
if err := ensureAllowOutV3InnerMap(current, ifindex); err != nil {
return err
}
destination, err := lookupInnerMap(current, ifindex)
destination, err := lookupInnerMap(current, ifindex, MapNameAllowOutV3)
if err != nil {
return err
}
defer destination.Close()

switch info.ValueSize {
case legacyAllowOutValueSize:
Expand Down Expand Up @@ -310,11 +309,10 @@ func migrateDNSAllowInnerMap(current *ebpf.Map, ifindex uint32, sourceName strin
if err := ensureDNSAllowInnerMap(current, ifindex); err != nil {
return err
}
destination, err := lookupInnerMap(current, ifindex)
destination, err := lookupInnerMap(current, ifindex, MapNameDNSAllowV2)
if err != nil {
return err
}
defer destination.Close()

if info.ValueSize == legacySize {
var key dnsAllowKey
Expand Down
12 changes: 4 additions & 8 deletions CubeNet/cubevs/migration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,10 @@ func TestMigrateDNSAllowInnerMapFromLegacy(t *testing.T) {
t.Fatalf("migrateDNSAllowInnerMap: %v", err)
}

dest, err := lookupInnerMap(current, ifindex)
dest, err := lookupInnerMap(current, ifindex, MapNameDNSAllowV2)
if err != nil {
t.Fatalf("lookupInnerMap: %v", err)
}
defer dest.Close()

for _, e := range entries {
key, want, err := makeDNSAllowRule(e.domain, e.flags)
Expand Down Expand Up @@ -175,11 +174,10 @@ func TestMigrateDNSAllowInnerMapFromCurrent(t *testing.T) {
t.Fatalf("migrateDNSAllowInnerMap: %v", err)
}

dest, err := lookupInnerMap(current, ifindex)
dest, err := lookupInnerMap(current, ifindex, MapNameDNSAllowV2)
if err != nil {
t.Fatalf("lookupInnerMap: %v", err)
}
defer dest.Close()

var got dnsAllowValue
if err := dest.Lookup(&key, &got); err != nil {
Expand Down Expand Up @@ -311,11 +309,10 @@ func TestMigrateDNSAllowMapOuterWithBpffs(t *testing.T) {
}

// The new outer must now hold the migrated rules (NameLen+Flags, PortCount=0).
dest, err := lookupInnerMap(newOuter, ifindex)
dest, err := lookupInnerMap(newOuter, ifindex, MapNameDNSAllowV2)
if err != nil {
t.Fatalf("lookupInnerMap: %v", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finding (Medium, CONFIRMED by inspection): cross-test cache contamination makes this assertion vacuous.

policyInnerMaps is a process-global sync.Map keyed only by (mapName, ifindex) — not by the outer map object — and clearPolicyInnerCacheForTest() (inner_cache.go:304) is never called by any test. This test reuses ifindex = 42 with dns_allow_v2, as do TestMigrateDNSAllowInnerMapFromLegacy (line 102) and TestMigrateDNSAllowInnerMapFromCurrent (line 220), which run earlier and populate the cache.

So in TestMigrateDNSAllowMapOuterWithBpffs:

  1. migrateDNSAllowMapensureDNSAllowInnerMap(newOuter, 42)acquireInnerMap cache-hits the earlier test's inner FD and skips outerMap.Put(&ifindex, ...) — the freshly pinned dns_allow_v2 outer is never populated.
  2. lookupInnerMap(newOuter, 42, MapNameDNSAllowV2) returns that same stale inner.
  3. The assertions read the stale inner, so the test passes while verifying nothing about the pinned newOuter map the test is named after.

Please call clearPolicyInnerCacheForTest() in setup (or scope the cache per-test / make the key include the outer map identity) so these tests actually exercise the maps they construct.

}
defer dest.Close()
for _, e := range entries {
key, want, err := makeDNSAllowRule(e.domain, e.flags)
if err != nil {
Expand Down Expand Up @@ -581,11 +578,10 @@ func TestMigrateAllowOutMapOuterWithBpffs(t *testing.T) {
t.Fatalf("migrateAllowOutMap: %v", err)
}

dest, err := lookupInnerMap(newOuter, ifindex)
dest, err := lookupInnerMap(newOuter, ifindex, MapNameAllowOutV3)
if err != nil {
t.Fatalf("lookupInnerMap: %v", err)
}
defer dest.Close()

// The L7 entry expands to the default {80/http, 443/https} /48 set.
for _, tc := range []struct {
Expand Down
Loading
Loading