From 296872faeedb533c3cf16d0698cbc1108fb940fd Mon Sep 17 00:00:00 2001 From: Jay Date: Tue, 11 Aug 2026 16:09:54 -0400 Subject: [PATCH] reject sub-10ns ping intervals and clamp pinger jitter --- CONFIG.md | 4 ++-- transport/connection.go | 10 +++++++++- transport/connection_test.go | 31 +++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/CONFIG.md b/CONFIG.md index 6365f45..b118896 100644 --- a/CONFIG.md +++ b/CONFIG.md @@ -13,7 +13,7 @@ All configuration uses option functions applied at construction time. Validation | Session | `ReconnectDelay` | 2s | `0` | 0 means reconnect immediately | | Session | Retry | enabled | `WithRetryDisabled()` | Skips RetryConfig validation when disabled | | Connection | `WriteTimeout` | 30s | `0` | Per-message write deadline | -| Connection | `PingInterval` | 20s | `0` | ±10% jitter applied per interval | +| Connection | `PingInterval` | 20s | `0` | ±10% jitter applied per interval; positive minimum 10ns | | Connection | `IncomingBufferSize` | 100 | — | Must be positive | | Connection | `ErrorsBufferSize` | 10 | — | Must be positive | | Connection | `ReadLimit` | nil | 0 | Does nothing when unset, 0 = unlimited | @@ -56,7 +56,7 @@ These are passed to `transport.NewConnectionConfig`. Import `git.wisehodl.dev/ja **`WithWriteTimeout(duration)`** — sets a per-message write deadline. Applied before every `WriteMessage` call. Set to zero to disable. Must not be negative. -**`WithPingInterval(duration)`** — sets the interval between WebSocket ping frames. A ±10% jitter is applied to each interval. Set to zero to disable pings entirely. Must not be negative. +**`WithPingInterval(duration)`** — sets the interval between WebSocket ping frames. A ±10% jitter is applied to each interval. Set to zero to disable pings entirely. A positive interval must be at least 10ns; smaller positive values leave no room for the jitter window and are rejected. **`WithIncomingBufferSize(int)`** — sets the capacity of the channel buffering inbound messages. Must be at least 1. diff --git a/transport/connection.go b/transport/connection.go index e3bd81d..e624766 100644 --- a/transport/connection.go +++ b/transport/connection.go @@ -115,6 +115,10 @@ func ValidateConnectionConfig(c ConnectionConfig) error { if c.PingInterval < 0 { return fmt.Errorf("invalid ping interval: %v", c.PingInterval) } + if c.PingInterval > 0 && c.PingInterval < 10*time.Nanosecond { + return fmt.Errorf("invalid ping interval: %v "+ + "(positive minimum is 10ns)", c.PingInterval) + } if c.IncomingBufferSize < 1 { return fmt.Errorf("invalid incoming buffer size: %d", c.IncomingBufferSize) @@ -380,8 +384,12 @@ func (c *Connection) setupPongHandler() { func (c *Connection) startPinger() { defer c.shutdownInternal() - // Calculate 10% jitter window + // Calculate 10% jitter window, clamped so Int63n can never panic + // even if config validation is bypassed jitter := c.config.PingInterval / 10 + if jitter < 1 { + jitter = 1 + } for { offset := time.Duration(rand.Int63n(int64(jitter*2))) - jitter diff --git a/transport/connection_test.go b/transport/connection_test.go index 0be187f..d56ceec 100644 --- a/transport/connection_test.go +++ b/transport/connection_test.go @@ -322,6 +322,37 @@ func TestConnection_Heartbeat(t *testing.T) { }) } +func TestStartPingerSubMinimumInterval(t *testing.T) { + // Construct directly to bypass NewConnectionConfig validation; the + // jitter clamp alone must prevent the Int63n(0) panic. + conn := &Connection{ + socket: honeybeetest.NewMockSocket(), + config: ConnectionConfig{PingInterval: 5 * time.Nanosecond}, + incoming: make(chan []byte, 1), + errors: make(chan error, 1), + done: make(chan struct{}), + } + + finished := make(chan struct{}) + go func() { + defer close(finished) + conn.startPinger() + }() + + // let several jittered intervals elapse + time.Sleep(50 * time.Millisecond) + conn.Close() + + honeybeetest.Eventually(t, func() bool { + select { + case <-finished: + return true + default: + return false + } + }, "pinger should exit cleanly after Close") +} + // ---------------------------------------------------------------------------- // Send // ----------------------------------------------------------------------------