Skip to content

Commit b90058a

Browse files
ackhandler: fix panic in probe packet tracking logic (#4998)
Under certain circumstances (loss and acknowledgment patterns), the probe packet tracking logic could run into a nil-pointer dereference.
1 parent d726a79 commit b90058a

File tree

2 files changed

+98
-4
lines changed

2 files changed

+98
-4
lines changed

internal/ackhandler/sent_packet_handler.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,10 @@ func (h *sentPacketHandler) detectAndRemoveAckedPackets(ack *wire.AckFrame, encL
460460
}
461461
if p.isPathProbePacket {
462462
probePacket := pnSpace.history.RemovePathProbe(p.PacketNumber)
463-
if probePacket == nil {
464-
panic(fmt.Sprintf("path probe doesn't exist: %d", p.PacketNumber))
463+
// the probe packet might already have been declared lost
464+
if probePacket != nil {
465+
h.ackedPackets = append(h.ackedPackets, probePacket)
465466
}
466-
h.ackedPackets = append(h.ackedPackets, probePacket)
467467
continue
468468
}
469469
h.ackedPackets = append(h.ackedPackets, p)
@@ -658,7 +658,6 @@ func (h *sentPacketHandler) detectLostPathProbes(now time.Time) {
658658
for _, f := range p.Frames {
659659
f.Handler.OnLost(f.Frame)
660660
}
661-
h.appDataPackets.history.Remove(p.PacketNumber)
662661
h.appDataPackets.history.RemovePathProbe(p.PacketNumber)
663662
}
664663
}

internal/ackhandler/sent_packet_handler_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package ackhandler
22

33
import (
4+
"encoding/binary"
45
"fmt"
6+
"math/rand/v2"
57
"slices"
68
"testing"
79
"time"
@@ -39,6 +41,11 @@ type packetTracker struct {
3941
Lost []protocol.PacketNumber
4042
}
4143

44+
func (t *packetTracker) Reset() {
45+
t.Acked = nil
46+
t.Lost = nil
47+
}
48+
4249
func (t *packetTracker) NewPingFrame(pn protocol.PacketNumber) Frame {
4350
return Frame{
4451
Frame: &wire.PingFrame{},
@@ -1227,3 +1234,91 @@ func TestSentPacketHandlerPathProbeAckAndLoss(t *testing.T) {
12271234

12281235
require.Equal(t, t2.Add(pathProbePacketLossTimeout), sph.GetLossDetectionTimeout())
12291236
}
1237+
1238+
// The packet tracking logic is pretty complex.
1239+
// We test it with a randomized approach, to make sure that it doesn't panic under any circumstances.
1240+
func TestSentPacketHandlerRandomized(t *testing.T) {
1241+
seed := uint64(time.Now().UnixNano())
1242+
for i := range 5 {
1243+
t.Run(fmt.Sprintf("run %d (seed %d)", i+1, seed), func(t *testing.T) {
1244+
testSentPacketHandlerRandomized(t, seed)
1245+
})
1246+
seed++
1247+
}
1248+
}
1249+
1250+
func testSentPacketHandlerRandomized(t *testing.T, seed uint64) {
1251+
var b [32]byte
1252+
binary.BigEndian.PutUint64(b[:], seed)
1253+
r := rand.New(rand.NewChaCha8(b))
1254+
1255+
var rttStats utils.RTTStats
1256+
rtt := []time.Duration{10 * time.Millisecond, 100 * time.Millisecond, 1000 * time.Millisecond}[r.IntN(3)]
1257+
t.Logf("rtt: %dms", rtt.Milliseconds())
1258+
rttStats.UpdateRTT(rtt, 0) // RTT of the original path
1259+
1260+
randDuration := func(min, max time.Duration) time.Duration {
1261+
return time.Duration(rand.Int64N(int64(max-min))) + min
1262+
}
1263+
1264+
sph := newSentPacketHandler(
1265+
0,
1266+
1200,
1267+
&rttStats,
1268+
true,
1269+
false,
1270+
protocol.PerspectiveClient,
1271+
nil,
1272+
utils.DefaultLogger,
1273+
)
1274+
sph.DropPackets(protocol.EncryptionInitial, time.Now())
1275+
sph.DropPackets(protocol.EncryptionHandshake, time.Now())
1276+
1277+
var packets packetTracker
1278+
sendPacket := func(ti time.Time, isPathProbe bool) protocol.PacketNumber {
1279+
pn := sph.PopPacketNumber(protocol.Encryption1RTT)
1280+
sph.SentPacket(ti, pn, protocol.InvalidPacketNumber, nil, []Frame{packets.NewPingFrame(pn)}, protocol.Encryption1RTT, protocol.ECNNon, 1200, false, isPathProbe)
1281+
return pn
1282+
}
1283+
1284+
now := time.Now()
1285+
start := now
1286+
var pns []protocol.PacketNumber
1287+
for range 4 {
1288+
isProbe := r.Int()%2 == 0
1289+
pn := sendPacket(now, isProbe)
1290+
t.Logf("t=%dms: sending packet %d (probe packet: %t)", now.Sub(start).Milliseconds(), pn, isProbe)
1291+
pns = append(pns, pn)
1292+
now = now.Add(randDuration(0, 500*time.Millisecond))
1293+
if r.Int()%3 == 0 {
1294+
sph.OnLossDetectionTimeout(now)
1295+
t.Logf("t=%dms: loss detection timeout (lost: %v)", now.Sub(start).Milliseconds(), packets.Lost)
1296+
packets.Reset()
1297+
now = now.Add(randDuration(0, 500*time.Millisecond))
1298+
}
1299+
if r.Int()%3 == 0 {
1300+
// acknowledge up to 2 random packet numbers from the pns slice
1301+
var ackPns []protocol.PacketNumber
1302+
if len(pns) > 0 {
1303+
numToAck := min(1+r.IntN(2), len(pns))
1304+
for range numToAck {
1305+
ackPns = append(ackPns, pns[r.IntN(len(pns))])
1306+
}
1307+
}
1308+
if len(ackPns) > 1 {
1309+
slices.Sort(ackPns)
1310+
ackPns = slices.Compact(ackPns)
1311+
}
1312+
sph.ReceivedAck(&wire.AckFrame{AckRanges: ackRanges(ackPns...)}, protocol.Encryption1RTT, now)
1313+
t.Logf("t=%dms: received ACK for packets %v (acked: %v, lost: %v)", now.Sub(start).Milliseconds(), ackPns, packets.Acked, packets.Lost)
1314+
packets.Reset()
1315+
now = now.Add(randDuration(0, 500*time.Millisecond))
1316+
}
1317+
if r.Int()%10 == 0 {
1318+
sph.MigratedPath(now, 1200)
1319+
now = now.Add(randDuration(0, 500*time.Millisecond))
1320+
}
1321+
}
1322+
t.Logf("t=%dms: loss detection timeout (lost: %v)", now.Sub(start).Milliseconds(), packets.Lost)
1323+
sph.OnLossDetectionTimeout(now)
1324+
}

0 commit comments

Comments
 (0)