Skip to content

Commit c75015a

Browse files
committed
vtctld: guard ev.ShardInfo before overwriting response keyspace/shard in reparent RPCs
PlannedReparentShard and EmergencyReparentShard both initialize the response with req.Keyspace/req.Shard, then overwrite from ev.ShardInfo inside an `if ev != nil` block. ev itself is always non-nil (allocated before the reparent call), but ev.ShardInfo is zero-valued whenever reparentShardLocked returns before populating it — for example when no valid reparent target exists in the same cell as the current primary. Calling .Keyspace() / .ShardName() on a zero-valued topo.ShardInfo dereferences an internal nil pointer, crashing vtctld. Fix: check whether ev.ShardInfo.Keyspace() is non-empty before overwriting the response fields. When it is empty the response already holds the correct values from the req initialization, so the fallback is safe. Signed-off-by: Lucas Morduchowicz <lmorduch@gmail.com>
1 parent cff3492 commit c75015a

2 files changed

Lines changed: 57 additions & 4 deletions

File tree

go/vt/vtctl/grpcvtctldserver/server.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,9 +1332,14 @@ func (s *VtctldServer) EmergencyReparentShard(ctx context.Context, req *vtctldat
13321332
Shard: req.Shard,
13331333
}
13341334

1335+
// ev is always non-nil (initialized before the reparent call), but ev.ShardInfo may be
1336+
// zero-valued if reparentShardLocked returned before populating it (e.g. error paths where
1337+
// no primary could be found). Guard against a nil pointer dereference.
13351338
if ev != nil {
1336-
resp.Keyspace = ev.ShardInfo.Keyspace()
1337-
resp.Shard = ev.ShardInfo.ShardName()
1339+
if k := ev.ShardInfo.Keyspace(); k != "" {
1340+
resp.Keyspace = k
1341+
resp.Shard = ev.ShardInfo.ShardName()
1342+
}
13381343

13391344
if ev.NewPrimary != nil && !topoproto.TabletAliasIsZero(ev.NewPrimary.Alias) {
13401345
resp.PromotedPrimary = ev.NewPrimary.Alias
@@ -3333,9 +3338,14 @@ func (s *VtctldServer) PlannedReparentShard(ctx context.Context, req *vtctldatap
33333338
Shard: req.Shard,
33343339
}
33353340

3341+
// ev is always non-nil (initialized before the reparent call), but ev.ShardInfo may be
3342+
// zero-valued if reparentShardLocked returned before populating it (e.g. error paths where
3343+
// no primary could be found). Guard against a nil pointer dereference.
33363344
if ev != nil {
3337-
resp.Keyspace = ev.ShardInfo.Keyspace()
3338-
resp.Shard = ev.ShardInfo.ShardName()
3345+
if k := ev.ShardInfo.Keyspace(); k != "" {
3346+
resp.Keyspace = k
3347+
resp.Shard = ev.ShardInfo.ShardName()
3348+
}
33393349

33403350
if ev.NewPrimary != nil && !topoproto.TabletAliasIsZero(ev.NewPrimary.Alias) {
33413351
resp.PromotedPrimary = ev.NewPrimary.Alias

go/vt/vtctl/grpcvtctldserver/server_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9011,6 +9011,46 @@ func TestPlannedReparentShard(t *testing.T) {
90119011
expectEventsToOccur: true,
90129012
expectedErr: "global status vars failed",
90139013
},
9014+
{
9015+
// Regression test: when reparentShardLocked returns an error before populating
9016+
// ev.ShardInfo (e.g. the ExpectedPrimary check fires before ev.ShardInfo is
9017+
// assigned), the response must still carry the keyspace/shard from the request
9018+
// rather than empty strings.
9019+
name: "response preserves keyspace/shard when reparent fails before ShardInfo is populated",
9020+
ts: memorytopo.NewServer(ctx, "zone1"),
9021+
tablets: []*topodatapb.Tablet{
9022+
{
9023+
Alias: &topodatapb.TabletAlias{
9024+
Cell: "zone1",
9025+
Uid: 100,
9026+
},
9027+
Type: topodatapb.TabletType_PRIMARY,
9028+
PrimaryTermStartTime: &vttime.Time{
9029+
Seconds: 100,
9030+
},
9031+
Keyspace: "testkeyspace",
9032+
Shard: "-",
9033+
},
9034+
},
9035+
tmc: &testutil.TabletManagerClient{},
9036+
req: &vtctldatapb.PlannedReparentShardRequest{
9037+
Keyspace: "testkeyspace",
9038+
Shard: "-",
9039+
// ExpectedPrimary deliberately points to a non-existent tablet so that
9040+
// reparentShardLocked fails the expected-primary check before assigning
9041+
// ev.ShardInfo, leaving ev.ShardInfo zero-valued.
9042+
ExpectedPrimary: &topodatapb.TabletAlias{
9043+
Cell: "zone1",
9044+
Uid: 999,
9045+
},
9046+
},
9047+
expected: &vtctldatapb.PlannedReparentShardResponse{
9048+
Keyspace: "testkeyspace",
9049+
Shard: "-",
9050+
},
9051+
expectEventsToOccur: false,
9052+
expectedErr: "primary zone1-0000000100 is not equal to expected alias zone1-0000000999",
9053+
},
90149054
}
90159055

90169056
for _, tt := range tests {
@@ -9041,6 +9081,9 @@ func TestPlannedReparentShard(t *testing.T) {
90419081

90429082
if tt.expectedErr != "" {
90439083
assert.EqualError(t, err, tt.expectedErr)
9084+
if tt.expected != nil {
9085+
testutil.AssertPlannedReparentShardResponsesEqual(t, tt.expected, resp)
9086+
}
90449087

90459088
return
90469089
}

0 commit comments

Comments
 (0)