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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1982c2289..3aa74aa89 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,19 @@ 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 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 instead of the extension-stripped 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 +97,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
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
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/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 @@
-
+
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);
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 @@
+
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()
{