| // 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") |
| } |
| } |