Skip to content

Commit 479fbb8

Browse files
committed
Add ConnectionFailedTimeout to ConnectionFailureReason
Lori 0.12.0 adds ConnectionFailedTimeout to its ConnectionFailureReason type. We pass the new variant through to our own union so users can match on it when connection timeouts are in use. The upgrade also renames expect() to buffer_until() with new types (Expect → BufferSize, None → Streaming).
1 parent 32ed8c3 commit 479fbb8

File tree

7 files changed

+57
-19
lines changed

7 files changed

+57
-19
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Add ConnectionFailedTimeout to ConnectionFailureReason
2+
3+
`ConnectionFailureReason` now includes `ConnectionFailedTimeout` for when a connection attempt times out before a TCP or TLS connection is established. If you have an exhaustive match on `ConnectionFailureReason`, you'll need to add the new arm:
4+
5+
Before:
6+
7+
```pony
8+
match reason
9+
| ConnectionFailedDNS => _env.out.print("DNS resolution failed")
10+
| ConnectionFailedTCP => _env.out.print("TCP connection failed")
11+
| SSLServerRefused => _env.out.print("Server refused SSL")
12+
| TLSAuthFailed => _env.out.print("TLS certificate error")
13+
| TLSHandshakeFailed => _env.out.print("TLS handshake failed")
14+
end
15+
```
16+
17+
After:
18+
19+
```pony
20+
match reason
21+
| ConnectionFailedDNS => _env.out.print("DNS resolution failed")
22+
| ConnectionFailedTCP => _env.out.print("TCP connection failed")
23+
| SSLServerRefused => _env.out.print("Server refused SSL")
24+
| TLSAuthFailed => _env.out.print("TLS certificate error")
25+
| TLSHandshakeFailed => _env.out.print("TLS handshake failed")
26+
| ConnectionFailedTimeout => _env.out.print("Connection timed out")
27+
end
28+
```

.release-notes/next-release.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,7 @@ be pg_session_connection_failed(session: Session,
822822
| SSLServerRefused => _env.out.print("Server refused SSL")
823823
| TLSAuthFailed => _env.out.print("TLS certificate error")
824824
| TLSHandshakeFailed => _env.out.print("TLS handshake failed")
825+
| ConnectionFailedTimeout => _env.out.print("Connection timed out")
825826
end
826827
```
827828

CLAUDE.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ SSL version is mandatory. Tests run with `--sequential`. Integration tests requi
1818

1919
## Dependencies
2020

21-
- `ponylang/ssl` 2.0.0 (MD5 password hashing, SCRAM-SHA-256 crypto primitives via `ssl/crypto`, SSL/TLS via `ssl/net`)
22-
- `ponylang/lori` 0.11.0 (TCP networking, STARTTLS support)
21+
- `ponylang/ssl` 2.0.1 (MD5 password hashing, SCRAM-SHA-256 crypto primitives via `ssl/crypto`, SSL/TLS via `ssl/net`)
22+
- `ponylang/lori` 0.12.0 (TCP networking, STARTTLS support)
2323

2424
Managed via `corral`.
2525

@@ -138,7 +138,7 @@ Only one operation is in-flight at a time. The queue serializes execution. `quer
138138
- `SSLMode` — union type `(SSLDisabled | SSLPreferred | SSLRequired)`. `SSLDisabled` is the default (plaintext). `SSLPreferred` wraps an `SSLContext val` and attempts SSL with plaintext fallback on server refusal (`sslmode=prefer`). `SSLRequired` wraps an `SSLContext val` and aborts on server refusal.
139139
- `ErrorResponseMessage` — full PostgreSQL error with all standard fields
140140
- `AuthenticationFailureReason` = `(InvalidAuthenticationSpecification | InvalidPassword | ServerVerificationFailed | UnsupportedAuthenticationMethod)`
141-
- `ConnectionFailureReason` = `(ConnectionFailedDNS | ConnectionFailedTCP | SSLServerRefused | TLSAuthFailed | TLSHandshakeFailed)`. Delivered via `pg_session_connection_failed` callback
141+
- `ConnectionFailureReason` = `(ConnectionFailedDNS | ConnectionFailedTCP | SSLServerRefused | TLSAuthFailed | TLSHandshakeFailed | ConnectionFailedTimeout)`. Delivered via `pg_session_connection_failed` callback
142142

143143
### Type Conversion (PostgreSQL OID → Pony)
144144

@@ -193,7 +193,7 @@ Tests live in the main `postgres/` package (private test classes), organized acr
193193

194194
## Supported PostgreSQL Features
195195

196-
**SSL/TLS:** Optional SSL negotiation via `SSLRequired` (mandatory) or `SSLPreferred` (fallback to plaintext on server refusal). CVE-2021-23222 mitigated via `expect(1)` before SSLRequest. Design: [discussion #76](https://github.com/ponylang/postgres/discussions/76).
196+
**SSL/TLS:** Optional SSL negotiation via `SSLRequired` (mandatory) or `SSLPreferred` (fallback to plaintext on server refusal). CVE-2021-23222 mitigated via `buffer_until(BufferSize)` before SSLRequest. Design: [discussion #76](https://github.com/ponylang/postgres/discussions/76).
197197

198198
**Authentication:** MD5 password and SCRAM-SHA-256. No SCRAM-SHA-256-PLUS (channel binding), Kerberos, GSS, or certificate auth. Design: [discussion #83](https://github.com/ponylang/postgres/discussions/83).
199199

corral.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"deps": [
66
{
77
"locator": "github.com/ponylang/lori.git",
8-
"version": "0.11.0"
8+
"version": "0.12.0"
99
},
1010
{
1111
"locator": "github.com/ponylang/ssl.git",

postgres/_cancel_sender.pony

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,9 @@ actor _CancelSender is (lori.TCPConnectionActor & lori.ClientLifecycleEventRecei
3636
| SSLDisabled =>
3737
_send_cancel_and_close()
3838
| let _: (SSLRequired | SSLPreferred) =>
39-
// CVE-2021-23222 mitigation: expect exactly 1 byte for SSL response.
40-
match \exhaustive\ lori.MakeExpect(1)
41-
| let e: lori.Expect => _tcp_connection.expect(e)
39+
// CVE-2021-23222 mitigation: buffer exactly 1 byte for SSL response.
40+
match \exhaustive\ lori.MakeBufferSize(1)
41+
| let e: lori.BufferSize => _tcp_connection.buffer_until(e)
4242
else
4343
_Unreachable()
4444
end
@@ -65,7 +65,7 @@ actor _CancelSender is (lori.TCPConnectionActor & lori.ClientLifecycleEventRecei
6565
match _info.ssl_mode
6666
| let _: SSLPreferred =>
6767
// SSLPreferred: fall back to plaintext cancel
68-
_tcp_connection.expect(None)
68+
_tcp_connection.buffer_until(lori.Streaming)
6969
_send_cancel_and_close()
7070
else
7171
// SSLRequired or unexpected: silently give up
@@ -79,8 +79,9 @@ actor _CancelSender is (lori.TCPConnectionActor & lori.ClientLifecycleEventRecei
7979
end
8080

8181
fun ref _on_tls_ready() =>
82-
// Reset expect from 1 back to 0 (same pattern as _SessionSSLNegotiating)
83-
_tcp_connection.expect(None)
82+
// Reset buffer_until from 1 to streaming (same pattern as
83+
// _SessionSSLNegotiating)
84+
_tcp_connection.buffer_until(lori.Streaming)
8485
_send_cancel_and_close()
8586

8687
fun ref _on_tls_failure(reason: lori.TLSFailureReason) =>

postgres/connection_failure_reason.pony

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ primitive TLSHandshakeFailed
2323
The TLS handshake failed.
2424
"""
2525

26+
primitive ConnectionFailedTimeout
27+
"""
28+
The connection attempt timed out before a TCP or TLS connection was
29+
established.
30+
"""
31+
2632
type ConnectionFailureReason is
2733
(ConnectionFailedDNS | ConnectionFailedTCP |
28-
SSLServerRefused | TLSAuthFailed | TLSHandshakeFailed)
34+
SSLServerRefused | TLSAuthFailed | TLSHandshakeFailed |
35+
ConnectionFailedTimeout)

postgres/session.pony

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ actor Session is (lori.TCPConnectionActor & lori.ClientLifecycleEventReceiver)
184184
| let _: lori.ConnectionFailedDNS => ConnectionFailedDNS
185185
| let _: lori.ConnectionFailedTCP => ConnectionFailedTCP
186186
| let _: lori.ConnectionFailedSSL => TLSHandshakeFailed
187+
| let _: lori.ConnectionFailedTimeout => ConnectionFailedTimeout
187188
end
188189
state.on_failure(this, r)
189190

@@ -393,11 +394,11 @@ class ref _SessionSSLNegotiating
393394
_proceed_to_connected(s)
394395

395396
fun ref _proceed_to_connected(s: Session ref) =>
396-
// Reset expect from 1 (set during SSLRequest) to 0 (deliver all available
397-
// bytes). Critical: lori preserves the expect(1) value across start_tls()
398-
// via _ssl_expect. Without this reset, decrypted data would be delivered
397+
// Reset buffer_until from 1 (set during SSLRequest) to streaming (deliver
398+
// all available bytes). Critical: lori preserves the buffer_until value
399+
// across start_tls(). Without this reset, decrypted data would be delivered
399400
// 1 byte at a time, breaking _ResponseParser.
400-
s._connection().expect(None)
401+
s._connection().buffer_until(lori.Streaming)
401402
s.state = _SessionConnected(_notify, _database_connect_info,
402403
_codec_registry)
403404
_notify.pg_session_connected(s)
@@ -2651,12 +2652,12 @@ trait _ConnectableState is _UnconnectedState
26512652
fun _start_ssl_negotiation(s: Session ref, ctx: SSLContext val,
26522653
fallback_on_refusal: Bool)
26532654
=>
2654-
// Set expect(1) BEFORE sending SSLRequest so lori delivers exactly
2655+
// Set buffer_until(1) BEFORE sending SSLRequest so lori delivers exactly
26552656
// one byte per _on_received call. Any MITM-injected bytes stay in
26562657
// lori's internal buffer, causing start_tls() to return
26572658
// StartTLSNotReady (CVE-2021-23222 mitigation).
2658-
match \exhaustive\ lori.MakeExpect(1)
2659-
| let e: lori.Expect => s._connection().expect(e)
2659+
match \exhaustive\ lori.MakeBufferSize(1)
2660+
| let e: lori.BufferSize => s._connection().buffer_until(e)
26602661
else
26612662
_Unreachable()
26622663
end

0 commit comments

Comments
 (0)