();
+ if (Directory.Exists(crossgenDir))
+ args.Add($"-p:Crossgen2InBuildDir=\"{crossgenDir}\"");
+ if (File.Exists(shimProps))
+ args.Add($"-p:Crossgen2SdkOverridePropsPath=\"{shimProps}\"");
+ if (File.Exists(shimTargets))
+ args.Add($"-p:Crossgen2SdkOverrideTargetsPath=\"{shimTargets}\"");
+ return string.Join(" ", args);
+ }
+
+ private static void AssertCoreLibReadyToRun(string frameworkDir, bool expectReadyToRun)
+ {
+ string? coreLib = Directory.EnumerateFiles(frameworkDir, "System.Private.CoreLib*.wasm").FirstOrDefault();
+ Assert.True(coreLib is not null, $"Expected a System.Private.CoreLib webcil under '{frameworkDir}'.");
+
+ using FileStream stream = File.OpenRead(coreLib!);
+ bool ok = WebcilReader.TryReadWebcilInWasmSizes(stream, out _, out int tableSize, out string? failureReason);
+ Assert.True(ok, failureReason);
+
+ if (expectReadyToRun)
+ Assert.True(tableSize > 0, $"Expected a ReadyToRun table in '{coreLib}', but the R2R table size was 0.");
+ else
+ Assert.Equal(0, tableSize);
+ }
+ }
+}
diff --git a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
index b02b1d10bcbf81..fc0719e5f8610f 100644
--- a/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
+++ b/src/mono/wasm/Wasm.Build.Tests/WebcilInWasmSizesTests.cs
@@ -200,6 +200,43 @@ public void ConvertDllsToWebcil_StagesR2RWebcilWithDllExtension()
Assert.True(r2rWebcil.SequenceEqual(File.ReadAllBytes(Path.Combine(outputDirectory, "R2RAssembly.wasm"))));
}
+ [Fact]
+ public void ConvertDllsToWebcil_FallsBackToIL_WhenPrebuiltMvidMismatches()
+ {
+ // A prebuilt R2R image whose MVID differs from the candidate must never be staged: it would fail-fast
+ // at load against the current version bubble. Use two real assemblies with distinct MVIDs.
+ string candidatePath = typeof(System.Console).Assembly.Location;
+ string mismatchedAssembly = typeof(object).Assembly.Location;
+ Assert.True(File.Exists(candidatePath), $"Candidate assembly not found: '{candidatePath}'.");
+ Assert.True(File.Exists(mismatchedAssembly), $"Mismatched assembly not found: '{mismatchedAssembly}'.");
+
+ using var directory = new TempDirectory();
+ string prebuiltDirectory = Path.Combine(directory.Path, "prebuilt");
+ string outputDirectory = Path.Combine(directory.Path, "output");
+ Directory.CreateDirectory(prebuiltDirectory);
+ File.Copy(mismatchedAssembly, Path.Combine(prebuiltDirectory, "System.Console.wasm"));
+
+ var candidate = new TaskItem(candidatePath);
+ candidate.SetMetadata("RelativePath", "System.Console.dll");
+
+ var task = new ConvertDllsToWebcil
+ {
+ BuildEngine = new TestBuildEngine(),
+ Candidates = [candidate],
+ IntermediateOutputPath = Path.Combine(directory.Path, "intermediate"),
+ IsEnabled = true,
+ OutputPath = outputDirectory,
+ PrebuiltR2RDirectory = prebuiltDirectory,
+ };
+
+ Assert.True(task.Execute());
+
+ // The output must be a freshly converted IL webcil (no R2R table), not the mismatched prebuilt.
+ using FileStream output = File.OpenRead(Path.Combine(outputDirectory, "System.Console.wasm"));
+ Assert.True(WebcilReader.TryReadWebcilInWasmSizes(output, out _, out int tableSize, out string? failureReason), failureReason);
+ Assert.Equal(0, tableSize);
+ }
+
private const byte SectionCustom = 0x00;
private const byte SectionData = 0x0b;
diff --git a/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor b/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor
new file mode 100644
index 00000000000000..1664dd16d557de
--- /dev/null
+++ b/src/mono/wasm/testassets/BlazorBasicTestApp/App/Pages/Weather.razor
@@ -0,0 +1,60 @@
+@page "/weather"
+@using System.Linq
+
+Weather
+
+Weather
+
+This component demonstrates showing data.
+
+@if (forecasts == null)
+{
+ Loading...
+}
+else
+{
+
+
+
+ | Date |
+ Temp. (C) |
+ Summary |
+
+
+
+ @foreach (var forecast in forecasts)
+ {
+
+ | @forecast.Date.ToShortDateString() |
+ @forecast.TemperatureC |
+ @forecast.Summary |
+
+ }
+
+
+}
+
+@code {
+ private WeatherForecast[]? forecasts;
+
+ protected override async Task OnInitializedAsync()
+ {
+ // Async load with local data (no HttpClient) so the page is self-contained on wasm.
+ await Task.Yield();
+ var startDate = DateOnly.FromDateTime(DateTime.Now);
+ var summaries = new[] { "Freezing", "Cool", "Mild", "Warm", "Hot" };
+ forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
+ {
+ Date = startDate.AddDays(index),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = summaries[Random.Shared.Next(summaries.Length)]
+ }).ToArray();
+ }
+
+ private sealed class WeatherForecast
+ {
+ public DateOnly Date { get; set; }
+ public int TemperatureC { get; set; }
+ public string? Summary { get; set; }
+ }
+}
diff --git a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
index ffb61356b95730..d348d2bc79f0e0 100644
--- a/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
+++ b/src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ConvertDllsToWebCil.cs
@@ -260,7 +260,7 @@ private bool PrebuiltR2RMatchesCandidate(string candidateDllPath, string prebuil
return true;
}
- candidateHasILCode = AssemblyHasILCode(candidateDllPath, out Version candidateVersion);
+ candidateHasILCode = AssemblyHasILCode(candidateDllPath, out Guid candidateMvid);
if (!candidateHasILCode)
{
Log.LogMessage(MessageImportance.Low,
@@ -268,15 +268,17 @@ private bool PrebuiltR2RMatchesCandidate(string candidateDllPath, string prebuil
return false;
}
- Version prebuiltVersion = TryReadAssemblyVersion(prebuiltImagePath);
+ Guid? prebuiltMvid = TryReadMvid(prebuiltImagePath);
- // If the prebuilt identity is unreadable, keep it (prior behavior) so the common case where
- // every candidate is the pack's own assembly is never regressed.
- if (prebuiltVersion is null || candidateVersion.Equals(prebuiltVersion))
+ // Compare MVIDs, not assembly versions: with cross-module inlining every image in the bundle shares one
+ // version bubble the runtime checks by MVID at load, and the assembly version rarely changes between
+ // incremental builds, so a stale prebuilt R2R would pass a version check yet fail-fast at startup.
+ // If the prebuilt identity is unreadable, keep it (prior behavior).
+ if (prebuiltMvid is null || candidateMvid.Equals(prebuiltMvid.Value))
return true;
Log.LogMessage(MessageImportance.Normal,
- $"Not staging prebuilt R2R image '{prebuiltImagePath}' (v{prebuiltVersion}) for '{candidateDllPath}' (v{candidateVersion}): assembly version mismatch; converting IL instead.");
+ $"Not staging prebuilt R2R image '{prebuiltImagePath}' (MVID {prebuiltMvid}) for '{candidateDllPath}' (MVID {candidateMvid}): module version mismatch; converting IL instead.");
return false;
}
@@ -287,12 +289,12 @@ private static bool IsR2RWebcil(string path)
&& tableSize > 0;
}
- private static bool AssemblyHasILCode(string path, out Version version)
+ private static bool AssemblyHasILCode(string path, out Guid mvid)
{
using FileStream stream = File.OpenRead(path);
using var peReader = new PEReader(stream);
MetadataReader metadataReader = peReader.GetMetadataReader();
- version = metadataReader.GetAssemblyDefinition().Version;
+ mvid = metadataReader.GetGuid(metadataReader.GetModuleDefinition().Mvid);
foreach (MethodDefinitionHandle methodDefinitionHandle in metadataReader.MethodDefinitions)
{
@@ -305,23 +307,52 @@ private static bool AssemblyHasILCode(string path, out Version version)
return false;
}
- private static Version TryReadAssemblyVersion(string path)
+ private static Guid? TryReadMvid(string path)
{
try
{
using FileStream stream = File.OpenRead(path);
- if (path.EndsWith(Utils.WebcilInWasmExtension, StringComparison.OrdinalIgnoreCase))
+ // Detect webcil-in-wasm by content (the '\0asm' magic), not by extension: a prebuilt R2R image
+ // may still be named *.dll, and a PEReader would throw on it, returning null and silently
+ // bypassing the MVID guard.
+ if (IsWebcilInWasm(stream))
{
using var webcilReader = new WebcilReader(stream, path);
- return webcilReader.GetMetadataReader().GetAssemblyDefinition().Version;
+ MetadataReader webcilMetadata = webcilReader.GetMetadataReader();
+ return webcilMetadata.GetGuid(webcilMetadata.GetModuleDefinition().Mvid);
}
using var peReader = new PEReader(stream);
- return peReader.GetMetadataReader().GetAssemblyDefinition().Version;
+ MetadataReader peMetadata = peReader.GetMetadataReader();
+ return peMetadata.GetGuid(peMetadata.GetModuleDefinition().Mvid);
}
catch
{
return null;
}
}
+
+ // The WebAssembly module magic "\0asm" (0x00 0x61 0x73 0x6D). Leaves the stream position unchanged.
+ private static bool IsWebcilInWasm(Stream stream)
+ {
+ long position = stream.Position;
+ try
+ {
+ byte[] magic = new byte[4];
+ int read = 0;
+ while (read < magic.Length)
+ {
+ int n = stream.Read(magic, read, magic.Length - read);
+ if (n == 0)
+ return false;
+ read += n;
+ }
+
+ return magic[0] == 0x00 && magic[1] == 0x61 && magic[2] == 0x73 && magic[3] == 0x6D;
+ }
+ finally
+ {
+ stream.Position = position;
+ }
+ }
}
diff --git a/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs b/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs
index 1975b55ecd666e..6c56803b1e15af 100644
--- a/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs
+++ b/src/tasks/Microsoft.NET.WebAssembly.Webcil/WebcilReader.cs
@@ -421,6 +421,10 @@ private unsafe ImmutableArray ReadSections()
public void Dispose()
{
+ // The provider owns a memory-mapped section over _stream; leaving it to the finalizer keeps the file
+ // mapped inside long-lived MSBuild task hosts and later writers fail with "user-mapped section open".
+ _metadataReaderProvider?.Dispose();
+ _metadataReaderProvider = null;
_stream.Dispose();
}