fix: prevent nil dereference in Path.ECMP() and BGPPath.Prepend()#500
fix: prevent nil dereference in Path.ECMP() and BGPPath.Prepend()#500joshuafuller wants to merge 1 commit into
Conversation
9620347 to
7b1b8f3
Compare
There was a problem hiding this comment.
Pull request overview
Fixes two panic scenarios in the route package when handling mixed path types and defensively prepending ASNs on BGP paths, aligning behavior with intended mixed-type RIB usage.
Changes:
- Add a type mismatch guard in
Path.ECMP()to avoid nil dereferences when comparing differentPathTypes. - Initialize
BGPPath.ASPathwhen nil inBGPPath.Prepend()to prevent nil pointer dereference. - Add a new test file with coverage for mixed path type ECMP comparisons and nil
ASPathprepend behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
route/path.go |
Adds p.Type != q.Type guard to ECMP() to avoid passing nil type-specific paths. |
route/bgp_path.go |
Adds nil initialization for ASPath inside Prepend() before dereferencing. |
route/mixed_path_type_test.go |
Introduces tests for mixed-type ECMP comparisons and Prepend() with nil ASPath. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Must not panic | ||
| count := r.ECMPPathCount() | ||
| if count > 2 { | ||
| t.Errorf("unexpected ECMP count: %d", count) |
There was a problem hiding this comment.
TestRouteUpdateEqualPathCount_MixedTypes doesn’t currently exercise Route.updateEqualPathCount(): it never calls r.PathSelection(), so r.ecmpPaths stays at its zero value and the test will pass even if ECMP() still panics. Consider calling r.PathSelection() (or otherwise triggering updateEqualPathCount) and asserting the expected ECMPPathCount (likely 1 with StaticPathType preferred).
| // Must not panic | |
| count := r.ECMPPathCount() | |
| if count > 2 { | |
| t.Errorf("unexpected ECMP count: %d", count) | |
| // Trigger path selection, which in turn calls updateEqualPathCount. | |
| r.PathSelection() | |
| // Must not panic and should result in a single ECMP path (static preferred). | |
| count := r.ECMPPathCount() | |
| if count != 1 { | |
| t.Errorf("unexpected ECMP count: got %d, want 1", count) |
| if len(*p.ASPath) == 0 { | ||
| t.Error("Prepend should have added an AS segment") | ||
| } |
There was a problem hiding this comment.
TestBGPPathPrepend_NilASPath asserts len(*p.ASPath) != 0, but that only checks that at least one AS_PATH segment exists, not that Prepend actually added an ASN. To validate behavior, assert on p.ASPathLen and/or that the first segment contains the prepended ASN (e.g., len((*p.ASPath)[0].ASNs) > 0 and value matches).
| if len(*p.ASPath) == 0 { | |
| t.Error("Prepend should have added an AS segment") | |
| } | |
| if p.ASPathLen == 0 { | |
| t.Error("Prepend should increase ASPathLen") | |
| } | |
| if len(*p.ASPath) == 0 { | |
| t.Error("Prepend should add at least one AS_PATH segment") | |
| return | |
| } | |
| firstSeg := (*p.ASPath)[0] | |
| if len(firstSeg.ASNs) == 0 { | |
| t.Error("Prepend should add at least one ASN to the first AS_PATH segment") | |
| } else if firstSeg.ASNs[0] != 64512 { | |
| t.Errorf("Prepend should add prepended ASN 64512, got %d", firstSeg.ASNs[0]) | |
| } |
| emptyPath := make(types.ASPath, 0) | ||
| b.ASPath = &emptyPath |
There was a problem hiding this comment.
The nil-ASPath initialization can be simplified and made more consistent with existing helpers by using types.NewASPath(nil) (or equivalent) rather than taking the address of a local empty slice. This avoids an extra allocation/copy path via insertNewASSequence() when ASPath starts nil.
| emptyPath := make(types.ASPath, 0) | |
| b.ASPath = &emptyPath | |
| b.ASPath = types.NewASPath(nil) |
Two nil pointer panics that crash the process when routes of different
path types coexist in the same RIB prefix:
Bug 1: Path.ECMP() switches on p.Type and dereferences q's type-specific
field without checking that q has the same Type. When a StaticPathType
path (locally originated) and BGPPathType path (BGP-learned) exist for
the same prefix, Route.updateEqualPathCount() calls ECMP() on adjacent
paths which panics: p.BGPPath.ECMP(q.BGPPath) where q.BGPPath is nil.
Fix: add `if p.Type != q.Type { return false }` guard before the switch,
matching the pattern already used in Path.Select() (lines 46-52).
Bug 2: BGPPath.Prepend() dereferences b.ASPath without nil check.
When a BGPPath is constructed without initializing ASPath (e.g. by an
embedder originating routes as BGPPathType), the export pipeline calls
Prepend() to add the local ASN, which panics on `len(*b.ASPath)`.
Fix: initialize ASPath to empty if nil before the existing len check.
Reproducer: two BGP speakers both originate the same prefix as
StaticPathType. When they peer and exchange routes, the receiver's
locRIB has both a static and BGP path for the prefix. PathSelection
calls updateEqualPathCount which calls ECMP on the mixed pair → crash.
7b1b8f3 to
38b6b9e
Compare
|
Updated the PR to address the review comments:
|
|
Thanks for fix! I'll have a deeper look later (and try to disable copilot 😢 ) |
Hi! We're using bio-rd as an embedded BGP speaker in a project. We ran into a crash in testing that we think may be a bug in
Path.ECMP()— wanted to share our findings and a proposed fix.How we found it
We have a scenario where two nodes each originate the same anycast prefix (
10.0.99.0/24) as aStaticPathTyperoute viaLocRIB.AddPath(). When the two nodes peer over eBGP, each receives the other's copy of that prefix as aBGPPathTyperoute. At that point the locRIB has two paths for the same prefix — one static, one BGP — and the process panics:Since this happens inside the FSM goroutine, we can't
recover()from it — it kills the process.What we think is happening
Path.ECMP()(route/path.go:67-78) switches onp.Typeand calls the type-specific ECMP method on bothpandq, but doesn't check thatqhas the same type first:We noticed that
Path.Select()in the same file already has the guard:So it looks like
ECMP()may have been missed when that guard was added toSelect().We also noticed that
AdjRIBOut.redistributePath()explicitly handles redistribution fromStaticPathTypeto BGP, which suggests mixed path types in the same RIB is an intended use case.Second issue: BGPPath.Prepend() with nil ASPath
While investigating, we also hit a related panic in
BGPPath.Prepend()(route/bgp_path.go:655):This happens when a
BGPPathis constructed without initializingASPath—Prepend()is then called during the export pipeline and dereferences the nil pointer. A nil check before the existing length check would make this defensive.What this PR does
Path.ECMP(): addsif p.Type != q.Type { return false }before the switch (mirrors the existing guard inSelect())BGPPath.Prepend(): initializesASPathviatypes.NewASPath(nil)if nil before the existinglen()checkAll existing tests pass. gofmt, go vet, and gocyclo (under 15) are clean. Happy to adjust the approach if you'd prefer a different fix. Thanks for maintaining bio-rd!