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"); // ObjectDisposedException
Stack 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 not ReadingPrependedMessagesMRE
OpenCore() retries recursively with GssEncryptionMode.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 calls Break() → 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)
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
ObjectDisposedExceptiononManualResetEventSlim.Reset()inResetCancellation().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:
Stack Trace
Version Comparison
Break()clears the pool after GSS failure, corrupted connector is evicted and never reusedReadingPrependedMessagesMREGssEncryptionMode=DisableRoot Cause Analysis
In
OpenCore(), when GSS encryption fails withGssEncryptionMode.Prefer:when (gssEncMode == GssEncryptionMode.Prefer || ...)filterconn.Cleanup()is called — disposes stream/buffers but notReadingPrependedMessagesMREOpenCore()retries recursively withGssEncryptionMode.DisableConnectorState.ReadyResetCancellation()→ReadingPrependedMessagesMRE.Reset()→ObjectDisposedExceptionBefore #6431, this was masked:
Break()always calledDataSource.Clear()during connection establishment failures, which incremented_clearCounter. When the connector was returned to the pool,Return()saw the counter mismatch and calledCloseConnector()→ the corrupted connector was never reused.After #6431,
Break()skipsDataSource.Clear()whenstate == ConnectorState.Connecting(to avoid unnecessarily clearing the pool on retriable failures). This is correct behavior, but it exposes the underlying issue: the connector'sReadingPrependedMessagesMREis somehow disposed during the GSS-fail-retry cycle despiteCleanup()not touching it.Proposed Fix
The
ReadingPrependedMessagesMREis declared asreadonlyand initialized once in the field initializer.Cleanup()does not dispose it — onlyFullCleanup()does. Yet it is disposed after the retry. This suggests either:Cleanup()(e.g., stream disposal triggering a callback that callsBreak()→FullCleanup())Suggested approach: After
Cleanup()in the GSS retry path, reinitialize theReadingPrependedMessagesMREto ensure it is in a valid state before the recursiveOpenCore()call. Alternatively, make the field non-readonly and recreate it inCleanup().Workaround
Set
GssEncryptionMode=Disablein the connection string for proxies that don't support GSS session encryption.Environment