From 7873fa9c3f873de0bb5125ce1f284ab5d458128d Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 19:24:58 +0200 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=90=9B=20scope=20module=20tracker=20m?= =?UTF-8?q?utex=20to=20session=20hits=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The unload mutex was named from the hits file name without its extension, which dropped the session GUID and last namespace segment. Unrelated modules and runtimes could therefore share one mutex through a common assembly-name prefix and block each other. The mutex is now named from the full hits file name, and a regression test covers the legacy prefix collision. --- .../Instrumentation/ModuleTrackerTemplate.cs | 4 +- .../ModuleTrackerTemplateTests.cs | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs index dd9ae76f3..403999551 100644 --- a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -103,7 +103,9 @@ public static void UnloadModule(object sender, EventArgs e) // The same module can be unloaded concurrently (different AppDomains, or ProcessExit racing an // in-proc-collector call). A global mutex serialises access; FlushHitFile is cleared inside the // lock so a waiting caller sees the updated value and skips a redundant write. - using var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew); + // Hit files have no extension. Preserve the full name, including the session GUID, + // so unrelated modules and runtimes never share a mutex through an assembly-name prefix. + using var mutex = new Mutex(true, Path.GetFileName(HitsFilePath) + "_Mutex", out bool createdNew); if (!createdNew) { mutex.WaitOne(); diff --git a/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs index f3ad5b269..3f9d3ee4b 100644 --- a/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs +++ b/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -222,7 +222,7 @@ public void MutexBlocksMultipleWriters() { using var ctx = new TrackerContext(); using var mutex = new Mutex( - true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew); + true, Path.GetFileName(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew); Assert.True(createdNew); ModuleTrackerTemplate.HitsArray = [0, 1, 2, 3]; @@ -249,6 +249,42 @@ public void MutexBlocksMultipleWriters() } + [Fact] + public void CoverageSessionDoesNotShareLegacyAssemblyPrefixMutex() + { + FunctionExecutor.Run(() => + { + using var ctx = new TrackerContext(); + string assemblyPrefix = "CoverageSession" + Guid.NewGuid().ToString("N"); + ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), assemblyPrefix + ".Core_" + Guid.NewGuid()); + string otherSessionHitsFilePath = Path.Combine(Path.GetTempPath(), assemblyPrefix + ".Data_" + Guid.NewGuid()); + + // Older collectors discard the last namespace segment and session GUID. + // A writer must not share that mutex with an unrelated coverage session. + using var otherSessionMutex = new Mutex(true, Path.GetFileNameWithoutExtension(otherSessionHitsFilePath) + "_Mutex", out bool createdNew); + Assert.True(createdNew); + + ModuleTrackerTemplate.HitsArray = [1, 2, 3]; + var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null)); + bool completedWhileOtherSessionOwnedMutex; + try + { +#pragma warning disable xUnit1031 // Keep acquisition and release on the mutex-owning thread. + completedWhileOtherSessionOwnedMutex = unloadTask.Wait(TimeSpan.FromSeconds(5)); +#pragma warning restore xUnit1031 // Keep acquisition and release on the mutex-owning thread. + } + finally + { + otherSessionMutex.ReleaseMutex(); + } + + unloadTask.GetAwaiter().GetResult(); + Assert.True(completedWhileOtherSessionOwnedMutex, "An unrelated coverage session blocked the writer."); + Assert.Equal(new[] { 1, 2, 3 }, ReadHitsFile()); + return s_success; + }); + } + [Fact] public void LockFileHeldDuringWriteAndReleasedAfter() { From 229b1e8a583740e6c2824e79f23416f2110e8e69 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 19:29:51 +0200 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=92=AC=20add=20changelog=20entry=20fo?= =?UTF-8?q?r=2010.1.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record the 10.1.1 patch so the fix for module unload mutex collisions between unrelated coverage sessions is documented for users upgrading from 10.1.0. --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1982c2289..934ae75b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.1.1] - 2026-10-20 + +This is a patch release that fixes module unload mutex collisions between unrelated coverage sessions, with no new capabilities or changed public behavior beyond the fix. + +### Fixed + +- Fix unrelated coverage sessions sharing the module unload mutex by naming it from the full hit file name, with regression coverage for the legacy assembly-prefix collision. + ## [10.1.0] - 2026-09-27 This release synchronizes the Codebelt.Coverlet.MTP fork with upstream coverlet 10.1.0 fixes that apply to the narrowed MTP/core package surface while preserving the fork's multi-targeted package layout. @@ -84,5 +92,6 @@ This is a minor release focused on the Codebelt.Coverlet.MTP fork with narrowed - .vscode/settings.json configuration, - .devcontainer legacy configuration. +[10.1.1]: https://github.com/codebeltnet/coverlet/compare/v10.1.0...v10.1.1 [10.1.0]: https://github.com/codebeltnet/coverlet/compare/v10.0.1...v10.1.0 [10.0.1]: https://github.com/codebeltnet/coverlet/compare/v10.0.0...v10.0.1 From e9c891e39941984802bdf5757ac85390034fdb1e Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 20 Oct 2026 19:41:47 +0200 Subject: [PATCH 3/8] bump ubuntu --- .github/workflows/ci-pipeline.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 10e0edb8b..01c73e172 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -21,7 +21,7 @@ jobs: release_version_guard: name: validate-release-version needs: [build] - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 5 permissions: contents: read @@ -49,7 +49,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2025, macos-26] + os: [ubuntu-26.04, windows-2025, macos-26] project_name: [core, core-coverage, mtp] include: - project_name: core @@ -88,7 +88,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-24.04, windows-2025, macos-26] + os: [ubuntu-26.04, windows-2025, macos-26] framework: [net9.0, net10.0] permissions: contents: read @@ -113,7 +113,7 @@ jobs: if: ${{ github.event_name != 'pull_request' && needs.build.result == 'success' && needs.test.result == 'success' && needs.pack.result == 'success' && needs.package_validation.result == 'success' }} name: publish-nuget needs: [build, test, pack, package_validation] - runs-on: ubuntu-24.04 + runs-on: ubuntu-26.04 timeout-minutes: 15 environment: Production permissions: From aa10acdff5ea632d25e4a6ab5825c2a04d337990 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 19:53:14 +0200 Subject: [PATCH 4/8] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20microsoft=20?= =?UTF-8?q?runtime=20pins=20to=2010.0.13=20and=209.0.21?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Moves the .NET 10 and .NET 9 runtime pins to their latest patch releases so the solution builds and tests against the current runtime baselines. --- Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 8fae233d5..cfd9a4250 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -11,8 +11,8 @@ versions are not uniform across packages. Test projects stay on the newest generation for every TFM because their tooling (Microsoft.CodeAnalysis, ReportGenerator.Core) already requires the 10.0.x packages. --> - 10.0.12 - 9.0.20 + 10.0.13 + 9.0.21 $(MicrosoftNet10RuntimeVersion) $(MicrosoftNet9RuntimeVersion) 18.10.1 From 734a5440ead8510f8a83e16a94874d217841fd82 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 19:53:16 +0200 Subject: [PATCH 5/8] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20upgrade=20system.secur?= =?UTF-8?q?ity.cryptography.pkcs=20to=208.0.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns the Pkcs override used by coverlet.core with the latest 8.0 patch release. --- src/coverlet.core/coverlet.core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index 20b2b601c..8522f42c8 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -30,7 +30,7 @@ - + From 90c64945923c3a40b389e2fce7dc2e7e3e84632b Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 19:55:34 +0200 Subject: [PATCH 6/8] =?UTF-8?q?=E2=9E=95=20add=20system.security.cryptogra?= =?UTF-8?q?phy.pkcs=20to=20mtp=20validation=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit References the Pkcs package from the MTP validation test project so its tests use the same centrally managed runtime version as the rest of the solution. --- .../coverlet.MTP.validation.tests.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj index e5c92d036..c0b6cec7e 100644 --- a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj +++ b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj @@ -16,6 +16,7 @@ + From d2d057d40eb1f85d9372e635e2da1cd99a18f1a7 Mon Sep 17 00:00:00 2001 From: gimlichael Date: Tue, 20 Oct 2026 20:00:01 +0200 Subject: [PATCH 7/8] changelog --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 934ae75b8..3aa74aa89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) ## [10.1.1] - 2026-10-20 -This is a patch release that fixes module unload mutex collisions between unrelated coverage sessions, with no new capabilities or changed public behavior beyond the fix. +This is a patch release that fixes module unload mutex collisions between unrelated coverage sessions and updates the Microsoft runtime pins and the `System.Security.Cryptography.Pkcs` override used by coverlet.core, with no new capabilities. + +### Changed + +- Update the central Microsoft .NET 10 and .NET 9 runtime pins from 10.0.12 and 9.0.20 to 10.0.13 and 9.0.21, +- Update the `System.Security.Cryptography.Pkcs` override in coverlet.core from 8.0.1 to 8.0.2. ### Fixed -- Fix unrelated coverage sessions sharing the module unload mutex by naming it from the full hit file name, with regression coverage for the legacy assembly-prefix collision. +- Fix unrelated coverage sessions sharing the module unload mutex by naming it from the full hit file name instead of the extension-stripped name, with regression coverage for the legacy assembly-prefix collision. ## [10.1.0] - 2026-09-27 From 0733a287b7645eb3ae2561fac735b0cf85d6a8a8 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Tue, 20 Oct 2026 20:15:13 +0200 Subject: [PATCH 8/8] =?UTF-8?q?=E2=9C=85=20read=20runtime=20package=20vers?= =?UTF-8?q?ions=20from=20central=20props=20in=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package layout assertions for net9.0 and net10.0 hard-coded Microsoft.Extensions versions, so they went stale whenever the central package file moved. MtpPackageVersions now reads those runtime versions from Directory.Packages.props, keeping the assertions aligned with the single source of truth. --- .../PackageLayoutTests.cs | 12 +++++----- .../TestInfrastructure/MtpPackageVersions.cs | 22 +++++++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs b/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs index 9a7ec4d92..6601a9ae5 100644 --- a/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs +++ b/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs @@ -70,14 +70,14 @@ public void NuspecContainsFrameworkAlignedDependencyGroups() AssertDependency(dependencyGroups["netstandard2.0"], "System.Buffers", "4.6.1"); AssertDependency(dependencyGroups["netstandard2.0"], "System.Memory", "4.6.3"); - AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration", "9.0.20"); - AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration.Json", "9.0.20"); - AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.DependencyInjection", "9.0.20"); + AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration", MtpPackageVersions.MicrosoftNet9Runtime); + AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration.Json", MtpPackageVersions.MicrosoftNet9Runtime); + AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.DependencyInjection", MtpPackageVersions.MicrosoftNet9Runtime); AssertDependency(dependencyGroups["net9.0"], "Microsoft.Testing.Platform", MtpPackageVersions.MicrosoftTestingPlatform); - AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration", "10.0.12"); - AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration.Json", "10.0.12"); - AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.DependencyInjection", "10.0.12"); + AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration", MtpPackageVersions.MicrosoftNet10Runtime); + AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration.Json", MtpPackageVersions.MicrosoftNet10Runtime); + AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.DependencyInjection", MtpPackageVersions.MicrosoftNet10Runtime); AssertDependency(dependencyGroups["net10.0"], "Microsoft.Testing.Platform", MtpPackageVersions.MicrosoftTestingPlatform); } diff --git a/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs b/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs index 0210cf1d8..c24cab9d3 100644 --- a/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs +++ b/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs @@ -7,12 +7,12 @@ namespace Coverlet.MTP.validation.tests; /// /// Reads package versions from Directory.Packages.props at the repository root so that -/// generated test projects always reference the same versions as the central package-management file. +/// generated test projects and package assertions use the same versions as the central package-management file. /// Values are resolved lazily and cached after the first read. /// internal static class MtpPackageVersions { - private static readonly Lazy<(string MicrosoftTestingPlatform, string XunitV3)> s_versions = + private static readonly Lazy<(string MicrosoftTestingPlatform, string XunitV3, string MicrosoftNet9Runtime, string MicrosoftNet10Runtime)> s_versions = new(ReadVersionsFromProps); /// Gets the value of MicrosoftTestingPlatformVersion from Directory.Packages.props. @@ -21,7 +21,13 @@ internal static class MtpPackageVersions /// Gets the value of XunitV3Version from Directory.Packages.props. internal static string XunitV3 => s_versions.Value.XunitV3; - private static (string MicrosoftTestingPlatform, string XunitV3) ReadVersionsFromProps() + /// Gets the value of MicrosoftNet9RuntimeVersion from Directory.Packages.props. + internal static string MicrosoftNet9Runtime => s_versions.Value.MicrosoftNet9Runtime; + + /// Gets the value of MicrosoftNet10RuntimeVersion from Directory.Packages.props. + internal static string MicrosoftNet10Runtime => s_versions.Value.MicrosoftNet10Runtime; + + private static (string MicrosoftTestingPlatform, string XunitV3, string MicrosoftNet9Runtime, string MicrosoftNet10Runtime) ReadVersionsFromProps() { string? dir = AppContext.BaseDirectory; while (!string.IsNullOrEmpty(dir)) @@ -44,7 +50,15 @@ private static (string MicrosoftTestingPlatform, string XunitV3) ReadVersionsFro ?? throw new InvalidOperationException( $"Property 'XunitV3Version' not found in '{candidate}'."); - return (mtp, xunit); + string net9Runtime = GetProp("MicrosoftNet9RuntimeVersion") + ?? throw new InvalidOperationException( + $"Property 'MicrosoftNet9RuntimeVersion' not found in '{candidate}'."); + + string net10Runtime = GetProp("MicrosoftNet10RuntimeVersion") + ?? throw new InvalidOperationException( + $"Property 'MicrosoftNet10RuntimeVersion' not found in '{candidate}'."); + + return (mtp, xunit, net9Runtime, net10Runtime); } dir = Path.GetDirectoryName(dir);