Skip to content

net: bound the WebSocket handshake and reply sends on SocketServer - #832

Merged
Yaraslaut merged 1 commit into
masterfrom
fix/534-socketserver-hardening
Sep 26, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
fix/534-socketserver-hardening

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Summary

  • Adds handshakeTimeout and sendTimeout to SocketServerConfig, closing the two real defects identified in net: SocketServer has no connection cap, handshake timeout or idle timeout #534's rescope: an unauthenticated peer that connects and sends one byte parks a clientLoop thread and a file descriptor forever (no SO_RCVTIMEO anywhere on the accepted socket), and nothing ever bounded a server-side reply write, leaving PR net: reject illegal WebSocket frames, and bound the handshake read #558's ClientConnection::sendText retire-on-partial-write-failure catch unreachable and untestable (no SO_SNDTIMEO on the accepted socket).
  • handshakeTimeout (default 10s, 0 disables) is enforced as a single deadline across the whole handshake read via a poll-with-remaining-budget loop added to readHttpHeaderBlock, deliberately not SO_RCVTIMEO (which only bounds each individual recv, as SocketBackendConfig's own doc comment already documents as a known weakness on the client side).
  • sendTimeout (default 30s, 0 disables) is SO_SNDTIMEO, applied once per accepted connection in acceptLoop via the existing TcpSocket::setSendTimeout (previously used only by SocketBackend, morph#506).
  • Extends the existing SocketServerConfig/SocketServer reference table in docs/spec/core/backend.md with the two new fields and the design rationale (that table already existed — the rescope comment's premise that no morph::net/SocketServer spec exists turned out to be stale, so this extends it rather than adding a new file).

Out of scope (per the rescope comment on #534)

  • idleTimeout — rejected: a desktop GUI client left open is idle by design, and with no ping/pong keepalive there's no way to distinguish "idle" from "dead" without disconnecting working clients.
  • maxConnections — real, but deferred to a follow-up ticket; not blocking this change.
  • The per-connection in-flight cap mentioned in the issue body — a separate finding per the rescope, not addressed here.

Regression tests

Both added under the [morph534] tag in tests/net/test_socket_server.cpp, confirmed to fail before the fix (one on a timeout-bound assertion, one on a bounded wait) and pass after:

  • handshakeTimeout: connects, sends one byte, never completes the handshake; confirms the stalled connection's fd is closed once reclaimed, instead of parking forever.
  • sendTimeout (the acceptance clause the rescope comment added to this ticket): shrinks the client's receive window and floods large replies without ever draining them — the connection's read direction stays fully live (no reset, no FIN) throughout. Confirms the connection is retired (its model reclaimed, clientLoop exits) instead of continuing to dispatch further frames whose replies would be silently dropped.

Verification

  • Full suite: ctest — 1800/1800 passing (net label: 193/193).
  • Doxygen (WARN_AS_ERROR = FAIL_ON_WARNINGS) builds clean.
  • Both new tests independently confirmed to fail on the pre-fix code (verified by temporarily reverting the wiring) and pass on the fix.

Adjacent finding filed, not fixed here

Two independent review passes (reuse and altitude, from the /simplify gate) flagged that the new poll-with-remaining-budget loop in readHttpHeaderBlock is a third independent copy of a pattern already inline in TcpSocket::connect() and in a test helper (FakeWsServer::acceptWithin) — filed as #831 rather than fixed here, since extracting a shared primitive means changing TcpSocket::connect(), which this change never otherwise touches, and carries its own design decision (the helper's shape).

Fixes #534

🤖 Generated with Claude Code

https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc

@Yaraslaut
Yaraslaut force-pushed the fix/534-socketserver-hardening branch 2 times, most recently from cd259fa to 7dd7951 Compare September 26, 2026 10:42
An unauthenticated peer that connects and sends one byte parks a clientLoop
thread and a file descriptor forever: recvSome is a blocking ::recv with no
SO_RCVTIMEO anywhere, and TcpSocket's fd-adopting constructor clears
O_NONBLOCK on every accepted connection. SocketServerConfig gains
handshakeTimeout, enforced as a single deadline across the whole handshake
read via a poll-with-remaining-budget loop in readHttpHeaderBlock (not
SO_RCVTIMEO, which restarts on every recv and would only bound each
individual read).

Separately, nothing ever set SO_SNDTIMEO on an accepted connection, so a
server-side sendAll against a peer that stopped reading blocked forever
instead of throwing -- leaving PR #558's ClientConnection::sendText retire-on-
partial-write-failure catch unreachable and untestable. SocketServerConfig
gains sendTimeout, applied via the existing setSendTimeout in acceptLoop, so
that catch now fires and retires the connection instead of continuing to
dispatch frames whose replies go nowhere.

idleTimeout and maxConnections, both named in the original finding, are out
of scope here per the issue's rescope: idleTimeout would disconnect a
desktop client that is idle by design (no ping/pong keepalive exists to tell
idle from dead), and maxConnections is real but deferred to a follow-up.

Fixes #534

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc
Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
@Yaraslaut
Yaraslaut force-pushed the fix/534-socketserver-hardening branch from 7dd7951 to a085b76 Compare September 26, 2026 10:59
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.31034% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/net/detail/ws_handshake.hpp 80.76% 3 Missing and 2 partials ⚠️
include/morph/net/socket_server.hpp 66.66% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@Yaraslaut
Yaraslaut merged commit f4a6b64 into master Sep 26, 2026
40 checks passed
@Yaraslaut
Yaraslaut deleted the fix/534-socketserver-hardening branch September 26, 2026 12:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

net: SocketServer has no connection cap, handshake timeout or idle timeout

1 participant