mirror of
https://github.com/soypat/lneto.git
synced 2026-08-21 06:59:04 +00:00
Fuzz fixes (#73)
* remove old fuzz corpus and stick to using PCG for seeding fuzz test * polish up drain logic and reduce action search space * improve fuzzing debug prints * more fuzz fixes also formatter capture printer fixes * refactor StackSeeded test * fix challenge ack infinite packet bug * more expressive challenge satisfy * remove printing
This commit is contained in:
+63
-12
@@ -14,6 +14,10 @@ const (
|
||||
retransmitAfterDupacks = 3
|
||||
// retransmitMaxQueued sets maximum amount of retransmits to queue while receiving dupacks.
|
||||
retransmitMaxQueued = 2
|
||||
// maxChallengeRejects is the number of consecutive challenge ACKs sent without
|
||||
// a successful Recv before aborting. Prevents infinite ACK ping-pong when both
|
||||
// sides have diverged state (e.g. after packet mutation).
|
||||
maxChallengeRejects = 8
|
||||
)
|
||||
|
||||
// ControlBlock is a partial Transmission Control Block (TCB) implementation as
|
||||
@@ -63,9 +67,14 @@ type ControlBlock struct {
|
||||
// pending is the queue of pending flags to be sent in the next 2 segments.
|
||||
// On a call to Send the queue is advanced and flags set in the segment are unset.
|
||||
// The second position of the queue is used for FIN segments.
|
||||
pending [2]Flags
|
||||
_state State // leading underscore so field not suggested on top of exported State method when developing.
|
||||
challengeAck bool
|
||||
pending [2]Flags
|
||||
_state State // leading underscore so field not suggested on top of exported State method when developing.
|
||||
|
||||
// challengeAcks counts consecutive challenge acks queued on receiving out of window segment.
|
||||
// challengeAcks signedness indicates whether the challengeAck is pending being sent.
|
||||
// A negative value of challengeAcks means a challenge ack is pending being sent.
|
||||
challengeAcks int8
|
||||
|
||||
// dupack counts received ACK==snd.UNA && ACK<snd.NXT received. Does not count ack that set UNA.
|
||||
dupack uint8
|
||||
// nRetransmit counts number of retransmits sent since last UNA update.
|
||||
@@ -152,6 +161,20 @@ func (tcb *ControlBlock) MakeDupACK() Segment {
|
||||
}
|
||||
}
|
||||
|
||||
// MakeChallengeAck returns a challenge ACK segment for the current ControlBlock state
|
||||
// used to respond to unexpected or ambiguous segments that require the remote peer to confirm
|
||||
// its connection state. A challenge ACK does not acknowledge new data,
|
||||
// consume sequence space, or carry a payload.
|
||||
func (tcb *ControlBlock) MakeChallengeACK() Segment {
|
||||
return Segment{
|
||||
SEQ: tcb.snd.NXT, // Current sequence number (no data)
|
||||
ACK: tcb.rcv.NXT, // Acknowledging expected next byte
|
||||
Flags: FlagACK, // Pure ACK, no SYN/FIN/RST
|
||||
WND: tcb.rcv.WND, // Current receive window size
|
||||
DATALEN: 0, // No payload
|
||||
}
|
||||
}
|
||||
|
||||
// sendSpace contains Send Sequence Space data. Its sequence numbers correspond to local data.
|
||||
type sendSpace struct {
|
||||
ISS Value // initial send sequence number, defined locally on connection start
|
||||
@@ -215,7 +238,7 @@ func (tcb *ControlBlock) prepareToHandshake(iss Value, wnd Size, newState State)
|
||||
|
||||
// HasPending returns true if there is a pending control segment to send. Calls to Send will advance the pending queue.
|
||||
func (tcb *ControlBlock) HasPending() bool {
|
||||
return tcb.pending[0] != 0 || tcb.challengeAck || tcb.HasPendingRetransmit()
|
||||
return tcb.pending[0] != 0 || tcb.pendingChallengeAck() || tcb.HasPendingRetransmit()
|
||||
}
|
||||
|
||||
// HasPending returns true if the control block is pending a retransmit according to simple optmist
|
||||
@@ -229,10 +252,10 @@ func (tcb *ControlBlock) HasPendingRetransmit() bool {
|
||||
// It does not modify the ControlBlock state or pending segment queue.
|
||||
func (tcb *ControlBlock) PendingSegment(payloadLen int) (_ Segment, ok bool) {
|
||||
pending := tcb.pending[0]
|
||||
if tcb.challengeAck {
|
||||
if tcb.pendingChallengeAck() {
|
||||
// Do not clear challengeAck here: PendingSegment is documented as read-only.
|
||||
// The flag is consumed in Send when the ACK segment is actually transmitted.
|
||||
return Segment{SEQ: tcb.snd.NXT, ACK: tcb.rcv.NXT, Flags: FlagACK, WND: tcb.rcv.WND}, true
|
||||
return tcb.MakeChallengeACK(), true
|
||||
} else if !pending.HasAny(flagctl) && tcb.HasPendingRetransmit() {
|
||||
// Optimist Strategy: retransmit oldest data once.
|
||||
return Segment{SEQ: tcb.snd.UNA, DATALEN: Size(payloadLen), ACK: tcb.rcv.NXT, WND: tcb.rcv.WND, Flags: FlagACK}, true
|
||||
@@ -305,7 +328,7 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
|
||||
|
||||
// RFC 9293 §3.10.7.4: SYN on synchronized connection → challenge ACK.
|
||||
if seg.Flags.HasAny(FlagSYN) && !tcb._state.IsPreestablished() {
|
||||
tcb.challengeAck = true
|
||||
tcb.triggerChallengeAckEmit()
|
||||
tcb.pending[0] |= FlagACK
|
||||
return errDropSegment
|
||||
}
|
||||
@@ -341,6 +364,7 @@ func (tcb *ControlBlock) Recv(seg Segment) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tcb.triggerChallengeAckSatisfied() // Successful Recv — reset challenge counter.
|
||||
|
||||
tcb.pending[0] |= pending
|
||||
if prevNxt != 0 && tcb.snd.NXT != prevNxt && tcb.logenabled(slog.LevelDebug) {
|
||||
@@ -430,8 +454,8 @@ func (tcb *ControlBlock) Send(seg Segment) error {
|
||||
tcb.pending[0] |= newPending
|
||||
|
||||
// Sending an ACK satisfies any outstanding challenge-ACK obligation.
|
||||
if tcb.challengeAck && seg.Flags.HasAny(FlagACK) {
|
||||
tcb.challengeAck = false
|
||||
if tcb.pendingChallengeAck() && seg.Flags.HasAny(FlagACK) {
|
||||
tcb.triggerChallengeAckSent()
|
||||
}
|
||||
|
||||
// The segment is valid, we can update TCB state.
|
||||
@@ -530,8 +554,10 @@ func (tcb *ControlBlock) validateIncomingSegment(seg Segment) (err error) {
|
||||
switch err {
|
||||
case errSeqNotInWindow, errLastNotInWindow, errRequireSequential, errZeroWindow:
|
||||
if !flags.HasAny(FlagRST) {
|
||||
tcb.challengeAck = true
|
||||
tcb.pending[0] |= FlagACK
|
||||
if tcb.tooManyChallengeAcks() {
|
||||
return net.ErrClosed
|
||||
}
|
||||
tcb.triggerChallengeAckEmit()
|
||||
}
|
||||
}
|
||||
return err
|
||||
@@ -614,7 +640,7 @@ func (tcb *ControlBlock) handleRST(seq Value) error {
|
||||
}
|
||||
// Synchronized states: exact SEQ match required; challenge ACK for in-window non-exact.
|
||||
if seq != tcb.rcv.NXT {
|
||||
tcb.challengeAck = true
|
||||
tcb.triggerChallengeAckEmit()
|
||||
tcb.pending[0] |= FlagACK
|
||||
return errDropSegment
|
||||
}
|
||||
@@ -680,3 +706,28 @@ func (tcb *ControlBlock) Close() (err error) {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (tcb *ControlBlock) triggerChallengeAckSatisfied() {
|
||||
tcb.challengeAcks = 0
|
||||
}
|
||||
func (tcb *ControlBlock) triggerChallengeAckEmit() {
|
||||
if tcb.challengeAcks >= 0 {
|
||||
// Only increment challenge ack counter if last challenge ack already sent.
|
||||
tcb.challengeAcks = -tcb.challengeAcks - 1
|
||||
}
|
||||
}
|
||||
func (tcb *ControlBlock) triggerChallengeAckSent() {
|
||||
if tcb.challengeAcks < 0 {
|
||||
tcb.challengeAcks = -tcb.challengeAcks // Make positive.
|
||||
}
|
||||
}
|
||||
func (tcb *ControlBlock) pendingChallengeAck() bool {
|
||||
return tcb.challengeAcks < 0
|
||||
}
|
||||
func (tcb *ControlBlock) tooManyChallengeAcks() bool {
|
||||
if tcb.challengeAcks >= 0 {
|
||||
return tcb.challengeAcks > maxChallengeRejects
|
||||
} else {
|
||||
return tcb.challengeAcks < -maxChallengeRejects
|
||||
}
|
||||
}
|
||||
|
||||
+53
-2
@@ -187,7 +187,7 @@ func TestPendingSegment_ChallengeACK_Idempotent(t *testing.T) {
|
||||
var tcb ControlBlock
|
||||
tcb.HelperInitState(StateEstablished, 100, 101, 1024)
|
||||
tcb.HelperInitRcv(500, 501, 1024)
|
||||
tcb.challengeAck = true
|
||||
tcb.triggerChallengeAckEmit()
|
||||
|
||||
seg1, ok1 := tcb.PendingSegment(0)
|
||||
if !ok1 {
|
||||
@@ -196,7 +196,7 @@ func TestPendingSegment_ChallengeACK_Idempotent(t *testing.T) {
|
||||
|
||||
// PendingSegment must not consume challengeAck (read-only contract).
|
||||
// Currently FAILS: challengeAck is set to false on the first call.
|
||||
if !tcb.challengeAck {
|
||||
if !tcb.pendingChallengeAck() {
|
||||
t.Error("PendingSegment cleared challengeAck flag; violates documented read-only contract")
|
||||
}
|
||||
|
||||
@@ -375,3 +375,54 @@ func TestPendingSegment_RetransmitAfter3DupACKs(t *testing.T) {
|
||||
t.Fatalf("nRetransmit after progress = %d; want 0", tcb.nRetransmit)
|
||||
}
|
||||
}
|
||||
|
||||
// TestACKLoop_MutualOutOfWindow verifies that two TCBs with diverged state
|
||||
// (simulating post-mutation) don't enter an infinite challenge-ACK ping-pong.
|
||||
// Each side sees the other's segment as out-of-window → challenge ACK → loop.
|
||||
func TestACKLoop_MutualOutOfWindow(t *testing.T) {
|
||||
const wnd Size = 64
|
||||
|
||||
// Setup: A.snd.NXT=101, B.rcv.NXT=5000 → A's segments are outside B's window.
|
||||
// B.snd.NXT=501, A.rcv.NXT=8000 → B's segments are outside A's window.
|
||||
// Both will reject each other's challenge ACKs forever without a limit.
|
||||
var tcbA ControlBlock
|
||||
tcbA.HelperInitState(StateEstablished, 100, 101, wnd)
|
||||
tcbA.HelperInitRcv(8000, 8001, wnd) // A expects seq from B around 8001
|
||||
tcbA.snd.UNA = 101
|
||||
tcbA.snd.WND = wnd
|
||||
tcbA.snd.WL1 = 8001
|
||||
tcbA.snd.WL2 = 101
|
||||
|
||||
var tcbB ControlBlock
|
||||
tcbB.HelperInitState(StateEstablished, 500, 501, wnd)
|
||||
tcbB.HelperInitRcv(5000, 5001, wnd) // B expects seq from A around 5001
|
||||
tcbB.snd.UNA = 501
|
||||
tcbB.snd.WND = wnd
|
||||
tcbB.snd.WL1 = 5001
|
||||
tcbB.snd.WL2 = 501
|
||||
|
||||
// Kick off: A has a pending ACK (simulating normal data exchange trigger).
|
||||
tcbA.pending[0] = FlagACK
|
||||
|
||||
const maxRounds = 50
|
||||
for round := 0; round < maxRounds; round++ {
|
||||
segA, okA := tcbA.PendingSegment(0)
|
||||
if okA {
|
||||
tcbA.Send(segA)
|
||||
// A sends seq=101, but B expects [5001, 5001+64) → out of window → challenge ACK
|
||||
tcbB.Recv(segA)
|
||||
}
|
||||
|
||||
segB, okB := tcbB.PendingSegment(0)
|
||||
if okB {
|
||||
tcbB.Send(segB)
|
||||
// B sends seq=501, but A expects [8001, 8001+64) → out of window → challenge ACK
|
||||
tcbA.Recv(segB)
|
||||
}
|
||||
|
||||
if !okA && !okB {
|
||||
return // Converged.
|
||||
}
|
||||
}
|
||||
t.Fatal("ACK ping-pong did not converge after", maxRounds, "rounds — infinite loop bug")
|
||||
}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ func (tcb *ControlBlock) traceRcv(msg string) {
|
||||
slog.String("state", tcb._state.String()),
|
||||
slog.Uint64("rcv.nxt", uint64(tcb.rcv.NXT)),
|
||||
slog.Uint64("rcv.wnd", uint64(tcb.rcv.WND)),
|
||||
slog.Bool("challenge", tcb.challengeAck),
|
||||
slog.Bool("challenge", tcb.pendingChallengeAck()),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -934,7 +934,7 @@ func TestChallengeACKWithBufferedData(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("expected error from out-of-order segment")
|
||||
}
|
||||
if !server.scb.challengeAck {
|
||||
if !server.scb.pendingChallengeAck() {
|
||||
t.Fatal("challengeAck flag not set after out-of-order segment")
|
||||
}
|
||||
if server.State() != StateEstablished {
|
||||
|
||||
Reference in New Issue
Block a user