fix(EN-1874): require identities for all Raft members - #1799
Conversation
🛑 Changes requested — automated reviewThe active prior finding remains: payload-less AddNode entries can still introduce identity-less voters. This violates the universal member-identity invariant. |
Codecov Report❌ Patch coverage is ❌ Your patch check has failed because the patch coverage (66.16%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## release/v3.0 #1799 +/- ##
================================================
- Coverage 76.84% 76.79% -0.05%
================================================
Files 472 472
Lines 50356 50445 +89
================================================
+ Hits 38696 38740 +44
- Misses 8262 8293 +31
- Partials 3398 3412 +14
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Arbitration — PR #1799
|
1314b4c to
33bd364
Compare
a15e83c to
011db6c
Compare
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #1799 (comment)
011db6c to
dbbeff0
Compare
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot review complete: no remaining inline findings.
Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).
Summary: #1799 (comment)
dbbeff0 to
f6e2326
Compare
33bd364 to
aa0ed6e
Compare
f6e2326 to
57bd9f8
Compare
aa0ed6e to
33504b9
Compare
57bd9f8 to
a8807ad
Compare
33504b9 to
9b8a453
Compare
a8807ad to
c029da3
Compare
9b8a453 to
db672a5
Compare
c029da3 to
89377a7
Compare
NumaryBot
left a comment
There was a problem hiding this comment.
NumaryBot posted 1 new inline finding.
Summary: #1799 (comment)
| func ValidateConfChangeIdentities(cc *raftpb.ConfChangeV2) error { | ||
| return WalkConfChangeContexts(cc, func(t raftpb.ConfChangeType, nodeID uint64, ctx *ConfChangeContext) error { | ||
| switch t { | ||
| case raftpb.ConfChangeAddNode: |
There was a problem hiding this comment.
🟠 [major] Reject payload-less AddNode for unknown members
When a committed ConfChangeAddNode has no registration payload, this validator always treats it as a promotion, but etcd/raft will also accept that shape for a previously unknown node and create an identity-less voter. A malformed or corrupted WAL entry can therefore pass both validation sites and mutate ConfState without a membership row, contrary to the universal identity invariant; only allow this exception after confirming the node is already a learner. This is also required by the impossible-state guardrail in AGENTS.md.
There was a problem hiding this comment.
Agree, and the consequence is worse than stated: under this PR's hard checks a voter created this way is unremovable — RemoveNode and ForceRemoveNode both fail on the missing membership row, and ListPeers errors permanently, which also breaks discovery for new joiners. Pre-PR the empty-identity path could still remove such a node. A row-existence check for payload-less AddNode in WriteConfChange/finishReady closes it (a promotion implies a previously registered learner).
Related nit for the same fix: HasPeerRegistration is true if any field is set, so a registration payload carrying a valid instance_id but empty addresses also passes both sites and lands an address-less row — worth validating the full payload here too.
db672a5 to
979224a
Compare
1f4fe74 to
38dac34
Compare
38dac34 to
742f179
Compare
Azorlogh
left a comment
There was a problem hiding this comment.
Reviewed at 742f179. The identity invariant is enforced coherently at every boundary I traced (peer store, FSM WriteConfChange, finishReady apply, remove/force-remove/auto-promotion, discovery, both gRPC surfaces), ForceRemoveNode validates before mutating RawNode, and the EnsureInstanceID fail-closed check is backed by the Fx ordering fix (provideWAL depending on NodeConfig) with a test pinning it. The UpdateNode transport rewire resolves the earlier blocker. Tests and docs match the code.
Remaining points:
- I agree with NumaryBot's open payload-less-AddNode finding — replied in-thread, the consequence is worse than stated (the resulting member is unremovable).
- One inline comment: a small race the new
ListPeersidentity check introduces. - The PR body's stacking section is stale: #1794 is already merged, this branch sits on plain release/v3.0.
| serviceAddr = s.servicePool.GetPeerAddress(nodeID) | ||
| } | ||
|
|
||
| instanceID, ok := s.infraMembership.GetInstanceID(nodeID) |
There was a problem hiding this comment.
The member list is snapshotted inside execClusterCommand, but this cache read happens afterwards on the RPC goroutine — a removal committing in between makes ListPeers fail with invariant: cluster member N has no membership row for a state that was never inconsistent (spurious GetPeers/discovery error, and the "invariant" wording is misleading there). Capturing the identities inside the same closure would close it.
Summary
Stacking
This PR is intentionally stacked on #1794 and should be reviewed and merged before #1794 is merged into the default branch. After this PR lands into the parent branch, #1794 can be merged with the universal member-identity invariant in place.
Validation