From 183b096576f1a9a9906b23cbb06cdea1d3d5b859 Mon Sep 17 00:00:00 2001 From: Travis Thieman Date: Wed, 25 Mar 2026 16:12:12 -0400 Subject: [PATCH 1/3] fix(ss): reduce Windows DLL buffer cap from 64 MiB to 5 MiB MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MaxBufSize: 64 MiB → 5 MiB The grow-loop for GetExtendedTcpTable/GetExtendedUdpTable could allocate up to 64 MiB on Windows systems with many sockets. 5 MiB is more than sufficient for any realistic socket table. Co-Authored-By: Claude Sonnet 4.6 --- builtins/internal/winnet/winnet_windows.go | 3 ++- builtins/ss/ss.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/builtins/internal/winnet/winnet_windows.go b/builtins/internal/winnet/winnet_windows.go index b0684472..07b8dfea 100644 --- a/builtins/internal/winnet/winnet_windows.go +++ b/builtins/internal/winnet/winnet_windows.go @@ -34,7 +34,8 @@ const ( // calling GetExtendedTcpTable / GetExtendedUdpTable. This cap is // intentionally defined here (where the DLL calls live) so that the limit // stays co-located with the code that enforces it. - MaxBufSize = 64 << 20 // 64 MiB + // 5 MiB is sufficient for any realistic socket table on Windows. + MaxBufSize = 5 << 20 // 5 MiB ) var ( diff --git a/builtins/ss/ss.go b/builtins/ss/ss.go index 153ee339..b68bb6da 100644 --- a/builtins/ss/ss.go +++ b/builtins/ss/ss.go @@ -118,7 +118,7 @@ const MaxLineBytes = 1 << 20 // 1 MiB // MaxWinBufSize is the maximum buffer size used by the Windows grow-loop // when calling GetExtendedTcpTable / GetExtendedUdpTable. This must match // winnet.MaxBufSize; the winnet package owns the authoritative value. -const MaxWinBufSize = 64 << 20 // 64 MiB — keep in sync with winnet.MaxBufSize +const MaxWinBufSize = 5 << 20 // 5 MiB — keep in sync with winnet.MaxBufSize // socketType identifies the protocol family of a socket entry. type socketType int From 950ef8ac9cb1a7a46f562fa2df54b2c4093bd52e Mon Sep 17 00:00:00 2001 From: Travis Thieman Date: Wed, 25 Mar 2026 16:28:18 -0400 Subject: [PATCH 2/3] fix(ss): correct stale doc comments and add compile-time sync guard for MaxBufSize --- builtins/internal/winnet/winnet_windows.go | 2 +- builtins/ss/ss.go | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/builtins/internal/winnet/winnet_windows.go b/builtins/internal/winnet/winnet_windows.go index 07b8dfea..fac8599c 100644 --- a/builtins/internal/winnet/winnet_windows.go +++ b/builtins/internal/winnet/winnet_windows.go @@ -96,7 +96,7 @@ func Collect() ([]SocketEntry, error) { } // callExtendedTable calls GetExtendedTcpTable or GetExtendedUdpTable with a -// grow-loop, capped at MaxWinBufSize. Returns the raw buffer on success. +// grow-loop, capped at MaxBufSize. Returns the raw buffer on success. func callExtendedTable(proc *syscall.Proc, af, tableClass uintptr) ([]byte, error) { size := uint32(4096) for { diff --git a/builtins/ss/ss.go b/builtins/ss/ss.go index b68bb6da..5f9277df 100644 --- a/builtins/ss/ss.go +++ b/builtins/ss/ss.go @@ -93,7 +93,7 @@ // macOS: sysctl returns a bounded []byte. Every offset dereference is // bounds-checked against len(data) before reading. // -// Windows: the DLL grow-loop is capped at MaxWinBufSize (64 MiB). +// Windows: the DLL grow-loop is capped at winnet.MaxBufSize (5 MiB). // unsafe.Pointer is used only to pass &buf[0] to the DLL call; the // returned data is parsed entirely with encoding/binary.LittleEndian. package ss @@ -115,11 +115,6 @@ var Cmd = builtins.Command{ // MaxLineBytes is the per-line buffer cap for the Linux /proc/net/ scanner. const MaxLineBytes = 1 << 20 // 1 MiB -// MaxWinBufSize is the maximum buffer size used by the Windows grow-loop -// when calling GetExtendedTcpTable / GetExtendedUdpTable. This must match -// winnet.MaxBufSize; the winnet package owns the authoritative value. -const MaxWinBufSize = 5 << 20 // 5 MiB — keep in sync with winnet.MaxBufSize - // socketType identifies the protocol family of a socket entry. type socketType int From 5ba379ea5c6309d6e811843f367abd474b891d3c Mon Sep 17 00:00:00 2001 From: Travis Thieman Date: Fri, 27 Mar 2026 10:09:15 -0400 Subject: [PATCH 3/3] fix(ss): return error on any partial collection failure in winnet.Collect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously Collect() only returned an error if all four sub-collections (tcp4, tcp6, udp4, udp6) failed. A single failing family (e.g. buffer size limit exceeded for tcp4) would be silently omitted, producing incomplete and misleading ss output with no indication of failure. Now any sub-collection error causes an immediate return with a wrapped error identifying the failing family, ensuring callers always receive complete data or a clear failure. Also fix stale MaxWinBufSize → MaxBufSize in callExtendedTable doc comment. Co-Authored-By: Claude Sonnet 4.6 --- builtins/internal/winnet/winnet_windows.go | 45 ++++++++++++---------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/builtins/internal/winnet/winnet_windows.go b/builtins/internal/winnet/winnet_windows.go index fac8599c..cbb8c6d4 100644 --- a/builtins/internal/winnet/winnet_windows.go +++ b/builtins/internal/winnet/winnet_windows.go @@ -62,36 +62,39 @@ var tcpStateNames = map[uint32]string{ // Collect enumerates all TCP and UDP sockets on Windows via iphlpapi.dll. // The narrow use of unsafe.Pointer is limited to two DLL call sites only. -// If every sub-collection fails, the first error is returned so callers can -// distinguish a real API failure from an empty socket table. +// If any sub-collection fails, an error is returned immediately so callers +// always receive complete data or a clear failure — never silent partial results. func Collect() ([]SocketEntry, error) { var out []SocketEntry - var firstErr error - collect := func(e []SocketEntry, err error) { - if err != nil { - if firstErr == nil { - firstErr = err - } - return - } - out = append(out, e...) + // TCP IPv4 + e, err := collectTCP(afINET) + if err != nil { + return nil, fmt.Errorf("tcp4: %w", err) } + out = append(out, e...) - // TCP IPv4 - collect(collectTCP(afINET)) // TCP IPv6 - collect(collectTCP(afINET6)) + e, err = collectTCP(afINET6) + if err != nil { + return nil, fmt.Errorf("tcp6: %w", err) + } + out = append(out, e...) + // UDP IPv4 - collect(collectUDP(afINET)) - // UDP IPv6 - collect(collectUDP(afINET6)) + e, err = collectUDP(afINET) + if err != nil { + return nil, fmt.Errorf("udp4: %w", err) + } + out = append(out, e...) - // Return an error only when every collection failed and nothing was returned. - // Partial results (e.g. IPv6 unavailable) are still returned without error. - if len(out) == 0 && firstErr != nil { - return nil, firstErr + // UDP IPv6 + e, err = collectUDP(afINET6) + if err != nil { + return nil, fmt.Errorf("udp6: %w", err) } + out = append(out, e...) + return out, nil }