Repository navigation
fix(824): publish ILGlobals opcode tables from an explicit static constructor - #833
Merged
Conversation
…structor Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
…kbox Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
Records the policy audit, code review and feature audit for the ILGlobals static-publication fix. Zero blocking findings; all twelve acceptance criteria evaluated PASS against the committed evidence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
drmoisan
added a commit
that referenced
this pull request
Sep 9, 2026
… gate Feature 824 merged as PR #833 (merge commit 96fd3dd), verified against gh pr view and the remote integration tip; the fan-in introduced zero deletions. The integration CI run dispatched after 823's merge concluded success, and a sixth run is dispatched against 96fd3dd. Feature 825 is the last of wave 0 and the load-bearing dependency for the single wave-1 feature, so wave 1 does not open until its merge is durably confirmed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh
drmoisan
deleted the
bug/ilglobals-loadopcodes-unsynchronised-static-race-824-exec
branch
September 12, 2026 13:53
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.
Summary
Removes the unsynchronised static-initialisation race in
ILGlobals.LoadOpCodes().Both opcode tables become
public static readonlywith no initializer. An explicit private static constructor builds eachOpCode[0x100]in a local, runs the reflection loop to completion against those locals, and assigns each field exactly once after the loop.LoadOpCodes()is retained, with the single statementRuntimeHelpers.RunClassConstructor(typeof(ILGlobals).TypeHandle);.The previous implementation reassigned a shared static table to a freshly allocated all-default array and then filled it element by element, with no lock, no
Lazy<T>, novolatileand no static constructor. Under[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)], a reader on one worker could observe a table between the reassignment and the fill. The CLR's type-initialization guarantee now serialises concurrent triggering accesses, so a thread that observes a non-null field observes the completed array, and any future assignment outside the static constructor is a compile-time CS0198 rather than a runtime race.Closes #824.
This pull request targets the epic integration branch
epic/review-residuals-2026-09-08-integration, not the default branch, so the closing keyword above will not fire automatically; closure follows when the integration branch reaches main.Changed files
Two source files:
UtilitiesCS/NewtonsoftHelpers/SDIL Reader/ILGlobals.csUtilitiesCS.Test/NewtonsoftHelpers/SDILReader/ILGlobals_Tests.csPlus this feature's plan, spec, 51 evidence artifacts and 3 audit artifacts. No project file was edited, which is the preferred AC10 outcome: the new tests went into the already-registered
ILGlobals_Tests.cs. Four further files were owned by this feature so no sibling would edit them concurrently, and all four end the change unchanged:MethodBodyReader.csat 299 lines,MethodBodyReader_Tests.csat 489 lines against the 500-line cap,UtilitiesCS.Test/Properties/AssemblyInfo.cs, andUtilitiesCS.Test/UtilitiesCS.Test.csproj.Tests
The two gates are deterministic and were red before the fix and green after. No sleep, delay, retry, timing tolerance, extra thread or repeated-run count is used anywhere, which the repository unit-test policy requires and which matters especially here because the defect is a race.
LoadOpCodes_DoesNotRepublishPublishedTablescaptures both fields into locals, callsLoadOpCodes(), and asserts reference identity with FluentAssertionsBeSameAs. Fail-before run: exit 1, TOTAL=13, FAILED=1, failing atReferenceTypeAssertions.BeSameAs.SingleByteOpCodes_FieldIsInitOnlyandMultiByteOpCodes_FieldIsInitOnlyresolve each field by reflection and assertFieldInfo.IsInitOnly. Fail-before run: exit 1, TOTAL=15, FAILED=3.The pinned totals are discovery-count controls, so a run in which a named test merely failed to appear would not satisfy the condition.
OpCodeTables_ContainEveryOpCodeDeclaredOnOpCodesreplaces two weak spot checks with an exhaustive enumeration. It passes on the unfixed tree as well, so it is a supporting test and not a gate for this defect, and it is labelled as such in the source and in every artifact that mentions it.The four existing
LoadOpCodes_*tests were reworked: two renamed toSingleByteOpCodes_IsPublishedWithFullLengthandMultiByteOpCodes_IsPublishedWithFullLengthwith theirLoadOpCodes()Act removed so that the first read of the static field is what triggers publication, and two spot checks deleted.Toolchain
Full C# toolchain, in repository order, one loop restart (CSharpier reflowed this change's own new code on the first pass, so the loop restarted at step 1 as required):
dotnet tool run csharpier format .thendotnet tool run csharpier check .— exit 0, second pass rewrote nothing.msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true— exit 0. Non-vacuity measured, not assumed: 0 skippedCoreCompile, 12CoreCompilelines, 0 diagnostics.msbuild TaskMaster.sln /t:Rebuild /m /p:Configuration=Debug "/p:Platform=Any CPU" /p:TreatWarningsAsErrors=true— exit 0. 0 skippedCoreCompile, 13CoreCompilelines, zero CS86xx errors and zero CS86xx warnings.Step 3 discharges a risk the spec recorded as unverified: a non-nullable
static readonlyfield definitely assigned in the static constructor does satisfy the compiler's null-state analysis, so no CS8618 is raised and nonull!suppression is needed.Coverage
Repository line coverage 85.6204% before, 85.6241% after; branch 79.8129%. Both the 80% floor in CLAUDE.md and the 85% floor in the general unit-test rule are met, as is the 75% branch floor.
Per class, which is what the acceptance criterion actually compares:
SDILReader.ILGlobalsSDILReader.MethodBodyReaderChanged-code coverage is 9 of 9 instrumented added lines. The only uncovered lines in the class are the unreachable
Invalid OpCode.throw, which was uncovered at baseline too.No coverage threshold, exclusion list or analyzer severity was lowered, weakened or deleted, and no production file was added to an exclusion list. No
[ExcludeFromCodeCoverage]attribute was added.Acceptance criteria
Work mode is
full-bug, sospec.mdis the sole acceptance-criteria source. All 12 of AC1..AC12 are checked off, verified against the committed tree rather than accepted from a report. An independent feature review evaluated all 12 and returned 12 PASS, 0 PARTIAL, 0 FAIL, 0 UNVERIFIED, with zero blocking findings.Known items carried forward, not silently dropped
The doc comment introduced by this change at
ILGlobals.cs:172-173saysLoadOpCodes()is retained for "the five existing call sites". That count was correct before this change; the test rework in AC7 removed three of the four call sites inILGlobals_Tests.cs, so exactly two remain. It affects no behaviour and no acceptance criterion, and the method is retained regardless of caller count. It was left unfixed deliberately rather than overlooked: editing production source after the recorded final clean toolchain pass would make that evidence no longer describe the delivered tree, which would require re-running all five gates for a comment.ILGlobals.CacheandILGlobals.modulesremain unsynchronised public mutable statics. Both are recorded as explicit non-goals in the spec, with a local follow-up record under the feature's evidence tree. A real issue is still owed for them, because feature-folder prose does not survive archival.The outcome still needs reporting to issue 811, whose acceptance criterion AC4 is blocked by this race. That is deliberately deferred until the fix reaches main. This pull request does not close 811.
The approved plan's base-anchor decision anchors every diff to the merge base with
origin/main. For an epic child based on an integration branch that merge base sits behind every already-merged sibling, so the anchored diff attributes inherited sibling paths to this feature. One plan task is left unchecked on exactly that ground rather than falsely ticked. The change footprint was instead measured against the true branch base and is exactly the owned write set. The defect is in the planning decision, not the delivery, and belongs upstream.🤖 Generated with Claude Code
https://claude.ai/code/session_01URyGon15RvDXcUEo96tvyh