Skip to content

Commit b576267

Browse files
nowhereslyclaude
andcommitted
address moby#3232 review: narrow fix to Snapshot clone
The shared *Snapshot pointer is the real cause of moby#3231: the shallow copy of the raft message meant re-slicing raftMsg.Snapshot.Data also re-sliced m.Snapshot.Data, shrinking its capacity until a later iteration panicked with "slice bounds out of range". Cloning the Snapshot struct alone is sufficient. Drop the 64 KiB payload-size clamp (which could not produce a sendable message when struct overhead already exceeds GRPCMaxMsgSize) and the redundant fullData capture. Replace the tests with a single regression test that feeds a realistic MsgSnap of 3*GRPCMaxMsgSize and asserts the input message's Snapshot.Data is untouched after splitting. Signed-off-by: Sylvere Richard <sylvere.richard@gmail.com> Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 22ab07d commit b576267

2 files changed

Lines changed: 42 additions & 76 deletions

File tree

manager/state/raft/transport/peer.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -145,15 +145,8 @@ func raftMessageStructSize(m *raftpb.Message) int {
145145

146146
// Returns the max allowable payload based on MaxRaftMsgSize and
147147
// the struct size for the given raftpb.Message.
148-
// If the struct overhead exceeds GRPCMaxMsgSize (e.g. very large
149-
// snapshot metadata), the payload size is clamped to a minimum of
150-
// 64 KiB to guarantee forward progress when splitting.
151148
func raftMessagePayloadSize(m *raftpb.Message) int {
152-
s := GRPCMaxMsgSize - raftMessageStructSize(m)
153-
if s < 64<<10 {
154-
s = 64 << 10
155-
}
156-
return s
149+
return GRPCMaxMsgSize - raftMessageStructSize(m)
157150
}
158151

159152
// Split a large raft message into smaller messages.
@@ -171,11 +164,6 @@ func splitSnapshotData(_ context.Context, m *raftpb.Message) []api.StreamRaftMes
171164
// Get the max payload size.
172165
payloadSize := raftMessagePayloadSize(m)
173166

174-
// Capture the full snapshot data before the loop, because
175-
// we need to copy the Snapshot struct for each chunk to avoid
176-
// mutating m.Snapshot.Data through the shared pointer.
177-
fullData := m.Snapshot.Data
178-
179167
// split the snapshot into smaller messages.
180168
for snapDataIndex := 0; snapDataIndex < size; {
181169
chunkSize := size - snapDataIndex
@@ -184,13 +172,13 @@ func splitSnapshotData(_ context.Context, m *raftpb.Message) []api.StreamRaftMes
184172
}
185173

186174
raftMsg := *m
187-
// Copy the Snapshot to avoid mutating the original message's
188-
// Snapshot.Data through the shared pointer.
175+
// Clone Snapshot so that re-slicing Snapshot.Data below
176+
// does not mutate m.Snapshot.Data through the shared pointer.
189177
snap := *m.Snapshot
190178
raftMsg.Snapshot = &snap
191179

192180
// sub-slice for this snapshot chunk.
193-
raftMsg.Snapshot.Data = fullData[snapDataIndex : snapDataIndex+chunkSize]
181+
raftMsg.Snapshot.Data = m.Snapshot.Data[snapDataIndex : snapDataIndex+chunkSize]
194182

195183
snapDataIndex += chunkSize
196184

manager/state/raft/transport/transport_test.go

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -105,71 +105,49 @@ func testSend(ctx context.Context, c *mockCluster, from uint64, to []uint64, msg
105105
}
106106
}
107107

108-
func TestSplitSnapshotData(t *testing.T) {
108+
// TestSplitSnapshotDataDoesNotMutateInput is a regression test for #3231.
109+
// Before the fix, splitSnapshotData did a shallow copy of the raft message
110+
// and re-sliced the shared Snapshot.Data on each iteration, shrinking the
111+
// original slice's capacity and eventually panicking with
112+
// "slice bounds out of range".
113+
func TestSplitSnapshotDataDoesNotMutateInput(t *testing.T) {
109114
ctx := context.Background()
110115

111-
t.Run("NormalSnapshot", func(t *testing.T) {
112-
m := newSnapshotMessage(1, 2)
113-
msgs := splitSnapshotData(ctx, &m)
114-
assert.NotEmpty(t, msgs)
115-
// reassemble and verify
116-
var assembled []byte
117-
for _, msg := range msgs {
118-
assembled = append(assembled, msg.Message.Snapshot.Data...)
119-
}
120-
assert.Equal(t, m.Snapshot.Data, assembled)
121-
})
122-
123-
t.Run("LargeMetadataDoesNotPanic", func(t *testing.T) {
124-
// Simulate a snapshot where struct overhead exceeds GRPCMaxMsgSize.
125-
// This is the scenario from the bug: many cluster objects cause
126-
// large raft message metadata, making payloadSize negative without
127-
// the fix.
128-
data := make([]byte, 5<<20) // 5 MiB snapshot data
129-
for i := range data {
130-
data[i] = byte(i % 256)
131-
}
132-
m := raftpb.Message{
133-
Type: raftpb.MsgSnap,
134-
From: 1,
135-
To: 2,
136-
Snapshot: &raftpb.Snapshot{
137-
Data: data,
138-
Metadata: raftpb.SnapshotMetadata{
139-
Index: uint64(len(data)),
140-
// Large ConfState to push struct size above GRPCMaxMsgSize
141-
ConfState: raftpb.ConfState{
142-
Voters: make([]uint64, 1<<20),
143-
},
144-
},
145-
},
146-
}
147-
// Must not panic
148-
msgs := splitSnapshotData(ctx, &m)
149-
assert.NotEmpty(t, msgs)
150-
// reassemble and verify
151-
var assembled []byte
152-
for _, msg := range msgs {
153-
assembled = append(assembled, msg.Message.Snapshot.Data...)
154-
}
155-
assert.Equal(t, data, assembled)
156-
})
157-
}
158-
159-
func TestRaftMessagePayloadSizeMinimum(t *testing.T) {
160-
// When struct overhead exceeds GRPCMaxMsgSize, payloadSize must
161-
// be clamped to the minimum (64 KiB) instead of going negative.
162-
// Use Entries to inflate the struct size well beyond GRPCMaxMsgSize.
163-
bigEntry := raftpb.Entry{Data: make([]byte, GRPCMaxMsgSize)}
164-
m := &raftpb.Message{
165-
Type: raftpb.MsgSnap,
166-
Entries: []raftpb.Entry{bigEntry},
116+
// Build a MsgSnap whose Snapshot.Data clearly exceeds GRPCMaxMsgSize so
117+
// that the split loop runs multiple iterations (where the bug manifests).
118+
const dataSize = 3 * GRPCMaxMsgSize
119+
data := make([]byte, dataSize)
120+
for i := range data {
121+
data[i] = byte(i % (1 << 8))
122+
}
123+
m := raftpb.Message{
124+
Type: raftpb.MsgSnap,
125+
From: 1,
126+
To: 2,
167127
Snapshot: &raftpb.Snapshot{
168-
Data: nil,
128+
Data: data,
129+
Metadata: raftpb.SnapshotMetadata{
130+
Index: uint64(len(data)),
131+
},
169132
},
170133
}
171-
ps := raftMessagePayloadSize(m)
172-
assert.Equal(t, 64<<10, ps, "payloadSize should be clamped to minimum 64 KiB")
134+
origData := m.Snapshot.Data
135+
origLen, origCap := len(origData), cap(origData)
136+
137+
msgs := splitSnapshotData(ctx, &m)
138+
require.Greater(t, len(msgs), 1, "data larger than GRPCMaxMsgSize must split into multiple chunks")
139+
140+
// Chunks must reassemble to the original data.
141+
var assembled []byte
142+
for _, msg := range msgs {
143+
assembled = append(assembled, msg.Message.Snapshot.Data...)
144+
}
145+
assert.Equal(t, data, assembled)
146+
147+
// The input message's Snapshot.Data must be untouched (regression guard).
148+
assert.Equal(t, origLen, len(m.Snapshot.Data))
149+
assert.Equal(t, origCap, cap(m.Snapshot.Data))
150+
assert.Equal(t, data, m.Snapshot.Data)
173151
}
174152

175153
func TestSend(t *testing.T) {

0 commit comments

Comments
 (0)