Skip to content

Fix SRI integrity failures in Blazor WASM incremental builds#52847

Closed
lewing wants to merge 1 commit intodotnet:mainfrom
lewing:wasm-compression-ordering
Closed

Fix SRI integrity failures in Blazor WASM incremental builds#52847
lewing wants to merge 1 commit intodotnet:mainfrom
lewing:wasm-compression-ordering

Conversation

@lewing
Copy link
Member

@lewing lewing commented Feb 5, 2026

Summary

Fixes dotnet/aspnetcore#65271

During incremental builds, the compression task could read stale content from the destination file (\wwwroot_framework\dotnet.js) instead of the updated source file (\obj...\dotnet.js) because the file copy hadn't happened yet.

This caused SRI integrity check failures when:

  1. _GenerateBuildWasmBootJson\ creates \obj\dotnet.js\ with NEW fingerprints
  2. \GenerateBuildCompressedStaticWebAssets\ compresses \wwwroot\dotnet.js\ (OLD)
  3. _BuildStaticWebAssetsPreserveNewest\ copies to \wwwroot\dotnet.js\ (too late)

Regression

This was caused by #52283 which changed the order to prefer \RelatedAsset\ over \RelatedAssetOriginalItemSpec. While that fixed the esproj scenario, it broke incremental builds where the source file is newer than the stale destination.

Fix

When both \RelatedAsset\ and \RelatedAssetOriginalItemSpec\ exist and point to different files, compare timestamps and prefer the newer file. This ensures the compression task uses the freshly-generated source file during incremental builds, while still preferring \RelatedAsset\ in the normal case after copying.

Testing

  • Added unit tests that verify the timestamp comparison behavior
  • Verified the key test \TryFindInputFilePath_PrefersNewerFile_WhenBothFilesExistAndOriginalItemSpecIsNewer\ fails without the fix and passes with it
  • All 9 \AssetToCompressTest\ tests pass

During incremental builds, the compression task could read stale content from
the destination file (wwwroot\_framework\dotnet.js) instead of the updated
source file (obj\...\dotnet.js) because the file copy hadn't happened yet.

This caused SRI integrity check failures when:
1. _GenerateBuildWasmBootJson creates obj\dotnet.js with NEW fingerprints
2. GenerateBuildCompressedStaticWebAssets compresses wwwroot\dotnet.js (OLD)
3. _BuildStaticWebAssetsPreserveNewest copies to wwwroot\dotnet.js (too late)

Fix: When both RelatedAsset and RelatedAssetOriginalItemSpec exist and point
to different files, compare timestamps and prefer the newer file. This ensures
the compression task uses the freshly-generated source file during incremental
builds, while still preferring RelatedAsset in the normal case after copying.

Fixes: dotnet/aspnetcore#65271
Copilot AI review requested due to automatic review settings February 5, 2026 05:44
@github-actions github-actions bot added the Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK label Feb 5, 2026
@dotnet-policy-service
Copy link
Contributor

Thanks for your PR, @@lewing.
To learn about the PR process and branching schedule of this repo, please take a look at the SDK PR Guide.

@lewing lewing requested a review from javiercn February 5, 2026 05:46
@lewing lewing added this to the 11.0.1xx milestone Feb 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes SRI (Subresource Integrity) integrity failures in Blazor WebAssembly incremental builds by ensuring the compression task uses the most recently updated asset file. The issue occurred when the compression task read stale content from the destination file because the file copy operation hadn't completed yet during incremental builds.

Changes:

  • Added timestamp comparison logic to AssetToCompress.TryFindInputFilePath that prefers the newer file when both RelatedAsset and RelatedAssetOriginalItemSpec exist and point to different files
  • Added comprehensive unit tests covering the bug scenario, normal post-copy scenario, same-file scenario, and existing edge cases
  • Updated existing tests to explicitly set timestamps to ensure deterministic behavior

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/StaticWebAssetsSdk/Tasks/Utils/AssetToCompress.cs Added timestamp comparison logic to prefer the newer file when both source and destination exist, fixing the incremental build race condition
test/Microsoft.NET.Sdk.StaticWebAssets.Tests/StaticWebAssets/AssetToCompressTest.cs Added three new tests for timestamp-based file selection and updated two existing tests to explicitly set file timestamps for deterministic behavior

Comment on lines +15 to +39
var relatedAsset = assetToCompress.GetMetadata("RelatedAsset");
var relatedAssetOriginalItemSpec = assetToCompress.GetMetadata("RelatedAssetOriginalItemSpec");

var relatedAssetExists = File.Exists(relatedAsset);
var originalItemSpecExists = File.Exists(relatedAssetOriginalItemSpec);

// When both paths exist and point to different files, prefer the newer one.
// This handles incremental builds where the source file (OriginalItemSpec) may be
// newer than the destination (RelatedAsset), which hasn't been copied yet.
if (relatedAssetExists && originalItemSpecExists &&
!string.Equals(relatedAsset, relatedAssetOriginalItemSpec, StringComparison.OrdinalIgnoreCase))
{
var relatedAssetTime = File.GetLastWriteTimeUtc(relatedAsset);
var originalItemSpecTime = File.GetLastWriteTimeUtc(relatedAssetOriginalItemSpec);

if (originalItemSpecTime > relatedAssetTime)
{
log.LogMessage(MessageImportance.Low, "Asset '{0}' using original item spec '{1}' because it is newer than '{2}'.",
assetToCompress.ItemSpec,
relatedAssetOriginalItemSpec,
relatedAsset);
fullPath = relatedAssetOriginalItemSpec;
return true;
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, no. This task shouldn't worry about incrementalism.

This is an artifact of the magic that we do on webassembly where we define the asset pointing to a non-existing place. We should do that as opposed to checking for newer files here.

this is ~4 file accesses per compression check that will get very costly soon. DefineStaticWebAssets is the only task that is "allowed" to read the file from disk to ensure we only do it once per file. Other than when we need to actually process the file.

This now points out to a bigger problem (surprised that we haven't seen it before) where the outputs from the build are considered inputs to the current one.

We need to do this on the wasm sdk, otherwise doing it here will destroy perf. One way to do this would be to delete the file at the beginning of the build when it changes, but better yet would be to stop defining these assets with an item spec in the wwwroot folder and just define them in their original location on disk.

Copy link
Member Author

@lewing lewing Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I was certain this would annoy you, I think in the common cases it only adds ~one more file exists call (both calls were already in the code the second was just short circuited) but I confess I'm not that familiar with exactly what amounts to common for these cases. The regression is unfortunate so whatever we do lets do it quickly.

@lewing
Copy link
Member Author

lewing commented Feb 7, 2026

closed in favor of dotnet/runtime#124125

@lewing lewing closed this Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-AspNetCore RazorSDK, BlazorWebAssemblySDK, StaticWebAssetsSDK

Projects

None yet

2 participants