blob: 2266863064fc4a25c1e06a1e3e6d07e3ff8734fb [file] [edit]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ssh
import (
"math/bits"
"testing"
"time"
"unsafe"
)
func TestMinPayloadSize(t *testing.T) {
// 4 GiB (2^32). Declared as a var (not a const) so that int(bigPayload)
// is a runtime conversion: a constant conversion would fail to compile
// on 32-bit platforms with "constant 4294967296 overflows int". On
// 32-bit the value truncates to 0 at runtime, but the is64Bit cases
// that reference it are skipped by the runtime check below.
var bigPayload int64 = 1 << 32
tests := []struct {
name string
maxPayload uint32
dataLen int
want uint32
is64Bit bool // Flag to run only on 64-bit architectures
}{
{
name: "Normal Case - Data fits in payload",
maxPayload: 32768,
dataLen: 1000,
want: 1000,
},
{
name: "Normal Case - Data larger than payload",
maxPayload: 32768,
dataLen: 50000,
want: 32768,
},
{
name: "Boundary Case - Data zero",
maxPayload: 32768,
dataLen: 0,
want: 0,
},
{
name: "Overflow Case - Data is exactly 4GB (1<<32)",
maxPayload: 32768,
dataLen: int(bigPayload),
want: 32768,
is64Bit: true,
},
{
name: "Overflow Case - Data is 4GB + small amount",
maxPayload: 32768,
dataLen: int(bigPayload + 100),
want: 32768,
is64Bit: true,
},
}
is64Bit := bits.UintSize == 64
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.is64Bit && !is64Bit {
t.Skip("Skipping test requiring 64-bit int")
}
got := minPayloadSize(tt.maxPayload, tt.dataLen)
if got != tt.want {
t.Errorf("minPayloadSize(%d, %d) = %d; want %d", tt.maxPayload, tt.dataLen, got, tt.want)
}
})
}
}
// TestWriteExtendedNoInfiniteLoopOnLargeWrite is an end-to-end regression
// test for the integer-overflow bug in WriteExtended. Before the fix, a
// write whose len(data) was a multiple of 2^32 caused minPayloadSize to
// return 0; WriteExtended then spun forever, reserving 0 bytes per
// iteration and never advancing the data slice.
//
// We exercise the real WriteExtended path with a slice whose declared
// length is exactly 2^32. Allocating 4 GiB is unnecessary: each iteration
// only reads up to maxRemotePayload bytes from the head of the slice, and
// the loop blocks in remoteWin.reserve() once the channel window is
// exhausted — before the slice base advances past the underlying buffer.
//
// With the fix, the loop blocks in reserve(); we detect that via
// waitWriterBlocked(), then close the window to let WriteExtended return.
// With the bug, the loop never blocks and the test times out.
//
//go:nocheckptr
func TestWriteExtendedNoInfiniteLoopOnLargeWrite(t *testing.T) {
if bits.UintSize < 64 {
t.Skip("test requires 64-bit int to construct a slice with len >= 2^32")
}
reader, writer, mux := channelPair(t)
defer reader.Close()
defer writer.Close()
defer mux.Close()
// Sized to hold the full pre-update remote window so that no iteration
// reads past the backing buffer before reserve() blocks.
backing := make([]byte, channelWindowSize)
var bigLen int64 = 1 << 32
bigSlice := unsafe.Slice(&backing[0], int(bigLen))
done := make(chan int, 1)
go func() {
n, _ := writer.Write(bigSlice)
done <- n
}()
blocked := make(chan struct{})
go func() {
writer.remoteWin.waitWriterBlocked()
close(blocked)
}()
select {
case <-blocked:
// Good — the loop made progress and is now blocked in reserve().
// Close the window to let WriteExtended return.
writer.remoteWin.close()
case <-time.After(2 * time.Second):
t.Fatal("WriteExtended did not block in reserve within 2s — minPayloadSize likely returned 0 (integer overflow regression)")
}
select {
case n := <-done:
if n == 0 {
t.Fatalf("WriteExtended returned n=0; expected progress")
}
case <-time.After(2 * time.Second):
t.Fatal("WriteExtended did not return after closing the window")
}
}