From 1b457784d9a6c43e4fa2b19854f765ef50eb292c Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 5 Sep 2026 01:55:55 -0400 Subject: [PATCH] fix(pglite): answer client handshakes from the bridge after the first one A client that starts a handshake and disconnects before the password step (a pool connection attempt timing out behind a long scanner transaction) left the single backend in ClientAuthInProgress, and it then answered other clients' queries without ReadyForQuery, or read their query as an empty one, until the next handshake completed. Each such reply broke a pooled connection and, during a scan, the scanner transaction on it. The bridge now replies AuthenticationOk, the cached ParameterStatus messages, BackendKeyData and ReadyForQuery itself for every client after the first, answers SSLRequest with N, and drops CancelRequest connections. --- db/pglite/pglite.go | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/db/pglite/pglite.go b/db/pglite/pglite.go index 038e2e5e6..8306e12ec 100644 --- a/db/pglite/pglite.go +++ b/db/pglite/pglite.go @@ -424,6 +424,7 @@ func (pg *PGlite) handleConn(conn net.Conn, ioBase string) { }() handshakeDone := false startupDone := false + startupSeen := false var pending []byte if pg.trace { fmt.Fprintln(pg.cfg.Stderr, "# bridge: client connected") @@ -453,6 +454,29 @@ func (pg *PGlite) handleConn(conn net.Conn, ioBase string) { if pg.trace { fmt.Fprintf(pg.cfg.Stderr, "# bridge C> %s (%d bytes)\n", wireTags(packet), n) } + if !handshakeDone && !startupSeen { + startupSeen = true + switch startupCode(packet) { + case sslRequestCode: + startupSeen, startupDone = false, false // the real startup packet follows + if _, err := conn.Write([]byte{'N'}); err != nil { + return + } + continue + case cancelRequestCode: + return // one backend: nothing to cancel + } + // After the first handshake, answer new clients from the cache without involving the + // backend: a handshake it starts and never finishes leaves it unable to send + // ReadyForQuery to anyone until the next one completes. + if pg.paramStatus != nil { + handshakeDone = true + if !pg.sendReplies(conn, [][]byte{syntheticHandshake(pg.paramStatus)}) { + return + } + continue + } + } // Take the session before touching .in: it is shared by every client. if !holding { pg.sessionMu.Lock() @@ -821,6 +845,28 @@ func ensureReadyForQuery(replies [][]byte, trapErr error) [][]byte { return append(replies, []byte{'Z', 0, 0, 0, 5, 'I'}) } +const ( + sslRequestCode = 80877103 + cancelRequestCode = 80877102 +) + +// startupCode returns the protocol/request code of an untagged startup packet, or 0. +func startupCode(packet []byte) uint32 { + if len(packet) < 8 || packet[0] != 0 { + return 0 + } + return uint32(packet[4])<<24 | uint32(packet[5])<<16 | uint32(packet[6])<<8 | uint32(packet[7]) +} + +// syntheticHandshake is AuthenticationOk, the cached ParameterStatus messages, BackendKeyData and +// ReadyForQuery, i.e. what the backend answered the first client. +func syntheticHandshake(params []byte) []byte { + out := []byte{'R', 0, 0, 0, 8, 0, 0, 0, 0} + out = append(out, params...) + out = append(out, 'K', 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0) + return append(out, 'Z', 0, 0, 0, 5, 'I') +} + func errorResponse(code, msg string) []byte { body := "SERROR\x00VERROR\x00C" + code + "\x00M" + msg + "\x00\x00" n := len(body) + 4