graph: fix graph cache population for channels with both policies disabled
What changed, and why it matters
This commit fixes a bug in LND's routing graph cache. Previously, if a Lightning channel had both routing policies disabled when the node started up, the channel was never loaded into the in-memory routing cache. Later, if a policy update re-enabled one direction, the database would record the change but the cache would stay out of sync, so the channel could not actually be used for routing. The fix always loads the channel structure into the cache and only skips attaching the disabled policies, allowing later updates to enable it correctly.
Treat as a functional bug with availability impact rather than an exploitable security vulnerability. Users running LND nodes should upgrade to a release containing this fix to avoid channels becoming stuck unroutable after policy re-enabling. No immediate incident response is required.
Security signals we found
Routing cache/database inconsistency
Denial of routing availability for affected channels
Logic bug in graph cache population
No input validation or memory-safety issue evident
Evidence from the diff
In graph/db/graph_cache.go, AddChannel previously returned early when both policies were disabled, leaving the channel entirely absent from c.updateOrAddEdge. A subsequent policy update that enabled a direction would therefore have no cached edge to mutate, creating a cache/database inconsistency and making the channel unavailable to pathfinding. The patch removes the early return, always inserts the DirectedChannel entries for both nodes under the cache lock, then conditionally skips attaching policies only when both are disabled. A debug log was added to record the skip.
Changed components
graph/db/graph_cache.goLND routing/pathfinding subsystemChannel policy update handlingInspect captured patch +16 / −7
diff --git a/graph/db/graph_cache.go b/graph/db/graph_cache.go
index a691361..5934837 100644
--- a/graph/db/graph_cache.go
+++ b/graph/db/graph_cache.go
@@ -121,13 +121,9 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo,
return
}
- if policy1 != nil && policy1.IsDisabled() &&
- policy2 != nil && policy2.IsDisabled() {
-
- return
- }
-
- // Create the edge entry for both nodes.
+ // Create the edge entry for both nodes. We always add the channel
+ // structure to the cache, even if both policies are currently disabled,
+ // so that later policy updates can find and update the channel entry.
c.mtx.Lock()
c.updateOrAddEdge(info.NodeKey1Bytes, &DirectedChannel{
ChannelID: info.ChannelID,
@@ -143,6 +139,19 @@ func (c *GraphCache) AddChannel(info *models.CachedEdgeInfo,
})
c.mtx.Unlock()
+ // Skip adding policies if both are disabled, as the channel is
+ // currently unusable for routing. However, we still add the channel
+ // structure above so that policy updates can later enable it.
+ if policy1 != nil && policy1.IsDisabled() &&
+ policy2 != nil && policy2.IsDisabled() {
+
+ log.Debugf("Skipping policies for channel %v: both "+
+ "policies are disabled (channel structure still "+
+ "cached for future updates)", info.ChannelID)
+
+ return
+ }
+
// The policy's node is always the to_node. So if policy 1 has to_node
// of node 2 then we have the policy 1 as seen from node 1.
if policy1 != nil {
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.