From 4c1dc1e90cd681f948a2dced13191f77cc1e55ba Mon Sep 17 00:00:00 2001 From: Nick Pellegrino Date: Wed, 23 Sep 2026 17:44:50 -0400 Subject: [PATCH] enclave: set a write deadline and check deadline errors on vsock conns A peer that stops reading the response held its worker slot until the connection closed. The response write now has a deadline, and a failure to set either deadline closes the connection instead of serving it unbounded. Co-Authored-By: Claude Opus 5.5 --- enclave/server.go | 18 ++++++++++- enclave/server_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 enclave/server_test.go diff --git a/enclave/server.go b/enclave/server.go index dccb241..fc0dde8 100644 --- a/enclave/server.go +++ b/enclave/server.go @@ -102,6 +102,14 @@ func (s *EnclaveServer) dispatch(conn net.Conn) { } } +// Connection deadlines. The write deadline starts once the request is handled, +// so a peer that stops reading releases its worker slot. Variables so tests can +// shorten them. +var ( + readTimeout = 30 * time.Second + writeTimeout = 30 * time.Second +) + func (s *EnclaveServer) handleConnection(conn net.Conn) { defer func() { if r := recover(); r != nil { @@ -112,7 +120,10 @@ func (s *EnclaveServer) handleConnection(conn net.Conn) { } }() - _ = conn.SetReadDeadline(time.Now().Add(30 * time.Second)) + if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil { + log.Printf("ERROR: Failed to set read deadline: %v", err) + return + } var buf bytes.Buffer _, err := io.Copy(&buf, conn) @@ -183,6 +194,11 @@ func (s *EnclaveServer) handleConnection(conn net.Conn) { } } + if err := conn.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil { + log.Printf("ERROR: Failed to set write deadline: %v", err) + return + } + encoder := json.NewEncoder(conn) if err := encoder.Encode(response); err != nil { log.Printf("ERROR: Failed to encode response: %v", err) diff --git a/enclave/server_test.go b/enclave/server_test.go new file mode 100644 index 0000000..368ee24 --- /dev/null +++ b/enclave/server_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "errors" + "io" + "net" + "strings" + "testing" + "time" + + "github.com/peterldowns/testy/assert" +) + +// requestConn serves a fixed request on Read and passes writes and deadlines +// through to the embedded conn. +type requestConn struct { + net.Conn + req io.Reader +} + +func (c *requestConn) Read(p []byte) (int, error) { return c.req.Read(p) } + +// readDeadlineFailConn fails SetReadDeadline and records any Read. +type readDeadlineFailConn struct { + net.Conn + read bool +} + +func (*readDeadlineFailConn) SetReadDeadline(time.Time) error { + return errors.New("set read deadline failed") +} + +func (c *readDeadlineFailConn) Read([]byte) (int, error) { + c.read = true + return 0, io.EOF +} + +func TestHandleConnection_WriteDeadlineReleasesStalledPeer(t *testing.T) { + orig := writeTimeout + writeTimeout = 50 * time.Millisecond + t.Cleanup(func() { writeTimeout = orig }) + + // net.Pipe is unbuffered, so the response write blocks until the peer reads. + // The peer never reads. + server, peer := net.Pipe() + t.Cleanup(func() { _ = peer.Close() }) + conn := &requestConn{Conn: server, req: strings.NewReader(`{"type":"ping"}`)} + s := testServer(t, 1) + + done := make(chan struct{}) + go func() { + s.handleConnection(conn) + close(done) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("handleConnection still blocked writing to a peer that never reads") + } +} + +func TestHandleConnection_ClosesWithoutReadingWhenReadDeadlineFails(t *testing.T) { + server, peer := net.Pipe() + t.Cleanup(func() { _ = peer.Close() }) + conn := &readDeadlineFailConn{Conn: server} + + testServer(t, 1).handleConnection(conn) + + assert.False(t, conn.read) + _, err := server.Write([]byte("x")) + assert.Error(t, err) +}