-
Notifications
You must be signed in to change notification settings - Fork 874
Description
Description
PR #6431 (released in 10.0.2) introduced a regression where pooled connections are corrupted after a successful GSS encryption fallback. The first query succeeds, but any subsequent query that reuses the pooled connection throws ObjectDisposedException on ManualResetEventSlim.Reset() in ResetCancellation().
This is not latency-dependent and is 100% reproducible with any proxy that rejects GSS session encryption (PlanetScale, Supavisor).
Reproduction
Minimal — no EF Core, no high latency, no concurrency required:
using Npgsql;
// Any PostgreSQL proxy that rejects GSS session encryption
var cs = "Host=aws-us-east-2-1.pg.psdb.cloud;Database=postgres;Username=...;Password=...;Port=5432;SSL Mode=Require;Trust Server Certificate=true";
await using var ds = new NpgsqlDataSourceBuilder(cs).EnableDynamicJson().Build();
// First query — OK (connector opens, GSS fails, retries without GSS, succeeds)
await using (var conn = await ds.OpenConnectionAsync())
await conn.ExecuteScalarAsync("SELECT 1");
// Second query — FAILS (reuses pooled connector with corrupted ManualResetEventSlim)
await using (var conn = await ds.OpenConnectionAsync())
await conn.ExecuteScalarAsync("SELECT 1"); // ObjectDisposedExceptionStack Trace
System.ObjectDisposedException: Cannot access a disposed object.
Object name: 'System.Threading.ManualResetEventSlim'.
at System.Threading.ManualResetEventSlim.Reset()
at Npgsql.Internal.NpgsqlConnector.ResetCancellation()
at Npgsql.NpgsqlCommand.ExecuteReader(Boolean async, CommandBehavior behavior, CancellationToken cancellationToken)
Version Comparison
| Version | Behavior |
|---|---|
| 10.0.0 | Works — Break() clears the pool after GSS failure, corrupted connector is evicted and never reused |
| 10.0.2 | Fails — #6431 correctly stopped clearing the pool, but the connector that went through GSS-fail-retry has a disposed ReadingPrependedMessagesMRE |
10.0.2 + GssEncryptionMode=Disable |
Works — GSS negotiation skipped entirely |
Root Cause Analysis
In OpenCore(), when GSS encryption fails with GssEncryptionMode.Prefer:
- Exception is caught by the
when (gssEncMode == GssEncryptionMode.Prefer || ...)filter conn.Cleanup()is called — disposes stream/buffers but notReadingPrependedMessagesMREOpenCore()retries recursively withGssEncryptionMode.Disable- Retry succeeds, connector enters pool in
ConnectorState.Ready - On reuse,
ResetCancellation()→ReadingPrependedMessagesMRE.Reset()→ObjectDisposedException
Before #6431, this was masked: Break() always called DataSource.Clear() during connection establishment failures, which incremented _clearCounter. When the connector was returned to the pool, Return() saw the counter mismatch and called CloseConnector() → the corrupted connector was never reused.
After #6431, Break() skips DataSource.Clear() when state == ConnectorState.Connecting (to avoid unnecessarily clearing the pool on retriable failures). This is correct behavior, but it exposes the underlying issue: the connector's ReadingPrependedMessagesMRE is somehow disposed during the GSS-fail-retry cycle despite Cleanup() not touching it.
Proposed Fix
The ReadingPrependedMessagesMRE is declared as readonly and initialized once in the field initializer. Cleanup() does not dispose it — only FullCleanup() does. Yet it is disposed after the retry. This suggests either:
- An indirect disposal path triggered by
Cleanup()(e.g., stream disposal triggering a callback that callsBreak()→FullCleanup()) - A race between the retry and another thread
Suggested approach: After Cleanup() in the GSS retry path, reinitialize the ReadingPrependedMessagesMRE to ensure it is in a valid state before the recursive OpenCore() call. Alternatively, make the field non-readonly and recreate it in Cleanup().
Workaround
Set GssEncryptionMode=Disable in the connection string for proxies that don't support GSS session encryption.
Environment
- Npgsql: 10.0.2
- .NET: 10.0
- OS: Windows 11
- Database proxies tested: PlanetScale, Supavisor (reported by others)