CLR-accurate .NET Framework binder for net48 roots#154
Merged
Conversation
#153) Resolution for net48 EXEs now walks the actual CLR binder: framework unification, machine.config, publisher policy assemblies in the GAC, app.config <bindingRedirect>, then locate via GAC, the framework runtime directory under Microsoft.NET\Framework[64]\v4.0.30319, configured <codeBase> hrefs (fail-fast), and finally the application base plus <probing privatePath> with culture-aware probing. Layers chain sequentially so a 1.0 -> 2.0 app redirect followed by a 2.0 -> 3.0 machine redirect ends at 3.0; a runtime-scoped <publisherPolicy apply="no"/> suppresses publisher policy across the AppDomain. Dep graph nodes are keyed on the bound identity, so two upstream refs at different versions that both redirect to one loaded version collapse onto a single node. Per-edge RequestedIdentity records the pre-redirect version. The new ↪ glyph marks redirected nodes; × marks a configured <codeBase> whose href does not exist on disk. The same NetFxBindingContext flows through every resolution surface (dep graph builder, ResolveAssemblyByIdentity, ImplementationAssemblyResolver, IL navigation, General-tab drill-in) so all tabs agree on what the runtime would load. The forwarder chase uses the *current* assembly's metadata to recover the next hop's full identity, not the original referencer's. .NET Core / .NET 5+ behavior is unchanged when no NetFxBindingContext is built. samples/NetFxBindingRedirects (root EXE plus five helpers: OldDep, NewDep, PrivatePathLib, CodeBaseLib, CulturedLib) acts as a runtime oracle: --oracle <path> writes a JSON map of Assembly.FullName / Assembly.Location for every interesting load so tests compare the binder's output against what the actual CLR loaded. Closes #153
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc303b9f0f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…by appliesTo ResolveCodeBaseHref stripped file:// without re-attaching the leading \, so file://server/share/lib.dll was returned as the relative-looking server\share\lib.dll and File.Exists treated it as a local relative path. Switch to new Uri(href).LocalPath, which round-trips UNC and drive-letter local URIs the way the CLR does. NetFxBindingContext.ParsePrivatePaths re-walked the config XML without honoring the appliesTo attribute on the surrounding <assemblyBinding> block, so privatePath segments under e.g. appliesTo="v2.0.50727" leaked into a net48 root's probe list. Drop the duplicate parser and reuse BindingPolicy.ParseConfigFile, which already filters non-v4 blocks.
…e-dir table The first-cut roll-forward in TryFrameworkRuntimeDir misreported the location for unified binds (the file lives in the GAC, not Framework [64]\v4.0.30319), accepted compatibility-pack PKTs the CLR doesn't unify (System.ValueTuple, System.Memory family), and didn't update Loaded.Version on the mscorlib fast path so callers saw the requested version instead of the actually-loaded one. Replace it with a per-identity unification table built once at BindingPolicy.LoadFrom by scanning the architecture-correct framework runtime directory and recording (Name, PKT) -> Version for every in-box framework assembly. Apply consults this table first; matches rewrite the effective identity in either direction so the GAC scan locates the file at its real slot. Verified against live net48 PowerShell: Microsoft.VisualBasic v8.0.0.0 -> v10.0.0.0 from GAC_MSIL System.IO.Compression v4.2.0.0 -> v4.0.0.0 from GAC_MSIL mscorlib v8.0.0.0 -> v4.0.0.0 from Framework64 System.ValueTuple v4.1.0.0 -> FileNotFoundException Split the well-known framework PKT set in AssemblyAnalyzer into the broader classification set (used by IsFrameworkAssembly for the dep-graph filter) and the narrower unification set (only the in-box BCL/tooling keys). Compatibility-pack tokens cc7b13ffcd2ddd51 and adb9793829ddae60 stay in the classification set but are excluded from unification, matching the CLR's behavior for those NuGet shims.
…architecture Two functional-correctness fixes flagged in PR review: 1. The GAC walk that builds FrameworkUnificationTable was including any GAC entry whose PKT happened to match a framework public key token. Microsoft signs many non-framework assemblies (VS, SQL Server, Office PIAs, MSBuild tasks) with those PKTs, so Apply could rewrite a reference to the highest GAC version even when no real binding redirect was in play. Add LoadFrameworkAssemblyNames, which builds an allowlist from the Reference Assemblies tree (%ProgramFiles(x86)%\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.x plus its Facades subdir) — the canonical list of names that ship as part of .NET Framework. AddGacEntries now gates each candidate on that allowlist before recording it. 2. The same GAC walk scanned GAC_MSIL + GAC_64 + GAC_32 unconditionally, so a higher version installed in the cross-architecture bucket could land in the table at a path the locate stage never probes (locate only walks GAC_MSIL plus the root's matching arch). AddGacEntries now takes NetFxArchitecture and walks GAC_MSIL plus the matching slot only, so anything we record is reachable later.
willibrandon
added a commit
that referenced
this pull request
May 2, 2026
Same shape as #154 (the net48 binder), different probe locations for the CLR 2 generation: %WINDIR%\assembly\{GAC_MSIL, GAC_<arch>, GAC} with no-prefix <version>__<pkt> tokens, v2.0.50727 runtime + machine.config, and the v3.5 + v3.0 reference-assemblies trees (covers WPF/WCF). Detection runs off the mscorlib v2 AssemblyRef because pre-4.0 assemblies have no TargetFrameworkAttribute, so the existing v4-prefix gate misses them. A new NetFxRuntimeVersion enum threads through NetFxBindingContext, BindingPolicy (eight methods), and NetFxBinder.TryGac. Public LoadFrom and ParseConfigFile keep their existing call shape via a default Clr4 argument, so the v4 path stays byte-identical. DotsiderState now reads RootNetFxBindingContext for IsNetFramework and exposes EffectiveTargetFrameworkDisplay so a no-TFA root no longer shows "(unknown)" and the Dynamic-tab EventPipe gate stays correct. samples/NetFxBindingRedirects.Clr2 is the runtime oracle. SharedDep.V1/V2 share key + name so the redirect collapses through transitive UsesShared* metadata edges (not standalone Assembly.Load). app.config pins to v2.0.50727 so the oracle activates CLR 2 even on hosts with .NET 4.x present, and exercises five appliesTo variants including a v4-only poison block the binder must exclude. The fr satellite is built via the v3.5 framework csc.exe (the SDK's al.exe only emits CLR4 metadata); both the satellite-build and the root EXE's satellite Copy are gated on Exists() so Linux/macOS and Windows-without-.NET-3.5 skip cleanly. Four new test files mirror the v4 suite. Synthetic temp-tree GAC and publisher-policy tests run on any Windows host; live-runtime tests gate on Clr2RuntimePresent. The staged SharedDep identity assertion catches copy-local leaks since V1 and V2 emit identical filenames. 1024 Dotsider.Tests + 81 Mcp + 4 Website pass on all three CI runners. Fixes: #158
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For a net48 EXE whose bin folder has no
*.deps.jsonand nomscorlib.dll, the existing probe chain misses the GAC, the framework runtime directory underMicrosoft.NET\Framework[64]\v4.0.30319, and every<bindingRedirect>declared inapp.config— so the dep graph fills with?and!leaves that don't reflect what the CLR actually loads.This adds a
NetFxBinderthat walks the real .NET Framework binding order: framework unification,machine.config, publisher-policy assemblies in the GAC,app.configredirects, then locate via the GAC, the framework runtime directory, configured<codeBase>hrefs (fail-fast), and finally the application base plus<probing privatePath>with culture-aware probing. The four policy layers chain sequentially, so a1.0 → 2.0app redirect followed by a2.0 → 3.0machine redirect ends at3.0; a runtime-scoped<publisherPolicy apply="no"/>suppresses publisher policy across the AppDomain. mscorlib gets a fast path to the framework runtime directory because the CLR special-cases the bootstrap assembly even when a GAC copy exists.Dep graph nodes for net48 roots are keyed on the bound (post-policy) identity, so two upstream refs at different versions that both redirect to one loaded version collapse onto a single node — and each
GraphEdgekeeps its pre-redirectRequestedIdentityfor diagnostics. A new↪glyph marks redirected nodes;×marks a<codeBase>whosehrefdoes not exist on disk, and the configured href rides through toGraphNavigationContextso the status line surfaces it instead of(unknown).The same
NetFxBindingContextflows through every resolution surface — dep graph builder,ResolveAssemblyByIdentity,ImplementationAssemblyResolver, IL navigation, General-tab drill-in — so every tab agrees on what the runtime would load. The type-forwarder chase now reads the next hop's full identity (name, version, culture, PKT) from the current assembly's metadata, not from the original referencer's, so a forwarder pointing at a different version of an implementation lands correctly. .NET Core / .NET 5+ behavior is unchanged whenNetFxBindingContext.TryBuildreturns null.samples/NetFxBindingRedirects(root EXE plus five helpers — OldDep, NewDep, PrivatePathLib, CodeBaseLib, CulturedLib) acts as a runtime oracle. Running the EXE with--oracle <path>writes a JSON map ofAssembly.FullName/Assembly.Locationfor every interesting load, and the test fixture compares the binder's output against what the actual CLR loaded. The new tests use the test machine's real GAC andFramework[64]directory; no mocks. 950/950 tests pass.Fixes: #153