Skip to content

Commit b125f75

Browse files
committed
Only check error length during startup, limit response size for some types
#1248 is not entirely correct, as it should only apply on startup messages, as that check is done in PQconnectPoll(): if (beresp == PqMsg_ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN) We can re-use the txnStatus field to check if we're in the connection phase. In addition, in pqParseInput3(), it does limit the message length, but only for some respose types: #define VALID_LONG_MESSAGE_TYPE(id) \ ((id) == PqMsg_CopyData || \ (id) == PqMsg_DataRow || \ (id) == PqMsg_ErrorResponse || \ (id) == PqMsg_FunctionCallResponse || \ (id) == PqMsg_NoticeResponse || \ (id) == PqMsg_NotificationResponse || \ (id) == PqMsg_RowDescription) if (msgLength > 30000 && !VALID_LONG_MESSAGE_TYPE(id)) So check for that as well. Fixes #1324 Closes #1326
1 parent 76df97d commit b125f75

4 files changed

Lines changed: 110 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ unreleased
1313

1414
### Fixes
1515

16+
- The limit on error responses added in v1.11.0 ([#1248]) should only apply on
17+
errors during the connection phase ([#1326]).
18+
19+
- Limit the size of some server responses, identical to libpq ([#1326]).
20+
1621
- Add Redshift-specific OID mappings ([#1291], [#1317]).
1722

1823
- Use correct environment variable name for `PGSSLMINPROTOCOLVERSION` and
@@ -21,6 +26,7 @@ unreleased
2126
[#1291]: https://github.com/lib/pq/pull/1291
2227
[#1310]: https://github.com/lib/pq/pull/1310
2328
[#1317]: https://github.com/lib/pq/pull/1317
29+
[#1326]: https://github.com/lib/pq/pull/1326
2430

2531

2632
v1.12.3 (2026-04-03)

‎conn.go‎

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,7 @@ func dial(ctx context.Context, d Dialer, cfg Config) (net.Conn, error) {
476476
}
477477

478478
func (cn *conn) isInTransaction() bool {
479-
return cn.txnStatus == txnStatusIdleInTransaction ||
480-
cn.txnStatus == txnStatusInFailedTransaction
479+
return cn.txnStatus == txnStatusIdleInTransaction || cn.txnStatus == txnStatusInFailedTransaction
481480
}
482481

483482
func (cn *conn) checkIsInTransaction(intxn bool) error {
@@ -1131,10 +1130,15 @@ func (cn *conn) recvMessage(r *readBuf) (proto.ResponseCode, error) {
11311130
//
11321131
// libpq checks "if ErrorResponse && (msgLength < 8 || msgLength > MAX_ERRLEN)",
11331132
// but check < 4 since n represents bytes remaining to be read after length.
1134-
if t == proto.ErrorResponse && (n < 4 || n > proto.MaxErrlen) {
1133+
//
1134+
// Use txnStatus to check if we're in the startup phase.
1135+
if cn.txnStatus == 0 && t == proto.ErrorResponse && (n < 4 || n > proto.MaxMsgLen) {
11351136
msg, _ := cn.buf.ReadString('\x00')
11361137
return 0, fmt.Errorf("pq: server error: %s%s", string(x[1:]), strings.TrimSuffix(msg, "\x00"))
11371138
}
1139+
if !proto.ValidLongMessageType(t) && n > proto.MaxMsgLen {
1140+
return 0, fmt.Errorf("pq: lost synchronization with server: got message type %q, length %d", t, n)
1141+
}
11381142

11391143
var y []byte
11401144
if n <= len(cn.scratch) {
@@ -1153,11 +1157,11 @@ func (cn *conn) recvMessage(r *readBuf) (proto.ResponseCode, error) {
11531157
return t, nil
11541158
}
11551159

1156-
// recv receives a message from the backend, returning an error if an error
1157-
// happened while reading the message or the received message an ErrorResponse.
1158-
// NoticeResponses are ignored. This function should generally be used only
1159-
// during the startup sequence.
1160-
func (cn *conn) recv() (proto.ResponseCode, *readBuf, error) {
1160+
// recvError receives a message from the backend, returning an error if an error
1161+
// happened while reading the message or the received message is an
1162+
// ErrorResponse. NoticeResponses are ignored. This function should generally be
1163+
// used only during the startup sequence.
1164+
func (cn *conn) recvError() (proto.ResponseCode, *readBuf, error) {
11611165
for {
11621166
r := new(readBuf)
11631167
t, err := cn.recvMessage(r)
@@ -1370,7 +1374,7 @@ func (cn *conn) startup(cfg Config) error {
13701374

13711375
var didauth bool
13721376
for {
1373-
t, r, err := cn.recv()
1377+
t, r, err := cn.recvError()
13741378
if err != nil {
13751379
return err
13761380
}
@@ -1515,7 +1519,7 @@ func (cn *conn) auth(code proto.AuthCode, r *readBuf, cfg Config) error {
15151519
return err
15161520
}
15171521

1518-
t, r, err := cn.recv()
1522+
t, r, err := cn.recvError()
15191523
if err != nil {
15201524
return err
15211525
}
@@ -1541,7 +1545,7 @@ func (cn *conn) auth(code proto.AuthCode, r *readBuf, cfg Config) error {
15411545
return err
15421546
}
15431547

1544-
t, r, err = cn.recv()
1548+
t, r, err = cn.recvError()
15451549
if err != nil {
15461550
return err
15471551
}

‎conn_test.go‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1784,6 +1784,83 @@ func TestPreProtocolError(t *testing.T) {
17841784
}
17851785
}
17861786

1787+
func TestLargeMessage(t *testing.T) {
1788+
type resp struct {
1789+
c proto.ResponseCode
1790+
r string
1791+
}
1792+
long := strings.Repeat("Y", 35_000)
1793+
tests := []struct {
1794+
responses []resp
1795+
want string
1796+
wantErr string
1797+
}{
1798+
{ // DataRow can be unlimited length
1799+
[]resp{
1800+
{proto.RowDescription, "\x00\x01col\x00\x00\x00A\xc6\x00\x01\x00\x00\x00\x19\xff\xff\xff\xff\xff\xff\x00\x00"},
1801+
{proto.DataRow, "\x00\x01\x00\x00\x88\xb8" + long},
1802+
{proto.CommandComplete, "SELECT 1\x00"},
1803+
},
1804+
long, "",
1805+
},
1806+
{ // ErrorResponse as well (after startup)
1807+
[]resp{
1808+
{proto.ErrorResponse, "SERROR\x00C58030\x00M" + long + "\x00\x00"},
1809+
},
1810+
"", "pq: " + long + " (58030)",
1811+
},
1812+
{ // But e.g. Empty Query can't
1813+
[]resp{
1814+
{proto.EmptyQueryResponse, long},
1815+
{proto.CommandComplete, "SELECT 1\x00"},
1816+
},
1817+
"", `pq: lost synchronization with server: got message type "(I) EmptyQueryResponse", length 35000`,
1818+
},
1819+
}
1820+
1821+
for _, tt := range tests {
1822+
t.Run("", func(t *testing.T) {
1823+
t.Parallel()
1824+
f := pqtest.NewFake(t, func(f pqtest.Fake, cn net.Conn) {
1825+
f.Startup(cn, nil)
1826+
for {
1827+
code, q, ok := f.ReadMsg(cn)
1828+
if !ok {
1829+
return
1830+
}
1831+
switch code {
1832+
case proto.Query:
1833+
switch q := string(q[:bytes.IndexByte(q, 0)]); {
1834+
case q == ";": // Ping()
1835+
f.WriteMsg(cn, proto.EmptyQueryResponse, "")
1836+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
1837+
default:
1838+
for _, r := range tt.responses {
1839+
f.WriteMsg(cn, r.c, r.r)
1840+
}
1841+
f.WriteMsg(cn, proto.ReadyForQuery, "I")
1842+
}
1843+
case proto.Terminate:
1844+
cn.Close()
1845+
return
1846+
}
1847+
}
1848+
})
1849+
defer f.Close()
1850+
1851+
db := pqtest.MustDB(t, f.DSN())
1852+
var have string
1853+
err := db.QueryRow(`select t from tbl`).Scan(&have)
1854+
if !pqtest.ErrorContains(err, tt.wantErr) {
1855+
t.Fatalf("wrong error:\nhave: %s\nwant: %s", err, tt.wantErr)
1856+
}
1857+
if have != tt.want {
1858+
t.Fatal("rows not equal") // Don't output content as this deals with lots of text.
1859+
}
1860+
})
1861+
}
1862+
}
1863+
17871864
// reading from circularConn yields content[:prefixLen] once, followed by
17881865
// content[prefixLen:] over and over again. It never returns EOF.
17891866
type circularConn struct {

‎internal/proto/proto.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,20 @@ const (
1616
NegotiateGSSCode = (1234 << 16) | 5680
1717
)
1818

19+
// Constants from fe-protocol3.c
20+
21+
func ValidLongMessageType(c ResponseCode) bool {
22+
switch c {
23+
case CopyDataResponse, DataRow, ErrorResponse, FunctionCallResponse,
24+
NoticeResponse, NotificationResponse, RowDescription:
25+
return true
26+
}
27+
return false
28+
}
29+
1930
// Constants from fe-connect.c
2031
const (
21-
MaxErrlen = 30_000 // https://github.com/postgres/postgres/blob/c6a10a89f/src/interfaces/libpq/fe-connect.c#L4067
32+
MaxMsgLen = 30_000 // https://github.com/postgres/postgres/blob/c6a10a89f/src/interfaces/libpq/fe-connect.c#L4067
2233
)
2334

2435
// RequestCode is a request codes sent by the frontend.

0 commit comments

Comments
 (0)