Skip to content

Add async counterparts to the enclave provider hierarchy (Phase 3, partial) - #4541

Open
cheenamalhotra wants to merge 1 commit into
mainfrom
dev/automation/async-enclave-providers
Open

Add async counterparts to the enclave provider hierarchy (Phase 3, partial)#4541
cheenamalhotra wants to merge 1 commit into
mainfrom
dev/automation/async-enclave-providers

Conversation

@cheenamalhotra

@cheenamalhotra cheenamalhotra commented Aug 14, 2026

Copy link
Copy Markdown
Member

Description

Implements the enclave-provider portion of Phase 3 of specs/002-async-always-encrypted/spec.md, building on the SqlColumnEncryptionKeyStoreProvider async members that landed in Phase 1 (#3673).

The motivation is that the Always Encrypted enclave path currently blocks a thread on real network I/O even when the caller invoked an async API. Two call sites perform sync-over-async, and the attestation gate that serialises concurrent cold starts is an AutoResetEvent with a 15 second timeout, so contending callers park a thread pool thread for up to 15 seconds each. This PR lays the async groundwork to remove all three.

API changes, backwards compatibility

None. SqlColumnEncryptionEnclaveProvider is internal abstract and appears in neither netcore/ref/ nor netfx/ref/, so there is no public API surface and no binary compatibility constraint. The hierarchy is closed and entirely internal.

Functionality

1. Four async counterparts on the base type. GetEnclaveSessionAsync, GetAttestationParametersAsync, CreateEnclaveSessionAsync and InvalidateEnclaveSessionAsync. They are virtual, with defaults that defer to the existing sync overloads, so providers with no I/O need no boilerplate. This mirrors the Phase 1 Task.FromCanceled / Task.FromResult / Task.FromException + ADP.IsCatchableExceptionType pattern exactly.

C# forbids out parameters on async methods, so the two members that report multiple values return tuples instead (spec Design Decision 4):

Task<(SqlEnclaveSession SqlEnclaveSession, long Counter, byte[] CustomData, int CustomDataLength)> GetEnclaveSessionAsync(...)
Task<(SqlEnclaveSession SqlEnclaveSession, long Counter)> CreateEnclaveSessionAsync(...)

2. Both HTTP providers are genuinely async. A virtual default would leave a real network call blocking a thread, so the two providers that perform I/O explicitly override rather than inheriting the fallback. This removes both sync-over-async blocking calls in the hierarchy:

Site Before After
AzureAttestationBasedEnclaveProvider GetConfigurationAsync(CancellationToken.None).Result await GetConfigurationAsync(cancellationToken)
HostGuardianServiceEnclaveProvider GetStreamAsync(url).ConfigureAwait(false).GetAwaiter().GetResult() await GetStreamAsync(url, cancellationToken)

The HGS path also awaits JsonSerializer.DeserializeAsync and replaces Thread.Sleep in the retry backoff with Task.Delay. MakeRequestAsync is declared protected abstract rather than virtual so no derived provider can silently inherit a blocking implementation.

Left on the base defaults deliberately: NoneAttestationEnclaveProvider (no I/O), and the CPU bound GetAttestationParameters / InvalidateEnclaveSession operations (ECDH key generation, nonce generation, MemoryCache access).

3. FR-015, attestation gating. The spec calls for converting lock to SemaphoreSlim where an async operation precedes session storage. On inspection, the five lock statements named in the spec were not the ones that needed converting: their bodies only touch MemoryCache and flags, no await occurs inside them, and none can, because the awaited attestation happens before AddEnclaveSessionToCache is called. lock remains correct and cheaper there.

The construct that genuinely blocks is EnclaveProviderBase.sessionLockEvent, an AutoResetEvent with no awaitable wait. GetEnclaveSessionHelperAsync uses a SemaphoreSlim gate instead.

The sync and async gates are deliberately independent, per the requirement never to hold a gate across an awaited network call that a synchronous caller can also block on. The trade off is that a concurrent sync plus async cold start may perform two attestations. The existing design already tolerates duplicates (see the documented lock timeout cases) and the session cache is idempotent.

Behavioral differences worth reviewer attention

  1. Cancellation releases the gate. The sync UpdateEnclaveSessionLockStatus releases only when a session was created. The async version also releases when the token is cancelled, otherwise a caller cancelled mid attestation would stall every other async caller for the full 15 second lock timeout. The sync path has no equivalent case because it has no cancellation.
  2. SemaphoreSlim.Release() throws when the semaphore is full, unlike AutoResetEvent.Set() which is a harmless no-op. Releases are therefore guarded by an s_isAsyncSessionLockAcquired flag, preserving the sync design's "any caller may signal" semantics while staying idempotent.
  3. Sync and async calls must not be mixed within one attestation sequence. GetEnclaveSessionAsync -> GetAttestationParametersAsync -> CreateEnclaveSessionAsync is one unit. Pairing an async Get with a sync Create leaks a gate because the two use different gates. This is documented in the snippet XML <remarks> and is the main thing to watch during Phase 5 integration.
  4. ThreadRetryCache is keyed by ManagedThreadId, which is not stable across await boundaries. A caller resuming on a different thread may miss the same thread retry optimisation, costing an extra attestation. Never a correctness or deadlock issue. Worth replacing with a correlation token in Phase 5.

FR-010, sync paths unchanged

Every source change is an insertion. git diff --stat reports +690 / -1 across the five source files, and the single deletion is in the docs XML (a <remarks> line that was expanded). No existing sync statement was moved, reordered or edited. Each new async method was verified statement by statement against its sync twin.

Documentation

XML docs for the new members use <include> into doc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionEnclaveProvider.xml, consistent with the existing sync members. The class level <remarks> gained the three call attestation sequence and the do-not-mix-sync-and-async contract. No localization impact.

Not in scope

EnclaveDelegate async dispatchers, SqlSecurityUtility.DecryptSymmetricKeyAsync and the SqlCommand.Encryption.cs call sites are Phase 3 remainder through Phase 5. Nothing calls these new members yet, so there is no end to end behaviour change from this PR. It is purely additive groundwork.

Issues

Implements part of Phase 3 of specs/002-async-always-encrypted/spec.md. Follows on from #3673 (Phase 1).

No GitHub issue is closed by this PR.

Testing

12 new unit tests in src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlColumnEncryptionEnclaveProviderAsyncShould.cs, using four in-file test doubles so no SQL Server or attestation service is required:

  • default fallback dispatch, and fault propagation through Task.FromException
  • cancellation on every async member, including pre-cancelled tokens
  • gate release on cancellation. This test was validated by temporarily reverting the fix and confirming it fails with Attestation took 00:00:15.05, so it is a proven regression guard rather than an assumed one
  • concurrent async cold start, cache reuse after attestation, and mixed sync and async callers
  • the sync concurrency path, as an unchanged behaviour regression guard for FR-010
  • HGS MakeRequestAsync cancellation, and mapping of request failure to SqlException

Results:

  • 28 async Always Encrypted unit tests pass on net8.0 (12 new plus 16 existing Phase 1), 12 new pass on net9.0
  • Library builds clean with 0 warnings for net8.0, net9.0 and net462

No integration or manual tests were added. The new members are not yet reachable from any public API (see "Not in scope"), so there is no end to end path to exercise. Integration coverage against a live enclave belongs with the Phase 5 call site work, where SqlCommand actually invokes these methods.

Additional verification against the spec's hard requirements:

  • Every await in library code uses .ConfigureAwait(false) (Design Decision 5)
  • CancellationToken is propagated through all new async methods
  • All source is under src/Microsoft.Data.SqlClient/src/, none in netfx/src/ or netcore/src/

…rtial)

Implements the enclave-provider portion of Phase 3 of the async Always
Encrypted spec (specs/002-async-always-encrypted/spec.md).

SqlColumnEncryptionEnclaveProvider gains four `virtual` async counterparts
whose default implementations defer to the existing sync overloads, mirroring
the pattern already established for SqlColumnEncryptionKeyStoreProvider in
Phase 1. Because C# forbids `out` parameters on async methods, the two members
that report multiple values return tuples instead (spec Design Decision 4).

The two providers that perform real network I/O explicitly override the
defaults rather than inheriting the blocking fallback, which removes both
sync-over-async blocking calls in this hierarchy:

  * AzureAttestationBasedEnclaveProvider now awaits
    ConfigurationManager.GetConfigurationAsync instead of blocking on .Result.
  * HostGuardianServiceEnclaveProvider now awaits GetStreamAsync,
    JsonSerializer.DeserializeAsync and the retry backoff instead of blocking
    on .GetAwaiter().GetResult() and Thread.Sleep.

FR-015: the attestation gate (AutoResetEvent) has no awaitable wait, so the
async path gets its own SemaphoreSlim gate via GetEnclaveSessionHelperAsync.
The gates are deliberately independent so that a synchronous caller can never
block a thread for the duration of an awaited attestation round trip. The five
`lock` statements called out in the spec are left as-is: their bodies only
touch MemoryCache and flags, and the awaited attestation happens before session
storage rather than inside those regions.

Unlike the sync path, the async gate is also released when the caller cancels,
since a cancelled caller never goes on to create the session.

FR-010: no existing sync code path is modified. Every change is an insertion.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cf57fdc0-1c77-40f1-a1d5-7c0bd98c1a89
@cheenamalhotra
cheenamalhotra requested a review from a team as a code owner August 14, 2026 05:42
Copilot AI lite review requested due to automatic review settings August 14, 2026 05:42
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Aug 14, 2026
@cheenamalhotra cheenamalhotra added this to the 7.1.0-preview3 milestone Aug 14, 2026
@cheenamalhotra cheenamalhotra moved this from To triage to In review in SqlClient Board Aug 14, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds internal async counterparts to the Always Encrypted enclave provider hierarchy, enabling non-blocking attestation/session creation (notably for the HTTP-based providers) while preserving existing sync behavior via default virtual fallbacks. It lays groundwork for later phases to wire async enclave attestation into the command execution flow without introducing public API surface changes.

Changes:

  • Added GetEnclaveSessionAsync, GetAttestationParametersAsync, CreateEnclaveSessionAsync, and InvalidateEnclaveSessionAsync to SqlColumnEncryptionEnclaveProvider, with default implementations delegating to the sync members and supporting cancellation.
  • Introduced an async attestation gate in EnclaveProviderBase (via SemaphoreSlim) and implemented truly-async network paths for Azure Attestation and HGS/VSM signing certificate retrieval.
  • Added a new unit test suite validating default fallback behavior, cancellation, and concurrency characteristics across sync/async paths.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlColumnEncryptionEnclaveProvider.cs Adds the new internal virtual async members with sync-fallback defaults, cancellation, and exception-to-faulted-task behavior.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/EnclaveProviderBase.cs Implements async session gating + async helper APIs (tuple returns) to support non-blocking attestation flows.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/AzureAttestationBasedEnclaveProvider.cs Overrides async members to avoid sync-over-async blocking and propagate cancellation through network calls.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProviderBase.cs Adds async counterparts for VSM attestation logic, including async signing cert retrieval via an abstract MakeRequestAsync.
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/VirtualSecureModeEnclaveProvider.cs Implements truly async HTTP request + retry/backoff + async JSON deserialization for HGS signing cert retrieval.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/AlwaysEncrypted/SqlColumnEncryptionEnclaveProviderAsyncShould.cs Adds unit tests for fallback dispatch, cancellation semantics, gate behavior, and concurrency on sync/async paths.
doc/snippets/Microsoft.Data.SqlClient/SqlColumnEncryptionEnclaveProvider.xml Documents the new async members and the “don’t mix sync/async within one attestation sequence” constraint.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@codecov

codecov Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 44.94382% with 196 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.90%. Comparing base (ee529d4) to head (eed54c8).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
.../SqlClient/VirtualSecureModeEnclaveProviderBase.cs 0.00% 93 Missing ⚠️
.../SqlClient/AzureAttestationBasedEnclaveProvider.cs 10.52% 85 Missing ⚠️
...Data/SqlClient/VirtualSecureModeEnclaveProvider.cs 53.84% 12 Missing ⚠️
...rc/Microsoft/Data/SqlClient/EnclaveProviderBase.cs 92.94% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4541      +/-   ##
==========================================
- Coverage   64.78%   62.90%   -1.88%     
==========================================
  Files         288      284       -4     
  Lines       44418    67766   +23348     
==========================================
+ Hits        28774    42628   +13854     
- Misses      15644    25138    +9494     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 62.90% <44.94%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

5 participants