Skip to content

Fix | Scope configurable retry logic assembly resolution to opt-in callers - #4547

Open
priyankatiwari08 wants to merge 8 commits into
mainfrom
dev/prtiwar/fix-configurable-retry-assembly-resolution
Open

Fix | Scope configurable retry logic assembly resolution to opt-in callers#4547
priyankatiwari08 wants to merge 8 commits into
mainfrom
dev/prtiwar/fix-configurable-retry-assembly-resolution

Conversation

@priyankatiwari08

@priyankatiwari08 priyankatiwari08 commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Summary

SqlConfigurableRetryLogicLoader subscribed a handler to AssemblyLoadContext.Default.Resolving in its constructor and never removed it.

SqlConfigurableRetryLogicManager builds that loader lazily on the default RetryLogicProvider path, so simply reading SqlCommand.RetryLogicProvider or SqlConnection.RetryLogicProvider — which any app using retry logic does — installed a permanent, process-wide assembly resolution hook.

Two consequences:

  1. It applied to everything. Once installed, the handler participated in resolving every assembly the host application failed to find, even though the application had never configured a custom retry logic type. Apps saw failures (and in Stack overflow caused by SqlConfigurableRetryLogicLoader.Defaul_resolving #2214 a stack overflow) surfacing from inside SqlClient for assemblies that have nothing to do with SqlClient.
  2. It probed the wrong directory. The handler resolved against Environment.CurrentDirectory. The working directory is ambient process state that any code in the process can change and that bears no relationship to where the application's binaries live, so assemblies could be resolved from an unintended location.

Only .NET is affected — the .NET Framework code path does not use AssemblyLoadContext.

Scope

This is the mitigation intended for the hotfix branches: 7.1.0, backported to 7.0 and 6.1 so the issue is resolved on all production branches. It deliberately does not change the underlying design of how retry logic assemblies are loaded.

The two larger design questions raised in review — loading configured retry logic into a dedicated AssemblyLoadContext, and replacing type-name resolution with an explicit registration API — are tracked separately in #4623 for 8.0.

Changes

# Change
1 Probe AppContext.BaseDirectory instead of Environment.CurrentDirectory.
2 Subscribe the resolving handler only when a custom retryLogicType is configured, and only for the duration of resolving and constructing that provider.
3 Skip type resolution entirely when no retryLogicType is configured.

On (3): in AppConfigManager, retryLogicType is optional while retryMethod is IsRequired = true. So a config that selected a built-in retry method still went through the custom-type resolution path and installed the handler. It now short-circuits straight to SqlConfigurableRetryFactory.

On (2): the handler spans both LoadType and CreateInstance. LoadType only produces a Type — the configured type's constructor and retry method run inside CreateInstance, and a provider that touches its own private dependencies while being constructed needs the handler installed at that point.

Net effect: the handler is never installed unless the application explicitly configured a custom retry logic type, and it is removed again once that provider has been constructed.

On the one case scoping does not cover

A provider that loads a private dependency after it has been constructed — at retry time — will no longer find that dependency through the driver's handler. We intentionally do not keep the process-wide handler installed for that case because the driver should not alter assembly resolution for the whole application on behalf of configurable retry logic.

An affected provider can reference the dependency normally so it lands in deps.json, or register a resolving handler in the application. The dedicated AssemblyLoadContext tracked in #4623 closes this case properly.

Tests

  • New SqlConfigurableRetryLogicLoaderTest (UnitTests — needs InternalsVisibleTo, which FunctionalTests does not have). Covers: no configuration, config without retryLogicType, whitespace-only retryLogicType, and an unresolvable retryLogicType — asserting in each case that no handler declared by the loader remains attached to AssemblyLoadContext.Default.Resolving.
  • New RetryLogicTypeResolution_KeepsAssemblyProbingEnabledWhileProviderIsConstructed. Plants a retry factory whose constructor asserts, from inside CreateInstance, that the handler is installed, and asserts it is gone once resolution returns — so both the add and the remove are observed, at the point in the sequence that matters.
  • New AssemblyResolutionSubscription_DisposeRemovesAssemblyProbingHandler. Directly verifies that disposing the scoped subscription removes its handler.
  • New RetryLogicProviderDoesNotEnableCurrentDirectoryAssemblyProbing (FunctionalTests). Writes a non-assembly file named <name>.dll into a temp directory, makes it the working directory, and asserts Assembly.Load reports FileNotFoundException rather than BadImageFormatException.

The directory and handler-leak tests were confirmed to fail against the pre-fix code and pass after.

Validated on net462, net8.0, net9.0, and net10.0.

Checklist

Suggested release note entry

Fixed SqlConfigurableRetryLogicLoader installing a permanent, process-wide AssemblyLoadContext.Default.Resolving handler that probed the current working directory. The handler is now installed only while a configured custom retry logic type is being resolved and constructed, and probes the application base directory. (#2214)

Related

Refs #2214, #2134
Follow-up design work tracked in #4623

SqlConfigurableRetryLogicLoader subscribed a handler to
AssemblyLoadContext.Default.Resolving in its constructor and never removed
it. Because SqlConfigurableRetryLogicManager builds that loader on the
default RetryLogicProvider path, simply reading
SqlCommand.RetryLogicProvider or SqlConnection.RetryLogicProvider installed
a permanent, process-wide assembly resolution hook.

The hook then participated in resolving every assembly the host application
failed to find, even though the application had not configured any custom
retry logic type. It also probed Environment.CurrentDirectory, which is
ambient process state unrelated to where the application's binaries live,
so assemblies could be resolved from an unintended location.

Applications observed this as load failures, and in #2214 as a stack
overflow, originating inside SqlClient for assemblies unrelated to
SqlClient.

Changes:

- Probe AppContext.BaseDirectory instead of Environment.CurrentDirectory.
- Subscribe the resolving handler only for the duration of the Type.GetType
  call in LoadType, and remove it in a finally block.
- Skip type resolution entirely when no retryLogicType is configured.
  retryLogicType is optional while retryMethod is required, so
  configurations selecting a built-in retry method previously still ran the
  custom type resolution path.

Together these mean the handler is never installed unless the application
explicitly configured a custom retry logic type, and is gone again as soon
as that type has been resolved.

Only .NET is affected; the .NET Framework code path does not use
AssemblyLoadContext.

Refs #2214, #2134

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI lite review requested due to automatic review settings August 18, 2026 09:35
@priyankatiwari08
priyankatiwari08 requested a review from a team as a code owner August 18, 2026 09:35
@github-project-automation github-project-automation Bot moved this to To triage in SqlClient Board Aug 18, 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 fixes SqlConfigurableRetryLogicLoader’s .NET AssemblyLoadContext.Default.Resolving usage so it only applies to explicit opt-in callers (custom retry-logic type resolution) and no longer probes the process working directory.

Changes:

  • Switch configurable retry-logic assembly probing from Environment.CurrentDirectory to AppContext.BaseDirectory.
  • Scope AssemblyLoadContext.Default.Resolving subscription to the duration of the Type.GetType(...) call (subscribe + finally unsubscribe).
  • Skip custom type resolution entirely when retryLogicType is not configured (use built-in factory directly).
  • Add unit + functional regression tests validating the handler is not left attached and that current-directory probing is not enabled.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/Reliability/SqlConfigurableRetryLogicLoader.cs Scopes the resolving handler to opt-in type resolution and switches probing to AppContext.BaseDirectory.
src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConfigurableRetryLogicLoaderTest.cs New unit tests validating no SqlConfigurableRetryLogicLoader resolving delegates remain attached.
src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs Adds functional regression test ensuring retry-logic initialization does not enable current-directory assembly probing.

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

@priyankatiwari08
priyankatiwari08 marked this pull request as draft August 19, 2026 09:54
@cheenamalhotra cheenamalhotra added this to the 7.1.0 milestone Aug 24, 2026

@benrr101 benrr101 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.

Since it's still a draft, leaving a comment

@priyankatiwari08
priyankatiwari08 marked this pull request as ready for review August 25, 2026 09:58
Replaces the internals-based assertions in the configurable retry logic
regression tests with a behavioural probe, and restores the UTF-8 BOM that
was dropped from the functional test file.

The unit test previously read AssemblyLoadContext's private _resolving
field to check whether a handler was still subscribed. That reflects into
runtime internals we do not own, so the value cannot simply be exposed
internally as review suggested. The functional test took a different but
also problematic approach, mutating Environment.CurrentDirectory, which is
process-wide state and unsafe under parallel test execution.

Both now plant a file that is not a valid assembly in the loader's probing
directory (AppContext.BaseDirectory) under a name no other component could
request, then assert that Assembly.Load reports it as not found. A
subscribed handler would locate that file and surface
BadImageFormatException instead, so the assertion discriminates cleanly
while observing only public behaviour and touching no shared process state.

Verified by temporarily reintroducing the unconditional subscription: all
four unit tests and the functional test fail, and pass again once removed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings August 25, 2026 10:24

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@paulmedynski paulmedynski 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.

These changes don't remove the attack surface - they narrow it. Can we do better?

Also, we're missing tests that prove we add and then remove Default_Resolving from the default ALC under all of the code paths, and that we don't add it at all in some cases.

@github-project-automation github-project-automation Bot moved this from To triage to Waiting for customer in SqlClient Board Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.65217% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 63.76%. Comparing base (bd36b43) to head (6476d99).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
...ent/Reliability/SqlConfigurableRetryLogicLoader.cs 95.65% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4547      +/-   ##
==========================================
- Coverage   64.75%   63.76%   -1.00%     
==========================================
  Files         288      284       -4     
  Lines       44418    67920   +23502     
==========================================
+ Hits        28763    43307   +14544     
- Misses      15655    24613    +8958     
Flag Coverage Δ
CI-SqlClient ?
PR-SqlClient-Project 63.76% <95.65%> (?)

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.

The existing tests only covered the code paths where the assembly probing
handler is never subscribed. The path that legitimately subscribes it, a
configured custom retry logic type that actually resolves, was untested, so
nothing verified that the handler is removed again afterwards.

Add a test that resolves a retry logic factory out of the loader's probing
directory and asserts that no probing handler remains subscribed once the
loader has been constructed. An invocation counter on the factory confirms the
configured type really was resolved and used, rather than the loader silently
falling back to the built-in factory.

Verified the test is sensitive to both behaviours it covers: pointing the
loader's probing directory elsewhere makes it fail, and restoring the
unconditional handler subscription makes it fail.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings August 26, 2026 16:05

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

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

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs:127

  • Cleanup is described as best-effort, but File.Delete can throw exceptions other than IOException (e.g., UnauthorizedAccessException). To keep this regression test from failing due to cleanup issues in restrictive environments, catch a broader exception (or explicitly include UnauthorizedAccessException) in the cleanup block.
                catch (IOException)

@mdaigle mdaigle removed their assignment Aug 26, 2026
File.Delete can fail with UnauthorizedAccessException as well as IOException.
Catching only the latter meant a cleanup failure could surface as a test
failure that had nothing to do with the behaviour under test.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings August 27, 2026 12:11

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

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@priyankatiwari08
priyankatiwari08 marked this pull request as draft August 27, 2026 12:16
@priyankatiwari08
priyankatiwari08 marked this pull request as ready for review September 1, 2026 16:46
Addresses review feedback that scoping the assembly resolving handler to type
resolution alone could break existing consumers whose configured retryLogicType
has private dependencies.

The handler is now subscribed before LoadType and removed only after
CreateInstance has run the configured type's constructor and invoked its retry
method, so dependency loads triggered during construction are still resolved.

Adds Switch.Microsoft.Data.SqlClient.UseLegacyRetryLogicAssemblyResolution as an
escape hatch that restores the process-lifetime handler. The switch restores
lifetime only; the probing directory remains AppContext.BaseDirectory, so it
cannot re-enable the binary planting vector.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings September 2, 2026 11:15

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.

🟡 Changes recommended

A few small correctness/robustness issues were found in the changed regions (whitespace-only config handling, a trace-message typo, and functional-test cleanup resilience).

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs:125

  • The test’s best-effort cleanup only catches IOException. File.Delete can also throw UnauthorizedAccessException (e.g., read-only output directory or restricted permissions), which would make the test fail for cleanup rather than behavior under test.
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

A whitespace value has no type to resolve, so it previously installed the
resolving handler, attempted resolution and then fell back to the built-in
factory. Skipping the subscription reaches the same provider without changing
assembly resolution behavior on the application's behalf.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings September 2, 2026 11:44

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.

🟡 Changes recommended

The PR description’s stated handler lifetime does not match the implementation (it remains installed through provider construction), and the description should be updated to avoid misleading future readers.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 1
  • Review effort level: Lite

@paulmedynski paulmedynski 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.

Restricting probing to the app directory is a breaking change regardless of the new app context switch. Do we need to update our API docs to explain this?

@github-project-automation github-project-automation Bot moved this from In review to Waiting for customer in SqlClient Board Sep 3, 2026
The switch restored the process-lifetime assembly resolving handler for the
one case that scoping cannot cover: a custom retry logic provider whose
private dependency is first touched after the provider has been constructed.

Shipping a supported way to permanently reinstate a process-wide handler on
AssemblyLoadContext.Default works against the point of the change. The driver
should not be altering assembly resolution for the whole application on
behalf of configurable retry logic, and an affected provider has a simple fix
of its own: reference the dependency normally so it lands in deps.json, or
register a resolving handler in the application.

The handler is now always subscribed only while a configured provider is
being resolved and constructed, and only when a custom retry logic type has
been configured.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings September 4, 2026 07:25
@priyankatiwari08

priyankatiwari08 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@paulmedynski two updates following our discussion with the team:

Handler lifetime: We will not provide a compatibility path that restores the process-wide handler. #4623 tracks the proper long-term solution.

Docs: The probing directory was never documented as a contract, so no documentation change is needed. The release note already calls out the new base-directory behavior.

Ready for another look.

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.

🟡 Changes recommended

The resolving handler can remain subscribed even after falling back to the built-in factory (and a best-effort cleanup block should catch UnauthorizedAccessException), so a small refinement is needed to fully align with the scoping goal and keep tests robust.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs:125

  • The cleanup in this functional test is intended to be best-effort, but File.Delete can throw UnauthorizedAccessException (e.g., file marked read-only or permission issues). Catch it as well so cleanup failures don’t fail the test.
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Encapsulate the temporary AssemblyLoadContext resolving handler in an
IDisposable subscription so cleanup is tied to a using scope. Remove the
handler immediately when custom type resolution falls back to the built-in
factory, and add direct unit coverage for disposal.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: e8018b20-0e43-4de9-93dd-c8080a81a801
Copilot AI review requested due to automatic review settings September 4, 2026 08:19

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.

🔵 Needs a closer look

The new functional test’s “best effort” cleanup should also swallow UnauthorizedAccessException to avoid failing due to cleanup/environment permissions rather than product behavior.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlConfigurableRetryLogicTest.cs:125

  • The cleanup in this new functional test is intended to be best-effort, but File.Delete can also throw UnauthorizedAccessException (e.g., permission/attribute issues). If that happens, the test would fail due to cleanup rather than validating the loader behavior. Consider swallowing UnauthorizedAccessException here as well (similar to the UnitTests helper).
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@paulmedynski paulmedynski 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.

Changes look good.

Here are the possible ways these changes could break existing apps:

Scenario Likelihood Legitimate Mitigation
Private deps loaded after construction Low Yes Add to deps.json or register handler in app
CurrentDirectory probing for custom types Low Questionable Move assembly to AppContext.BaseDirectory
Accidental benefit from process-wide hook Very Low No Register proper handler in app

Are we OK with this?

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

Labels

None yet

Projects

Status: Waiting for customer

Development

Successfully merging this pull request may close these issues.

7 participants