diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index cd2df3a916bec3..aff303248151d9 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -27,7 +27,7 @@ ] }, "microsoft.dotnet.helix.jobmonitor": { - "version": "11.0.0-beta.26465.108", + "version": "11.0.0-beta.26521.105", "commands": [ "dotnet-helix-job-monitor" ] diff --git a/Directory.Build.targets b/Directory.Build.targets index 193a0cd7065bd9..ea235b6827b289 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -54,7 +54,7 @@ Microsoft%AE .NET $(ProductVersion) - $(ProductVersion) + $(ProductVersion) $(ProductVersion)-$(VersionSuffix) diff --git a/NuGet.config b/NuGet.config index 5e7e33e4a5838b..671ab38936c684 100644 --- a/NuGet.config +++ b/NuGet.config @@ -7,6 +7,11 @@ + + + + + - 5.12.0-1.26465.108 - 5.12.0-1.26465.108 - 5.12.0-1.26465.108 - 5.12.0-1.26465.108 - 11.0.100-rc.2.26465.108 - 11.0.100-rc.2.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 0.11.5-preview.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 - 11.0.0-beta.26465.108 + 5.12.0-1.26521.105 + 5.12.0-1.26521.105 + 5.12.0-1.26521.105 + 5.12.0-1.26521.105 + 11.0.100 + 11.0.100 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 0.11.5-preview.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 + 11.0.0-beta.26521.105 3.2.2-beta.26257.113 - 2.9.3-beta.26465.108 - 11.0.0-beta.26465.108 - 5.12.0-1.26465.108 - 11.0.0-rc.2.26465.108 - 11.0.0-rc.2.26465.108 - 11.0.100-rc.2.26465.108 - 11.0.0-rc.2.26465.108 - 11.0.0-rc.2.26465.108 - 7.12.0-rc.46608 - 7.12.0-rc.46608 - 7.12.0-rc.46608 - 7.12.0-rc.46608 - 11.0.0-rc.2.26465.108 - 3.0.0-rc.2.26465.108 - 11.0.0-rc.2.26465.108 - 11.0.0-rc.2.26465.108 - 11.0.0-rc.2.26465.108 + 2.9.3-beta.26521.105 + 11.0.0-beta.26521.105 + 5.12.0-1.26521.105 + 11.0.0 + 11.0.0 + 11.0.100-rtm.26521.105 + 11.0.0 + 11.0.0 + 8.0.0-rc.52305 + 8.0.0-rc.52305 + 8.0.0-rc.52305 + 8.0.0-rc.52305 + 11.0.0 + 3.0.0 + 11.0.0 + 11.0.0 + 11.0.0 11.0.0-alpha.0.26180.1 11.0.0-alpha.1.26517.3 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 - 23.1.0-alpha.1.26457.1 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 + 23.1.0-alpha.1.26505.4 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 - 11.0.0-alpha.1.26459.1 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 + 11.0.0-alpha.1.26517.3 1.0.0-prerelease.26502.1 1.0.0-prerelease.26502.1 diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index acdd090cf7715b..20b51a488ec4a4 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -1,131 +1,131 @@ - + https://github.com/dotnet/icu cc0636e50d30b71ebf043284bb66cc77a4289446 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd https://github.com/dotnet/dotnet 0eae08ed2f094f44e0151e4815e7cdd1a334fcdf - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd https://github.com/dotnet/runtime-assets @@ -179,117 +179,117 @@ https://github.com/dotnet/runtime-assets 60aef74b20edf24857020e0b7baeab78233c1196 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/llvm-project - d9efae02c571d39f4e2a50971af239603c37b0d0 + ba8b85b327886a45b1574ba7b3a1cf8bd133a087 - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd https://github.com/dotnet/xharness @@ -303,9 +303,9 @@ https://github.com/dotnet/xharness 38ed2081279c3f4dee81c933aaa9721b50a44adb - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd https://dev.azure.com/dnceng/internal/_git/dotnet-optimization @@ -331,33 +331,33 @@ https://github.com/dotnet/runtime-assets 60aef74b20edf24857020e0b7baeab78233c1196 - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd https://dev.azure.com/dnceng/internal/_git/dotnet-optimization @@ -369,53 +369,53 @@ - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e - + https://github.com/dotnet/node - 4ae8e1c1d08d09e5025db77e22a832051c56fd28 + 1ab9161aaf6814d105d3aa78c752bfd222e0946e https://github.com/dotnet/runtime-assets @@ -425,9 +425,9 @@ https://github.com/dotnet/runtime-assets 60aef74b20edf24857020e0b7baeab78233c1196 - + https://github.com/dotnet/dotnet - e5abd3f0528a8467c64a855a6ff569566d0daf27 + 6fa58a2ef810f82b3197d69763cedfb34c88b9fd diff --git a/eng/Versions.props b/eng/Versions.props index fd39d847487526..5875ac42b07077 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -14,8 +14,8 @@ 8.0.$([MSBuild]::Add($([System.Version]::Parse('$(PackageVersionNet9)').Build),11)) 7.0.20 6.0.36 - rc - 2 + rtm + false release @@ -164,7 +164,7 @@ 10.0.0-preview-20251006.1 - 2.5.10 + 2.5.12 $(MicrosoftDotNetApiCompatTaskPackageVersion) diff --git a/eng/common/Get-GitHubAppToken.ps1 b/eng/common/Get-GitHubAppToken.ps1 index ea776bd6bc28a8..ec005e487c38e2 100644 --- a/eng/common/Get-GitHubAppToken.ps1 +++ b/eng/common/Get-GitHubAppToken.ps1 @@ -1,13 +1,11 @@ # Mints a short-lived GitHub App installation access token by signing a JWT -# with a private key stored in Azure Key Vault (RSA, RS256). The signed JWT is -# exchanged with the GitHub API for a token scoped to a single installation. +# with an RSA private key (RS256). The signed JWT is exchanged with the GitHub +# API for a token scoped to a single installation. # # Requirements: -# - A GitHub App whose private key has been uploaded into Key Vault as an RSA -# key (the PEM converted to a Key Vault *key*, NOT stored as a secret). -# - The caller (the federated Azure service connection used to run this script) -# must have the `Key Vault Crypto User` role (or at minimum the `Sign` -# action) on that key. +# - A GitHub App ID and PEM private key stored as Azure Key Vault secrets. +# - The federated Azure service connection running this script must have +# `Get` access to those two secrets. # - The App must be installed on the target organization/account # (`InstallationOwner`) with the permissions/repositories it needs. # @@ -16,17 +14,17 @@ [CmdletBinding()] param( - # Name of the Key Vault that holds the GitHub App's RSA signing key. + # Name of the Key Vault holding the GitHub App credentials. [Parameter(Mandatory = $true)] [string] $KeyVaultName, - # Name of the RSA key inside the Key Vault (the App's private key). + # Secret Manager projection containing the GitHub App ID. [Parameter(Mandatory = $true)] - [string] $KeyName, + [string] $AppIdSecretName, - # The GitHub App's Client ID (the value to put in the `iss` JWT claim). + # Secret Manager projection containing the PEM private key. [Parameter(Mandatory = $true)] - [string] $AppClientId, + [string] $AppPrivateKeySecretName, # Login of the organization or user account whose installation we should # mint the token for (e.g. `dotnet`, `microsoft`). @@ -39,16 +37,69 @@ param( [Parameter(Mandatory = $false)] [string] $OutputVariableName ) - $ErrorActionPreference = 'Stop' $PSNativeCommandUseErrorActionPreference = $true . $PSScriptRoot\pipeline-logging-functions.ps1 +if ($KeyVaultName -notmatch '^[A-Za-z][A-Za-z0-9-]{1,22}[A-Za-z0-9]$' -or $KeyVaultName.Contains('--')) { + Write-PipelineTelemetryError -Category 'Build' -Message "KeyVaultName '$KeyVaultName' is not a valid Azure Key Vault name." + exit 1 +} + function ConvertTo-Base64Url([byte[]] $bytes) { return [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_') } +$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference +try { + # Azure CLI can emit non-fatal Python warnings to stderr. + $PSNativeCommandUseErrorActionPreference = $false + $keyVaultAccessToken = az account get-access-token ` + --resource https://vault.azure.net ` + --query accessToken ` + --output tsv ` + --only-show-errors + $tokenExitCode = $LASTEXITCODE +} +catch { + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to acquire an Azure Key Vault access token: $_" + exit 1 +} +finally { + $PSNativeCommandUseErrorActionPreference = $previousNativeCommandErrorPreference +} +if ($tokenExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($keyVaultAccessToken)) { + Write-PipelineTelemetryError -Category 'Build' -Message "'az account get-access-token' exited with code $tokenExitCode while acquiring an Azure Key Vault access token." + exit 1 +} + +function Get-KeyVaultSecret([string] $SecretName) { + # Use the data-plane REST API because `az keyvault secret show` can fail + # with Errno 22 on hosted Windows agents when reading these projections. + $escapedSecretName = [Uri]::EscapeDataString($SecretName) + $secretUri = "https://$KeyVaultName.vault.azure.net/secrets/$escapedSecretName`?api-version=7.4" + try { + $response = Invoke-RestMethod ` + -Uri $secretUri ` + -Headers @{ Authorization = "Bearer $keyVaultAccessToken" } ` + -Method Get + } + catch { + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to read secret '$SecretName' from vault '$KeyVaultName': $_. Verify the secret exists and the service connection has 'Key Vault Secrets User' access to it." + exit 1 + } + if ([string]::IsNullOrWhiteSpace($response.value)) { + Write-PipelineTelemetryError -Category 'Build' -Message "Secret '$SecretName' in vault '$KeyVaultName' is empty." + exit 1 + } + return [string] $response.value +} + +Write-Host "Reading GitHub App credentials from vault '$KeyVaultName'..." +$appId = Get-KeyVaultSecret $AppIdSecretName +$privateKey = Get-KeyVaultSecret $AppPrivateKeySecretName + # Build JWT header and payload. Use [ordered] hashtables so JSON # serialization is deterministic. $jwtHeader = [ordered]@{ @@ -59,46 +110,38 @@ $now = [System.DateTimeOffset]::UtcNow $jwtPayload = [ordered]@{ iat = $now.AddMinutes(-1).ToUnixTimeSeconds() exp = $now.AddMinutes(5).ToUnixTimeSeconds() - iss = $AppClientId + iss = $appId } $headerEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtHeader | ConvertTo-Json -Compress))) $payloadEncoded = ConvertTo-Base64Url ([System.Text.Encoding]::UTF8.GetBytes(($jwtPayload | ConvertTo-Json -Compress))) $signingInput = "$headerEncoded.$payloadEncoded" -# Key Vault `sign` expects the *digest* (base64), not the raw bytes. -$sha256 = [System.Security.Cryptography.SHA256]::Create() -$digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput)) -$digestBase64 = [Convert]::ToBase64String($digestBytes) +$sha256 = [System.Security.Cryptography.SHA256]::Create() +try { + $digestBytes = $sha256.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($signingInput)) +} +finally { + $sha256.Dispose() +} -Write-Host "Signing JWT with key '$KeyName' in vault '$KeyVaultName'..." -$previousNativeCommandErrorPreference = $PSNativeCommandUseErrorActionPreference +Write-Host 'Signing JWT with the GitHub App private key...' +$rsa = [System.Security.Cryptography.RSA]::Create() try { - # Azure CLI can emit non-fatal Python warnings to stderr even when signing succeeds. - # Use the exit code to determine success for this invocation. - $PSNativeCommandUseErrorActionPreference = $false - $signatureBase64 = az keyvault key sign ` - --vault-name $KeyVaultName ` - --name $KeyName ` - --algorithm RS256 ` - --digest $digestBase64 ` - --query signature ` - --output tsv ` - --only-show-errors - $signExitCode = $LASTEXITCODE + $rsa.ImportFromPem($privateKey) + $signatureBytes = $rsa.SignHash( + $digestBytes, + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) + $signatureUrl = ConvertTo-Base64Url $signatureBytes } catch { - Write-PipelineTelemetryError -Category 'Build' -Message "Failed to sign the JWT via Key Vault (key '$KeyName', vault '$KeyVaultName'): $_. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key." + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to sign the GitHub App JWT with the supplied private key: $_" exit 1 } finally { - $PSNativeCommandUseErrorActionPreference = $previousNativeCommandErrorPreference -} -if ($signExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($signatureBase64)) { - Write-PipelineTelemetryError -Category 'Build' -Message "'az keyvault key sign' exited with code $signExitCode for key '$KeyName' in vault '$KeyVaultName'. Verify the service connection identity has the 'Key Vault Crypto User' role (Sign action) on the key." - exit 1 + $rsa.Dispose() } -$signatureUrl = $signatureBase64.Trim().TrimEnd('=').Replace('+', '-').Replace('/', '_') $jwt = "$signingInput.$signatureUrl" $headers = @{ @@ -126,7 +169,7 @@ try { } while ($pageInstallationCount -eq 100) } catch { - Write-PipelineTelemetryError -Category 'Build' -Message "Failed to list GitHub App installations: $_. The signed JWT may be invalid or the App's Client ID ('$AppClientId') may be incorrect." + Write-PipelineTelemetryError -Category 'Build' -Message "Failed to list GitHub App installations: $_. The signed JWT may be invalid or the App ID may be incorrect." exit 1 } $matchingInstallations = @($installations | Where-Object { $_.account.login -ieq $InstallationOwner }) diff --git a/eng/common/core-templates/job/helix-job-monitor.yml b/eng/common/core-templates/job/helix-job-monitor.yml index 7417afdf3ea48d..ef8ee08fbd2298 100644 --- a/eng/common/core-templates/job/helix-job-monitor.yml +++ b/eng/common/core-templates/job/helix-job-monitor.yml @@ -63,7 +63,7 @@ parameters: type: boolean default: false -# Azure service connection ID authorized for Helix. Required when +# Workload identity federation service connection name authorized for Helix. Required when # useEntraAuthentication is true. - name: azureSubscription type: string @@ -94,13 +94,12 @@ parameters: type: boolean default: false -# When true, test results are reported to Azure DevOps using the fully qualified test name -# (Namespace.Type.Method) as the stable automatedTestName and the visible title is qualified as -# well (--use-fully-qualified-test-name). Opt-in because it changes AzDO test identity and display; -# primarily useful for frameworks like MSTest whose display name is only the method name. +# When true (the default), test results are reported to Azure DevOps using the fully qualified test +# name (Namespace.Type.Method) as the stable automatedTestName and the visible title is qualified as +# well (--use-fully-qualified-test-name). Set to false to preserve framework-provided display names. - name: useFullyQualifiedTestName type: boolean - default: false + default: true # Controls per-test output attachments. Defaults to Failed. - name: testResultAttachmentMode @@ -136,6 +135,20 @@ jobs: displayName: Monitor Helix Jobs timeoutInMinutes: ${{ parameters.timeoutInMinutes }} continueOnError: ${{ parameters.continueOnError }} + variables: + HELIX_MONITOR_BASE_URI: ${{ parameters.helixBaseUri }} + HELIX_MONITOR_USE_ENTRA_AUTHENTICATION: ${{ parameters.useEntraAuthentication }} + HELIX_MONITOR_POLLING_INTERVAL_SECONDS: ${{ parameters.pollingIntervalSeconds }} + HELIX_MONITOR_FAIL_ON_FAILED_TESTS: ${{ parameters.failWorkItemsWithFailedTests }} + HELIX_MONITOR_ALLOW_NO_HELIX_JOBS: ${{ parameters.allowNoHelixJobs }} + HELIX_MONITOR_USE_FULLY_QUALIFIED_TEST_NAME: ${{ parameters.useFullyQualifiedTestName }} + HELIX_MONITOR_TIMEOUT_IN_MINUTES: ${{ parameters.timeoutInMinutes }} + HELIX_MONITOR_TEST_RESULT_UPLOAD_PARALLELISM: ${{ parameters.testResultUploadParallelism }} + HELIX_MONITOR_ORGANIZATION: ${{ parameters.organization }} + HELIX_MONITOR_REPOSITORY: ${{ parameters.repository }} + HELIX_MONITOR_TEST_RESULT_ATTACHMENT_MODE: ${{ parameters.testResultAttachmentMode }} + HELIX_MONITOR_TOOL_NUPKG_ARTIFACT_NAME: ${{ parameters.toolNupkgArtifactName }} + HELIX_MONITOR_TOOL_COMMAND: ${{ parameters.toolCommand }} ${{ if ne(length(parameters.dependsOn), 0) }}: dependsOn: ${{ parameters.dependsOn }} ${{ if ne(parameters.condition, '') }}: @@ -157,22 +170,6 @@ jobs: - pwsh: throw "azureSubscription must be set when useEntraAuthentication is true." displayName: Validate Helix Entra authentication - - ${{ if eq(parameters.useEntraAuthentication, true) }}: - - task: AzureCLI@2 - displayName: Initialize Helix Entra authentication - inputs: - azureSubscription: ${{ parameters.azureSubscription }} - addSpnToEnvironment: true - scriptType: pscore - scriptLocation: inlineScript - inlineScript: | - if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) { - throw "The Helix Azure service connection did not provide a service principal or tenant ID." - } - - Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId" - Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId" - - ${{ if ne(parameters.toolNupkgArtifactName, '') }}: - task: DownloadPipelineArtifact@2 displayName: Download Helix Job Monitor artifact @@ -241,76 +238,26 @@ jobs: - bash: ./eng/common/dotnet.sh tool restore displayName: Restore Helix Job Monitor - - bash: | - set -euo pipefail - - toolArgs=( - --helix-base-uri '${{ parameters.helixBaseUri }}' - --use-entra-authentication '${{ parameters.useEntraAuthentication }}' - --polling-interval-seconds '${{ parameters.pollingIntervalSeconds }}' - --fail-on-failed-tests '${{ parameters.failWorkItemsWithFailedTests }}' - --allow-no-helix-jobs '${{ parameters.allowNoHelixJobs }}' - --use-fully-qualified-test-name '${{ parameters.useFullyQualifiedTestName }}' - --max-wait-minutes "$((${{ parameters.timeoutInMinutes }} - 5))" # Set the tool's timeout slightly lower than the Azure DevOps job timeout to allow it to exit gracefully. - --stage-name '$(System.StageName)' - --stage-attempt '$(System.StageAttempt)' - --job-attempt '$(System.JobAttempt)' - --test-result-upload-parallelism '${{ parameters.testResultUploadParallelism }}' - ) - - organization='${{ parameters.organization }}' - repository='${{ parameters.repository }}' - testResultAttachmentMode='${{ parameters.testResultAttachmentMode }}' - - # Fall back to Azure DevOps-provided environment variables when the caller did not - # supply organization / repository explicitly. BUILD_REPOSITORY_NAME is typically - # 'owner/repo' for GitHub-backed builds and 'owner-repo' for internal builds. - if [ -z "$organization" ] || [ -z "$repository" ]; then - buildRepoName="${BUILD_REPOSITORY_NAME:-}" - if [ -n "$buildRepoName" ] && [[ "$buildRepoName" == */* ]]; then - repoOwner="${buildRepoName%%/*}" - repoName="${buildRepoName#*/}" - elif [ -n "$buildRepoName" ] && [[ "$buildRepoName" == *-* ]]; then - repoOwner="${buildRepoName%%-*}" - repoName="${buildRepoName#*-}" - fi - - if [ -n "${repoOwner:-}" ] && [ -n "${repoName:-}" ]; then - if [ -z "$organization" ]; then organization="$repoOwner"; fi - if [ -z "$repository" ]; then repository="$repoName"; fi - fi - fi - - if [ -n "$organization" ]; then toolArgs+=( --organization "$organization" ); fi - if [ -n "$repository" ]; then toolArgs+=( --repository "$repository" ); fi - if [ -n "$testResultAttachmentMode" ]; then - toolArgs+=( --test-result-attachment-mode "$testResultAttachmentMode" ) - fi - - # Build.Reason and Build.SourceBranch are required to derive the Helix source filter - # the same way the Helix SDK submitter does (PR -> 'pr', internal -> 'official', - # otherwise -> 'ci'). Without these, manually-queued / scheduled / CI builds would - # be looked up under the wrong source prefix and find zero jobs. - toolArgs+=( --build-reason "$(Build.Reason)" ) - toolArgs+=( --source-branch "$(Build.SourceBranch)" ) - - if [ -n '${{ parameters.toolNupkgArtifactName }}' ]; then - # Tool was installed from a local nupkg; run the DLL via the repo-local dotnet. - export DOTNET_ROOT="$(Build.SourcesDirectory)/.dotnet" - ./eng/common/dotnet.sh exec "$(HelixJobMonitorDll)" "${toolArgs[@]}" - else - # Tool was restored from the local .config/dotnet-tools.json manifest; invoke it - # through the manifest from the repo root. - pushd "$BUILD_SOURCESDIRECTORY" > /dev/null - trap 'popd > /dev/null' EXIT - ./eng/common/dotnet.sh tool run '${{ parameters.toolCommand }}' -- "${toolArgs[@]}" - fi - displayName: Monitor Helix Jobs - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - ${{ if eq(parameters.useEntraAuthentication, false) }}: + - ${{ if eq(parameters.useEntraAuthentication, true) }}: + - task: AzureCLI@2 + displayName: Monitor Helix Jobs + inputs: + azureSubscription: ${{ parameters.azureSubscription }} + keepAzSessionActive: true + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + set -euo pipefail + + # AzureCLI@2 replaces SYSTEM_ACCESSTOKEN with the service connection token. + export SYSTEM_ACCESSTOKEN="$HELIX_AZDO_ACCESSTOKEN" + bash "$BUILD_SOURCESDIRECTORY/eng/common/run-helix-job-monitor.sh" + env: + HELIX_AZDO_ACCESSTOKEN: $(System.AccessToken) + + - ${{ if eq(parameters.useEntraAuthentication, false) }}: + - bash: bash "$BUILD_SOURCESDIRECTORY/eng/common/run-helix-job-monitor.sh" + displayName: Monitor Helix Jobs + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) HELIX_ACCESSTOKEN: ${{ parameters.helixAccessToken }} - ${{ if eq(parameters.useEntraAuthentication, true) }}: - AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId) - AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId) - AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.azureSubscription }} diff --git a/eng/common/core-templates/job/onelocbuild.yml b/eng/common/core-templates/job/onelocbuild.yml index 25af2787900719..c859dc2b071bde 100644 --- a/eng/common/core-templates/job/onelocbuild.yml +++ b/eng/common/core-templates/job/onelocbuild.yml @@ -10,9 +10,9 @@ parameters: # GitHub App authentication for the OneLoc check-in PR. GitHubAppServiceConnection: 'dnceng-oneloc-githubapp' - GitHubAppClientId: 'Iv23lijBU8x3gc9lDOc9' GitHubAppKeyVaultName: 'EngKeyVault' - GitHubAppKeyName: 'oneloc-localization-app-key' + GitHubAppIdSecretName: 'oneloc-localization-app-app-id' + GitHubAppPrivateKeySecretName: 'oneloc-localization-app-app-private-key' SourcesDirectory: $(System.DefaultWorkingDirectory) CreatePr: true @@ -102,8 +102,8 @@ jobs: ${{ else }}: azureSubscription: ${{ parameters.GitHubAppServiceConnection }} keyVaultName: ${{ parameters.GitHubAppKeyVaultName }} - keyName: ${{ parameters.GitHubAppKeyName }} - appClientId: ${{ parameters.GitHubAppClientId }} + appIdSecretName: ${{ parameters.GitHubAppIdSecretName }} + appPrivateKeySecretName: ${{ parameters.GitHubAppPrivateKeySecretName }} installationOwner: ${{ parameters.GitHubOrg }} outputVariableName: 'GitHubAppInstallationToken' condition: ${{ parameters.condition }} diff --git a/eng/common/core-templates/steps/get-github-app-token.yml b/eng/common/core-templates/steps/get-github-app-token.yml index 6d42a48d3c3671..3eeb5a4c1bc94b 100644 --- a/eng/common/core-templates/steps/get-github-app-token.yml +++ b/eng/common/core-templates/steps/get-github-app-token.yml @@ -1,13 +1,11 @@ # Mints a short-lived GitHub App installation access token by signing a JWT -# with a private key stored in Azure Key Vault (RSA, RS256). The JWT is -# exchanged with the GitHub API for a token scoped to a single installation. +# with an RSA private key (RS256). The JWT is exchanged with the GitHub API +# for a token scoped to a single installation. # # Requirements (per GitHub App you want to authenticate as): -# - A GitHub App with its private key uploaded into Key Vault as an RSA key -# (PEM converted to a key, NOT stored as a secret). -# - The Azure service connection passed via `azureSubscription` must be -# granted the `Key Vault Crypto User` role (or at minimum `Sign` action) -# on that key. +# - A GitHub App ID and PEM private key stored as Azure Key Vault secrets. +# - The Azure service connection passed via `azureSubscription` must have +# `Get` access to those two secrets. # - The App must be installed on the target organization/account # (`installationOwner`) with the permissions/repositories you need. # @@ -17,23 +15,18 @@ # enterprise classic-PAT lifetime policy. parameters: -# Azure DevOps service connection (federated) that can call -# `az keyvault key sign` on the App's signing key. +# Azure DevOps service connection (federated) that can read the App credentials. - name: azureSubscription type: string -# Name of the Key Vault that holds the GitHub App's RSA signing key. +# Name of the Key Vault holding Secret Manager's github-app-secret projections. - name: keyVaultName type: string -# Name of the RSA key inside the Key Vault (the App's private key). -- name: keyName +- name: appIdSecretName type: string -# The GitHub App's Client ID (the value to put in the `iss` JWT claim). -# Prefer this over the numeric App ID; GitHub accepts either, but Client ID -# is the documented form going forward. -- name: appClientId +- name: appPrivateKeySecretName type: string # Login of the organization or user account whose installation we should @@ -73,7 +66,7 @@ steps: inlineScript: | & "$(System.DefaultWorkingDirectory)/eng/common/Get-GitHubAppToken.ps1" ` -KeyVaultName '${{ parameters.keyVaultName }}' ` - -KeyName '${{ parameters.keyName }}' ` - -AppClientId '${{ parameters.appClientId }}' ` + -AppIdSecretName '${{ parameters.appIdSecretName }}' ` + -AppPrivateKeySecretName '${{ parameters.appPrivateKeySecretName }}' ` -InstallationOwner '${{ parameters.installationOwner }}' ` -OutputVariableName '${{ parameters.outputVariableName }}' diff --git a/eng/common/core-templates/steps/send-to-helix.yml b/eng/common/core-templates/steps/send-to-helix.yml index 62cce4d4ea2a90..dba2f1b0afadcc 100644 --- a/eng/common/core-templates/steps/send-to-helix.yml +++ b/eng/common/core-templates/steps/send-to-helix.yml @@ -6,7 +6,7 @@ parameters: HelixTargetQueues: '' # required -- semicolon-delimited list of Helix queues to test on; see https://helix.dot.net/ for a list of queues HelixAccessToken: '' # optional -- legacy access token; not forwarded when HelixUseEntraAuthentication is true HelixUseEntraAuthentication: false # optional -- use refreshable Entra authentication instead of a PAT or anonymous access - HelixAzureSubscription: '' # required when HelixUseEntraAuthentication is true -- Azure service connection ID authorized for Helix + HelixAzureSubscription: '' # required when HelixUseEntraAuthentication is true -- workload identity federation service connection name authorized for Helix HelixProjectPath: 'eng/common/helixpublish.proj' # optional -- path to the project file to build relative to BUILD_SOURCESDIRECTORY HelixProjectArguments: '' # optional -- arguments passed to the build command HelixConfiguration: '' # optional -- additional property attached to a job @@ -41,106 +41,176 @@ steps: - ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}: - task: AzureCLI@2 - displayName: Initialize Helix Entra authentication + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) inputs: azureSubscription: ${{ parameters.HelixAzureSubscription }} - addSpnToEnvironment: true + keepAzSessionActive: true scriptType: pscore scriptLocation: inlineScript inlineScript: | - if ([string]::IsNullOrWhiteSpace($env:servicePrincipalId) -or [string]::IsNullOrWhiteSpace($env:tenantId)) { - throw "The Helix Azure service connection did not provide a service principal or tenant ID." - } + # AzureCLI@2 replaces SYSTEM_ACCESSTOKEN with the service connection token. + $env:SYSTEM_ACCESSTOKEN = $env:HELIX_AZDO_ACCESSTOKEN - Write-Host "##vso[task.setvariable variable=HelixEntraClientId]$env:servicePrincipalId" - Write-Host "##vso[task.setvariable variable=HelixEntraTenantId]$env:tenantId" - condition: ${{ parameters.condition }} + $(Build.SourcesDirectory)\eng\common\msbuild.ps1 ` + $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} ` + /restore ` + /p:TreatWarningsAsErrors=false ` + /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} ` + /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} ` + ${{ parameters.HelixProjectArguments }} ` + /t:Test ` + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + HELIX_AZDO_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - task: AzureCLI@2 + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + inputs: + azureSubscription: ${{ parameters.HelixAzureSubscription }} + keepAzSessionActive: true + scriptType: bash + scriptLocation: inlineScript + inlineScript: | + # AzureCLI@2 replaces SYSTEM_ACCESSTOKEN with the service connection token. + export SYSTEM_ACCESSTOKEN="$HELIX_AZDO_ACCESSTOKEN" + + $(Build.SourcesDirectory)/eng/common/msbuild.sh \ + $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} \ + /restore \ + /p:TreatWarningsAsErrors=false \ + /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} \ + /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} \ + ${{ parameters.HelixProjectArguments }} \ + /t:Test \ + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + HELIX_AZDO_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} - - powershell: > - $(Build.SourcesDirectory)\eng\common\msbuild.ps1 - $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} - /restore - /p:TreatWarningsAsErrors=false - /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} - /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} - ${{ parameters.HelixProjectArguments }} - /t:Test - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog - displayName: ${{ parameters.DisplayNamePrefix }} (Windows) - env: - BuildConfig: $(_BuildConfig) - HelixSource: ${{ parameters.HelixSource }} - HelixType: ${{ parameters.HelixType }} - HelixBuild: ${{ parameters.HelixBuild }} - HelixConfiguration: ${{ parameters.HelixConfiguration }} - HelixTargetQueues: ${{ parameters.HelixTargetQueues }} - ${{ if eq(parameters.HelixUseEntraAuthentication, false) }}: + - ${{ if eq(parameters.HelixUseEntraAuthentication, false) }}: + - powershell: > + $(Build.SourcesDirectory)\eng\common\msbuild.ps1 + $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} + /restore + /p:TreatWarningsAsErrors=false + /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} + /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} + ${{ parameters.HelixProjectArguments }} + /t:Test + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Windows) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} HelixAccessToken: ${{ parameters.HelixAccessToken }} - ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}: - AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId) - AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId) - AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }} - HelixPreCommands: ${{ parameters.HelixPreCommands }} - HelixPostCommands: ${{ parameters.HelixPostCommands }} - WorkItemDirectory: ${{ parameters.WorkItemDirectory }} - WorkItemCommand: ${{ parameters.WorkItemCommand }} - WorkItemTimeout: ${{ parameters.WorkItemTimeout }} - CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} - XUnitProjects: ${{ parameters.XUnitProjects }} - XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} - XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} - XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} - XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} - IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} - DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} - DotNetCliVersion: ${{ parameters.DotNetCliVersion }} - WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} - HelixBaseUri: ${{ parameters.HelixBaseUri }} - Creator: ${{ parameters.Creator }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) - continueOnError: ${{ parameters.continueOnError }} - - script: > - $(Build.SourcesDirectory)/eng/common/msbuild.sh - $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} - /restore - /p:TreatWarningsAsErrors=false - /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} - /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} - ${{ parameters.HelixProjectArguments }} - /t:Test - /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog - displayName: ${{ parameters.DisplayNamePrefix }} (Unix) - env: - BuildConfig: $(_BuildConfig) - HelixSource: ${{ parameters.HelixSource }} - HelixType: ${{ parameters.HelixType }} - HelixBuild: ${{ parameters.HelixBuild }} - HelixConfiguration: ${{ parameters.HelixConfiguration }} - HelixTargetQueues: ${{ parameters.HelixTargetQueues }} - ${{ if eq(parameters.HelixUseEntraAuthentication, false) }}: + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, eq(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} + - script: > + $(Build.SourcesDirectory)/eng/common/msbuild.sh + $(Build.SourcesDirectory)/${{ parameters.HelixProjectPath }} + /restore + /p:TreatWarningsAsErrors=false + /p:EnableHelixJobMonitor=${{ parameters.UseHelixMonitor }} + /p:HelixUseEntraAuthentication=${{ parameters.HelixUseEntraAuthentication }} + ${{ parameters.HelixProjectArguments }} + /t:Test + /bl:$(Build.SourcesDirectory)/artifacts/log/$(_BuildConfig)/SendToHelix.binlog + displayName: ${{ parameters.DisplayNamePrefix }} (Unix) + env: + BuildConfig: $(_BuildConfig) + HelixSource: ${{ parameters.HelixSource }} + HelixType: ${{ parameters.HelixType }} + HelixBuild: ${{ parameters.HelixBuild }} + HelixConfiguration: ${{ parameters.HelixConfiguration }} + HelixTargetQueues: ${{ parameters.HelixTargetQueues }} HelixAccessToken: ${{ parameters.HelixAccessToken }} - ${{ if eq(parameters.HelixUseEntraAuthentication, true) }}: - AZURESUBSCRIPTION_CLIENT_ID: $(HelixEntraClientId) - AZURESUBSCRIPTION_TENANT_ID: $(HelixEntraTenantId) - AZURESUBSCRIPTION_SERVICE_CONNECTION_ID: ${{ parameters.HelixAzureSubscription }} - HelixPreCommands: ${{ parameters.HelixPreCommands }} - HelixPostCommands: ${{ parameters.HelixPostCommands }} - WorkItemDirectory: ${{ parameters.WorkItemDirectory }} - WorkItemCommand: ${{ parameters.WorkItemCommand }} - WorkItemTimeout: ${{ parameters.WorkItemTimeout }} - CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} - XUnitProjects: ${{ parameters.XUnitProjects }} - XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} - XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} - XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} - XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} - IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} - DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} - DotNetCliVersion: ${{ parameters.DotNetCliVersion }} - WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} - HelixBaseUri: ${{ parameters.HelixBaseUri }} - Creator: ${{ parameters.Creator }} - SYSTEM_ACCESSTOKEN: $(System.AccessToken) - condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) - continueOnError: ${{ parameters.continueOnError }} + HelixPreCommands: ${{ parameters.HelixPreCommands }} + HelixPostCommands: ${{ parameters.HelixPostCommands }} + WorkItemDirectory: ${{ parameters.WorkItemDirectory }} + WorkItemCommand: ${{ parameters.WorkItemCommand }} + WorkItemTimeout: ${{ parameters.WorkItemTimeout }} + CorrelationPayloadDirectory: ${{ parameters.CorrelationPayloadDirectory }} + XUnitProjects: ${{ parameters.XUnitProjects }} + XUnitWorkItemTimeout: ${{ parameters.XUnitWorkItemTimeout }} + XUnitPublishTargetFramework: ${{ parameters.XUnitPublishTargetFramework }} + XUnitRuntimeTargetFramework: ${{ parameters.XUnitRuntimeTargetFramework }} + XUnitRunnerVersion: ${{ parameters.XUnitRunnerVersion }} + IncludeDotNetCli: ${{ parameters.IncludeDotNetCli }} + DotNetCliPackageType: ${{ parameters.DotNetCliPackageType }} + DotNetCliVersion: ${{ parameters.DotNetCliVersion }} + WaitForWorkItemCompletion: ${{ parameters.WaitForWorkItemCompletion }} + HelixBaseUri: ${{ parameters.HelixBaseUri }} + Creator: ${{ parameters.Creator }} + SYSTEM_ACCESSTOKEN: $(System.AccessToken) + condition: and(${{ parameters.condition }}, ne(variables['Agent.Os'], 'Windows_NT')) + continueOnError: ${{ parameters.continueOnError }} diff --git a/eng/common/run-helix-job-monitor.sh b/eng/common/run-helix-job-monitor.sh new file mode 100644 index 00000000000000..dda140bc9a4bcc --- /dev/null +++ b/eng/common/run-helix-job-monitor.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +scriptroot="$(cd -P "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +. "$scriptroot/pipeline-logging-functions.sh" + +toolArgs=( + --helix-base-uri "$HELIX_MONITOR_BASE_URI" + --use-entra-authentication "$HELIX_MONITOR_USE_ENTRA_AUTHENTICATION" + --polling-interval-seconds "$HELIX_MONITOR_POLLING_INTERVAL_SECONDS" + --fail-on-failed-tests "$HELIX_MONITOR_FAIL_ON_FAILED_TESTS" + --allow-no-helix-jobs "$HELIX_MONITOR_ALLOW_NO_HELIX_JOBS" + --use-fully-qualified-test-name "$HELIX_MONITOR_USE_FULLY_QUALIFIED_TEST_NAME" + --max-wait-minutes "$((HELIX_MONITOR_TIMEOUT_IN_MINUTES - 5))" + --stage-name "$SYSTEM_STAGENAME" + --stage-attempt "$SYSTEM_STAGEATTEMPT" + --job-attempt "$SYSTEM_JOBATTEMPT" + --test-result-upload-parallelism "$HELIX_MONITOR_TEST_RESULT_UPLOAD_PARALLELISM" +) + +organization="${HELIX_MONITOR_ORGANIZATION:-}" +repository="${HELIX_MONITOR_REPOSITORY:-}" + +# Fall back to Azure DevOps-provided environment variables when the caller did not +# supply organization or repository explicitly. +if [ -z "$organization" ] || [ -z "$repository" ]; then + buildRepoName="${BUILD_REPOSITORY_NAME:-}" + if [ -n "$buildRepoName" ] && [[ "$buildRepoName" == */* ]]; then + repoOwner="${buildRepoName%%/*}" + repoName="${buildRepoName#*/}" + elif [ -n "$buildRepoName" ] && [[ "$buildRepoName" == *-* ]]; then + repoOwner="${buildRepoName%%-*}" + repoName="${buildRepoName#*-}" + fi + + if [ -n "${repoOwner:-}" ] && [ -n "${repoName:-}" ]; then + if [ -z "$organization" ]; then organization="$repoOwner"; fi + if [ -z "$repository" ]; then repository="$repoName"; fi + fi +fi + +if [ -n "$organization" ]; then toolArgs+=( --organization "$organization" ); fi +if [ -n "$repository" ]; then toolArgs+=( --repository "$repository" ); fi +if [ -n "${HELIX_MONITOR_TEST_RESULT_ATTACHMENT_MODE:-}" ]; then + toolArgs+=( --test-result-attachment-mode "$HELIX_MONITOR_TEST_RESULT_ATTACHMENT_MODE" ) +fi + +# These values let the monitor derive the same Helix source filter as the SDK submitter. +toolArgs+=( --build-reason "$BUILD_REASON" ) +toolArgs+=( --source-branch "$BUILD_SOURCEBRANCH" ) + +cd "$BUILD_SOURCESDIRECTORY" +exitCode=0 +if [ -n "${HELIX_MONITOR_TOOL_NUPKG_ARTIFACT_NAME:-}" ]; then + export DOTNET_ROOT="$BUILD_SOURCESDIRECTORY/.dotnet" + ./eng/common/dotnet.sh exec "$HELIXJOBMONITORDLL" "${toolArgs[@]}" || exitCode=$? +else + ./eng/common/dotnet.sh tool run "$HELIX_MONITOR_TOOL_COMMAND" -- "${toolArgs[@]}" || exitCode=$? +fi + +if [ "$exitCode" -ne 0 ]; then + Write-PipelineTelemetryError -force -category 'Helix' "Helix job monitor failed (exit code '$exitCode')." + exit "$exitCode" +fi diff --git a/eng/native/Info.plist.in b/eng/native/Info.plist.in new file mode 100644 index 00000000000000..3c70234d95104e --- /dev/null +++ b/eng/native/Info.plist.in @@ -0,0 +1,12 @@ + + + + + + CFBundleIdentifier + ${MACOS_BUNDLE_IDENTIFIER} + CFBundleName + ${targetName} + + diff --git a/eng/native/functions.cmake b/eng/native/functions.cmake index f7b11dcab7cb4c..237309bb958226 100644 --- a/eng/native/functions.cmake +++ b/eng/native/functions.cmake @@ -700,6 +700,17 @@ function(add_library_clr targetName kind) endif() endfunction() +# Call only for concrete macOS executables, not apphost templates. +function(set_macos_bundle_identifier targetName identifier) + set(MACOS_BUNDLE_IDENTIFIER "${identifier}") + set(infoPlist "${CMAKE_CURRENT_BINARY_DIR}/${targetName}.Info.plist") + configure_file("${CMAKE_CURRENT_FUNCTION_LIST_DIR}/Info.plist.in" "${infoPlist}") + + # Let signing tools use a stable identifier instead of a UUID-derived fallback. + target_link_options(${targetName} PRIVATE "LINKER:-sectcreate,__TEXT,__info_plist,${infoPlist}") + set_property(TARGET ${targetName} APPEND PROPERTY LINK_DEPENDS "${infoPlist}") +endfunction() + # Adhoc sign targetName with the entitlements in entitlementsFile. function(adhoc_sign_with_entitlements targetName entitlementsFile) # Add a dependency from a source file for the target on the entitlements file to ensure that the target is rebuilt if only the entitlements file changes. diff --git a/eng/packaging.targets b/eng/packaging.targets index e5b1ee774dbbff..2437cb5e79d608 100644 --- a/eng/packaging.targets +++ b/eng/packaging.targets @@ -52,7 +52,7 @@ - + true diff --git a/eng/testing/linker/SupportFiles/Directory.Build.props b/eng/testing/linker/SupportFiles/Directory.Build.props index 4e33801ab12837..a2a17054a1133e 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.props +++ b/eng/testing/linker/SupportFiles/Directory.Build.props @@ -2,6 +2,8 @@ true true + + false full false true diff --git a/eng/toolAot.targets b/eng/toolAot.targets index 69c54bdbd00b4b..2ad2ab58552fbd 100644 --- a/eng/toolAot.targets +++ b/eng/toolAot.targets @@ -28,4 +28,40 @@ + + + + + + + + + <_HostInfoPlist>$(NativeIntermediateOutputPath)$(TargetName).Info.plist + <_HostBundleIdentifierXml>$(HostBundleIdentifier.Replace('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Replace("'", '&apos;')) + <_HostBundleNameXml>$(HostBundleName.Replace('&', '&amp;').Replace('<', '&lt;').Replace('>', '&gt;').Replace("'", '&apos;')) + + + + + + + + + + + + + + + + + diff --git a/global.json b/global.json index 1ec0f68f3a6c5c..1c9f730454cd82 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-rc.1.26425.128", + "version": "11.0.100-rc.2.26504.105", "allowPrerelease": true, "rollForward": "major", "paths": [ @@ -10,14 +10,14 @@ "errorMessage": "The required .NET SDK wasn't found. Please run ./eng/common/dotnet.sh (Unix) or eng\\common\\dotnet.cmd (Windows) to install it." }, "tools": { - "dotnet": "11.0.100-rc.1.26425.128" + "dotnet": "11.0.100-rc.2.26504.105" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26465.108", - "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26465.108", - "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.26465.108", + "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26521.105", + "Microsoft.DotNet.Helix.Sdk": "11.0.0-beta.26521.105", + "Microsoft.DotNet.SharedFramework.Sdk": "11.0.0-beta.26521.105", "Microsoft.Build.NoTargets": "3.7.0", "Microsoft.Build.Traversal": "3.4.0", - "Microsoft.NET.Sdk.IL": "11.0.0-rc.2.26465.108" + "Microsoft.NET.Sdk.IL": "11.0.0" } } diff --git a/src/coreclr/.nuget/coreclr-packages.proj b/src/coreclr/.nuget/coreclr-packages.proj index b6b91f217ebe45..2b5b4c2e1dccec 100644 --- a/src/coreclr/.nuget/coreclr-packages.proj +++ b/src/coreclr/.nuget/coreclr-packages.proj @@ -12,8 +12,12 @@ - + + + + + diff --git a/src/coreclr/README.md b/src/coreclr/README.md index eba8ddf3a998c6..3e2dd33aa11e0f 100644 --- a/src/coreclr/README.md +++ b/src/coreclr/README.md @@ -4,6 +4,28 @@ This folder contains the source for CoreCLR, the .NET runtime implementation used by most workloads. CoreCLR tests live in [`../tests`](../tests/) (the `src/tests` subtree). +## macOS NativeAOT tool metadata + +NativeAOT tool publishing projects set `HostBundleIdentifier` and `HostBundleName` +to embed an Info.plist in macOS executables. `eng/toolAot.targets` omits the optional +bundle version keys because assembly versions do not follow Apple's bundle version +format. +Bundle identifiers use `com.microsoft.dotnet.` in lowercase; +`HostBundleName` sets `CFBundleName` to the executable name without that prefix. +Native CMake executables use the same identifier and name convention. +Both builds use `eng/native/Info.plist.in`; NativeAOT generates it with +`Microsoft.DotNet.Build.Tasks.Templating`. + +This metadata does not create an app bundle. Final signing should use the embedded +bundle identifier, or explicitly supply the same value. The linker-generated ad +hoc signature does not automatically use the embedded bundle identifier. +`AdHocSignHost` applies the identifier after native linking and symbol stripping, +before the executable is copied to its publish directory. It preserves existing +entitlements, requirements, signing flags, hardened runtime version, and launch and +library constraints while explicitly setting the identifier, without adding JIT +entitlements. Official signing must preserve the identifier and security settings +or explicitly supply the same values. + ## Building and testing See the workflow docs for the authoritative instructions — they cover the diff --git a/src/coreclr/debug/createdump/CMakeLists.txt b/src/coreclr/debug/createdump/CMakeLists.txt index bdd648c2670c3c..f3f8fbd2d475c9 100644 --- a/src/coreclr/debug/createdump/CMakeLists.txt +++ b/src/coreclr/debug/createdump/CMakeLists.txt @@ -106,6 +106,10 @@ endif(CLR_CMAKE_HOST_WIN32) target_link_libraries(createdump PRIVATE minipal) +if(CLR_CMAKE_TARGET_OSX) + set_macos_bundle_identifier(createdump com.microsoft.dotnet.createdump) +endif() + if (CLR_CMAKE_HOST_APPLE) adhoc_sign_with_entitlements(createdump "${CLR_ENG_NATIVE_DIR}/createdump-entitlements.plist") endif() diff --git a/src/coreclr/ilasm/CMakeLists.txt b/src/coreclr/ilasm/CMakeLists.txt index 4830d08cea717a..e600f9246f4550 100644 --- a/src/coreclr/ilasm/CMakeLists.txt +++ b/src/coreclr/ilasm/CMakeLists.txt @@ -75,6 +75,10 @@ add_executable_clr(ilasm ${ILASM_RESOURCES} ) +if(CLR_CMAKE_TARGET_OSX) + set_macos_bundle_identifier(ilasm com.microsoft.dotnet.ilasm) +endif() + set(ILASM_LINK_LIBRARIES utilcodestaticnohost mscorpe diff --git a/src/coreclr/ildasm/exe/CMakeLists.txt b/src/coreclr/ildasm/exe/CMakeLists.txt index a1b3291c9847ae..e6b7531ad26d33 100644 --- a/src/coreclr/ildasm/exe/CMakeLists.txt +++ b/src/coreclr/ildasm/exe/CMakeLists.txt @@ -58,6 +58,10 @@ add_executable_clr(ildasm ${ILDASM_RESOURCES} ) +if(CLR_CMAKE_TARGET_OSX) + set_macos_bundle_identifier(ildasm com.microsoft.dotnet.ildasm) +endif() + set(ILDASM_LINK_LIBRARIES mdcompiler_wks mdruntime_wks diff --git a/src/coreclr/tools/aot/ILCompiler/ILCompiler_publish.csproj b/src/coreclr/tools/aot/ILCompiler/ILCompiler_publish.csproj index 239e3f83618a1b..e6fc3bd6592d09 100644 --- a/src/coreclr/tools/aot/ILCompiler/ILCompiler_publish.csproj +++ b/src/coreclr/tools/aot/ILCompiler/ILCompiler_publish.csproj @@ -9,6 +9,8 @@ true false true + com.microsoft.dotnet.ilc + ilc true diff --git a/src/coreclr/tools/aot/crossgen2/crossgen2_publish.csproj b/src/coreclr/tools/aot/crossgen2/crossgen2_publish.csproj index f5f2fe11f65886..ccb1ceaef713f1 100644 --- a/src/coreclr/tools/aot/crossgen2/crossgen2_publish.csproj +++ b/src/coreclr/tools/aot/crossgen2/crossgen2_publish.csproj @@ -9,6 +9,8 @@ true false true + com.microsoft.dotnet.crossgen2 + crossgen2 true diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MkDirAt.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MkDirAt.cs new file mode 100644 index 00000000000000..2ba471d05f4ab7 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.MkDirAt.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_MkDirAt", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial int MkDirAt(SafeFileHandle directoryHandle, string path, int mode); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenAt.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenAt.cs new file mode 100644 index 00000000000000..18f7676e56e21c --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenAt.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_OpenAt", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial SafeFileHandle OpenAt(SafeFileHandle directoryHandle, string path, OpenFlags flags, int mode); + } +} diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs index 7ab07052048f8b..6c5fa331c599e8 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.OpenFlags.cs @@ -22,6 +22,7 @@ internal enum OpenFlags O_TRUNC = 0x0080, O_SYNC = 0x0100, O_NOFOLLOW = 0x0200, + O_DIRECTORY = 0x1000, } } } diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UnlinkAt.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UnlinkAt.cs new file mode 100644 index 00000000000000..59da84dc6dcf77 --- /dev/null +++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.UnlinkAt.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Sys + { + [Flags] + internal enum UnlinkAtFlags + { + None = 0, + AT_REMOVEDIR = 0x0001, + } + + [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_UnlinkAt", StringMarshalling = StringMarshalling.Utf8, SetLastError = true)] + internal static partial int UnlinkAt(SafeFileHandle directoryHandle, string path, UnlinkAtFlags flags); + } +} diff --git a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs index 51453a32eaccf0..c6b3d8f94a48c7 100644 --- a/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs +++ b/src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Proxy.cs @@ -782,6 +782,112 @@ public static IEnumerable CredentialsForProxy() } } + [Fact] + public async Task ProxiedRequest_HttpToHttpsRedirect_ProxyAuthNotSentToServer() + { + if (IsWinHttpHandler) + { + return; // Test targets SocketsHttpHandler proxy behavior. + } + + const string Content = "Hello world"; + var proxyCreds = new NetworkCredential("username", "password"); + + // Normal HTTP proxy that requires Basic authentication. + var proxyOptions = new LoopbackProxyServer.Options { AuthenticationSchemes = AuthenticationSchemes.Basic }; + using LoopbackProxyServer proxyServer = LoopbackProxyServer.Create(proxyOptions); + + // HTTPS server that ultimately handles the request (reached via a CONNECT tunnel). + var httpsOptions = new LoopbackServer.Options { UseSsl = true }; + await LoopbackServer.CreateServerAsync(async (httpsServer, httpsUri) => + { + // Plain HTTP server that returns a redirect to the HTTPS server (reached via plain forwarding). + await LoopbackServer.CreateServerAsync(async (httpServer, httpUri) => + { + using HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true); + handler.Proxy = new WebProxy(proxyServer.Uri) { Credentials = proxyCreds }; + using HttpClient client = CreateHttpClient(handler); + + // Client sends the request to the HTTP server through the HTTP proxy. + Task clientTask = client.GetAsync(httpUri); + + // The HTTP server responds with a redirect pointing at the HTTPS server. + await httpServer.AcceptConnectionSendResponseAndCloseAsync( + HttpStatusCode.Redirect, + additionalHeaders: $"Location: {httpsUri.AbsoluteUri}\r\n"); + + // The HTTPS server handles the redirected request and captures the headers it received. + List serverRequestHeaders = null; + await httpsServer.AcceptConnectionAsync(async connection => + { + serverRequestHeaders = await connection.ReadRequestHeaderAndSendResponseAsync(content: Content); + }); + + using HttpResponseMessage response = await clientTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Content, await response.Content.ReadAsStringAsync()); + + // The credentials used to authenticate to the proxy must never be forwarded to the origin server. + Assert.DoesNotContain(serverRequestHeaders, h => h.StartsWith("Proxy-Authorization", StringComparison.OrdinalIgnoreCase)); + }, new LoopbackServer.Options()); + }, httpsOptions); + } + + [ConditionalFact(typeof(HttpClientHandler_Proxy_Test), nameof(IsSocketsHttpHandlerAndRemoteExecutorSupported))] + public async Task ProxiedRequest_HttpToNoProxyRedirect_ProxyAuthNotSentToServer() + { + await RemoteExecutor.Invoke(async (useVersionString) => + { + const string Content = "Hello world"; + var proxyCreds = new NetworkCredential("username", "password"); + + // Normal HTTP proxy that requires Basic authentication. + var proxyOptions = new LoopbackProxyServer.Options { AuthenticationSchemes = AuthenticationSchemes.Basic }; + using LoopbackProxyServer proxyServer = LoopbackProxyServer.Create(proxyOptions); + + // HTTP server that ultimately handles the request. + await LoopbackServer.CreateServerAsync(async (httpServer2, httpUri2) => + { + // Plain HTTP server that returns a redirect to the HTTPS server (reached via plain forwarding). + await LoopbackServer.CreateServerAsync(async (httpServer1, httpUri1) => + { + Console.WriteLine($"{proxyServer.Uri}, {httpUri1}, {httpUri2}"); + Environment.SetEnvironmentVariable("http_proxy", proxyServer.Uri.AbsoluteUri.ToString()); + Environment.SetEnvironmentVariable("no_proxy", "localhost"); + + using HttpClientHandler handler = CreateHttpClientHandler(allowAllCertificates: true); + handler.DefaultProxyCredentials = proxyCreds; + using HttpClient client = CreateHttpClient(handler, useVersionString); + + // Client sends the request to the HTTP server through the HTTP proxy. + Task clientTask = client.GetAsync(httpUri1); + + // The HTTP server responds with a redirect pointing at the HTTPS server. + await httpServer1.AcceptConnectionSendResponseAndCloseAsync( + HttpStatusCode.Redirect, + additionalHeaders: $"Location: {httpUri2.Scheme}://localhost:{httpUri2.Port}\r\n"); + + // The HTTPS server handles the redirected request and captures the headers it received. + List serverRequestHeaders = null; + await httpServer2.AcceptConnectionAsync(async connection => + { + serverRequestHeaders = await connection.ReadRequestHeaderAndSendResponseAsync(content: Content); + }); + + using HttpResponseMessage response = await clientTask; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(Content, await response.Content.ReadAsStringAsync()); + + Assert.Equal(2, proxyServer.Requests.Count); + Assert.NotNull(proxyServer.Requests[1].AuthorizationHeaderValueToken); + + // The credentials used to authenticate to the proxy must never be forwarded to the origin server. + Assert.DoesNotContain(serverRequestHeaders, h => h.StartsWith("Proxy-Authorization", StringComparison.OrdinalIgnoreCase)); + }, new LoopbackServer.Options()); + }); + }, UseVersion.ToString()).DisposeAsync(); + } + private sealed class TrackDisposalProxy : IWebProxy, IDisposable { public bool Disposed; diff --git a/src/libraries/Common/tests/System/Net/Http/LoopbackProxyServer.cs b/src/libraries/Common/tests/System/Net/Http/LoopbackProxyServer.cs index 583db4d9417082..811eb14e518f50 100644 --- a/src/libraries/Common/tests/System/Net/Http/LoopbackProxyServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/LoopbackProxyServer.cs @@ -212,7 +212,9 @@ private async Task ProcessRequest(Socket clientSocket, StreamReader reader requestMessage.Headers.Add("Via", ViaHeaderValue); } - var handler = new HttpClientHandler() { UseProxy = false }; + // A forwarding proxy relays responses (including 3xx redirects) back to the client + // instead of following them itself, so disable auto-redirect on the outbound client. + var handler = new HttpClientHandler() { UseProxy = false, AllowAutoRedirect = false }; using (HttpClient outboundClient = new HttpClient(handler)) using (HttpResponseMessage response = await outboundClient.SendAsync(requestMessage).ConfigureAwait(false)) { diff --git a/src/libraries/Directory.Build.targets b/src/libraries/Directory.Build.targets index dbe74e9c4c4330..5366b60033546d 100644 --- a/src/libraries/Directory.Build.targets +++ b/src/libraries/Directory.Build.targets @@ -71,7 +71,7 @@ - <_IsWindowsDesktopApp Condition="$(WindowsDesktopCoreAppLibrary.Contains('$(AssemblyName);'))">true <_IsAspNetCoreApp Condition="$(AspNetCoreAppLibrary.Contains('$(AssemblyName);'))">true diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs index 96009d99e1054a..746cc4186161c3 100644 --- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs +++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/ProcessStartInfo.cs @@ -258,7 +258,8 @@ public string Arguments /// Only and are supported in this list. /// /// - /// Setting this property on Unix systems that do not have native support for controlling handle inheritance can severely degrade process start performance. + /// Setting this property on Unix systems that do not have native support for controlling handle inheritance + /// causes to throw . /// /// /// Handles in this list should not have inheritance enabled beforehand. diff --git a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs index 28219cbac8c37c..f160efcf97f7a7 100644 --- a/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs +++ b/src/libraries/System.Diagnostics.Process/tests/ProcessHandlesTests.Unix.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.IO; using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.XUnitExtensions; @@ -12,6 +13,117 @@ namespace System.Diagnostics.Tests { public partial class ProcessHandlesTests { + // RLIMIT_NOFILE's numeric value is Linux-specific (7); other Unix platforms use different values. + private const int RLIMIT_NOFILE = 7; + private static readonly nuint RLIM_INFINITY = nuint.MaxValue; + + // struct rlimit uses native rlim_t fields, which are pointer-sized (32-bit on 32-bit Linux). + [StructLayout(LayoutKind.Sequential)] + private struct RLimit + { + public nuint rlim_cur; + public nuint rlim_max; + } + + [DllImport("libc", SetLastError = true)] + private static extern int dup2(int oldfd, int newfd); + + [DllImport("libc", SetLastError = true)] + private static extern int close(int fd); + + [DllImport("libc", SetLastError = true)] + private static extern int getrlimit(int resource, out RLimit rlim); + + [DllImport("libc", SetLastError = true)] + private static extern int setrlimit(int resource, ref RLimit rlim); + + // Regression test for a bug where SetCloexecForAllFdsFallback() (the fallback used when + // close_range()/fdwalk() are unavailable, in pal_process.c) bounded its cleanup loop by the + // *current* RLIMIT_NOFILE soft limit. If a high-numbered fd was opened and RLIMIT_NOFILE was + // subsequently lowered below it (which does not close the already-open fd, only future opens), + // the loop would stop short of that fd, leaving FD_CLOEXEC unset on it and leaking it into + // child processes even though it wasn't in the InheritedHandles allow-list. + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.Linux)] + public void HighNumberedFd_NotLeakedAfterLoweringRLimit() + { + // Everything below -- including the RLIMIT_NOFILE change -- runs inside a RemoteExecutor-invoked + // process, so that lowering the fd limit does not affect other tests running concurrently + // in this test process. + using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(static () => + { + Assert.Equal(0, getrlimit(RLIMIT_NOFILE, out RLimit limit)); + + // Need enough headroom to safely carve out a high fd and then lower the limit below it, + // while leaving plenty of room for fds this process (and Process.Start machinery) still need. + const nuint MinRequiredLimit = 512; + if (limit.rlim_cur == RLIM_INFINITY || limit.rlim_cur < MinRequiredLimit) + { + return; // Cannot safely construct the scenario on this machine; skip. + } + + string path = Path.GetTempFileName(); + try + { + using SafeFileHandle fileHandle = File.OpenHandle(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None, FileOptions.None); + string id = GetSafeFileHandleId(fileHandle); + + // Duplicate the handle to a high fd number, just below the current soft limit. + // dup2() always clears FD_CLOEXEC on the new fd, so it starts out inheritable, + // exactly like a raw fd a user opened and wants to keep alive across the limit change. + int highFd = (int)(limit.rlim_cur - 10); + if (dup2((int)fileHandle.DangerousGetHandle(), highFd) < 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + try + { + // Lower RLIMIT_NOFILE below highFd. This does not close the already-open highFd, + // it only restricts future opens above the new limit. + RLimit newLimit = limit; + newLimit.rlim_cur = (nuint)highFd - 5; + Assert.Equal(0, setrlimit(RLIMIT_NOFILE, ref newLimit)); + + RemoteInvokeOptions options = new() { CheckExitCode = true }; + options.StartInfo.InheritedHandles = []; // restrict all handle inheritance + + using RemoteInvokeHandle grandchildHandle = RemoteExecutor.Invoke( + static (string fdStr, string fileId) => + { + int fd = int.Parse(fdStr); + using SafeFileHandle handle = new SafeFileHandle((nint)fd, ownsHandle: false); + + string? childId; + try + { + childId = GetSafeFileHandleId(handle); + } + catch (Win32Exception) + { + // Not a valid fd in the grandchild process: not leaked, as expected. + return RemoteExecutor.SuccessExitCode; + } + + Assert.NotEqual(fileId, childId); + return RemoteExecutor.SuccessExitCode; + }, + highFd.ToString(), + id, + options); + } + finally + { + close(highFd); + } + } + finally + { + File.Delete(path); + } + }); + } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] public unsafe void StartWithCallback_PosixSpawn_CanRedirectOutput() { diff --git a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx index b79340ceb97b24..c272265e6a7f8c 100644 --- a/src/libraries/System.Formats.Tar/src/Resources/Strings.resx +++ b/src/libraries/System.Formats.Tar/src/Resources/Strings.resx @@ -217,4 +217,7 @@ The file appears to be a {0} archive. TAR format expected. + + Too many levels of symbolic links in '{0}'. + diff --git a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj index d6464be9ee414b..ffec4b4ecf9829 100644 --- a/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj +++ b/src/libraries/System.Formats.Tar/src/System.Formats.Tar.csproj @@ -49,12 +49,14 @@ - - - + + + + + diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs index 3716d082b684bb..fed9ff533e8172 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs @@ -217,8 +217,8 @@ public void ExtractToFile(string destinationFileName, bool overwrite) { throw new InvalidOperationException(SR.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType)); } - // HardLink entries are rejected above. hardLinkMode will not be used. - ExtractToFileInternal(destinationFileName, linkTargetPath: null, overwrite, TarHardLinkMode.PreserveLink); + + ExtractToFileInternal(destinationFileName, linkTargetPath: null, overwrite, TarHardLinkMode.PreserveLink, pendingLinks: null); } /// @@ -252,8 +252,8 @@ public Task ExtractToFileAsync(string destinationFileName, bool overwrite, Cance { return Task.FromException(new InvalidOperationException(SR.Format(SR.TarEntryTypeNotSupportedForExtracting, EntryType))); } - // HardLink entries are rejected above. hardLinkMode will not be used. - return ExtractToFileInternalAsync(destinationFileName, linkTargetPath: null, overwrite, TarHardLinkMode.PreserveLink, cancellationToken); + + return ExtractToFileInternalAsync(destinationFileName, linkTargetPath: null, overwrite, TarHardLinkMode.PreserveLink, pendingLinks: null, cancellationToken); } /// @@ -319,49 +319,68 @@ public Stream? DataStream internal abstract bool IsDataStreamSetterSupported(); // Extracts the current entry to a location relative to the specified directory. - internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool overwrite, SortedDictionary? pendingModes, Stack<(string, DateTimeOffset)> directoryModificationTimes, TarHardLinkMode hardLinkMode) + internal void ExtractRelativeToDirectory(string destinationDirectoryPath, bool overwrite, ref TarHelpers.ExtractState extractState) { - (string destinationFullPath, string? linkTargetPath) = GetDestinationAndLinkPaths(destinationDirectoryPath); + (string destinationFullPath, string? linkTargetPath, bool deferred) = GetDestinationAndLinkPaths(destinationDirectoryPath); + + if (deferred) + { + // Create a placeholder file for the deferred symlink. + Debug.Assert(EntryType is TarEntryType.SymbolicLink); + TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, extractState.PendingModes); + CreateSymlinkPlaceholderFile(destinationFullPath, linkTargetPath!, overwrite, extractState.GetOrCreatePendingLinks()); + return; + } if (EntryType == TarEntryType.Directory) { - TarHelpers.CreateDirectory(destinationFullPath, Mode, pendingModes); - TarHelpers.UpdatePendingModificationTimes(directoryModificationTimes, destinationFullPath, ModificationTime); + TarHelpers.CreateDirectory(destinationFullPath, Mode, extractState.PendingModes); + TarHelpers.UpdatePendingModificationTimes(extractState.DirectoryModificationTimes, destinationFullPath, ModificationTime); } else { // If it is a file, create containing directory. - TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, pendingModes); - ExtractToFileInternal(destinationFullPath, linkTargetPath, overwrite, hardLinkMode); + TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, extractState.PendingModes); + ExtractToFileInternal(destinationFullPath, linkTargetPath, overwrite, extractState.HardLinkMode, extractState.PendingLinks); } } // Asynchronously extracts the current entry to a location relative to the specified directory. - internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, SortedDictionary? pendingModes, Stack<(string, DateTimeOffset)> directoryModificationTimes, TarHardLinkMode hardLinkMode, CancellationToken cancellationToken) + internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, bool overwrite, ref TarHelpers.ExtractState extractState, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - (string destinationFullPath, string? linkTargetPath) = GetDestinationAndLinkPaths(destinationDirectoryPath); + (string destinationFullPath, string? linkTargetPath, bool deferred) = GetDestinationAndLinkPaths(destinationDirectoryPath); + + if (deferred) + { + // Create a placeholder file for the deferred symlink. + Debug.Assert(EntryType is TarEntryType.SymbolicLink); + TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, extractState.PendingModes); + CreateSymlinkPlaceholderFile(destinationFullPath, linkTargetPath!, overwrite, extractState.GetOrCreatePendingLinks()); + return Task.CompletedTask; + } if (EntryType == TarEntryType.Directory) { - TarHelpers.CreateDirectory(destinationFullPath, Mode, pendingModes); - TarHelpers.UpdatePendingModificationTimes(directoryModificationTimes, destinationFullPath, ModificationTime); + TarHelpers.CreateDirectory(destinationFullPath, Mode, extractState.PendingModes); + TarHelpers.UpdatePendingModificationTimes(extractState.DirectoryModificationTimes, destinationFullPath, ModificationTime); return Task.CompletedTask; } else { // If it is a file, create containing directory. - TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, pendingModes); - return ExtractToFileInternalAsync(destinationFullPath, linkTargetPath, overwrite, hardLinkMode, cancellationToken); + TarHelpers.CreateDirectory(Path.GetDirectoryName(destinationFullPath)!, mode: null, extractState.PendingModes); + return ExtractToFileInternalAsync(destinationFullPath, linkTargetPath, overwrite, extractState.HardLinkMode, extractState.PendingLinks, cancellationToken); } } // Gets the sanitized paths for the file destination and link target paths to be used when extracting relative to a directory. - private (string, string?) GetDestinationAndLinkPaths(string destinationDirectoryPath) + // Returns deferred=true when a symlink is deferred (placeholder file created, real link created after all entries are extracted). + private (string DestinationFullPath, string? LinkTargetPath, bool Deferred) GetDestinationAndLinkPaths(string destinationDirectoryPath) { Debug.Assert(!string.IsNullOrEmpty(destinationDirectoryPath)); Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); @@ -370,7 +389,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b string? fileDestinationPath = GetFullDestinationPath( destinationDirectoryPath, Path.IsPathFullyQualified(name) ? name : Path.Join(destinationDirectoryPath, name)); - if (fileDestinationPath is null || FilePathEscapesDirectory(destinationDirectoryPath, fileDestinationPath)) + if (fileDestinationPath is null) { throw new IOException(SR.Format(SR.TarExtractingResultsFileOutside, name, destinationDirectoryPath)); } @@ -388,13 +407,17 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); } - string? linkDestination = GetFullDestinationPath( - destinationDirectoryPath, - Path.IsPathFullyQualified(linkName) ? linkName : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); - if (linkDestination is null || FilePathEscapesDirectory(destinationDirectoryPath, linkDestination)) + + if (!LinkDestinationIsWithinDirectory(destinationDirectoryPath, fileDestinationPath, linkName, out bool deferred)) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); } + + if (deferred) + { + return (fileDestinationPath, linkName, true); + } + // Use the linkName for creating the symbolic link. linkTargetPath = linkName; } @@ -406,7 +429,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, Path.Join(destinationDirectoryPath, linkName)); - if (linkDestination is null || FilePathEscapesDirectory(destinationDirectoryPath, linkDestination)) + if (linkDestination is null) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); } @@ -414,114 +437,71 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b linkTargetPath = linkDestination; } - return (fileDestinationPath, linkTargetPath); + return (fileDestinationPath, linkTargetPath, false); } // Prevent an archive from escaping the extraction root through symlinks that were created by earlier entries in the same archive. // This protection applies only to links introduced by the archive itself. It is not intended to defend against preexisting symlinks - // already present on disk before extraction - private static bool FilePathEscapesDirectory(string destinationDirectoryPath, string fileDestinationPath) + // already present on disk before extraction. + private static bool LinkDestinationIsWithinDirectory(string destinationDirectoryPath, string linkFullPath, string linkTarget, out bool deferred) { - // Windows is case insensitive while Linux is case sensitive - // This ensures the comparison is consistent with how the OS would resolve the paths - StringComparison pathComparison = OperatingSystem.IsWindows() - ? StringComparison.OrdinalIgnoreCase - : StringComparison.Ordinal; - - string resolvedDest = ResolvePhysicalPath(destinationDirectoryPath); - - // Use the logical destination path for computing the relative path - string logicalDest = Path.GetFullPath(destinationDirectoryPath); - string logicalPrefix = logicalDest.EndsWith(Path.DirectorySeparatorChar) - ? logicalDest - : logicalDest + Path.DirectorySeparatorChar; + // Symbolic links with ".." segments are tricky as they resolve against the physical location of the link and not the lexical path used to access it (which may contain symlinks to "."). + // We defer the creation of such links until after all entries are extracted, at which point we can properly validate the destination. + deferred = HasDotDotSegment(linkTarget); - string destPrefix = resolvedDest.EndsWith(Path.DirectorySeparatorChar) - ? resolvedDest - : resolvedDest + Path.DirectorySeparatorChar; - - // Normalize file path (resolves .. and . but not symlinks) - string normalizedFile = Path.GetFullPath(fileDestinationPath); - - // Guard with StartsWith before computing relative path - if (!normalizedFile.StartsWith(logicalPrefix, pathComparison) && - !normalizedFile.Equals(logicalDest, pathComparison)) + // Require the target path to lexically reside within the destination. + if (deferred || Path.IsPathFullyQualified(linkTarget)) { - return true; - } - - // Walk relative components, resolving symlinks at each step - string relative = normalizedFile.Substring(logicalPrefix.Length) - .TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); - - string[] components = relative.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, - StringSplitOptions.RemoveEmptyEntries); - - string current = resolvedDest; - - foreach (string component in components) - { - current = Path.Combine(current, component); - current = ResolveSymlink(current); - - string normalizedCurrent = Path.GetFullPath(current); - if (!normalizedCurrent.StartsWith(destPrefix, pathComparison) && - !normalizedCurrent.Equals(resolvedDest, pathComparison)) + string resolvedPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(linkFullPath)!, linkTarget)); + if (!TarHelpers.FullPathIsEqualOrNested(destinationDirectoryPath, resolvedPath)) { - return true; + return false; } } - return false; + return true; } - private static string ResolveSymlink(string path) + private static bool HasDotDotSegment(ReadOnlySpan path) { - var info = new FileInfo(path); - - // Check LinkTarget first so dangling symlinks/junctions (whose final target doesn't exist yet) - // are still resolved to their raw target, rather than being treated as a non-link. - if (info.LinkTarget is null) + foreach (Range range in path.SplitAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) { - return Path.GetFullPath(path); + if (path[range] is "..") + { + return true; + } } - FileSystemInfo target = info.ResolveLinkTarget(returnFinalTarget: true) ?? info; - return target.FullName; + return false; } - // Resolves the full path of the specified path, resolving symlinks at each step. - // This is needed to mitigate malicious entries in the archive that could lead to writing files outside of the intended directory. - private static string ResolvePhysicalPath(string path) + private static void CreateSymlinkPlaceholderFile(string filePath, string linkTarget, bool overwrite, Dictionary pendingLinks) { - string fullPath = Path.GetFullPath(path); - string? root = Path.GetPathRoot(fullPath); + // Performs the same existing-destination handling as the non-deferred path, which includes + // dropping the pending entry of a placeholder that is being overwritten. + VerifyDestinationPath(filePath, overwrite, pendingLinks); - if (root is null) + var options = new FileStreamOptions { - return fullPath; - } - - string[] components = fullPath.Substring(root.Length) - .Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries); - string current = root; - foreach (string component in components) + Mode = FileMode.CreateNew, + Access = FileAccess.Write, + Share = FileShare.None, + BufferSize = 0, + }; + if (!OperatingSystem.IsWindows()) { - current = Path.Combine(current, component); - if (Path.Exists(current)) - { - current = ResolveSymlink(current); - } + options.UnixCreateMode = UnixFileMode.None; } - - return current; + new FileStream(filePath, options).Dispose(); + FileId fileId = TarHelpers.GetFileId(filePath); + pendingLinks[fileId] = (filePath, linkTarget); } // Returns the full destination path if the path is the destinationDirectory or a subpath. Otherwise, returns null. private static string? GetFullDestinationPath(string destinationDirectoryFullPath, string qualifiedPath) { Debug.Assert(Path.IsPathFullyQualified(qualifiedPath), $"{qualifiedPath} is not qualified"); - Debug.Assert(PathInternal.EndsInDirectorySeparator(destinationDirectoryFullPath), "caller must ensure the path ends with a separator."); + Debug.Assert(destinationDirectoryFullPath[^1] == Path.DirectorySeparatorChar, "caller must ensure the path ends with a separator."); string fullPath = Path.GetFullPath(qualifiedPath); // Removes relative segments @@ -529,9 +509,9 @@ private static string ResolvePhysicalPath(string path) } // Extracts the current entry into the filesystem, regardless of the entry type. - private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool overwrite, TarHardLinkMode hardLinkMode) + private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool overwrite, TarHardLinkMode hardLinkMode, Dictionary? pendingLinks) { - VerifyDestinationPath(filePath, overwrite); + VerifyDestinationPath(filePath, overwrite, pendingLinks); if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) { @@ -544,13 +524,13 @@ private void ExtractToFileInternal(string filePath, string? linkTargetPath, bool } // Asynchronously extracts the current entry into the filesystem, regardless of the entry type. - private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, TarHardLinkMode hardLinkMode, CancellationToken cancellationToken) + private Task ExtractToFileInternalAsync(string filePath, string? linkTargetPath, bool overwrite, TarHardLinkMode hardLinkMode, Dictionary? pendingLinks, CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) { return Task.FromCanceled(cancellationToken); } - VerifyDestinationPath(filePath, overwrite); + VerifyDestinationPath(filePath, overwrite, pendingLinks); if (EntryType is TarEntryType.RegularFile or TarEntryType.V7RegularFile or TarEntryType.ContiguousFile) { @@ -634,7 +614,7 @@ private void CreateNonRegularFile(string filePath, string? linkTargetPath, TarHa } // Verifies there's a writable destination. - private static void VerifyDestinationPath(string filePath, bool overwrite) + private static void VerifyDestinationPath(string filePath, bool overwrite, Dictionary? pendingLinks) { string? directoryPath = Path.GetDirectoryName(filePath); // If the destination contains a directory segment, need to check that it exists @@ -659,6 +639,15 @@ private static void VerifyDestinationPath(string filePath, bool overwrite) { throw new IOException(SR.Format(SR.IO_AlreadyExists_Name, filePath)); } + + // If this path is a deferred symlink placeholder, drop its pending entry so it is not + // materialized later and clobber the entry that is overwriting it. Use the no-follow + // file id (TryGetPathInfo) so we match the placeholder itself, not a symlink's target. + if (pendingLinks is { Count: > 0 } && TarHelpers.TryGetPathInfo(filePath, out FileId placeholderId, out _)) + { + pendingLinks.Remove(placeholderId); + } + File.Delete(filePath); } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs index 978eb75cc6048e..1ff32904c21d00 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs @@ -650,22 +650,22 @@ private static void ExtractToDirectoryInternal(Stream source, string destination VerifyExtractToDirectoryArguments(source, destinationDirectoryFullPath); bool overwriteFiles = options.OverwriteFiles; - TarHardLinkMode hardLinkMode = options.HardLinkMode; using TarReader reader = new TarReader(source, leaveOpen); - SortedDictionary? pendingModes = TarHelpers.CreatePendingModesDictionary(); - var directoryModificationTimes = new Stack<(string, DateTimeOffset)>(); + var extractState = new TarHelpers.ExtractState() + { + HardLinkMode = options.HardLinkMode, + }; TarEntry? entry; while ((entry = reader.GetNextEntry()) != null) { if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) { - entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, pendingModes, directoryModificationTimes, hardLinkMode); + entry.ExtractRelativeToDirectory(destinationDirectoryFullPath, overwriteFiles, ref extractState); } } - TarHelpers.SetPendingModes(pendingModes); - TarHelpers.SetPendingModificationTimes(directoryModificationTimes); + extractState.Apply(destinationDirectoryFullPath); } // Asynchronously extracts the contents of a tar file into the specified directory. @@ -697,10 +697,11 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string cancellationToken.ThrowIfCancellationRequested(); bool overwriteFiles = options.OverwriteFiles; - TarHardLinkMode hardLinkMode = options.HardLinkMode; + var extractState = new TarHelpers.ExtractState() + { + HardLinkMode = options.HardLinkMode, + }; - SortedDictionary? pendingModes = TarHelpers.CreatePendingModesDictionary(); - var directoryModificationTimes = new Stack<(string, DateTimeOffset)>(); TarReader reader = new TarReader(source, leaveOpen); await using (reader.ConfigureAwait(false)) { @@ -709,12 +710,14 @@ private static async Task ExtractToDirectoryInternalAsync(Stream source, string { if (entry.EntryType is not TarEntryType.GlobalExtendedAttributes) { - await entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, pendingModes, directoryModificationTimes, hardLinkMode, cancellationToken).ConfigureAwait(false); + // Pass extractState by ref in a synchronous statement; the ref is used only synchronously + // (a ref argument cannot be part of an awaited expression). + Task extractTask = entry.ExtractRelativeToDirectoryAsync(destinationDirectoryFullPath, overwriteFiles, ref extractState, cancellationToken); + await extractTask.ConfigureAwait(false); } } } - TarHelpers.SetPendingModes(pendingModes); - TarHelpers.SetPendingModificationTimes(directoryModificationTimes); + extractState.Apply(destinationDirectoryFullPath); } [Conditional("DEBUG")] diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs index 303d2f5d65771c..c0438072c0ee01 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Unix.cs @@ -7,6 +7,22 @@ namespace System.Formats.Tar { + internal struct FileId : IEquatable + { + internal long Dev; + internal long Ino; + + internal FileId(long dev, long ino) + { + Dev = dev; + Ino = ino; + } + + public bool Equals(FileId other) => Dev == other.Dev && Ino == other.Ino; + public override bool Equals(object? obj) => obj is FileId other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Dev, Ino); + } + internal static partial class TarHelpers { private static readonly Lazy s_umask = new Lazy(DetermineUMask); @@ -114,5 +130,30 @@ internal static void SetPendingModes(SortedDictionary? pen File.SetUnixFileMode(dir.Key, dir.Value & ~umask); } } + + internal static FileId GetFileId(string filePath) + { + Interop.CheckIo(Interop.Sys.Stat(filePath, out Interop.Sys.FileStatus status), filePath); + return new FileId(status.Dev, status.Ino); + } + + internal static bool TryGetPathInfo(string path, out FileId fileId, out bool isSymlink) + { + int rv = Interop.Sys.LStat(path, out Interop.Sys.FileStatus status); + if (rv != 0) + { + Interop.ErrorInfo errorInfo = Interop.Sys.GetLastErrorInfo(); + if (errorInfo.Error == Interop.Error.ENOENT || errorInfo.Error == Interop.Error.ENOTDIR) + { + fileId = default; + isSymlink = false; + return false; + } + throw Interop.GetExceptionForIoErrno(errorInfo, path); + } + fileId = new FileId(status.Dev, status.Ino); + isSymlink = (status.Mode & Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK; + return true; + } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs index 90a3c22bff6ea9..b3f9d3f75bc1bb 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.Windows.cs @@ -4,10 +4,27 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Text; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; namespace System.Formats.Tar { + internal struct FileId : IEquatable + { + internal long VolumeSerialNumber; + internal long FileIndex; + + internal FileId(long volumeSerialNumber, long fileIndex) + { + VolumeSerialNumber = volumeSerialNumber; + FileIndex = fileIndex; + } + + public bool Equals(FileId other) => VolumeSerialNumber == other.VolumeSerialNumber && FileIndex == other.FileIndex; + public override bool Equals(object? obj) => obj is FileId other && Equals(other); + public override int GetHashCode() => HashCode.Combine(VolumeSerialNumber, FileIndex); + } + internal static partial class TarHelpers { internal static SortedDictionary? CreatePendingModesDictionary() @@ -20,5 +37,49 @@ internal static void CreateDirectory(string fullPath, UnixFileMode? mode, Sorted internal static void SetPendingModes(SortedDictionary? pendingModes) => Debug.Assert(pendingModes is null); + + internal static FileId GetFileId(string filePath) + { + using SafeFileHandle handle = File.OpenHandle(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); + if (!Interop.Kernel32.GetFileInformationByHandle(handle, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo)) + { + throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastPInvokeError(), filePath); + } + ulong fileIndex = ((ulong)fileInfo.nFileIndexHigh << 32) | fileInfo.nFileIndexLow; + return new FileId((long)fileInfo.dwVolumeSerialNumber, (long)fileIndex); + } + + internal static bool TryGetPathInfo(string path, out FileId fileId, out bool isSymlink) + { + using SafeFileHandle handle = Interop.Kernel32.CreateFile( + path, + Interop.Kernel32.GenericOperations.GENERIC_READ, + FileShare.ReadWrite | FileShare.Delete, + FileMode.Open, + Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS); + + if (handle.IsInvalid) + { + int error = Marshal.GetLastPInvokeError(); + if (error == Interop.Errors.ERROR_FILE_NOT_FOUND || error == Interop.Errors.ERROR_PATH_NOT_FOUND) + { + fileId = default; + isSymlink = false; + return false; + } + throw Win32Marshal.GetExceptionForWin32Error(error, path); + } + + if (!Interop.Kernel32.GetFileInformationByHandle(handle, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo)) + { + throw Win32Marshal.GetExceptionForWin32Error(Marshal.GetLastPInvokeError(), path); + } + + ulong fileIndex = ((ulong)fileInfo.nFileIndexHigh << 32) | fileInfo.nFileIndexLow; + fileId = new FileId((long)fileInfo.dwVolumeSerialNumber, (long)fileIndex); + const uint FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400; + isSymlink = (fileInfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0; + return true; + } } } diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs index 6d0c618853a58b..a93eb69090e697 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHelpers.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; @@ -17,9 +18,36 @@ namespace System.Formats.Tar // Static class containing a variety of helper methods. internal static partial class TarHelpers { + private const int MaxSymlinkDepth = 40; + internal const short RecordSize = 512; internal const int MaxBufferLength = 4096; + internal struct ExtractState + { + internal TarHardLinkMode HardLinkMode; + internal SortedDictionary? PendingModes; + internal Stack<(string, DateTimeOffset)> DirectoryModificationTimes; + // Allocated lazily on the first deferred symlink; most archives have none. + internal Dictionary? PendingLinks; + + public ExtractState() + { + PendingModes = CreatePendingModesDictionary(); + DirectoryModificationTimes = new(); + } + + internal Dictionary GetOrCreatePendingLinks() + => PendingLinks ??= new(); + + internal readonly void Apply(string destinationDirectoryPath) + { + CreatePendingLinks(destinationDirectoryPath, PendingLinks); + SetPendingModes(PendingModes); + SetPendingModificationTimes(DirectoryModificationTimes); + } + } + internal const UnixFileMode ValidUnixFileModes = UnixFileMode.UserRead | UnixFileMode.UserWrite | @@ -485,5 +513,297 @@ private static void AttemptDirectorySetLastWriteTime(string fullPath, DateTimeOf // Some OSes like Android might not support setting the last write time, the extraction should not fail because of that } } + + // We delayed creating some symbolic links because they might target locations outside the destination. + // Any symlink that is in the destination directory at the time this method is called is either one that points in the destination + // or a pre-existing symlink that we are allowed to follow outside. + internal static void CreatePendingLinks(string destinationDirectoryFullPath, Dictionary? pendingLinks) + { + Debug.Assert(destinationDirectoryFullPath[^1] == Path.DirectorySeparatorChar); + + if (pendingLinks is not { Count: > 0 }) + { + // No pending links, nothing to do. + return; + } + + // Resolve the destination directory against the filesystem root, following any pre-existing symlinks in its path. + string resolvedDestinationDirectoryFullPath; + int destinationSymlinkDepth; + { + string root = Path.GetPathRoot(destinationDirectoryFullPath)!; + var rootResolver = new PendingLinkResolver(root, root, destinationSymlinkDepth: 0, pendingLinks: null); + resolvedDestinationDirectoryFullPath = rootResolver.ResolveDestinationDirectory(destinationDirectoryFullPath.AsSpan(root.Length)); + destinationSymlinkDepth = rootResolver.SymlinkDepth; + + // Ensure DirectorySeparatorChar. + if (!resolvedDestinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar)) + { + resolvedDestinationDirectoryFullPath += Path.DirectorySeparatorChar; + } + } + + // Now create all pending links (that have ".." segments) + var resolver = new PendingLinkResolver(destinationDirectoryFullPath, resolvedDestinationDirectoryFullPath, destinationSymlinkDepth, pendingLinks); + + // CreateLink removes the link from pendingLinks once it is created, and it may create (and remove) several + // more when a link resolves through other pending links. Because that mutates the dictionary, we can't + // enumerate it directly, so we take a snapshot of the keys and skip the ones that were already created. + // The order of creation doesn't matter. + FileId[] fileIds = new FileId[pendingLinks.Count]; + pendingLinks.Keys.CopyTo(fileIds, 0); + + foreach (FileId fileId in fileIds) + { + if (pendingLinks.TryGetValue(fileId, out (string SymlinkPath, string LinkTarget) pendingLink)) + { + resolver.CreateLink(fileId, pendingLink.SymlinkPath, pendingLink.LinkTarget); + } + } + + Debug.Assert(pendingLinks.Count == 0); + } + + // checks if a path is equal to or nested under a full path. The full path must end with a directory separator. + // examples: + // FullPathIsEqualOrNested("C:\foo\", "C:\foo\bar") => true + // FullPathIsEqualOrNested("C:\foo\", "C:\foo") => true + // FullPathIsEqualOrNested("C:\foo\", "C:\foobar") => false + internal static bool FullPathIsEqualOrNested(string fullPath, string nestedFullPath) + { + Debug.Assert(fullPath[^1] == Path.DirectorySeparatorChar); + + return nestedFullPath.StartsWith(fullPath, PathInternal.StringComparison) + || string.Equals(nestedFullPath, fullPath.TrimEnd(Path.DirectorySeparatorChar), PathInternal.StringComparison); + } + + // Resolves the targets of deferred (pending) symbolic links produced during extraction, ensuring none of them + // resolve outside the destination directory. Pre-existing symlinks that already point outside the destination + // widen the allowed region: a pending link may point under the location such a symlink resolved to. + private sealed class PendingLinkResolver + { + // Invariant for the lifetime of the resolver. + private readonly string _destination; + private readonly string _resolvedDestination; + private readonly int _destinationSymlinkDepth; + private readonly Dictionary? _pendingLinks; + + // Reset at the start of each top-level link resolution; shared (via ref semantics) across the recursive walk. + private int _symlinkDepth; + + internal PendingLinkResolver(string destination, string resolvedDestination, int destinationSymlinkDepth, Dictionary? pendingLinks) + { + Debug.Assert(destination[^1] == Path.DirectorySeparatorChar); + + _destination = destination; + _resolvedDestination = resolvedDestination; + _destinationSymlinkDepth = destinationSymlinkDepth; + _pendingLinks = pendingLinks; + _symlinkDepth = destinationSymlinkDepth; + } + + internal int SymlinkDepth => _symlinkDepth; + + // Resolves the destination directory itself (used by RealPath). Nothing is ever outside the filesystem root. + internal string ResolveDestinationDirectory(ReadOnlySpan relativePath) + { + string? rootPath = _destination; + Resolve(ref rootPath, _destination, relativePath, out string resolvedPath, visitedLinks: null); + return resolvedPath; + } + + // Resolves and creates a single top-level pending link. + internal void CreateLink(FileId fileId, string symlinkPath, string linkTarget) + { + // Each top-level link starts a fresh resolution walk. + _symlinkDepth = _destinationSymlinkDepth; + AddPendingLink(_resolvedDestination, fileId, symlinkPath, linkTarget, visitedLinks: null, out _); + } + + private void AddPendingLink(string rootPath, FileId fileId, string symlinkPath, string linkTarget, HashSet? visitedLinks, out string resolvedFullPath) + { + Debug.Assert(rootPath is not null); + + string? rootPathLocal = rootPath; + ReadOnlySpan relativeLinkPath = symlinkPath.AsSpan(_destination.Length); + string resolvedPath = _resolvedDestination; + ReadOnlySpan directoryPath = Path.GetDirectoryName(relativeLinkPath); + if (!directoryPath.IsEmpty) + { + if (!Resolve(ref rootPathLocal, resolvedPath, directoryPath, out resolvedPath, visitedLinks)) + { + throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkTarget, _destination)); + } + Debug.Assert(rootPathLocal is not null); + } + + ReadOnlySpan resolveTarget = linkTarget; + if (Path.IsPathFullyQualified(linkTarget)) + { + rootPathLocal = _resolvedDestination; + resolvedPath = Path.GetPathRoot(linkTarget)!; + resolveTarget = linkTarget.AsSpan(resolvedPath.Length); + } + + if (!Resolve(ref rootPathLocal, resolvedPath, resolveTarget, out resolvedFullPath, visitedLinks)) + { + throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkTarget, _destination)); + } + Debug.Assert(rootPathLocal is not null); + + if (_pendingLinks!.Remove(fileId)) + { + File.Delete(symlinkPath); + File.CreateSymbolicLink(symlinkPath, linkTarget); + } + } + + // Returns whether the path formed by combining baseFullPath and relativePath resides in either the root path or the destination path. + // The containmentRootPath path tracks a location that may not be in the destination path but which is reachable (via preexisting links) + // and considered a root that a delayed link target may point under. + private bool Resolve(ref string? containmentRootPath, string baseFullPath, ReadOnlySpan relativePath, out string resolvedFullPath, HashSet? visitedLinks) + { + resolvedFullPath = baseFullPath; + bool checkExists = true; + + // walk the relative path and resolve each component, following symlinks and checking for loops. + foreach (Range range in relativePath.SplitAny(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)) + { + ReadOnlySpan component = relativePath[range]; + if (component.IsEmpty || component is ".") + { + continue; + } + if (component is "..") + { + // Traverse to the parent. Strip any trailing separators first, otherwise + // GetDirectoryName only strips them and doesn't move up a level. + ReadOnlySpan resolvedFullPathSpan = resolvedFullPath.AsSpan(); + while (!resolvedFullPathSpan.IsEmpty && PathInternal.IsDirectorySeparator(resolvedFullPathSpan[^1])) + { + resolvedFullPathSpan = resolvedFullPathSpan[..^1]; + } + + // GetDirectoryName returns null at the root, map back to root (.. on / yields /). + resolvedFullPathSpan = Path.GetDirectoryName(resolvedFullPathSpan); + resolvedFullPath = resolvedFullPathSpan.IsEmpty ? resolvedFullPath : resolvedFullPathSpan.ToString(); + + if (containmentRootPath is not null && !IsInRootOrDestination(containmentRootPath, resolvedFullPath)) + { + return false; + } + continue; + } + + string componentPath = Path.Join(resolvedFullPath, component); + + // TryGetPathInfo throws on error and returns false when the path does not exist. + if (!checkExists || !TryGetPathInfo(componentPath, out FileId fileId, out bool isSymlink)) + { + // Path doesn't exist; remaining components can't exist either. + // short-circuit to simply appending the remaining components to the path for the final check. + resolvedFullPath = componentPath; + checkExists = false; + continue; + } + + string? linkTarget = null; + (string SymlinkPath, string LinkTarget) pendingLink = default; + if (isSymlink) + { + linkTarget = new FileInfo(componentPath).LinkTarget; + } + else if (_pendingLinks?.TryGetValue(fileId, out pendingLink) == true) + { + linkTarget = pendingLink.LinkTarget; + } + + if (linkTarget is null) + { + resolvedFullPath = componentPath; + continue; + } + + if (++_symlinkDepth > MaxSymlinkDepth) + { + throw new IOException(SR.Format(SR.IO_TooManySymbolicLinkLevels, componentPath)); + } + + // To allow creating symbolic links that cause loops we need to detect the loops. + // visitedLinks tracks the links we've followed. + visitedLinks ??= new HashSet(); + if (!visitedLinks.Add(fileId)) + { + // Already visited. + visitedLinks.Clear(); // Clear the set so callers detect circularity via Remove returning false below. + resolvedFullPath = componentPath; + break; + } + + if (isSymlink) + { + if (!FollowExistingSymlink(ref containmentRootPath, resolvedFullPath, linkTarget, visitedLinks, out resolvedFullPath)) + { + Debug.Fail("Should be able to follow existing symlinks"); + return false; + } + } + else + { + // Pending link. A fully qualified target must point inside the destination. + if (Path.IsPathFullyQualified(linkTarget)) + { + containmentRootPath = _resolvedDestination; + } + // If no rootPath was set yet, limit the pending link to the destination dir. + AddPendingLink(containmentRootPath ?? _resolvedDestination, fileId, pendingLink.SymlinkPath!, pendingLink.LinkTarget!, visitedLinks, out resolvedFullPath); + } + + if (!visitedLinks.Remove(fileId)) + { + // Circular reference detected while resolving the link above. + break; + } + } + + return containmentRootPath is null || IsInRootOrDestination(containmentRootPath, resolvedFullPath); + } + + // Follows a pre-existing symlink that is already on disk. Existing symlinks are not limited to a root path; + // after resolving, the root path is widened to the resolved location so pending links may also point under it. + private bool FollowExistingSymlink(ref string? rootPath, string linkDirectory, string linkTarget, HashSet? visitedLinks, out string resolvedFullPath) + { + string resolveFrom; + ReadOnlySpan resolveTarget; + if (Path.IsPathFullyQualified(linkTarget)) + { + resolveFrom = Path.GetPathRoot(linkTarget)!; + resolveTarget = linkTarget.AsSpan(resolveFrom.Length); + } + else + { + resolveFrom = linkDirectory; + resolveTarget = linkTarget; + } + + // set root path as null to disable the containment checks inside Resolve + string? symlinkRootPath = null; + if (!Resolve(ref symlinkRootPath, resolveFrom, resolveTarget, out resolvedFullPath, visitedLinks)) + { + return false; + } + + rootPath = resolvedFullPath; + if (!rootPath.EndsWith(Path.DirectorySeparatorChar)) + { + rootPath += Path.DirectorySeparatorChar; + } + return true; + } + + private bool IsInRootOrDestination(string rootPath, string resolvedPath) + => FullPathIsEqualOrNested(rootPath, resolvedPath) + || (!ReferenceEquals(rootPath, _resolvedDestination) && FullPathIsEqualOrNested(_resolvedDestination, resolvedPath)); + } } } diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs index 957a2fb49c115d..e3c487ec9a1694 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.cs @@ -10,6 +10,19 @@ namespace System.Formats.Tar.Tests { public partial class TarFile_ExtractToDirectory_File_Tests : TarTestsBase { + // ensures that the link name is in the correct format for the current OS. + private static string NormalizePathSeparators(string path) + { + if (OperatingSystem.IsWindows()) + { + return path.Replace('/', '\\'); + } + else + { + return path; + } + } + [Fact] public void InvalidPaths_Throw() { @@ -352,7 +365,7 @@ public void LinkBeforeTarget() using (TarWriter writer = new TarWriter(archiveStream)) { PaxTarEntry link = new PaxTarEntry(TarEntryType.SymbolicLink, "link"); - link.LinkName = "dir/file"; + link.LinkName = NormalizePathSeparators("dir/file"); writer.WriteEntry(link); PaxTarEntry file = new PaxTarEntry(TarEntryType.RegularFile, "dir/file"); @@ -486,7 +499,7 @@ public void ExtractToDirectory_RejectsSymlinkDirectoryTraversal_WithNestedFile() // symlink: "link" -> "/tmp/outside" writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") { - LinkName = linkTarget + LinkName = NormalizePathSeparators(linkTarget) }); // file: "link/test.txt" with "hello" @@ -514,9 +527,9 @@ public void ExtractToDirectory_RejectsSymlinkDirectoryTraversal_WithNestedFile() public void ExtractToDirectory_RejectsChainedSymlinkDirectoryTraversal_WithNestedFile() { // dir a/ - // symlink a/b ? . - // symlink a/b/c ? . - // symlink a/b/c/d ? ../../outside + // symlink a/b → . + // symlink a/b/c → . + // symlink a/b/c/d → ../../outside // file a/d/ pwned.txt escapes using TempDirectory root = new TempDirectory(); @@ -533,7 +546,7 @@ public void ExtractToDirectory_RejectsChainedSymlinkDirectoryTraversal_WithNeste writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b/c") { LinkName = "." }); - writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b/c/d") { LinkName = "../../outside" }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b/c/d") { LinkName = NormalizePathSeparators("../../outside") }); var pwned = new PaxTarEntry(TarEntryType.RegularFile, "a/d/pwned.txt") { @@ -542,29 +555,459 @@ public void ExtractToDirectory_RejectsChainedSymlinkDirectoryTraversal_WithNeste writer.WriteEntry(pwned); } - if (OperatingSystem.IsWindows()) + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + + string outsideDir = Path.Combine(root.Path, "outside"); + Assert.False(Directory.Exists(outsideDir), "outside/directory should not have been created."); + Assert.False(File.Exists(Path.Combine(outsideDir, "pwned.txt")), "pwned.txt should not have been written outside destination."); + + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_RejectsSymlinkDirectoryTraversal() + { + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "symlink_dir_traversal.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) { - // Windows always creates file symlinks (FileInfo.CreateAsSymbolicLink), so entry "a/b" becomes a - // *file* symlink whose target (".") is a *directory*. Processing the nested entries forces the - // extractor to resolve/descend through that type-mismatched reparse point, which Windows rejects, - // but the surfaced exception depends on the OS build: - // - Some builds throw UnauthorizedAccessException while resolving the link (FileInfo.ResolveLinkTarget - // during the traversal-safety walk in FilePathEscapesDirectory). - // - Others resolve the link successfully, then fail creating a directory where the file symlink - // already exists, throwing IOException ("a file or directory with the same name already exists"). - // Either way the archive is rejected and nothing escapes (verified below), so accept both. - Exception ex = Assert.ThrowsAny(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); - Assert.True(ex is IOException or UnauthorizedAccessException, $"Unexpected exception type: {ex}"); + // symlink: "link" -> "/tmp/outside" (absolute path outside destDir) + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") + { + LinkName = NormalizePathSeparators("/tmp/outside") + }); } - else + + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DefersSymlinkWithDotDot() + { + // Symlinks whose target contains '..' are deferred: a placeholder file is created + // during extraction, and the real symlink replaces it after all entries are processed. + // This prevents subsequent entries from following the symlink to escape the destination. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "deferred_symlink.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) { - Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); } - string outsideDir = Path.Combine(root.Path, "outside"); - Assert.False(Directory.Exists(outsideDir), "outside/directory should not have been created."); - Assert.False(File.Exists(Path.Combine(outsideDir, "pwned.txt")), "pwned.txt should not have been written outside destination."); + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "a", "link"); + FileSystemInfo linkInfo = new FileInfo(linkPath); + Assert.Equal(NormalizePathSeparators("../b"), linkInfo.LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DefersSymlinkWithDotDot_UnderSymlinkedParent() + { + using TempDirectory root = new TempDirectory(); + string realRoot = Path.Combine(root.Path, "real"); + Directory.CreateDirectory(realRoot); + + string linkedRoot = Path.Combine(root.Path, "link"); + Directory.CreateSymbolicLink(linkedRoot, realRoot); + + string destDir = Path.Combine(linkedRoot, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "deferred_symlink.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "a", "link"); + Assert.Equal(NormalizePathSeparators("../b"), new FileInfo(linkPath).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DeferredSymlinkBlocksTraversal() + { + // A file entry after a deferred symlink cannot follow the symlink to write + // outside the destination directory, because the symlink is a placeholder file + // during extraction. + // + // dir a/ + // symlink a/b -> . + // symlink a/b/c -> . + // symlink a/b/c/escape -> ../../.. (deferred: contains '..') + // file a/b/c/escape/pwned (would escape via chained symlinks if not deferred) + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "chained_symlink_traversal.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b") { LinkName = "." }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b/c") { LinkName = "." }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/b/c/escape") { LinkName = NormalizePathSeparators("../../..") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, "a/b/c/escape/pwned")); + } + + // The file entry "a/b/c/escape/pwned" cannot follow the 'escape' symlink because + // it is a placeholder file during extraction. Creating the file's parent directory + // will fail because 'escape' is a regular file, not a directory. + Assert.ThrowsAny(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + + // The file must not exist at the root (where the symlink chain would have led). + Assert.False(File.Exists(Path.Combine(root.Path, "pwned"))); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_AllowsSymlinkToDestinationDirectory() + { + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "symlink_to_dest.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + // symlink: "link" -> "." (points to the destination directory itself) + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") { LinkName = "." }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "link"); + FileSystemInfo linkInfo = new FileInfo(linkPath); + Assert.Equal(".", linkInfo.LinkTarget); + } + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DeferredSymlinkAllowedUnderExistingExternalSymlink() + { + // When the destination contains an existing symlink that points outside, + // a deferred symlink (with '..') under that external location is allowed + // as long as it stays within the directory the existing symlink resolved to. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + // Create an external directory and an existing symlink in dest pointing to it. + string externalDir = Path.Combine(root.Path, "external"); + Directory.CreateDirectory(Path.Combine(externalDir, "sub")); + Directory.CreateSymbolicLink(Path.Combine(destDir, "ext"), externalDir); + + string tarPath = Path.Combine(root.Path, "deferred_under_external.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + // ext/sub/link -> .. (resolves to externalDir, stays within it) + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "ext/sub/link") { LinkName = ".." }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "ext", "sub", "link"); + Assert.Equal("..", new FileInfo(linkPath).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DeferredSymlinkRejectedWhenEscapingExternalSymlink() + { + // When the destination contains an existing symlink that points outside, + // a deferred symlink that escapes beyond that external directory is rejected. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string externalDir = Path.Combine(root.Path, "external"); + Directory.CreateDirectory(externalDir); + Directory.CreateSymbolicLink(Path.Combine(destDir, "ext"), externalDir); + + string tarPath = Path.Combine(root.Path, "escape_external.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + // ext/link -> ../.. (escapes beyond externalDir) + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "ext/link") { LinkName = NormalizePathSeparators("../..") }); + } + + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_CircularDeferredSymlinksAllowed() + { + // Circular symlinks that contain '..' but stay within the destination are allowed. + // The cycle detection (visitedLinks) prevents infinite recursion during resolution. + // + // a/link1 -> ../b + // b/link2 -> ../a + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "circular_deferred.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link1") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "b/link2") { LinkName = NormalizePathSeparators("../a") }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string link1Path = Path.Combine(destDir, "a", "link1"); + string link2Path = Path.Combine(destDir, "b", "link2"); + Assert.Equal(NormalizePathSeparators("../b"), new FileInfo(link1Path).LinkTarget); + Assert.Equal(NormalizePathSeparators("../a"), new FileInfo(link2Path).LinkTarget); + } + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_OverwriteDeferredSymlink() + { + // When a second symlink entry overwrites a previously deferred symlink, + // the placeholder is replaced and the final symlink uses the second target. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "overwrite_deferred.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "c/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators("../c") }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "a", "link"); + Assert.Equal(NormalizePathSeparators("../c"), new FileInfo(linkPath).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_RegularFileOverwritesDeferredSymlink() + { + // When a regular file entry overwrites a previously deferred symlink, + // the placeholder is replaced by the file and its pending link entry is + // dropped, so the final entry is the file (not the symlink). + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "file_overwrites_deferred.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.RegularFile, "a/link") + { + DataStream = new MemoryStream(Encoding.UTF8.GetBytes("content")) + }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "a", "link"); + Assert.Null(new FileInfo(linkPath).LinkTarget); + Assert.Equal("content", File.ReadAllText(linkPath)); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_AbsoluteSymlinkWithinDestination() + { + // A symlink with a fully qualified target that points within the destination + // is allowed and is not deferred (no '..'). + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string absoluteTarget = Path.Combine(destDir, "target"); + + string tarPath = Path.Combine(root.Path, "absolute_symlink.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "target/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") { LinkName = NormalizePathSeparators(absoluteTarget) }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "link"); + Assert.Equal(absoluteTarget, new FileInfo(linkPath).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_DeferredSymlinkWithAbsoluteTarget() + { + // A deferred symlink (contains '..') with a fully qualified target that + // resolves within the destination is allowed. Exercises the + // rootPath = resolvedDestinationPath path in ResolvePathInRootOrDestination. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + // Absolute target with '..' that resolves back into dest. + string absoluteTarget = NormalizePathSeparators(Path.Combine(destDir, "a", "..", "b")); + + string tarPath = Path.Combine(root.Path, "deferred_absolute.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") { LinkName = absoluteTarget }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string linkPath = Path.Combine(destDir, "link"); + // Windows APIs collapse the intermediate ".." in the link target, so the expected target is destDir/b. On Unix, the link target is preserved as-is. + string expectedTarget = OperatingSystem.IsWindows() ? Path.Combine(destDir, "b") : absoluteTarget; + Assert.Equal(expectedTarget, new FileInfo(linkPath).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_ChainedDeferredSymlinks() + { + // Two deferred symlinks where resolving the second one's target + // passes through the first one, forcing recursive resolution + // during CreatePendingLinks. + // + // a/link1 -> ../b (deferred: contains '..') + // a/link2 -> link1/../b/sub (deferred: contains '..', + // resolving encounters link1 as pending link, + // triggering recursive creation of link1 first) + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "chained_deferred.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/sub/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link1") { LinkName = NormalizePathSeparators("../b") }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link2") { LinkName = NormalizePathSeparators("link1/../b/sub") }); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + + string link1Path = Path.Combine(destDir, "a", "link1"); + string link2Path = Path.Combine(destDir, "a", "link2"); + Assert.Equal(NormalizePathSeparators("../b"), new FileInfo(link1Path).LinkTarget); + Assert.Equal(NormalizePathSeparators("link1/../b/sub"), new FileInfo(link2Path).LinkTarget); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_EscapeViaSymlinkToCurrentDirectory_Throws() + { + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "escape.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a") { LinkName = "." }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "link") { LinkName = NormalizePathSeparators("a/../outside") }); + } + + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_ExceedsMaxSymlinkDepth() + { + // A chain of deferred symlinks that exceeds MaxSymlinkDepth (40) + // should throw during CreatePendingLinks. + // + // Each link_N -> ../link_{N-1} (deferred because of '..'). + // Resolving link_41's target chains through all 41 preceding + // symlinks, exceeding the depth limit. + + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + string tarPath = Path.Combine(root.Path, "deep_symlinks.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + const int count = 42; + for (int i = 0; i < count; i++) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, $"d{i}/")); + } + // link_0 -> ../d0 (base case) + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "d0/link_0") { LinkName = NormalizePathSeparators("../d0") }); + // link_N -> ../d{N-1}/link_{N-1} — each follows the previous, all deferred + for (int i = 1; i < count; i++) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, $"d{i}/link_{i}") { LinkName = NormalizePathSeparators($"../d{i - 1}/link_{i - 1}") }); + } + } + + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + } + + [ConditionalTheory(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + [InlineData("../b")] // deferred: contains '..' + [InlineData("b")] // not deferred + public void ExtractToDirectory_OverwritesExistingDanglingSymlink(string linkName) + { + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(Path.Combine(destDir, "a")); + + // A pre-existing symlink whose target does not exist. + File.CreateSymbolicLink(Path.Combine(destDir, "a", "link"), "does_not_exist"); + + string tarPath = Path.Combine(root.Path, "overwrite_dangling.tar"); + using (FileStream stream = new FileStream(tarPath, FileMode.Create, FileAccess.Write)) + using (TarWriter writer = new TarWriter(stream, leaveOpen: false)) + { + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "a/")); + writer.WriteEntry(new PaxTarEntry(TarEntryType.SymbolicLink, "a/link") { LinkName = NormalizePathSeparators(linkName) }); + writer.WriteEntry(new PaxTarEntry(TarEntryType.Directory, "b/")); + } + + TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true); + Assert.Equal(NormalizePathSeparators(linkName), new FileInfo(Path.Combine(destDir, "a", "link")).LinkTarget); } } } diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs index 20bf795b7a215a..f0da44ccf99440 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs @@ -49,8 +49,9 @@ internal override async ValueTask SendAsync(HttpRequestMess response.Dispose(); - // Clear the authorization header. + // Clear the authorization headers. request.Headers.Authorization = null; + request.Headers.ProxyAuthorization = null; if (HttpTelemetry.Log.IsEnabled()) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs index e6378c86f0ba86..5ccaa48d2bb9ac 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStream.IO.cs @@ -241,6 +241,18 @@ private async Task RenegotiateAsync(CancellationToken cancellationTo } while (token.Status.ErrorCode == SecurityStatusPalErrorCode.ContinueNeeded); + if (token.Failed) + { + Exception? exception = token.GetException(); + if (exception is not AuthenticationException) + { + exception = new AuthenticationException(SR.net_auth_SSPI, exception); + } + + // Any alert in the failed token has already been sent to the peer. + SendAuthResetSignal(default, ExceptionDispatchInfo.Capture(exception)); + } + CompleteHandshake(_sslAuthenticationOptions); } finally diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs index 92be7426324d13..837b1a769962ec 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslStreamPal.Unix.cs @@ -161,6 +161,10 @@ public static ProtocolToken Renegotiate( { return new ProtocolToken { Status = status }; } + + // Renegotiation resets peer verification. Require a certificate unless a callback can accept its absence. + Interop.Ssl.SslSetVerifyPeer((SafeSslHandle)context, failIfNoPeerCert: sslAuthenticationOptions.CertValidationDelegate is null); + return HandshakeInternal(ref context!, null, out _, sslAuthenticationOptions); } diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs index 42d031868ffea8..25684a5823b467 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs @@ -620,6 +620,126 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } + public static TheoryData NegotiateClientCertificateValidationData() + { + TheoryData data = new(); + foreach (SslProtocols protocol in SslProtocolSupport.EnumerateSupportedProtocols(SslProtocols.Tls12 | SslProtocols.Tls13)) + { + data.Add(protocol, true, true, false); + data.Add(protocol, true, true, true); + data.Add(protocol, true, false, false); + data.Add(protocol, false, true, false); + } + + return data; + } + + [ConditionalTheory(typeof(SslStreamNetworkStreamTest), nameof(SupportsRenegotiation))] + [MemberData(nameof(NegotiateClientCertificateValidationData))] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + public async Task SslStream_NegotiateClientCertificateAsync_ValidatesCertificate( + SslProtocols protocol, bool sendClientCertificate, bool trustClientCertificate, bool rejectInCallback) + { + using CancellationTokenSource cts = new(TestConfiguration.PassingTestTimeout); + // The default generator can select signature algorithms that require TLS 1.3. + using Configuration.Certificates.PkiHolder clientPki = Configuration.Certificates.GenerateCertificates( + nameof(SslStream_NegotiateClientCertificateAsync_ValidatesCertificate), serverCertificate: false, forceRsaCertificate: true); + + SslClientAuthenticationOptions clientOptions = new() + { + TargetHost = "localhost", + EnabledSslProtocols = protocol, + AllowTlsResume = false, + CertificateChainPolicy = new X509ChainPolicy + { + RevocationMode = X509RevocationMode.NoCheck, + TrustMode = X509ChainTrustMode.CustomRootTrust, + CustomTrustStore = { _certificates.ServerChain[^1] } + }, + // A selection callback also enables TLS 1.3 post-handshake authentication when it returns null. + LocalCertificateSelectionCallback = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => + sendClientCertificate ? clientPki.EndEntity : null + }; + SslServerAuthenticationOptions serverOptions = new() + { + ServerCertificateContext = _certificates.CreateSslStreamCertificateContext(), + EnabledSslProtocols = protocol, + AllowTlsResume = false, + ClientCertificateRequired = false, + CertificateChainPolicy = new X509ChainPolicy + { + RevocationMode = X509RevocationMode.NoCheck, + DisableCertificateDownloads = true, + TrustMode = X509ChainTrustMode.CustomRootTrust, + CustomTrustStore = { trustClientCertificate ? clientPki.IssuerChain[^1] : _certificates.ServerChain[^1] } + } + }; + serverOptions.CertificateChainPolicy.ExtraStore.AddRange(clientPki.IssuerChain); + + int validationCount = 0; + if (rejectInCallback) + { + serverOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => + { + if (certificate is null) + { + Assert.Equal(SslPolicyErrors.RemoteCertificateNotAvailable, sslPolicyErrors); + return true; + } + + Assert.Equal(clientPki.EndEntity, certificate); + Assert.Equal(SslPolicyErrors.None, sslPolicyErrors); + Interlocked.Increment(ref validationCount); + return false; + }; + } + + (SslStream client, SslStream server) = TestHelper.GetConnectedSslStreams(); + using (client) + using (server) + { + await TestConfiguration.WhenAllOrAnyFailedWithTimeout( + client.AuthenticateAsClientAsync(clientOptions, cts.Token), + server.AuthenticateAsServerAsync(serverOptions, cts.Token)); + await TestHelper.PingPong(client, server, cts.Token); + Assert.False(server.IsMutuallyAuthenticated); + + byte[] buffer = new byte[TestHelper.s_ping.Length]; + Task clientRead = client.ReadExactlyAsync(buffer, cts.Token).AsTask(); + Task negotiation = server.NegotiateClientCertificateAsync(cts.Token); + + if (sendClientCertificate && trustClientCertificate && !rejectInCallback) + { + await negotiation.WaitAsync(cts.Token); + using X509Certificate remoteCertificate = server.RemoteCertificate; + Assert.Equal(clientPki.EndEntity, remoteCertificate); + Assert.True(server.IsMutuallyAuthenticated); + await server.WriteAsync(TestHelper.s_ping, cts.Token); + await clientRead; + Assert.Equal(TestHelper.s_ping, buffer); + Assert.True(client.IsMutuallyAuthenticated); + await TestHelper.PingPong(client, server, cts.Token); + await TestHelper.PingPong(server, client, cts.Token); + } + else + { + Exception exception = await Record.ExceptionAsync(() => negotiation.WaitAsync(cts.Token)); + Assert.Equal(rejectInCallback ? 1 : 0, validationCount); + Assert.IsType(exception); + if (sendClientCertificate && !trustClientCertificate) + { + Assert.Contains(nameof(X509ChainStatusFlags.UntrustedRoot), exception.Message); + } + + Assert.False(server.IsAuthenticated); + Assert.False(server.IsMutuallyAuthenticated); + await Assert.ThrowsAsync(() => server.ReadAsync(buffer, cts.Token).AsTask()); + await Assert.ThrowsAsync(() => server.WriteAsync(TestHelper.s_ping, cts.Token).AsTask()); + await Assert.ThrowsAsync(() => clientRead.WaitAsync(cts.Token)); + } + } + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 3a09bcb0146c72..20e1c8c05c4912 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -2561,6 +2561,9 @@ Common\Interop\Unix\System.Native\Interop.MkDir.cs + + Common\Interop\Unix\System.Native\Interop.MkDirAt.cs + Common\Interop\Unix\System.Native\Interop.MkdTemp.cs @@ -2581,6 +2584,9 @@ Common\Interop\Unix\System.Native\Interop.Open.cs + + Common\Interop\Unix\System.Native\Interop.OpenAt.cs + Common\Interop\Unix\System.Native\Interop.OpenFlags.cs @@ -2650,6 +2656,9 @@ Common\Interop\Unix\System.Native\Interop.Unlink.cs + + Common\Interop\Unix\System.Native\Interop.UnlinkAt.cs + Common\Interop\Unix\System.Native\Interop.UTimensat.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs index 83725d79cefa44..8e94f9a7da2863 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs @@ -634,7 +634,7 @@ private static bool Lex(DS dps, ref __DTString str, scoped ref DateTimeToken dto { case TokenType.NumberToken: case TokenType.YearNumberToken: - if (raw.numCount == 3 || tokenValue == -1) + if (raw.numCount == DateTimeRawInfo.MaxBufferLength || tokenValue == -1) { result.SetBadDateTimeFailure(); LexTraceExit("0010", dps); @@ -928,6 +928,13 @@ private static bool Lex(DS dps, ref __DTString str, scoped ref DateTimeToken dto } else { + if (raw.numCount == DateTimeRawInfo.MaxBufferLength) + { + result.SetBadDateTimeFailure(); + LexTraceExit("0105 (Not enough space in the raw info buffer when parsing Hebrew numbers)", dps); + return false; + } + // This is a day number dtok.num = tokenValue; raw.AddNumber(dtok.num); @@ -6106,6 +6113,7 @@ internal struct DateTimeToken // internal struct DateTimeRawInfo { + internal const int MaxBufferLength = 3; private InlineArray3 num; internal int numCount; internal int month; @@ -6128,7 +6136,17 @@ internal void Init() internal void AddNumber(int value) { + if (numCount >= MaxBufferLength) + { + // defense in depth. we shouldn't come here though. + ThrowNumberBufferOverflow(); + + } + num[numCount++] = value; + + [DoesNotReturn] + static void ThrowNumberBufferOverflow() => throw new InvalidOperationException(SR.InvalidOperation_DateTimeParsing); } internal int GetNumber(int index) diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs index d5c94c045da2b8..3b2a9095ad8c32 100644 --- a/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/IO/SharedMemoryManager.Unix.cs @@ -13,16 +13,6 @@ namespace System.IO { internal readonly unsafe struct SharedMemoryId { - private const string UserUnscopedRuntimeTempDirectoryName = ".dotnet"; - - private const string UserScopedRuntimeTempDirectoryName = ".dotnet-uid"; - - private const string SharedMemoryGlobalDirectoryName = "global"; - - private const string SharedMemorySessionDirectoryName = "session"; - - private static int SessionId { get; } = Interop.Sys.GetSid(Environment.ProcessId); - public SharedMemoryId(string name, bool isUserScope) { if (name.StartsWith("Global\\", StringComparison.Ordinal)) @@ -54,30 +44,6 @@ public SharedMemoryId(string name, bool isUserScope) public bool IsSessionScope { get; } public bool IsUserScope { get; } public uint Uid { get; } - - internal readonly string GetRuntimeTempDirectoryName() - { - if (IsUserScope) - { - return $"{UserScopedRuntimeTempDirectoryName}{Uid}"; - } - else - { - return UserUnscopedRuntimeTempDirectoryName; - } - } - - internal readonly string GetSessionDirectoryName() - { - if (IsSessionScope) - { - return $"{SharedMemorySessionDirectoryName}{SessionId}"; - } - else - { - return SharedMemoryGlobalDirectoryName; - } - } } internal enum SharedMemoryType : byte @@ -153,9 +119,11 @@ public SharedMemoryProcessDataHeader(SharedMemoryId id, SafeFileHandle fileHandl bool createIfNotExist, bool acquireLockIfCreated, out bool created, - out AutoReleaseFileLock creationDeletionLockFileHandle) + out AutoReleaseFileLock creationDeletionLockFileHandle, + out SafeFileHandle runtimeDirectoryHandle) { created = false; + runtimeDirectoryHandle = new SafeFileHandle(); // If we don't create the shared memory file, the caller won't need to hold the creation/deletion lock file handle. // Return a placeholder to simplify the caller's logic and allow them to always dispose of the handle @@ -175,24 +143,37 @@ public SharedMemoryProcessDataHeader(SharedMemoryId id, SafeFileHandle fileHandl return processDataHeader; } - using AutoReleaseFileLock creationDeletionLock = SharedMemoryManager.Instance.AcquireCreationDeletionLockForId(id); - - string sessionDirectory = Path.Combine( - SharedMemoryHelpers.SharedFilesPath, - id.GetRuntimeTempDirectoryName(), - SharedMemoryManager.SharedMemorySharedMemoryDirectoryName, - id.GetSessionDirectoryName() - ); + using AutoReleaseFileLock creationDeletionLock = + SharedMemoryManager.Instance.AcquireCreationDeletionLockForId(id, out runtimeDirectoryHandle); + + string sessionDirectoryName = SharedMemoryHelpers.GetSessionDirectoryName(id); + string sessionDirectoryPath = Path.Combine( + SharedMemoryHelpers.GetSharedFilesSystemPath(id), + SharedMemoryHelpers.GetRuntimeDirectoryName(id), + SharedMemoryHelpers.SharedMemorySharedMemoryDirectoryName, + sessionDirectoryName); + using SafeFileHandle sessionDirectoryHandle = SharedMemoryHelpers.EnsureDirectoryExists( + creationDeletionLock.FileHandle, + sessionDirectoryName, + sessionDirectoryPath, + id, + createIfNotExist); - if (!SharedMemoryHelpers.EnsureDirectoryExists(sessionDirectory, id, isGlobalLockAcquired: true, createIfNotExist)) + if (sessionDirectoryHandle.IsInvalid) { Debug.Assert(!createIfNotExist); return null; } - string sharedMemoryFilePath = Path.Combine(sessionDirectory, id.Name); + string sharedMemoryFilePath = Path.Combine(sessionDirectoryPath, id.Name); - SafeFileHandle fileHandle = SharedMemoryHelpers.CreateOrOpenFile(sharedMemoryFilePath, id, createIfNotExist, out bool createdFile); + SafeFileHandle fileHandle = SharedMemoryHelpers.CreateOrOpenFile( + sessionDirectoryHandle, + id.Name, + sharedMemoryFilePath, + id, + createIfNotExist, + out bool createdFile); if (fileHandle.IsInvalid) { return null; @@ -331,7 +312,8 @@ private void Close() SharedMemoryManager.Instance.VerifyCreationDeletionProcessLockIsLocked(); SharedMemoryManager.Instance.RemoveProcessDataHeader(this); - using AutoReleaseFileLock autoReleaseFileLock = SharedMemoryManager.Instance.AcquireCreationDeletionLockForId(_id); + using AutoReleaseFileLock autoReleaseFileLock = + SharedMemoryManager.Instance.AcquireCreationDeletionLockForId(_id, out _); bool releaseSharedData = false; @@ -357,20 +339,31 @@ private void Close() if (releaseSharedData) { - string sessionDirectoryPath = Path.Combine( - SharedMemoryHelpers.SharedFilesPath, - _id.GetRuntimeTempDirectoryName(), - SharedMemoryManager.SharedMemorySharedMemoryDirectoryName, - _id.GetSessionDirectoryName() - ); - - string sharedMemoryFilePath = Path.Combine(sessionDirectoryPath, _id.Name); - // Directly call the underlying functions here as this is best-effort. // If we fail to delete, we don't want an exception. - Interop.Sys.Unlink(sharedMemoryFilePath); + string sessionDirectoryName = SharedMemoryHelpers.GetSessionDirectoryName(_id); + string sessionDirectoryPath = Path.Combine( + SharedMemoryHelpers.GetSharedFilesSystemPath(_id), + SharedMemoryHelpers.GetRuntimeDirectoryName(_id), + SharedMemoryHelpers.SharedMemorySharedMemoryDirectoryName, + sessionDirectoryName); + using (SafeFileHandle sessionDirectoryHandle = SharedMemoryHelpers.EnsureDirectoryExists( + autoReleaseFileLock.FileHandle, + sessionDirectoryName, + sessionDirectoryPath, + _id, + createIfNotExist: false)) + { + if (!sessionDirectoryHandle.IsInvalid) + { + Interop.Sys.UnlinkAt(sessionDirectoryHandle, _id.Name, Interop.Sys.UnlinkAtFlags.None); + } + } - Interop.Sys.RmDir(sessionDirectoryPath); + Interop.Sys.UnlinkAt( + autoReleaseFileLock.FileHandle, + sessionDirectoryName, + Interop.Sys.UnlinkAtFlags.AT_REMOVEDIR); } } } @@ -386,44 +379,211 @@ internal static class SharedMemoryHelpers private const int TransientRetryCount = 10; + internal const string SharedMemorySharedMemoryDirectoryName = "shm"; + + private const string RuntimeTempDirectoryName = ".dotnet"; + + private const string UserRuntimeTempDirectoryName = ".dotnet-uid"; + + private const string SharedMemoryGlobalDirectoryName = "global"; + + private const string SharedMemorySessionDirectoryName = "session"; + // See https://developer.apple.com/documentation/Foundation/FileManager/containerURL(forSecurityApplicationGroupIdentifier:)#App-Groups-in-macOS for details on this path. - private const string ApplicationContainerBasePathSuffix = "/Library/Group Containers/"; + private const string ApplicationContainerBasePathSuffix = "Library/Group Containers/"; - public static string SharedFilesPath { get; } = InitalizeSharedFilesPath(); + private static string? ApplicationContainerPath { get; } = + OperatingSystem.IsApplePlatform() ? GetApplicationContainerPath() : null; + + private static string SharedFilesPath { get; } = InitalizeSharedFilesPath(); private static string InitalizeSharedFilesPath() { if (OperatingSystem.IsApplePlatform()) { - string? applicationGroupId = Environment.GetEnvironmentVariable("DOTNET_SHARED_MEMORY_APPLICATION_GROUP_ID"); - if (applicationGroupId is not null) + return ApplicationContainerPath ?? "/tmp/"; + } + + return "/tmp/"; + } + + private static string? GetApplicationContainerPath() + { + string? applicationGroupId = Environment.GetEnvironmentVariable("DOTNET_SANDBOX_APPLICATION_GROUP_ID"); + if (!string.IsNullOrEmpty(applicationGroupId)) + { + string sharedFilesPath = Path.Combine( + PersistedFiles.GetHomeDirectoryFromPasswd(), + ApplicationContainerBasePathSuffix, + applicationGroupId + ); + + if (File.Exists(sharedFilesPath)) { - string sharedFilesPath = Path.Combine( - PersistedFiles.GetHomeDirectoryFromPasswd(), - ApplicationContainerBasePathSuffix, - applicationGroupId - ); + // If the path exists and is a file, throw an exception. + // If it's a directory, or does not exist, callers can correctly handle it. + throw new DirectoryNotFoundException(); + } + + return sharedFilesPath; + } + return null; + } + + private static string UserSpecificSharedFilesPath { get; } = InitalizeUserSpecificSharedFilesPath(); - if (File.Exists(sharedFilesPath)) + private static string InitalizeUserSpecificSharedFilesPath() + { + if (OperatingSystem.IsApplePlatform()) + { + // On apple platforms, TMPDIR points to the user-specific temporary directory. + // For compat, we will still check the application container path first as that path + // is also user-specific. + string? path = ApplicationContainerPath ?? Environment.GetEnvironmentVariable("TMPDIR"); + if (path is not null) + { + if (ApplicationContainerPath is not null) { - // If the path exists and is a file, throw an exception. - // If it's a directory, or does not exist, callers can correctly handle it. - throw new DirectoryNotFoundException(); + // An Apple application-group container is already user-specific, so both global-scoped and + // user-scoped mutexes would otherwise use the same .dotnet directory and conflict. Give + // user-scoped mutexes a parallel directory to preserve the distinction between the scopes. +#pragma warning disable CA1416 // Validate platform compatibility. This file is only included on Unix platforms. + // The application-group container is owned by the current user, so this directory can be + // created directly without validating each component. + Directory.CreateDirectory( + Path.Combine(ApplicationContainerPath, UserRuntimeTempDirectoryName), + PermissionsMask_OwnerUser_ReadWriteExecute); +#pragma warning restore CA1416 // Validate platform compatibility } - return sharedFilesPath; + return path; + } + } + else if (OperatingSystem.IsLinux()) + { + // XDG_RUNTIME_DIR is the user-specific, cleared on reboot, runtime files directory for systems + // that follow the XDG Base Directory Specification + // https://specifications.freedesktop.org/basedir + // For Linux platforms that follow the XDG specification, we will use it. + string? xdgRuntimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR"); + if (!string.IsNullOrEmpty(xdgRuntimeDir)) + { + return xdgRuntimeDir; } } - return "/tmp/"; + // Fallback to storing data in the home directory. + // The home directory is expected to be owned by the user and only accessible by the user. + // If it is not, then that was a choice by the user. + string userFeatureDirectory = PersistedFiles.GetUserFeatureDirectory("Runtime", "SharedMemory"); + +#pragma warning disable CA1416 // Validate platform compatibility. This file is only included on Unix platforms. + // This is not a system-created directory, so we need to guarantee that it exists. + // Default permissions are fine as we are under the user's home directory. + Directory.CreateDirectory(userFeatureDirectory, PermissionsMask_OwnerUser_ReadWriteExecute); + + return userFeatureDirectory; +#pragma warning restore CA1416 // Validate platform compatibility + } + + public static SafeFileHandle OpenSharedFilesSystemDirectory(SharedMemoryId id) + { + string directoryPath = GetSharedFilesSystemPath(id); + int transientRetryCount = 0; + + while (true) + { + SafeFileHandle directoryHandle = Interop.Sys.Open( + directoryPath, + Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC | Interop.Sys.OpenFlags.O_NOFOLLOW | Interop.Sys.OpenFlags.O_DIRECTORY, + 0); + + if (!directoryHandle.IsInvalid) + { + if (Interop.Sys.FStat(directoryHandle, out Interop.Sys.FileStatus fileStatus) != 0) + { + Interop.ErrorInfo fstatError = Interop.Sys.GetLastErrorInfo(); + directoryHandle.Dispose(); + if ((fstatError.Error == Interop.Error.ENOENT || fstatError.Error == Interop.Error.EACCES) && + RetryOnTransientPermissionFailure(ref transientRetryCount)) + { + continue; + } + + throw Interop.GetExceptionForIoErrno(fstatError, directoryPath); + } + + // For system directories (such as TEMP_DIRECTORY_PATH), require sufficient permissions only for the + // owner user. For instance, "docker run --mount ..." to mount /tmp to some directory on the host mounts the + // destination directory with the same permissions as the source directory, which may not include some permissions for + // other users. In the docker container, other user permissions are typically not relevant and relaxing the permissions + // requirement allows for that scenario to work without having to work around it by first giving sufficient permissions + // for all users. + // + // If the directory is being used for user-scoped shared memory data, also ensure that either it has the sticky bit or + // it's owned by the current user and without write access for other users. + if ((fileStatus.Mode & (int)PermissionsMask_OwnerUser_ReadWriteExecute) == (int)PermissionsMask_OwnerUser_ReadWriteExecute + && ( + !id.IsUserScope || + (fileStatus.Mode & (int)PermissionsMask_Sticky) == (int)PermissionsMask_Sticky || + (fileStatus.Uid == id.Uid && (fileStatus.Mode & (int)PermissionsMask_NonOwnerUsers_Write) == 0) + )) + { + return directoryHandle; + } + + directoryHandle.Dispose(); + if (RetryOnTransientPermissionFailure(ref transientRetryCount)) + { + continue; + } + + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); + } + + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + directoryHandle.Dispose(); + if ((error.Error == Interop.Error.ENOENT || error.Error == Interop.Error.EACCES) && + RetryOnTransientPermissionFailure(ref transientRetryCount)) + { + continue; + } + + throw Interop.GetExceptionForIoErrno(error, directoryPath); + } + } + + internal static string GetSharedFilesSystemPath(SharedMemoryId id) + { + return id.IsUserScope ? UserSpecificSharedFilesPath : SharedFilesPath; } - internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, SharedMemoryId id, bool createIfNotExist, out bool createdFile) + internal static string GetRuntimeDirectoryName(SharedMemoryId id) + { + return id.IsUserScope && ApplicationContainerPath is not null + ? UserRuntimeTempDirectoryName + : RuntimeTempDirectoryName; + } + + private static int SessionId { get; } = Interop.Sys.GetSid(Environment.ProcessId); + + public static string GetSessionDirectoryName(SharedMemoryId id) + { + return id.IsSessionScope ? $"{SharedMemorySessionDirectoryName}{SessionId}" : SharedMemoryGlobalDirectoryName; + } + + internal static SafeFileHandle CreateOrOpenFile( + SafeFileHandle directoryHandle, + string fileName, + string filePathForError, + SharedMemoryId id, + bool createIfNotExist, + out bool createdFile) { int transientRetryCount = 0; while (true) { - SafeFileHandle fd = Interop.Sys.Open(sharedMemoryFilePath, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC, 0); + SafeFileHandle fd = Interop.Sys.OpenAt(directoryHandle, fileName, Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_NOFOLLOW | Interop.Sys.OpenFlags.O_CLOEXEC, 0); Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); if (!fd.IsInvalid) { @@ -433,13 +593,13 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha { error = Interop.Sys.GetLastErrorInfo(); fd.Dispose(); - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + throw Interop.GetExceptionForIoErrno(error, filePathForError); } if (fileStatus.Uid != id.Uid) { fd.Dispose(); - throw new IOException(SR.Format(SR.IO_SharedMemory_FileNotOwnedByUid, sharedMemoryFilePath, id.Uid)); + throw new IOException(SR.Format(SR.IO_SharedMemory_FileNotOwnedByUid, filePathForError, id.Uid)); } if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) != (int)PermissionsMask_OwnerUser_ReadWrite) @@ -455,7 +615,7 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha continue; } - throw new IOException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, sharedMemoryFilePath, PermissionsMask_OwnerUser_ReadWrite)); + throw new IOException(SR.Format(SR.IO_SharedMemory_FilePermissionsIncorrect, filePathForError, PermissionsMask_OwnerUser_ReadWrite)); } } createdFile = false; @@ -473,7 +633,7 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha if (error.Error != Interop.Error.ENOENT) { - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + throw Interop.GetExceptionForIoErrno(error, filePathForError); } if (!createIfNotExist) @@ -488,9 +648,10 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha ? PermissionsMask_OwnerUser_ReadWrite : PermissionsMask_AllUsers_ReadWrite; - fd = Interop.Sys.Open( - sharedMemoryFilePath, - Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_CLOEXEC | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL, + fd = Interop.Sys.OpenAt( + directoryHandle, + fileName, + Interop.Sys.OpenFlags.O_RDWR | Interop.Sys.OpenFlags.O_NOFOLLOW | Interop.Sys.OpenFlags.O_CLOEXEC | Interop.Sys.OpenFlags.O_CREAT | Interop.Sys.OpenFlags.O_EXCL, (int)permissionsMask); if (fd.IsInvalid) @@ -504,7 +665,7 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha transientRetryCount++; continue; } - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + throw Interop.GetExceptionForIoErrno(error, filePathForError); } // The mode passed to open(..., O_CREAT|O_EXCL, permissionsMask) is filtered by process umask, so the @@ -515,8 +676,8 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha { error = Interop.Sys.GetLastErrorInfo(); fd.Dispose(); - Interop.Sys.Unlink(sharedMemoryFilePath); - throw Interop.GetExceptionForIoErrno(error, sharedMemoryFilePath); + Interop.Sys.UnlinkAt(directoryHandle, fileName, Interop.Sys.UnlinkAtFlags.None); + throw Interop.GetExceptionForIoErrno(error, filePathForError); } createdFile = true; @@ -524,7 +685,12 @@ internal static SafeFileHandle CreateOrOpenFile(string sharedMemoryFilePath, Sha } } - internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId id, bool isGlobalLockAcquired, bool createIfNotExist = true, bool isSystemDirectory = false) + internal static SafeFileHandle EnsureDirectoryExists( + SafeFileHandle parentDirectoryHandle, + string directoryName, + string directoryPathForError, + SharedMemoryId id, + bool createIfNotExist = true) { int transientRetryCount = 0; while (true) @@ -533,177 +699,164 @@ internal static bool EnsureDirectoryExists(string directoryPath, SharedMemoryId ? PermissionsMask_OwnerUser_ReadWriteExecute : PermissionsMask_AllUsers_ReadWriteExecute; - int statResult = Interop.Sys.Stat(directoryPath, out Interop.Sys.FileStatus fileStatus); + SafeFileHandle directoryHandle = Interop.Sys.OpenAt( + parentDirectoryHandle, + directoryName, + Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC | Interop.Sys.OpenFlags.O_NOFOLLOW | Interop.Sys.OpenFlags.O_DIRECTORY, + 0); - if (statResult != 0 && Interop.Sys.GetLastError() == Interop.Error.ENOENT) + if (directoryHandle.IsInvalid) { - if (!createIfNotExist) - { - // The directory does not exist and we are not allowed to create it. - return false; - } + Interop.ErrorInfo openError = Interop.Sys.GetLastErrorInfo(); + directoryHandle.Dispose(); - // The path does not exist, create the directory. The permissions mask passed to mkdir() is filtered by the process' - // permissions umask, so mkdir() may not set all of the requested permissions. We need to use chmod() to set the proper - // permissions. That creates a race when there is no global lock acquired when creating the directory. Another user's - // process may create the directory and this user's process may try to use it before the other process sets the full - // permissions. The validation below tolerates that transient state while using mkdir() on the final path avoids - // rename() replacing an existing empty directory that another process may already be locking. - - if (isGlobalLockAcquired) + if (openError.Error == Interop.Error.ENOENT) { -#pragma warning disable CA1416 // Validate platform compatibility. This file is only included on Unix platforms. - Directory.CreateDirectory(directoryPath, permissionsMask); -#pragma warning restore CA1416 // Validate platform compatibility - - try + if (!createIfNotExist) { - FileSystem.SetUnixFileMode(directoryPath, permissionsMask); + // The directory does not exist and we are not allowed to create it. + return new SafeFileHandle(); } - catch + + if (Interop.Sys.MkDirAt(parentDirectoryHandle, directoryName, (int)permissionsMask) == 0) { - try { Directory.Delete(directoryPath); } catch { } - throw; - } + directoryHandle = Interop.Sys.OpenAt( + parentDirectoryHandle, + directoryName, + Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_CLOEXEC | Interop.Sys.OpenFlags.O_NOFOLLOW | Interop.Sys.OpenFlags.O_DIRECTORY, + 0); - return true; - } + if (directoryHandle.IsInvalid) + { + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + directoryHandle.Dispose(); + Interop.Sys.UnlinkAt(parentDirectoryHandle, directoryName, Interop.Sys.UnlinkAtFlags.AT_REMOVEDIR); + throw Interop.GetExceptionForIoErrno(error, directoryPathForError); + } - if (Interop.Sys.MkDir(directoryPath, (int)permissionsMask) == 0) - { - try + if (Interop.Sys.FChMod(directoryHandle, (int)permissionsMask) != 0) + { + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + directoryHandle.Dispose(); + Interop.Sys.UnlinkAt(parentDirectoryHandle, directoryName, Interop.Sys.UnlinkAtFlags.AT_REMOVEDIR); + throw Interop.GetExceptionForIoErrno(error, directoryPathForError); + } + + return directoryHandle; + } + + Interop.ErrorInfo mkdirError = Interop.Sys.GetLastErrorInfo(); + if (mkdirError.Error != Interop.Error.EEXIST) { - FileSystem.SetUnixFileMode(directoryPath, permissionsMask); + throw Interop.GetExceptionForIoErrno(mkdirError, directoryPathForError); } - catch + + if (RetryOnTransientPermissionFailure(ref transientRetryCount)) { - try { Directory.Delete(directoryPath); } catch { } - throw; + // If we have two threads or processes racing to create this directory, + // the first one may have created it but not yet set the correct permissions. + // Retry briefly to let the other process finish setting permissions. + // We will not set permissions when we are not the creator of the directory. + continue; } - return true; + throw Interop.GetExceptionForIoErrno(mkdirError, directoryPathForError); } - Interop.ErrorInfo mkdirError = Interop.Sys.GetLastErrorInfo(); - if (mkdirError.Error != Interop.Error.EEXIST) + if ((openError.Error == Interop.Error.ENOENT || openError.Error == Interop.Error.EACCES) && + RetryOnTransientPermissionFailure(ref transientRetryCount)) { - throw Interop.GetExceptionForIoErrno(mkdirError, directoryPath); + // While another process is creating this path, openat() can temporarily fail with ENOENT before the + // directory appears, or with EACCES before the creating process has finished normalizing directory + // permissions for cross-user access. + continue; } - statResult = Interop.Sys.Stat(directoryPath, out fileStatus); - } - - if (statResult != 0) - { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - // While another process is creating this path, stat() can temporarily fail with ENOENT before the - // directory appears, or with EACCES before the creating process has finished normalizing directory - // permissions for cross-user access. - if ((error.Error == Interop.Error.ENOENT || error.Error == Interop.Error.EACCES) && RetryOnTransientPermissionFailure()) + if (openError.Error is Interop.Error.ELOOP or Interop.Error.ENOTDIR) { - continue; + // If the path exists, check that it's a directory. + throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPathForError)); } - throw Interop.GetExceptionForIoErrno(error, directoryPath); + throw Interop.GetExceptionForIoErrno(openError, directoryPathForError); } - // If the path exists, check that it's a directory - if ((fileStatus.Mode & Interop.Sys.FileTypes.S_IFDIR) == 0) + if (ValidateDirectory(directoryHandle, directoryPathForError, id, ref transientRetryCount)) { - throw new IOException(SR.Format(SR.IO_SharedMemory_PathExistsButNotDirectory, directoryPath)); + return directoryHandle; } - if (isSystemDirectory) - { - // For system directories (such as TEMP_DIRECTORY_PATH), require sufficient permissions only for the - // owner user. For instance, "docker run --mount ..." to mount /tmp to some directory on the host mounts the - // destination directory with the same permissions as the source directory, which may not include some permissions for - // other users. In the docker container, other user permissions are typically not relevant and relaxing the permissions - // requirement allows for that scenario to work without having to work around it by first giving sufficient permissions - // for all users. - // - // If the directory is being used for user-scoped shared memory data, also ensure that either it has the sticky bit or - // it's owned by the current user and without write access for other users. - - permissionsMask = PermissionsMask_OwnerUser_ReadWriteExecute; - if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask - && ( - !id.IsUserScope || - (fileStatus.Mode & (int)PermissionsMask_Sticky) == (int)PermissionsMask_Sticky || - (fileStatus.Uid == id.Uid && (fileStatus.Mode & (int)PermissionsMask_NonOwnerUsers_Write) == 0) - )) - { - return true; - } - - if (RetryOnTransientPermissionFailure()) - { - continue; - } - - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrect, directoryPath, fileStatus.Uid, Convert.ToString(fileStatus.Mode, 8))); - } - - // For non-system directories (such as SharedFilesPath/UserUnscopedRuntimeTempDirectoryName), - // require the sufficient permissions and try to update them if requested to create the directory, so that - // shared memory files may be shared according to its scope. + directoryHandle.Dispose(); + } + } - // For user-scoped directories, verify the owner UID - if (id.IsUserScope && fileStatus.Uid != id.Uid) + private static bool ValidateDirectory( + SafeFileHandle directoryHandle, + string directoryPathForError, + SharedMemoryId id, + ref int transientRetryCount) + { + if (Interop.Sys.FStat(directoryHandle, out Interop.Sys.FileStatus fileStatus) != 0) + { + Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); + if ((error.Error == Interop.Error.ENOENT || error.Error == Interop.Error.EACCES) && + RetryOnTransientPermissionFailure(ref transientRetryCount)) { - if (RetryOnTransientPermissionFailure()) - { - continue; - } - - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPath, id.Uid)); + return false; } - // Verify the permissions, or try to change them if possible - if ((fileStatus.Mode & (int)PermissionsMask_AllUsers_ReadWriteExecute) == (int)permissionsMask - || (createIfNotExist && Interop.Sys.ChMod(directoryPath, (int)permissionsMask) == 0)) - { - return true; - } + throw Interop.GetExceptionForIoErrno(error, directoryPathForError); + } - // We were not able to verify or set the necessary permissions. For user-scoped directories, this is treated as a failure - // since other users aren't sufficiently restricted in permissions. - if (id.IsUserScope) - { - if (RetryOnTransientPermissionFailure()) - { - continue; - } + UnixFileMode permissionsMask = id.IsUserScope + ? PermissionsMask_OwnerUser_ReadWriteExecute + : PermissionsMask_AllUsers_ReadWriteExecute; - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPath, Convert.ToString(fileStatus.Mode, 8))); - } + // For non-system directories (such as SharedFilesPath/UserUnscopedRuntimeTempDirectoryName), + // require the sufficient permissions, so that shared memory files may be shared according to its scope. - // For user-unscoped directories, as a last resort, check that at least the owner user has full access. - permissionsMask = PermissionsMask_OwnerUser_ReadWriteExecute; - if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask) + // For user-scoped directories, verify the owner UID. + if (id.IsUserScope && fileStatus.Uid != id.Uid) + { + if (RetryOnTransientPermissionFailure(ref transientRetryCount)) { - return true; + return false; } - if (RetryOnTransientPermissionFailure()) - { - continue; - } + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryNotOwnedByUid, directoryPathForError, id.Uid)); + } - throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPath, Convert.ToString(fileStatus.Mode, 8))); + // Verify the permissions correct. + if ((fileStatus.Mode & (int)permissionsMask) == (int)permissionsMask) + { + return true; } - bool RetryOnTransientPermissionFailure() + if (RetryOnTransientPermissionFailure(ref transientRetryCount)) { - if (transientRetryCount >= TransientRetryCount) - { - return false; - } + return false; + } - transientRetryCount++; - Thread.Sleep(1); - return true; + // We were not able to verify the necessary permissions. + // Throw an exception with a message that is specific to the scope of the shared memory data. + if (id.IsUserScope) + { + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryPermissionsIncorrectUserScope, directoryPathForError, Convert.ToString(fileStatus.Mode, 8))); } + + throw new IOException(SR.Format(SR.IO_SharedMemory_DirectoryOwnerPermissionsIncorrect, directoryPathForError, Convert.ToString(fileStatus.Mode, 8))); + } + + private static bool RetryOnTransientPermissionFailure(ref int transientRetryCount) + { + if (transientRetryCount >= TransientRetryCount) + { + return false; + } + + transientRetryCount++; + Thread.Sleep(1); + return true; } internal static MemoryMappedFileHolder MemoryMapFile(SafeFileHandle fileHandle, nuint sharedDataTotalByteCount) @@ -794,11 +947,10 @@ internal sealed class SharedMemoryManager { internal static SharedMemoryManager Instance { get; } = new SharedMemoryManager(); - internal const string SharedMemorySharedMemoryDirectoryName = "shm"; private readonly LowLevelLock _creationDeletionProcessLock = new(); - private SafeFileHandle? _creationDeletionLockFileHandle; - private readonly Dictionary _uidToFileHandleMap = []; + private (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle)? _creationDeletionDirectoryHandles; + private readonly Dictionary _uidToDirectoryHandlesMap = []; public WaitSubsystem.LockHolder AcquireCreationDeletionProcessLock() { @@ -815,55 +967,71 @@ public void VerifyCreationDeletionProcessLockIsNotLocked() _creationDeletionProcessLock.VerifyIsNotLocked(); } - public AutoReleaseFileLock AcquireCreationDeletionLockForId(SharedMemoryId id) + public AutoReleaseFileLock AcquireCreationDeletionLockForId( + SharedMemoryId id, + out SafeFileHandle runtimeDirectoryHandle) { _creationDeletionProcessLock.VerifyIsLocked(); - SafeFileHandle? fd = id.IsUserScope ? GetUserScopeCreationDeletionLockFileHandle(id.Uid) : _creationDeletionLockFileHandle; - if (fd is null) + (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle)? directoryHandles = + id.IsUserScope ? GetUserScopeDirectoryHandles(id.Uid) : _creationDeletionDirectoryHandles; + if (directoryHandles is null) { - if (!SharedMemoryHelpers.EnsureDirectoryExists(SharedMemoryHelpers.SharedFilesPath, id, isGlobalLockAcquired: false, createIfNotExist: false, isSystemDirectory: true)) + using SafeFileHandle systemDirectoryHandle = SharedMemoryHelpers.OpenSharedFilesSystemDirectory(id); + string runtimeDirectoryName = SharedMemoryHelpers.GetRuntimeDirectoryName(id); + string runtimeDirectoryPath = Path.Combine( + SharedMemoryHelpers.GetSharedFilesSystemPath(id), + runtimeDirectoryName); + SafeFileHandle newRuntimeDirectoryHandle = SharedMemoryHelpers.EnsureDirectoryExists( + systemDirectoryHandle, + runtimeDirectoryName, + runtimeDirectoryPath, + id); + + SafeFileHandle sharedMemoryDirectoryHandle; + try { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - throw Interop.GetExceptionForIoErrno(error, SharedMemoryHelpers.SharedFilesPath); + sharedMemoryDirectoryHandle = SharedMemoryHelpers.EnsureDirectoryExists( + newRuntimeDirectoryHandle, + SharedMemoryHelpers.SharedMemorySharedMemoryDirectoryName, + Path.Combine(runtimeDirectoryPath, SharedMemoryHelpers.SharedMemorySharedMemoryDirectoryName), + id); } - string runtimeTempDirectory = Path.Combine( - SharedMemoryHelpers.SharedFilesPath, - id.GetRuntimeTempDirectoryName()); - - SharedMemoryHelpers.EnsureDirectoryExists(runtimeTempDirectory, id, isGlobalLockAcquired: false); - - string sharedMemoryDirectory = Path.Combine( - runtimeTempDirectory, - SharedMemorySharedMemoryDirectoryName); - - SharedMemoryHelpers.EnsureDirectoryExists(sharedMemoryDirectory, id, isGlobalLockAcquired: false); - - fd = Interop.Sys.Open(sharedMemoryDirectory, Interop.Sys.OpenFlags.O_RDONLY, 0); - if (fd.IsInvalid) + catch { - Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); - fd.Dispose(); - throw Interop.GetExceptionForIoErrno(error, sharedMemoryDirectory); + newRuntimeDirectoryHandle.Dispose(); + throw; } + (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle) newDirectoryHandles = + (newRuntimeDirectoryHandle, sharedMemoryDirectoryHandle); + directoryHandles = newDirectoryHandles; + if (id.IsUserScope) { - _uidToFileHandleMap.Add(id.Uid, fd); + _uidToDirectoryHandlesMap.Add(id.Uid, newDirectoryHandles); } else { - _creationDeletionLockFileHandle = fd; + _creationDeletionDirectoryHandles = newDirectoryHandles; } } - bool acquired = SharedMemoryHelpers.TryAcquireFileLock(fd, nonBlocking: false, exclusive: true); + (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle) handles = directoryHandles.Value; + bool acquired = SharedMemoryHelpers.TryAcquireFileLock( + handles.SharedMemoryDirectoryHandle, + nonBlocking: false, + exclusive: true); Debug.Assert(acquired); - return new AutoReleaseFileLock(fd); + runtimeDirectoryHandle = handles.RuntimeDirectoryHandle; + return new AutoReleaseFileLock(handles.SharedMemoryDirectoryHandle); - SafeFileHandle? GetUserScopeCreationDeletionLockFileHandle(uint uid) + (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle)? GetUserScopeDirectoryHandles(uint uid) { - _uidToFileHandleMap.TryGetValue(uid, out SafeFileHandle? fileHandle); - return fileHandle; + return _uidToDirectoryHandlesMap.TryGetValue( + uid, + out (SafeFileHandle RuntimeDirectoryHandle, SafeFileHandle SharedMemoryDirectoryHandle) handles) + ? handles + : null; } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs index 9ad25fa61ecf0c..518c563c93e5e2 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Mutex.Unix.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.InteropServices; using Microsoft.Win32.SafeHandles; @@ -10,6 +11,10 @@ namespace System.Threading { public sealed partial class Mutex { + [FeatureSwitchDefinition("System.Threading.Mutex.UnsafeDefaultMutexScopeGlobal")] + private static bool UnsafeDefaultMutexScopeGlobal { get; } = + AppContext.TryGetSwitch("System.Threading.Mutex.UnsafeDefaultMutexScopeGlobal", out bool enable) ? enable : false; + private void CreateMutexCore(bool initiallyOwned) => SafeWaitHandle = WaitSubsystem.NewMutex(initiallyOwned); private void CreateMutexCore( @@ -22,7 +27,7 @@ private void CreateMutexCore( { name = BuildNameForOptions(name, options); - SafeWaitHandle? safeWaitHandle = WaitSubsystem.CreateNamedMutex(initiallyOwned, name, isUserScope: options.WasSpecified && options.CurrentUserOnly, out createdNew); + SafeWaitHandle? safeWaitHandle = WaitSubsystem.CreateNamedMutex(initiallyOwned, name, isUserScope: CalculateUserScope(options), out createdNew); if (safeWaitHandle == null) { throw new WaitHandleCannotBeOpenedException(SR.Format(SR.Threading_WaitHandleCannotBeOpenedException_InvalidHandle, name)); @@ -44,7 +49,7 @@ private static OpenExistingResult OpenExistingWorker( name = BuildNameForOptions(name, options); - OpenExistingResult status = WaitSubsystem.OpenNamedMutex(name, isUserScope: options.WasSpecified && options.CurrentUserOnly, out SafeWaitHandle? safeWaitHandle); + OpenExistingResult status = WaitSubsystem.OpenNamedMutex(name, isUserScope: CalculateUserScope(options), out SafeWaitHandle? safeWaitHandle); result = status == OpenExistingResult.Success ? new Mutex(safeWaitHandle!) : null; return status; } @@ -65,6 +70,11 @@ private static string BuildNameForOptions(string name, NamedWaitHandleOptionsInt return name; } + private static bool CalculateUserScope(NamedWaitHandleOptionsInternal options) + { + return options.WasSpecified ? options.CurrentUserOnly : !UnsafeDefaultMutexScopeGlobal; + } + public void ReleaseMutex() { // The field value is modifiable via the public property, save it locally diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/NamedMutex.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/NamedMutex.Unix.cs index 72029662211c45..c21c06a96ec82d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/NamedMutex.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/NamedMutex.Unix.cs @@ -209,7 +209,8 @@ public void Abandon(NamedMutexOwnershipChain chain, Thread abandonedThread) createIfNotExist, acquireLockIfCreated, out created, - out AutoReleaseFileLock creationDeletionLockFileScope); + out AutoReleaseFileLock creationDeletionLockFileScope, + out SafeFileHandle runtimeDirectoryHandle); if (processDataHeader is null) { @@ -231,7 +232,10 @@ public void Abandon(NamedMutexOwnershipChain chain, Thread abandonedThread) } else { - processDataHeader._processData = new NamedMutexProcessDataNoPThreads(processDataHeader, created); + processDataHeader._processData = new NamedMutexProcessDataNoPThreads( + processDataHeader, + runtimeDirectoryHandle, + created); } if (created && acquireLockIfCreated) @@ -419,34 +423,46 @@ public override void Close(bool releaseSharedData) internal sealed unsafe class NamedMutexProcessDataNoPThreads : NamedMutexProcessDataBase { private const string SharedMemoryLockFilesDirectoryName = "lockfiles"; + private readonly Mutex _processLevelMutex = new Mutex(initiallyOwned: false); private readonly SafeFileHandle _sharedLockFileHandle; private readonly SharedData* _sharedData; - public NamedMutexProcessDataNoPThreads(SharedMemoryProcessDataHeader processDataHeader, bool created) : base(processDataHeader) + public NamedMutexProcessDataNoPThreads( + SharedMemoryProcessDataHeader processDataHeader, + SafeFileHandle runtimeDirectoryHandle, + bool created) : base(processDataHeader) { _sharedData = (SharedData*)SharedMemoryProcessDataHeader.GetDataPointer(processDataHeader); - string lockFileDirectory = Path.Combine( - SharedMemoryHelpers.SharedFilesPath, - Id.GetRuntimeTempDirectoryName(), - SharedMemoryLockFilesDirectoryName - ); - - if (created) - { - SharedMemoryHelpers.EnsureDirectoryExists(lockFileDirectory, Id, isGlobalLockAcquired: true); - } - - string sessionDirectory = Path.Combine(lockFileDirectory, Id.GetSessionDirectoryName()); - - if (created) - { - SharedMemoryHelpers.EnsureDirectoryExists(sessionDirectory, Id, isGlobalLockAcquired: true); - } - - string lockFilePath = Path.Combine(sessionDirectory, Id.Name); - _sharedLockFileHandle = SharedMemoryHelpers.CreateOrOpenFile(lockFilePath, Id, created, out _); + string lockFilesDirectoryPath = Path.Combine( + SharedMemoryHelpers.GetSharedFilesSystemPath(Id), + SharedMemoryHelpers.GetRuntimeDirectoryName(Id), + SharedMemoryLockFilesDirectoryName); + using SafeFileHandle lockFilesDirectoryHandle = SharedMemoryHelpers.EnsureDirectoryExists( + runtimeDirectoryHandle, + SharedMemoryLockFilesDirectoryName, + lockFilesDirectoryPath, + Id, + created); + + string sessionDirectoryName = SharedMemoryHelpers.GetSessionDirectoryName(Id); + using SafeFileHandle sessionDirectoryHandle = lockFilesDirectoryHandle.IsInvalid + ? new SafeFileHandle() + : SharedMemoryHelpers.EnsureDirectoryExists( + lockFilesDirectoryHandle, + sessionDirectoryName, + Path.Combine(lockFilesDirectoryPath, sessionDirectoryName), + Id, + created); + + _sharedLockFileHandle = SharedMemoryHelpers.CreateOrOpenFile( + sessionDirectoryHandle, + Id.Name, + Path.Combine(lockFilesDirectoryPath, sessionDirectoryName, Id.Name), + Id, + created, + out _); } protected override bool IsLockOwnedByThreadInThisProcess(Thread thread) diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs index 54dd4c7580aab2..a32cec24c00aff 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/LocalAppContextSwitches.cs @@ -119,5 +119,17 @@ public static bool UseLegacyXmlListSeparation return SwitchesHelpers.GetCachedSwitchValue("Switch.System.Xml.Serialization.UseLegacyXmlListSeparation", ref s_useLegacyXmlListSeparation); } } + + private static int s_disableOptimizedXmlTextReaderAttributeBufferInvalidation; + public static bool DisableOptimizedXmlTextReaderAttributeBufferInvalidation + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return SwitchesHelpers.GetCachedSwitchValue( + "Switch.System.Xml.DisableOptimizedXmlTextReaderAttributeBufferInvalidation", + ref s_disableOptimizedXmlTextReaderAttributeBufferInvalidation); + } + } } } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs index c32af941e9c937..da4f9f3d57f61b 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImpl.cs @@ -163,6 +163,7 @@ private enum InitInputType // attributes info private int _curAttrIndex = -1; private int _attrCount; + private int _attrBufferInvalidationIndex; private int _attrHashtable; private int _attrDuplWalkCount; private bool _attrNeedNamespaceLookup; @@ -3362,10 +3363,25 @@ private int ReadData() // the character buffer is full -> allocate a new one if (_ps.charsUsed == _ps.chars.Length - 1) { - // invalidate node values kept in buffer - applies to attribute values only - for (int i = 0; i < _attrCount; i++) + if (LocalAppContextSwitches.DisableOptimizedXmlTextReaderAttributeBufferInvalidation) { - _nodes[_index + i + 1].OnBufferInvalidated(); + for (int i = 0; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + } + else + { + // invalidate node values kept in buffer - applies to attribute values only. + // Attribute values below _attrBufferInvalidationIndex have already been materialized + // into strings and no longer reference the buffer. The last attribute may still be in + // the middle of being re-parsed and can have its buffer-backed value assigned after this + // point, so it is intentionally left to be re-checked on the next buffer reload. + for (int i = _attrBufferInvalidationIndex; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + _attrBufferInvalidationIndex = Math.Max(_attrCount - 1, 0); } char[] newChars = new char[_ps.chars.Length * 2]; @@ -3399,10 +3415,25 @@ private int ReadData() int charsLen = _ps.chars.Length; if (charsLen - _ps.charsUsed <= charsLen / 2) { - // invalidate node values kept in buffer - applies to attribute values only - for (int i = 0; i < _attrCount; i++) + if (LocalAppContextSwitches.DisableOptimizedXmlTextReaderAttributeBufferInvalidation) { - _nodes[_index + i + 1].OnBufferInvalidated(); + for (int i = 0; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + } + else + { + // invalidate node values kept in buffer - applies to attribute values only. + // Attribute values below _attrBufferInvalidationIndex have already been materialized + // into strings and no longer reference the buffer. The last attribute may still be in + // the middle of being re-parsed and can have its buffer-backed value assigned after this + // point, so it is intentionally left to be re-checked on the next buffer reload. + for (int i = _attrBufferInvalidationIndex; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + _attrBufferInvalidationIndex = Math.Max(_attrCount - 1, 0); } // move unparsed characters to front, unless the whole buffer contains unparsed characters @@ -7834,6 +7865,7 @@ private void ResetAttributes() _curAttrIndex = -1; _attrCount = 0; + _attrBufferInvalidationIndex = 0; _attrHashtable = 0; _attrDuplWalkCount = 0; } diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImplAsync.cs b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImplAsync.cs index 8edafd8e7ef408..dcda73aa9f91b2 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImplAsync.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Core/XmlTextReaderImplAsync.cs @@ -1034,10 +1034,25 @@ private async Task ReadDataAsync() // the character buffer is full -> allocate a new one if (_ps.charsUsed == _ps.chars.Length - 1) { - // invalidate node values kept in buffer - applies to attribute values only - for (int i = 0; i < _attrCount; i++) + if (LocalAppContextSwitches.DisableOptimizedXmlTextReaderAttributeBufferInvalidation) { - _nodes[_index + i + 1].OnBufferInvalidated(); + for (int i = 0; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + } + else + { + // invalidate node values kept in buffer - applies to attribute values only. + // Attribute values below _attrBufferInvalidationIndex have already been materialized + // into strings and no longer reference the buffer. The last attribute may still be in + // the middle of being re-parsed and can have its buffer-backed value assigned after this + // point, so it is intentionally left to be re-checked on the next buffer reload. + for (int i = _attrBufferInvalidationIndex; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + _attrBufferInvalidationIndex = Math.Max(_attrCount - 1, 0); } char[] newChars = new char[_ps.chars.Length * 2]; @@ -1070,10 +1085,25 @@ private async Task ReadDataAsync() int charsLen = _ps.chars.Length; if (charsLen - _ps.charsUsed <= charsLen / 2) { - // invalidate node values kept in buffer - applies to attribute values only - for (int i = 0; i < _attrCount; i++) + if (LocalAppContextSwitches.DisableOptimizedXmlTextReaderAttributeBufferInvalidation) { - _nodes[_index + i + 1].OnBufferInvalidated(); + for (int i = 0; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + } + else + { + // invalidate node values kept in buffer - applies to attribute values only. + // Attribute values below _attrBufferInvalidationIndex have already been materialized + // into strings and no longer reference the buffer. The last attribute may still be in + // the middle of being parsed and can have its buffer-backed value assigned after this + // point, so it is intentionally left to be re-checked on the next buffer reload. + for (int i = _attrBufferInvalidationIndex; i < _attrCount; i++) + { + _nodes[_index + i + 1].OnBufferInvalidated(); + } + _attrBufferInvalidationIndex = Math.Max(_attrCount - 1, 0); } // move unparsed characters to front, unless the whole buffer contains unparsed characters diff --git a/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj b/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj index fd0263004573b2..bb6382ddfb9562 100644 --- a/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj +++ b/src/libraries/System.Private.Xml/tests/System.Private.Xml.Tests.csproj @@ -335,6 +335,7 @@ + diff --git a/src/libraries/System.Private.Xml/tests/XmlReader/Tests/AttributeBufferInvalidationTests.cs b/src/libraries/System.Private.Xml/tests/XmlReader/Tests/AttributeBufferInvalidationTests.cs new file mode 100644 index 00000000000000..29b489d4fa2ad4 --- /dev/null +++ b/src/libraries/System.Private.Xml/tests/XmlReader/Tests/AttributeBufferInvalidationTests.cs @@ -0,0 +1,94 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace System.Xml.XmlReaderTests +{ + public static class AttributeBufferInvalidationTests + { + // The reader keeps attribute values as lazy references into its internal character buffer and only + // materializes ("invalidates") them into strings when that buffer has to be grown or shifted while a + // start tag is still being parsed. Parsing an element whose start tag is much larger than the internal + // buffer (4 KB for the synchronous reader, 32 KB for the asynchronous one) forces ReadData/ReadDataAsync + // to reload the buffer many times, so the invalidation loop runs multiple passes over the attribute nodes. + // + // These tests exercise that path and verify that every attribute value survives the reloads intact. + // Multiple elements ensure the per-element invalidation bookkeeping is reset between elements, and one + // attribute per element is deliberately written with a huge run of whitespace between its name and the + // '=' sign: because that run is larger than the internal buffer, a buffer reload is guaranteed to happen + // after the attribute has been counted but before its buffer-backed value has been assigned. That is + // exactly the window in which the bookkeeping must not mark the still-pending attribute as already + // materialized; a regression there leaves a dangling reference into the buffer and corrupts the value. + private const int ElementCount = 3; + private const int AttributesPerElement = 3000; + private const int ValuePadding = 40; + + // Larger than the asynchronous reader's 32 KB internal buffer so a reload is guaranteed while the + // attribute carrying it is counted but not yet valued. + private const int PendingAttributeWhitespace = 40 * 1024; + + // The attribute (per element) that is preceded by the huge whitespace run. + private const int PendingAttributeIndex = 1; + + private static string MakeAttributeValue(int element, int attribute) => + $"E{element}_A{attribute}_" + new string((char)('a' + (attribute % 26)), ValuePadding); + + private static string BuildXml() + { + var sb = new StringBuilder(ElementCount * (PendingAttributeWhitespace + AttributesPerElement * (ValuePadding + 24))); + sb.Append(""); + for (int e = 0; e < ElementCount; e++) + { + sb.Append(""); + } + sb.Append(""); + return sb.ToString(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static async Task AttributeValuesRemainValidAcrossBufferReloads(bool async) + { + string xml = BuildXml(); + var settings = new XmlReaderSettings { Async = async }; + + using XmlReader reader = XmlReader.Create(new StringReader(xml), settings); + + int elementsChecked = 0; + while (async ? await reader.ReadAsync() : reader.Read()) + { + if (reader.NodeType != XmlNodeType.Element || reader.Depth == 0) + { + continue; + } + + Assert.Equal($"e{elementsChecked}", reader.Name); + Assert.Equal(AttributesPerElement, reader.AttributeCount); + + for (int a = 0; a < AttributesPerElement; a++) + { + Assert.Equal(MakeAttributeValue(elementsChecked, a), reader.GetAttribute(a)); + } + + elementsChecked++; + } + + Assert.Equal(ElementCount, elementsChecked); + } + } +} diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/Canonicalization/XmlCanonicalizationTest.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/Canonicalization/XmlCanonicalizationTest.cs index ec65d0f3a614ac..8ea336f4818f70 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/Canonicalization/XmlCanonicalizationTest.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/Canonicalization/XmlCanonicalizationTest.cs @@ -300,7 +300,8 @@ public static void ReaderWriter_C14N_DifferentReadersWriters() } //TestC14NWriterWithManyAttributes - int numberOfAttributes = 1000; + // Stay below the default per-element C14N limit while preserving the interoperability stress test. + const int numberOfAttributes = 500; int seed = (int)DateTime.Now.Ticks; Random rndGen = new Random(seed); StringBuilder sb = new StringBuilder(); diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs index 379fbd98cdc66b..0eb19837a2cd8d 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs @@ -2018,6 +2018,27 @@ public static void TestTryParseAtBoundaries() "DateTime parsing expected to throw with any dates greater than DateTime.MaxValue"); } + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))] + [InlineData("\u6642")] // CJK hour suffix 時 + [InlineData("\u5206")] // CJK minute suffix 分 + [InlineData("\u79D2")] // CJK second suffix 秒 + public static void TryParse_TooManyNumbersViaHebrewLexer_FailsWithoutOverflow(string timeSuffix) + { + // Any letter in the Hebrew Unicode block (U+0590-U+05FF) is lexed as a "Hebrew number" + // under every non-invariant culture -- this does NOT require a Hebrew calendar or a Hebrew + // culture. The CJK hour/minute/second suffixes are likewise recognized everywhere. Using + // spaces as separators (a separator in every culture) makes the repro culture-independent: + // "1" and "2" fill two of DateTimeRawInfo's three number slots; + // "3" is consumed as a time-suffixed number without taking a slot; + // the Hebrew numerals are then lexed as additional day numbers, and the second one + // attempts a fourth push into the three-slot buffer. + // Previously that fourth push overflowed the buffer and corrupted adjacent memory; ensure + // the input now fails to parse cleanly instead. + // Example (hour suffix): "1 2 3時 א' ב' ג'" (\u05D0='א', \u05D1='ב', \u05D2='ג', \u0027=geresh) + string input = $"1 2 3{timeSuffix} \u05D0\u0027 \u05D1\u0027 \u05D2\u0027"; + Assert.False(DateTime.TryParse(input, CultureInfo.InvariantCulture, DateTimeStyles.None, out _), $"Input '{input}' should fail to parse."); + } + public static IEnumerable Parse_ValidInput_Succeeds_MemberData() { yield return new object[] { "1234 12", CultureInfo.InvariantCulture, new DateTime(1234, 12, 1, 0, 0, 0) }; diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/Windows/HelpersWindows.cs b/src/libraries/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/Windows/HelpersWindows.cs index 109c39635f6c28..f14788bdce0684 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/Windows/HelpersWindows.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/Windows/HelpersWindows.cs @@ -496,7 +496,7 @@ private static unsafe void AddCryptAttribute(CryptographicAttributeObjectCollect attributeCollection.Add(attributeObject); } - collection.Add(new CryptographicAttributeObject(oid, attributeCollection)); + collection.AddWithoutMerge(new CryptographicAttributeObject(oid, attributeCollection)); } } } diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampRequest.cs b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampRequest.cs index a4980d667ac23a..d318ce3df39f66 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampRequest.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampRequest.cs @@ -461,7 +461,7 @@ private Rfc3161RequestResponseStatus ValidateResponse( // If no certificate was requested then the CMS Certificates collection // MUST be empty. - if (tokenCms.Certificates.Count != 0) + if (tokenCms.GetCertificatesNoClone().Count != 0) { if (shouldThrow) { diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs index 1ec7b1097ac481..3e3e52769a5607 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs @@ -19,6 +19,7 @@ public sealed partial class SignedCms { private SignedDataAsn _signedData; private bool _hasData; + private readonly CertificateList _certificates; private readonly SubjectIdentifierType _signerIdentifierType; // A defensive copy of the relevant portions of the data to Decode @@ -68,6 +69,17 @@ public SignedCms(SubjectIdentifierType signerIdentifierType, ContentInfo content ContentInfo = contentInfo; Detached = detached; Version = 0; + _certificates = new CertificateList(this); + } + + internal X509Certificate2? FindCertificate(SubjectIdentifier signerIdentifier) + { + return _certificates.FindCertificate(signerIdentifier); + } + + internal X509Certificate2Collection GetCertificatesNoClone() + { + return _certificates.GetCollection(); } public X509Certificate2Collection Certificates @@ -81,19 +93,16 @@ public X509Certificate2Collection Certificates return coll; } - CertificateChoiceAsn[]? certChoices = _signedData.CertificateSet; - - if (certChoices == null) + foreach (X509Certificate2 cert in GetCertificatesNoClone()) { - return coll; - } - - foreach (CertificateChoiceAsn choice in certChoices) - { - if (choice.Certificate.HasValue) - { - coll.Add(X509CertificateLoader.LoadCertificate(choice.Certificate.Value.Span)); - } + coll.Add( + X509CertificateLoader.LoadCertificate( +#if NET + cert.RawDataMemory.Span +#else + cert.RawData +#endif + )); } return coll; @@ -548,7 +557,7 @@ internal void UpdateCertificatesFromAddition(X509Certificate2Collection newCerts if (existingLength > 0 || newCerts.Count > 1) { - var certs = new HashSet(Certificates.OfType()); + var certs = new HashSet(GetCertificatesNoClone().OfType()); for (int i = 0; i < newCerts.Count; i++) { @@ -718,5 +727,156 @@ void RemoveCertificate(X509Certificate2 certificate) throw new CryptographicException(SR.Cryptography_Cms_NoCertificateFound); } + + private sealed class CertificateList + { + private readonly SignedCms _parent; + private X509Certificate2Collection? _coll; + private CertificateChoiceAsn[]? _arr; + private Dictionary? _skiLookup; + private Dictionary? _issuerSerialLookup; + + internal CertificateList(SignedCms parent) + { + _parent = parent; + } + + internal X509Certificate2Collection GetCollection() + { + CertificateChoiceAsn[]? certSet = _parent._signedData.CertificateSet; + + if (!ReferenceEquals(_arr, certSet)) + { + _coll = null; + _skiLookup = null; + _issuerSerialLookup = null; + } + + if (_coll == null) + { + X509Certificate2Collection newColl = new(); + + if (certSet != null) + { + foreach (CertificateChoiceAsn cert in certSet) + { + if (cert.Certificate is not null) + { + newColl.Add(X509CertificateLoader.LoadCertificate(cert.Certificate.Value.Span)); + } + } + } + + _coll = newColl; + _arr = certSet; + } + + return _coll; + } + + internal X509Certificate2? FindCertificate(SubjectIdentifier signerIdentifier) + { + const int LinearSearchThreshold = 10; + + X509Certificate2Collection coll = GetCollection(); + + if (coll.Count > LinearSearchThreshold) + { + if (signerIdentifier.Type == SubjectIdentifierType.SubjectKeyIdentifier) + { + if (_skiLookup is null) + { + _skiLookup = new Dictionary(coll.Count, StringComparer.Ordinal); + + foreach (X509Certificate2 item in coll) + { + string ski = PkcsPal.Instance.GetSubjectKeyIdentifier(item).ToSkiString(); + + // The first certificate in the collection with the same SKI wins, + // since the linear method returns the first match. +#if NET + _ = _skiLookup.TryAdd(ski, item); +#else + try + { + _skiLookup.Add(ski, item); + } + catch (ArgumentException) + { + } +#endif + } + } + + string key = (string)signerIdentifier.Value!; + key = key.ToUpperInvariant(); + + Debug.Assert(signerIdentifier.Value is not null); + _ = _skiLookup.TryGetValue(key, out X509Certificate2? cert); + return CloneCert(cert); + } + else if (signerIdentifier.Type == SubjectIdentifierType.IssuerAndSerialNumber) + { + if (_issuerSerialLookup is null) + { + _issuerSerialLookup = new Dictionary(coll.Count, StringComparer.Ordinal); + + foreach (X509Certificate2 item in coll) + { + // Serial number is a hexadecimal string, so it cannot contain a pipe character. + // Therefore, the separator cannot be caller-influenced. + string itemKey = $"{item.SerialNumber}||{item.IssuerName.Name}"; + + // The first certificate in the collection with the same issuer/serial wins. +#if NET + _ = _issuerSerialLookup.TryAdd(itemKey, item); +#else + try + { + _issuerSerialLookup.Add(itemKey, item); + } + catch (ArgumentException) + { + } +#endif + } + } + + var issuerSerial = (Xml.X509IssuerSerial)signerIdentifier.Value!; + string key = $"{issuerSerial.SerialNumber.ToUpperInvariant()}||{issuerSerial.IssuerName}"; + + _ = _issuerSerialLookup.TryGetValue(key, out X509Certificate2? cert); + return CloneCert(cert); + } + else if (signerIdentifier.Type == SubjectIdentifierType.NoSignature) + { + return null; + } + else + { + // Fail in debug, but at runtime fall back to the other function. + Debug.Fail($"Missing handler for SubjectIdentifierType {signerIdentifier.Type}"); + } + } + + return SignerInfo.FindSignerCertificate(signerIdentifier, coll); + + static X509Certificate2? CloneCert(X509Certificate2? cert) + { + if (cert is null) + { + return null; + } + + return X509CertificateLoader.LoadCertificate( +#if NET + cert.RawDataMemory.Span +#else + cert.RawData +#endif + ); + } + } + } } } diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs index f9d7d37f7d161d..24e6d4727dd529 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignerInfo.cs @@ -501,10 +501,10 @@ private bool CheckHash(bool compatMode) private X509Certificate2? FindSignerCertificate() { - return FindSignerCertificate(SignerIdentifier, _document.Certificates); + return _document.FindCertificate(SignerIdentifier); } - private static X509Certificate2? FindSignerCertificate( + internal static X509Certificate2? FindSignerCertificate( SubjectIdentifier signerIdentifier, X509Certificate2Collection? extraStore) { diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/EnvelopedCms/UnprotectedAttributeTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/EnvelopedCms/UnprotectedAttributeTests.cs index a558ac405b5faf..90e970a076f7bb 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/EnvelopedCms/UnprotectedAttributeTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/EnvelopedCms/UnprotectedAttributeTests.cs @@ -471,6 +471,41 @@ public static void TestVersionNumber_RoundTrip(bool addUnprotectedAttrs, int exp Assert.Equal(expectedVersion, ecms.Version); } + [Fact] + public static void DecodeDoesNotMergeAttributes() + { + // This payload has two unprotected attribute sets, both the exact same signing time. + // If the implementation merges them via CryptographicAttributeObjectCollection.Add, + // then we'd notice two things: + // 1) cms.UnprotectedAttributes.Count would be 1 instead of 2 + // 2) The document won't load, since CAOC.Add blocks multiple signing times. + // + // Both elements get tested here to make sure we don't just add a merge allowing duplicate signing times. + byte[] encoded = ( + "308201D706092A864886F70D010703A08201C8308201C4020102318201413082013D0201003025301" + + "8311630140603550403130D706F632D726563697069656E74020900E67536515F29D64B300D06092A" + + "864886F70D010101050004820100B8CC481FF8BED28B411370842708D4551C2422E3F8A0C7E1C9835" + + "534E5B6E7E657765A5DFDAE2CEAF7D036ED0B7919D04FCE43CB88ECCCD995587DB30D6C7A33DC0275" + + "F1BFE615AA522826A3D4C1471A2C4A0504F0637C90F12BF811ACE90DC966D7F8E449E9712E86C22F8" + + "B959D5730FB0CA23EC0DE1814EC65433681B3095D7B30C08FF1E5804C9E725A033359E7BD6A36C598" + + "17C659E1663DF68902A35208DD1C93BF3489A02949F52CF82FF8D911A2AEB68D38E1BBA0A3BACB463" + + "FA77C24B7896FC6C9D2EBD4AE80891C8CB9973C1778B697ED68803380B8FD240C9D1450F22CA3A54A" + + "1861B65DA8E2BEDADF6AD9D8BFA66CDB2E9B58EA2C3ECB61D15581303C06092A864886F70D0107013" + + "01D060960864801650304012A0410D21007E2DFD902CBE983CAA85375891B80103B5BEA9B220BFD2C" + + "85D9AB7B153299A1A13C301C06092A864886F70D010905310F170D3236303832303139353530395A3" + + "01C06092A864886F70D010905310F170D3236303832303139353530395A").HexToByteArray(); + + EnvelopedCms ecms = new EnvelopedCms(); + ecms.Decode(encoded); + + CryptographicAttributeObjectCollection attrs = ecms.UnprotectedAttributes; + Assert.Equal(2, attrs.Count); + Assert.Equal(1, attrs[0].Values.Count); + Assert.Equal(1, attrs[1].Values.Count); + Assert.IsType(attrs[0].Values[0]); + Assert.IsType(attrs[1].Values[0]); + } + private static void AssertIsDocumentationDescription(this AsnEncodedData attribute, string expectedDocumentDescription) { Assert.Equal(Oids.DocumentDescription, attribute.Oid.Value); diff --git a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs index 530c5f93bc557b..72f360d1f8f6af 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs +++ b/src/libraries/System.Security.Cryptography.Pkcs/tests/SignedCms/SignedCmsTests.cs @@ -1741,6 +1741,159 @@ public static void Decode_CanDecodeWithAttributeCertificate() cms.CheckSignature(verifySignatureOnly: true); } + [Fact] + public static void LargeSignerCountSanityTest() + { + // Ignoring the fact that they long preceded computers, pretend the 1789 U.S. Electoral College + // signed their vote tallies with CMS. + // + // Build that, and make sure the document works at scale. + + const int ElectorsCount = 69; + X509Certificate2 root = null; + X509Certificate2 issuer = null; + X509Certificate2 lastSigner = null; + string dateSuffix = $"{DateTime.UtcNow:yyyy-MM-dd HH:mm:ss.ffff}"; + + ContentInfo content = new ContentInfo( + Text.Encoding.ASCII.GetBytes(@" +President: +George Washington (69) + +Vice President: +John Adams (34) + +No Office: +John Jay (9) +Robert H. Harrison (6) +John Rutledge (6) +John Hancock (4) +George Clinton (3) +Samuel Huntington (2) +John Milton (2) +James Armstrong (1) +Benjamin Lincoln (1) +Edward Telfair (1) +")); + + byte[] encoded; + + try + { + SignedCms builder = new SignedCms(content, detached: false); + + root = MakeRoot(dateSuffix); + issuer = MakeChild("Elector Issuing Authority", root, dateSuffix, isCA: true); + + for (int i = 0; i < ElectorsCount; i++) + { + lastSigner?.Dispose(); + lastSigner = MakeChild($"Elector {i + 1} of {ElectorsCount}", issuer, dateSuffix); + + // Alternate between IssuerAndSerialNumber and SubjectKeyIdentifier to make sure both work. + SubjectIdentifierType type = (SubjectIdentifierType)(1 + i & 1); + CmsSigner signer = new CmsSigner(type, lastSigner) + { + IncludeOption = X509IncludeOption.EndCertOnly, + }; + + if (i == 0) + { + signer.Certificates.Add(issuer); + signer.Certificates.Add(root); + } + + builder.ComputeSignature(signer, silent: true); + } + + encoded = builder.Encode(); + } + finally + { + lastSigner?.Dispose(); + issuer?.Dispose(); + root?.Dispose(); + } + + SignedCms verifier = new SignedCms(); + verifier.Decode(encoded); + + AssertExtensions.Equal(ElectorsCount + 2, verifier.Certificates.Count); + + foreach (SignerInfo signerInfo in verifier.SignerInfos) + { + Assert.NotNull(signerInfo.Certificate); + + // Assert.NoThrow + signerInfo.CheckSignature(verifySignatureOnly: true); + } + + // Assert.NoThrow + verifier.CheckSignature(verifySignatureOnly: true); + + static X509Certificate2 MakeRoot(string dateSuffix) + { + using (ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP521)) + { + CertificateRequest req = new CertificateRequest( + $"CN=Elector Root Authority {dateSuffix}, O=Mythical Elector Attestation", + key, + HashAlgorithmName.SHA512); + + req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + req.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + DateTimeOffset now = DateTimeOffset.UtcNow; + return req.CreateSelfSigned(now.AddMinutes(-3), now.AddMinutes(120)); + } + } + + static X509Certificate2 MakeChild(string cn, X509Certificate2 issuer, string dateSuffix, bool isCA = false) + { + ECCurve curve = isCA ? ECCurve.NamedCurves.nistP384 : ECCurve.NamedCurves.nistP256; + + using (ECDsa key = ECDsa.Create(curve)) + { + CertificateRequest req = new CertificateRequest( + $"CN={cn} {dateSuffix}, O=Mythical Elector Attestation", + key, + HashAlgorithmName.SHA512); + + req.CertificateExtensions.Add(new X509BasicConstraintsExtension(isCA, false, 0, true)); + req.CertificateExtensions.Add( + new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); + +#if NET + req.CertificateExtensions.Add( + X509AuthorityKeyIdentifierExtension.CreateFromCertificate(issuer, true, true)); +#endif + + byte[] serial = new byte[20]; +#if NET + RandomNumberGenerator.Fill(serial); +#else + using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(serial); + } +#endif + + // Ensure positive serial number + serial[0] &= 0x7F; + + X509Certificate2 pubOnly = req.Create( + issuer, + issuer.NotBefore.AddMinutes(1), + issuer.NotAfter.AddMinutes(-1), + serial); + + X509Certificate2 withKey = pubOnly.CopyWithPrivateKey(key); + pubOnly.Dispose(); + return withKey; + } + } + } + // Ed25519 certificate from https://datatracker.ietf.org/doc/html/rfc8410#section-10.2 private const string UnknownAlgorithmCert = """ diff --git a/src/libraries/System.Security.Cryptography.Xml/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.Xml/src/Resources/Strings.resx index 89776629fa7c59..5fa8c2cb89ef23 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.Xml/src/Resources/Strings.resx @@ -183,12 +183,18 @@ Signing key is not loaded. - - The number of EncryptedData elements processed during decryption has exceeded the maximum allowed for a single operation. + + The XML element has exceeded the maximum number of attributes or namespace declarations allowed for canonicalization. + + + The XML document has exceeded the maximum number of elements allowed for decryption. The XML element has exceeded the maximum nesting depth allowed for decryption. + + The number of EncryptedData elements processed during decryption has exceeded the maximum allowed for a single operation. + Symmetric algorithm is not specified. If the application has been trimmed, ensure the required algorithm implementations are preserved. @@ -405,4 +411,4 @@ Xslts are only supported when dynamic code is supported. - \ No newline at end of file + diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/C14NAncestralNamespaceContextManager.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/C14NAncestralNamespaceContextManager.cs index fbc2200bc229c1..9670b951f4069a 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/C14NAncestralNamespaceContextManager.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/C14NAncestralNamespaceContextManager.cs @@ -34,9 +34,9 @@ private void GetNamespaceToRender(string nsPrefix, SortedList attrListToRender, { nsLocallyDeclared.Remove(nsPrefix); if (Utils.IsXmlNamespaceNode(local)) - attrListToRender.Add(local, null); + Utils.AddToRenderList(attrListToRender, local); else - nsListToRender.Add(local, null); + Utils.AddToRenderList(nsListToRender, local); } } else @@ -46,9 +46,9 @@ private void GetNamespaceToRender(string nsPrefix, SortedList attrListToRender, if (uAncestral != null && uDepth > rDepth && Utils.IsNonRedundantNamespaceDecl(uAncestral, rAncestral)) { if (Utils.IsXmlNamespaceNode(uAncestral)) - attrListToRender.Add(uAncestral, null); + Utils.AddToRenderList(attrListToRender, uAncestral); else - nsListToRender.Add(uAncestral, null); + Utils.AddToRenderList(nsListToRender, uAncestral); } } } @@ -67,9 +67,9 @@ internal override void GetNamespacesToRender(XmlElement element, SortedList attr { nsLocallyDeclared.Remove(Utils.GetNamespacePrefix(attrib)); if (Utils.IsXmlNamespaceNode(attrib)) - attrListToRender.Add(attrib, null); + Utils.AddToRenderList(attrListToRender, attrib); else - nsListToRender.Add(attrib, null); + Utils.AddToRenderList(nsListToRender, attrib); } } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXml.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXml.cs index f8564b06eaaba8..9dcc75615bcc87 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXml.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXml.cs @@ -18,7 +18,16 @@ internal CanonicalXml(Stream inputStream, bool includeComments, XmlResolver? res _c14nDoc = new CanonicalXmlDocument(true, includeComments); _c14nDoc.XmlResolver = resolver; - _c14nDoc.Load(Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri)); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(_c14nDoc, Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri), maxDepth); + } + else + { + _c14nDoc.Load(Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri)); + } + _ancMgr = new C14NAncestralNamespaceContextManager(); } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXmlElement.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXmlElement.cs index 1a39fceb0169a1..d8db432f3ed91c 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXmlElement.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/CanonicalXmlElement.cs @@ -47,7 +47,7 @@ public void Write(StringBuilder strBuilder, DocPosition docPos, AncestralNamespa } else if (IsInNodeSet) { - attrListToRender.Add(attr, null); + Utils.AddToRenderList(attrListToRender, attr); } } } @@ -119,7 +119,7 @@ public void WriteHash(HashAlgorithm hash, DocPosition docPos, AncestralNamespace } else if (IsInNodeSet) { - attrListToRender.Add(attr, null); + Utils.AddToRenderList(attrListToRender, attr); } } } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/EncryptedXml.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/EncryptedXml.cs index edd847aa085a3b..547d6241485c37 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/EncryptedXml.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/EncryptedXml.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Security.Cryptography.X509Certificates; @@ -55,6 +56,18 @@ public class EncryptedXml public const string XmlEncSHA256Url = "http://www.w3.org/2001/04/xmlenc#sha256"; public const string XmlEncSHA512Url = "http://www.w3.org/2001/04/xmlenc#sha512"; + [ThreadStatic] + private static int t_decryptionScopeDepth; + + [ThreadStatic] + private static int t_decryptionCount; + + [ThreadStatic] + private static int t_decryptDataCallCount; + + [ThreadStatic] + private static int t_encryptedDataCandidateCount; + // // private members // @@ -293,33 +306,41 @@ public virtual byte[] GetDecryptionIV(EncryptedData encryptedData, string? symme { ArgumentNullException.ThrowIfNull(encryptedData); - int initBytesSize; - // If the Uri is not provided by the application, try to get it from the EncryptionMethod - if (symmetricAlgorithmUri == null) + EnterDecryptionOperation(); + try { - if (encryptedData.EncryptionMethod == null) - throw new CryptographicException(SR.Cryptography_Xml_MissingAlgorithm); - symmetricAlgorithmUri = encryptedData.EncryptionMethod.KeyAlgorithm; + int initBytesSize; + // If the Uri is not provided by the application, try to get it from the EncryptionMethod + if (symmetricAlgorithmUri == null) + { + if (encryptedData.EncryptionMethod == null) + throw new CryptographicException(SR.Cryptography_Xml_MissingAlgorithm); + symmetricAlgorithmUri = encryptedData.EncryptionMethod.KeyAlgorithm; + } + switch (symmetricAlgorithmUri) + { + case EncryptedXml.XmlEncDESUrl: + case EncryptedXml.XmlEncTripleDESUrl: + initBytesSize = 8; + break; + case EncryptedXml.XmlEncAES128Url: + case EncryptedXml.XmlEncAES192Url: + case EncryptedXml.XmlEncAES256Url: + initBytesSize = 16; + break; + default: + // The Uri is not supported. + throw new CryptographicException(SR.Cryptography_Xml_UriNotSupported); + } + byte[] IV = new byte[initBytesSize]; + byte[] cipherValue = GetCipherValue(encryptedData.CipherData); + Buffer.BlockCopy(cipherValue, 0, IV, 0, IV.Length); + return IV; } - switch (symmetricAlgorithmUri) + finally { - case EncryptedXml.XmlEncDESUrl: - case EncryptedXml.XmlEncTripleDESUrl: - initBytesSize = 8; - break; - case EncryptedXml.XmlEncAES128Url: - case EncryptedXml.XmlEncAES192Url: - case EncryptedXml.XmlEncAES256Url: - initBytesSize = 16; - break; - default: - // The Uri is not supported. - throw new CryptographicException(SR.Cryptography_Xml_UriNotSupported); + ExitDecryptionOperation(); } - byte[] IV = new byte[initBytesSize]; - byte[] cipherValue = GetCipherValue(encryptedData.CipherData); - Buffer.BlockCopy(cipherValue, 0, IV, 0, IV.Length); - return IV; } // default behaviour is to look for keys defined by an EncryptedKey clause @@ -330,6 +351,21 @@ public virtual byte[] GetDecryptionIV(EncryptedData encryptedData, string? symme { ArgumentNullException.ThrowIfNull(encryptedData); + EnterDecryptionOperation(); + try + { + return GetDecryptionKeyCore(encryptedData, symmetricAlgorithmUri); + } + finally + { + ExitDecryptionOperation(); + } + } + + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "ctors are marked as RDC")] + [UnconditionalSuppressMessage("ILLink", "IL2026:RequiresUnreferencedCode", Justification = "ctors are marked as RUC")] + private SymmetricAlgorithm? GetDecryptionKeyCore(EncryptedData encryptedData, string? symmetricAlgorithmUri) + { if (encryptedData.KeyInfo == null) return null; IEnumerator keyInfoEnum = encryptedData.KeyInfo.GetEnumerator(); @@ -416,6 +452,22 @@ public virtual byte[] GetDecryptionIV(EncryptedData encryptedData, string? symme { ArgumentNullException.ThrowIfNull(encryptedKey); + EnterDecryptionOperation(); + try + { + IncrementDecryptionCount(); + return DecryptEncryptedKeyCore(encryptedKey); + } + finally + { + ExitDecryptionOperation(); + } + } + + [UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "ctors are marked as RDC")] + [UnconditionalSuppressMessage("ILLink", "IL2026:RequiresUnreferencedCode", Justification = "ctors are marked as RUC")] + private byte[]? DecryptEncryptedKeyCore(EncryptedKey encryptedKey) + { if (encryptedKey.KeyInfo == null) return null; @@ -670,24 +722,32 @@ public EncryptedData Encrypt(XmlElement inputElement, string keyName) [UnconditionalSuppressMessage("ILLink", "IL2026:RequiresUnreferencedCode", Justification = "ctors are marked as RUC")] public void DecryptDocument() { - // Look for all EncryptedData elements and decrypt them - XmlNamespaceManager nsm = new XmlNamespaceManager(_document.NameTable); - nsm.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl); - XmlNodeList? encryptedDataList = _document.SelectNodes("//enc:EncryptedData", nsm); - if (encryptedDataList != null) + EnterDecryptionOperation(); + try { - foreach (XmlNode encryptedDataNode in encryptedDataList) + // Look for all EncryptedData elements and decrypt them + XmlNamespaceManager nsm = new XmlNamespaceManager(_document.NameTable); + nsm.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl); + XmlNodeList? encryptedDataList = _document.SelectNodes("//enc:EncryptedData", nsm); + if (encryptedDataList != null) { - XmlElement encryptedDataElement = (encryptedDataNode as XmlElement)!; - EncryptedData ed = new EncryptedData(); - ed.LoadXml(encryptedDataElement); - SymmetricAlgorithm? symAlg = GetDecryptionKey(ed, null); - if (symAlg == null) - throw new CryptographicException(SR.Cryptography_Xml_MissingDecryptionKey); - byte[] decrypted = DecryptData(ed, symAlg); - ReplaceData(encryptedDataElement, decrypted); + foreach (XmlNode encryptedDataNode in encryptedDataList) + { + XmlElement encryptedDataElement = (encryptedDataNode as XmlElement)!; + EncryptedData ed = new EncryptedData(); + ed.LoadXml(encryptedDataElement); + SymmetricAlgorithm? symAlg = GetDecryptionKey(ed, null); + if (symAlg == null) + throw new CryptographicException(SR.Cryptography_Xml_MissingDecryptionKey); + byte[] decrypted = DecryptData(ed, symAlg); + ReplaceData(encryptedDataElement, decrypted); + } } } + finally + { + ExitDecryptionOperation(); + } } // encrypts the supplied arbitrary data @@ -747,43 +807,54 @@ public byte[] DecryptData(EncryptedData encryptedData, SymmetricAlgorithm symmet ArgumentNullException.ThrowIfNull(encryptedData); ArgumentNullException.ThrowIfNull(symmetricAlgorithm); - // get the cipher value and decrypt - byte[] cipherValue = GetCipherValue(encryptedData.CipherData); + EnterDecryptionOperation(); + try + { + IncrementDecryptionCount(); + IncrementDecryptDataCallCount(); - // save the original symmetric algorithm - CipherMode origMode = symmetricAlgorithm.Mode; - PaddingMode origPadding = symmetricAlgorithm.Padding; - byte[] origIV = symmetricAlgorithm.IV; + // get the cipher value and decrypt + byte[] cipherValue = GetCipherValue(encryptedData.CipherData); - // read the IV from cipherValue - byte[]? decryptionIV = null; - if (_mode != CipherMode.ECB) - decryptionIV = GetDecryptionIV(encryptedData, null); + // save the original symmetric algorithm + CipherMode origMode = symmetricAlgorithm.Mode; + PaddingMode origPadding = symmetricAlgorithm.Padding; + byte[] origIV = symmetricAlgorithm.IV; - byte[]? output = null; - try - { - int lengthIV = 0; - if (decryptionIV != null) + // read the IV from cipherValue + byte[]? decryptionIV = null; + if (_mode != CipherMode.ECB) + decryptionIV = GetDecryptionIV(encryptedData, null); + + byte[]? output = null; + try { - symmetricAlgorithm.IV = decryptionIV; - lengthIV = decryptionIV.Length; + int lengthIV = 0; + if (decryptionIV != null) + { + symmetricAlgorithm.IV = decryptionIV; + lengthIV = decryptionIV.Length; + } + symmetricAlgorithm.Mode = _mode; + symmetricAlgorithm.Padding = _padding; + + using ICryptoTransform dec = symmetricAlgorithm.CreateDecryptor(); + output = dec.TransformFinalBlock(cipherValue, lengthIV, cipherValue.Length - lengthIV); + } + finally + { + // now restore the original symmetric algorithm + symmetricAlgorithm.Mode = origMode; + symmetricAlgorithm.Padding = origPadding; + symmetricAlgorithm.IV = origIV; } - symmetricAlgorithm.Mode = _mode; - symmetricAlgorithm.Padding = _padding; - using ICryptoTransform dec = symmetricAlgorithm.CreateDecryptor(); - output = dec.TransformFinalBlock(cipherValue, lengthIV, cipherValue.Length - lengthIV); + return output; } finally { - // now restore the original symmetric algorithm - symmetricAlgorithm.Mode = origMode; - symmetricAlgorithm.Padding = origPadding; - symmetricAlgorithm.IV = origIV; + ExitDecryptionOperation(); } - - return output; } // This method replaces an EncryptedData element with the decrypted sequence of bytes @@ -806,7 +877,15 @@ public void ReplaceData(XmlElement inputElement, byte[] decryptedData) { using (XmlReader xr = XmlReader.Create(sr, Utils.GetSecureXmlReaderSettings(_xmlResolver))) { - importDocument.Load(xr); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(importDocument, xr, maxDepth); + } + else + { + importDocument.Load(xr); + } } } @@ -825,13 +904,21 @@ public void ReplaceData(XmlElement inputElement, byte[] decryptedData) // Replace the children of the dummy node with the sequence of bytes passed in. // The string will be parsed into DOM objects in the context of the parent of the EncryptedData element. - dummy.InnerXml = _encoding.GetString(decryptedData); + string decryptedString = _encoding.GetString(decryptedData); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + bool allowDtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing; + if (maxDepth > 0 || !allowDtdProcessing) + { + Utils.ValidateInputXmlMaxDepth(dummy, decryptedString, maxDepth, allowDtdProcessing); + } + + dummy.InnerXml = decryptedString; // Move the children of the dummy node up to the parent. XmlNode? child = dummy.FirstChild; XmlNode? sibling = inputElement.NextSibling; - XmlNode? nextChild = null; + XmlNode? nextChild; while (child != null) { nextChild = child.NextSibling; @@ -998,5 +1085,94 @@ private static bool IsSafeTransform(string transformAlgorithm) return false; } + internal static void EnterDecryptionOperation() + { + Debug.Assert(t_decryptionScopeDepth >= 0); + Debug.Assert(t_decryptionScopeDepth > 0 || t_decryptionCount == 0, "Decryption count must be zero when entering the first decryption scope."); + Debug.Assert(t_decryptionScopeDepth > 0 || t_decryptDataCallCount == 0, "Decrypt data call count must be zero when entering the first decryption scope."); + Debug.Assert(t_decryptionScopeDepth > 0 || t_encryptedDataCandidateCount == 0, "Encrypted data candidate count must be zero when entering the first decryption scope."); + + // Defensively reset the decryption count, decrypt data call count, and encrypted data candidate count + if (t_decryptionScopeDepth == 0) + { + t_decryptionCount = 0; + t_decryptDataCallCount = 0; + t_encryptedDataCandidateCount = 0; + } + + t_decryptionScopeDepth++; + } + + internal static void ExitDecryptionOperation() + { + Debug.Assert(t_decryptionScopeDepth > 0); + + t_decryptionScopeDepth--; + + if (t_decryptionScopeDepth == 0) + { + // Reset the decryption count, decrypt data call count, and encrypted data candidate count when exiting the last decryption scope + t_decryptionCount = 0; + t_decryptDataCallCount = 0; + t_encryptedDataCandidateCount = 0; + } + } + + internal static void IncrementEncryptedDataCandidateCount() + { + Debug.Assert(t_decryptionScopeDepth > 0); + + int maxDecryptionCount = LocalAppContextSwitches.DangerousMaxDecryptionCount; + + if (maxDecryptionCount <= 0) + { + return; + } + + if (t_encryptedDataCandidateCount >= maxDecryptionCount) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxEncryptedDataElementsExceeded); + } + + t_encryptedDataCandidateCount++; + } + + internal static void IncrementDecryptDataCallCount() + { + Debug.Assert(t_decryptionScopeDepth > 0); + + int maxDecryptedDataElements = LocalAppContextSwitches.MaxDecryptedDataElements; + + if (maxDecryptedDataElements <= 0) + { + return; + } + + if (t_decryptDataCallCount >= maxDecryptedDataElements) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxEncryptedDataElementsExceeded); + } + + t_decryptDataCallCount++; + } + + internal static void IncrementDecryptionCount() + { + Debug.Assert(t_decryptionScopeDepth > 0); + + int maxDecryptionCount = LocalAppContextSwitches.DangerousMaxDecryptionCount; + + if (maxDecryptionCount <= 0) + { + return; + } + + if (t_decryptionCount >= maxDecryptionCount) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxDecryptionCountExceeded); + } + + t_decryptionCount++; + } } } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcAncestralNamespaceContextManager.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcAncestralNamespaceContextManager.cs index 27c54d9d38feec..7b5490640ffbb6 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcAncestralNamespaceContextManager.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcAncestralNamespaceContextManager.cs @@ -41,7 +41,7 @@ private void GatherNamespaceToRender(string nsPrefix, SortedList nsListToRender, if (Utils.IsNonRedundantNamespaceDecl(local, rAncestral)) { nsLocallyDeclared.Remove(nsPrefix); - nsListToRender.Add(local, null); + Utils.AddToRenderList(nsListToRender, local); } } else @@ -50,7 +50,7 @@ private void GatherNamespaceToRender(string nsPrefix, SortedList nsListToRender, XmlAttribute? uAncestral = GetNearestUnrenderedNamespaceWithMatchingPrefix(nsPrefix, out uDepth); if (uAncestral != null && uDepth > rDepth && Utils.IsNonRedundantNamespaceDecl(uAncestral, rAncestral)) { - nsListToRender.Add(uAncestral, null); + Utils.AddToRenderList(nsListToRender, uAncestral); } } } @@ -69,7 +69,7 @@ internal override void GetNamespacesToRender(XmlElement element, SortedList attr internal override void TrackNamespaceNode(XmlAttribute attr, SortedList nsListToRender, Hashtable nsLocallyDeclared) { if (HasNonRedundantInclusivePrefix(attr)) - nsListToRender.Add(attr, null); + Utils.AddToRenderList(nsListToRender, attr); else nsLocallyDeclared.Add(Utils.GetNamespacePrefix(attr), attr); } @@ -77,7 +77,7 @@ internal override void TrackNamespaceNode(XmlAttribute attr, SortedList nsListTo internal override void TrackXmlNamespaceNode(XmlAttribute attr, SortedList nsListToRender, SortedList attrListToRender, Hashtable nsLocallyDeclared) { // exclusive canonicalization treats Xml namespaces as simple attributes. They are not propagated. - attrListToRender.Add(attr, null); + Utils.AddToRenderList(attrListToRender, attr); } } } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcCanonicalXml.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcCanonicalXml.cs index 9541f24bf224f0..2c4b2f22aa3d4d 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcCanonicalXml.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/ExcCanonicalXml.cs @@ -18,7 +18,16 @@ internal ExcCanonicalXml(Stream inputStream, bool includeComments, string inclus _c14nDoc = new CanonicalXmlDocument(true, includeComments); _c14nDoc.XmlResolver = resolver; - _c14nDoc.Load(Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri)); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(_c14nDoc, Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri), maxDepth); + } + else + { + _c14nDoc.Load(Utils.PreProcessStreamInput(inputStream, resolver, strBaseUri)); + } + _ancMgr = new ExcAncestralNamespaceContextManager(inclusiveNamespacesPrefixList); } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/LocalAppContextSwitches.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/LocalAppContextSwitches.cs index b79d50a110fc4f..b20011a8bc6640 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/LocalAppContextSwitches.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/LocalAppContextSwitches.cs @@ -8,10 +8,17 @@ namespace System internal static partial class LocalAppContextSwitches { internal const int DefaultMaxRecursionDepth = 64; + internal const int DefaultMaxCanonicalizationAttributesPerElement = 512; + internal const int DefaultMaxCount = 256; internal const string DangerousMaxRecursionDepthAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxRecursionDepth"; + internal const string DangerousMaxCanonicalizationAttributesPerElementAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxCanonicalizationAttributesPerElement"; internal const string AllowDangerousEncryptedXmlTransformsAppContextSwitch = "System.Security.Cryptography.Xml.AllowDangerousEncryptedXmlTransforms"; + internal const string CryptoXmlMaxInputDocumentDepthAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlMaxInputDocumentDepth"; + internal const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; internal const string MaxTransformsPerChainAppContextSwitch = "System.Security.Cryptography.Xml.MaxTransformsPerChain"; internal const string MaxDecryptedDataElementsAppContextSwitch = "System.Security.Cryptography.Xml.MaxDecryptedDataElements"; + internal const string DangerousMaxDecryptionCountAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount"; + internal const string AllowUnsafeTruncatedHmacSignatureVerificationAppContextSwitch = "Switch.System.Security.Cryptography.Xml.SignedXml.AllowUnsafeTruncatedHmacSignatureVerification"; internal const int DefaultMaxTransformsPerChain = 20; internal const int DefaultMaxDecryptedDataElements = 100; @@ -20,9 +27,56 @@ internal static partial class LocalAppContextSwitches internal static int DangerousMaxRecursionDepth { get; } = GetInt32Config(DangerousMaxRecursionDepthAppContextSwitch, DefaultMaxRecursionDepth, allowNegative: false); + /// + /// Gets the maximum number of attributes, and separately of namespace declarations, that + /// canonicalization will render for a single element. + /// Configurable via AppContext data "System.Security.Cryptography.Xml.DangerousMaxCanonicalizationAttributesPerElement". + /// Default value is 512. A value of 0 means infinite (no limit). + /// + /// + /// Canonicalization scans the pending render lists linearly for each candidate name, so lists + /// of unbounded size make the work quadratic in the size of a single element, on a path that + /// processes attacker-supplied documents. Bounding the lists keeps that work constant per + /// element. The bound also depends on to limit how + /// many ancestor declarations can be in scope at once; setting that switch to 0 removes the + /// depth limit and reintroduces the quadratic behavior. + /// + internal static int DangerousMaxCanonicalizationAttributesPerElement { get; } = + GetInt32Config( + DangerousMaxCanonicalizationAttributesPerElementAppContextSwitch, + DefaultMaxCanonicalizationAttributesPerElement, + allowNegative: false); + + /// + /// Gets whether to enforce safe transforms for XML encryption by default. + /// Configurable via AppContext switch "System.Security.Cryptography.Xml.AllowDangerousEncryptedXmlTransforms". + /// Default value is false. + /// internal static bool AllowDangerousEncryptedXmlTransforms { get; } = GetBooleanConfig(AllowDangerousEncryptedXmlTransformsAppContextSwitch, defaultValue: false); + /// + /// Gets the maximum input-document depth (XmlReader.Depth) accepted by the cryptographic XML + /// ingestion and decrypt re-parse paths. + /// Configurable via AppContext data "System.Security.Cryptography.Xml.CryptoXmlMaxInputDocumentDepth". + /// Defaults to . A value of 0 means infinite (no limit). + /// + internal static int CryptoXmlMaxInputDocumentDepth { get; } = GetCryptoXmlMaxInputDocumentDepth(); + + private static int GetCryptoXmlMaxInputDocumentDepth() + { + int value = GetInt32Config(CryptoXmlMaxInputDocumentDepthAppContextSwitch, DangerousMaxRecursionDepth, allowNegative: false); + return value; + } + + /// + /// Gets whether DTD processing is allowed on the cryptographic XML ingestion and re-parse paths. + /// Configurable via AppContext switch "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing". + /// Default value is false. + /// + internal static bool CryptoXmlAllowDtdProcessing { get; } = + GetBooleanConfig(CryptoXmlAllowDtdProcessingAppContextSwitch, defaultValue: false); + /// /// Gets the maximum number of transforms allowed in a deserialized transform chain. /// Configurable via AppContext data "System.Security.Cryptography.Xml.MaxTransformsPerChain". @@ -43,6 +97,22 @@ internal static partial class LocalAppContextSwitches internal static int MaxReferencesPerSignedInfo { get; } = GetInt32Config("System.Security.Cryptography.MaxReferencesPerSignedInfo", defaultValue: 100); + /// + /// Gets the maximum number of encrypted elements processed during a decryption operation. + /// Configurable via AppContext data "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount". + /// Default value is 256. A value of 0 means infinite (no limit). + /// + internal static int DangerousMaxDecryptionCount { get; } = + GetInt32Config(DangerousMaxDecryptionCountAppContextSwitch, DefaultMaxCount, allowNegative: false); + + /// + /// Gets whether HMAC signature verification accepts truncated signature values. + /// Configurable via AppContext switch "Switch.System.Security.Cryptography.Xml.SignedXml.AllowUnsafeTruncatedHmacSignatureVerification". + /// Default value is false. + /// + internal static bool AllowUnsafeTruncatedHmacSignatureVerification { get; } = + GetBooleanConfig(AllowUnsafeTruncatedHmacSignatureVerificationAppContextSwitch, defaultValue: false); + private static int GetInt32Config(string appContextName, int defaultValue, bool allowNegative = true) { object? data = AppContext.GetData(appContextName); diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/SignedXml.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/SignedXml.cs index 959fd9e8271f1a..a37e936a4ef844 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/SignedXml.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/SignedXml.cs @@ -115,6 +115,15 @@ public SignedXml(XmlElement elem) [RequiresUnreferencedCode(CryptoHelpers.CreateFromNameUnreferencedCodeMessage)] private void Initialize(XmlElement? element) { + if (element != null) + { + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.ValidateInputXmlMaxDepth(element, maxDepth); + } + } + _containingDocument = element?.OwnerDocument; _context = element; m_signature = new Signature(); @@ -224,6 +233,12 @@ public void LoadXml(XmlElement value) { ArgumentNullException.ThrowIfNull(value); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.ValidateInputXmlMaxDepth(value, maxDepth); + } + m_signature.LoadXml(value); _context ??= value; @@ -247,220 +262,275 @@ public void AddObject(DataObject dataObject) public bool CheckSignature() { - return CheckSignatureReturningKey(out _); + EncryptedXml.EnterDecryptionOperation(); + try + { + return CheckSignatureReturningKey(out _); + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } public bool CheckSignatureReturningKey(out AsymmetricAlgorithm? signingKey) { - SignedXmlDebugLog.LogBeginSignatureVerification(this, _context); - - signingKey = null; - bool bRet = false; - AsymmetricAlgorithm? key; - - if (!CheckSignatureFormat()) + EncryptedXml.EnterDecryptionOperation(); + try { - return false; - } + SignedXmlDebugLog.LogBeginSignatureVerification(this, _context); - do - { - key = GetPublicKey(); - if (key != null) + signingKey = null; + bool bRet = false; + AsymmetricAlgorithm? key; + + if (!CheckSignatureFormat()) { - bRet = CheckSignature(key); - SignedXmlDebugLog.LogVerificationResult(this, key, bRet); + return false; } - } while (key != null && !bRet); + do + { + key = GetPublicKey(); + if (key != null) + { + bRet = CheckSignature(key); + SignedXmlDebugLog.LogVerificationResult(this, key, bRet); + } + } while (key != null && !bRet); - signingKey = key; - return bRet; + signingKey = key; + return bRet; + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } public bool CheckSignature(AsymmetricAlgorithm key) { - if (!CheckSignatureFormat()) + EncryptedXml.EnterDecryptionOperation(); + try { - return false; - } + if (!CheckSignatureFormat()) + { + return false; + } - if (!CheckSignedInfo(key)) - { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_SignedInfo); - return false; - } + if (!CheckSignedInfo(key)) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_SignedInfo); + return false; + } - // Now is the time to go through all the references and see if their DigestValues are good - if (!CheckDigestedReferences()) + // Now is the time to go through all the references and see if their DigestValues are good + if (!CheckDigestedReferences()) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_References); + return false; + } + + SignedXmlDebugLog.LogVerificationResult(this, key, true); + return true; + } + finally { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_References); - return false; + EncryptedXml.ExitDecryptionOperation(); } - - SignedXmlDebugLog.LogVerificationResult(this, key, true); - return true; } public bool CheckSignature(KeyedHashAlgorithm macAlg) { - if (!CheckSignatureFormat()) + EncryptedXml.EnterDecryptionOperation(); + try { - return false; - } + if (!CheckSignatureFormat()) + { + return false; + } - if (!CheckSignedInfo(macAlg)) - { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_SignedInfo); - return false; - } + if (!CheckSignedInfo(macAlg)) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_SignedInfo); + return false; + } + + if (!CheckDigestedReferences()) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_References); + return false; + } - if (!CheckDigestedReferences()) + SignedXmlDebugLog.LogVerificationResult(this, macAlg, true); + return true; + } + finally { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_References); - return false; + EncryptedXml.ExitDecryptionOperation(); } - - SignedXmlDebugLog.LogVerificationResult(this, macAlg, true); - return true; } public bool CheckSignature(X509Certificate2 certificate, bool verifySignatureOnly) { - if (!verifySignatureOnly) + EncryptedXml.EnterDecryptionOperation(); + try { - // Check key usages to make sure it is good for signing. - foreach (X509Extension extension in certificate.Extensions) + if (!verifySignatureOnly) { - if (string.Equals(extension.Oid!.Value, "2.5.29.15" /* szOID_KEY_USAGE */, StringComparison.OrdinalIgnoreCase)) + // Check key usages to make sure it is good for signing. + foreach (X509Extension extension in certificate.Extensions) { - X509KeyUsageExtension keyUsage = new X509KeyUsageExtension(); - keyUsage.CopyFrom(extension); - SignedXmlDebugLog.LogVerifyKeyUsage(this, certificate, keyUsage); - - bool validKeyUsage = (keyUsage.KeyUsages & X509KeyUsageFlags.DigitalSignature) != 0 || - (keyUsage.KeyUsages & X509KeyUsageFlags.NonRepudiation) != 0; - - if (!validKeyUsage) + if (string.Equals(extension.Oid!.Value, "2.5.29.15" /* szOID_KEY_USAGE */, StringComparison.OrdinalIgnoreCase)) { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_X509KeyUsage); - return false; + X509KeyUsageExtension keyUsage = new X509KeyUsageExtension(); + keyUsage.CopyFrom(extension); + SignedXmlDebugLog.LogVerifyKeyUsage(this, certificate, keyUsage); + + bool validKeyUsage = (keyUsage.KeyUsages & X509KeyUsageFlags.DigitalSignature) != 0 || + (keyUsage.KeyUsages & X509KeyUsageFlags.NonRepudiation) != 0; + + if (!validKeyUsage) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_X509KeyUsage); + return false; + } + break; } - break; } - } - // Do the chain verification to make sure the certificate is valid. - bool chainVerified = false; - using (X509Chain chain = new X509Chain()) - { - chain.ChainPolicy.ExtraStore.AddRange(BuildBagOfCerts()); - chainVerified = chain.Build(certificate); - SignedXmlDebugLog.LogVerifyX509Chain(this, chain, certificate); + // Do the chain verification to make sure the certificate is valid. + bool chainVerified = false; + using (X509Chain chain = new X509Chain()) + { + chain.ChainPolicy.ExtraStore.AddRange(BuildBagOfCerts()); + chainVerified = chain.Build(certificate); + SignedXmlDebugLog.LogVerifyX509Chain(this, chain, certificate); + } + + if (!chainVerified) + { + SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_X509Chain); + return false; + } } - if (!chainVerified) + using (AsymmetricAlgorithm? publicKey = Utils.GetAnyPublicKey(certificate)) { - SignedXmlDebugLog.LogVerificationFailure(this, SR.Log_VerificationFailed_X509Chain); - return false; + if (!CheckSignature(publicKey!)) + { + return false; + } } - } - using (AsymmetricAlgorithm? publicKey = Utils.GetAnyPublicKey(certificate)) + SignedXmlDebugLog.LogVerificationResult(this, certificate, true); + return true; + } + finally { - if (!CheckSignature(publicKey!)) - { - return false; - } + EncryptedXml.ExitDecryptionOperation(); } - - SignedXmlDebugLog.LogVerificationResult(this, certificate, true); - return true; } [UnconditionalSuppressMessage("ILLink", "IL2026:RequiresUnreferencedCode", Justification = "ctors are marked as RDC")] public void ComputeSignature() { - SignedXmlDebugLog.LogBeginSignatureComputation(this, _context!); + EncryptedXml.EnterDecryptionOperation(); + try + { + SignedXmlDebugLog.LogBeginSignatureComputation(this, _context!); - BuildDigestedReferences(); + BuildDigestedReferences(); - // Load the key - AsymmetricAlgorithm? key = SigningKey; + // Load the key + AsymmetricAlgorithm? key = SigningKey; - if (key == null) - throw new CryptographicException(SR.Cryptography_Xml_LoadKeyFailed); + if (key == null) + throw new CryptographicException(SR.Cryptography_Xml_LoadKeyFailed); - // Check the signature algorithm associated with the key so that we can accordingly set the signature method - if (SignedInfo!.SignatureMethod == null) - { - if (key is DSA) + // Check the signature algorithm associated with the key so that we can accordingly set the signature method + if (SignedInfo!.SignatureMethod == null) { - SignedInfo.SignatureMethod = XmlDsigDSAUrl; - } - else if (key is RSA) - { - // Default to RSA-SHA256 - SignedInfo.SignatureMethod ??= XmlDsigRSASHA256Url; - } - else - { - throw new CryptographicException(SR.Cryptography_Xml_CreatedKeyFailed); + if (key is DSA) + { + SignedInfo.SignatureMethod = XmlDsigDSAUrl; + } + else if (key is RSA) + { + // Default to RSA-SHA256 + SignedInfo.SignatureMethod ??= XmlDsigRSASHA256Url; + } + else + { + throw new CryptographicException(SR.Cryptography_Xml_CreatedKeyFailed); + } } - } - // See if there is a signature description class defined in the Config file - SignatureDescription? signatureDescription = CryptoHelpers.CreateNonTransformFromName(SignedInfo.SignatureMethod); - if (signatureDescription == null) - throw new CryptographicException(SR.Cryptography_Xml_SignatureDescriptionNotCreated); - HashAlgorithm? hashAlg = signatureDescription.CreateDigest(); - if (hashAlg == null) - throw new CryptographicException(SR.Cryptography_Xml_CreateHashAlgorithmFailed); + // See if there is a signature description class defined in the Config file + SignatureDescription? signatureDescription = CryptoHelpers.CreateNonTransformFromName(SignedInfo.SignatureMethod); + if (signatureDescription == null) + throw new CryptographicException(SR.Cryptography_Xml_SignatureDescriptionNotCreated); + HashAlgorithm? hashAlg = signatureDescription.CreateDigest(); + if (hashAlg == null) + throw new CryptographicException(SR.Cryptography_Xml_CreateHashAlgorithmFailed); - // Updates the HashAlgorithm's state for signing with the signature formatter below. - // The return value is not needed. - GetC14NDigest(hashAlg); + // Updates the HashAlgorithm's state for signing with the signature formatter below. + // The return value is not needed. + GetC14NDigest(hashAlg); - AsymmetricSignatureFormatter asymmetricSignatureFormatter = signatureDescription.CreateFormatter(key); + AsymmetricSignatureFormatter asymmetricSignatureFormatter = signatureDescription.CreateFormatter(key); - SignedXmlDebugLog.LogSigning(this, key, signatureDescription, hashAlg, asymmetricSignatureFormatter); - m_signature.SignatureValue = asymmetricSignatureFormatter.CreateSignature(hashAlg); + SignedXmlDebugLog.LogSigning(this, key, signatureDescription, hashAlg, asymmetricSignatureFormatter); + m_signature.SignatureValue = asymmetricSignatureFormatter.CreateSignature(hashAlg); + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } public void ComputeSignature(KeyedHashAlgorithm macAlg) { ArgumentNullException.ThrowIfNull(macAlg); - HMAC? hash = macAlg as HMAC; - if (hash == null) - throw new CryptographicException(SR.Cryptography_Xml_SignatureMethodKeyMismatch); - - int signatureLength; - if (m_signature.SignedInfo!.SignatureLength == null) - signatureLength = hash.HashSize; - else - signatureLength = Convert.ToInt32(m_signature.SignedInfo.SignatureLength, null); - // signatureLength should be less than hash size - if (signatureLength < 0 || signatureLength > hash.HashSize) - throw new CryptographicException(SR.Cryptography_Xml_InvalidSignatureLength); - if (signatureLength % 8 != 0) - throw new CryptographicException(SR.Cryptography_Xml_InvalidSignatureLength2); + EncryptedXml.EnterDecryptionOperation(); + try + { + HMAC? hash = macAlg as HMAC; + if (hash == null) + throw new CryptographicException(SR.Cryptography_Xml_SignatureMethodKeyMismatch); - BuildDigestedReferences(); - SignedInfo!.SignatureMethod = hash.HashName switch + int signatureLength; + if (m_signature.SignedInfo!.SignatureLength == null) + signatureLength = hash.HashSize; + else + signatureLength = Convert.ToInt32(m_signature.SignedInfo.SignatureLength, null); + // signatureLength should be less than hash size + if (signatureLength < 0 || signatureLength > hash.HashSize) + throw new CryptographicException(SR.Cryptography_Xml_InvalidSignatureLength); + if (signatureLength % 8 != 0) + throw new CryptographicException(SR.Cryptography_Xml_InvalidSignatureLength2); + + BuildDigestedReferences(); + SignedInfo!.SignatureMethod = hash.HashName switch + { + "SHA1" => SignedXml.XmlDsigHMACSHA1Url, + "SHA256" => SignedXml.XmlDsigMoreHMACSHA256Url, + "SHA384" => SignedXml.XmlDsigMoreHMACSHA384Url, + "SHA512" => SignedXml.XmlDsigMoreHMACSHA512Url, + "MD5" => SignedXml.XmlDsigMoreHMACMD5Url, + "RIPEMD160" => SignedXml.XmlDsigMoreHMACRIPEMD160Url, + _ => throw new CryptographicException(SR.Cryptography_Xml_SignatureMethodKeyMismatch), + }; + byte[] hashValue = GetC14NDigest(hash); + + SignedXmlDebugLog.LogSigning(this, hash); + m_signature.SignatureValue = new byte[signatureLength / 8]; + Buffer.BlockCopy(hashValue, 0, m_signature.SignatureValue, 0, signatureLength / 8); + } + finally { - "SHA1" => SignedXml.XmlDsigHMACSHA1Url, - "SHA256" => SignedXml.XmlDsigMoreHMACSHA256Url, - "SHA384" => SignedXml.XmlDsigMoreHMACSHA384Url, - "SHA512" => SignedXml.XmlDsigMoreHMACSHA512Url, - "MD5" => SignedXml.XmlDsigMoreHMACMD5Url, - "RIPEMD160" => SignedXml.XmlDsigMoreHMACRIPEMD160Url, - _ => throw new CryptographicException(SR.Cryptography_Xml_SignatureMethodKeyMismatch), - }; - byte[] hashValue = GetC14NDigest(hash); - - SignedXmlDebugLog.LogSigning(this, hash); - m_signature.SignatureValue = new byte[signatureLength / 8]; - Buffer.BlockCopy(hashValue, 0, m_signature.SignatureValue, 0, signatureLength / 8); + EncryptedXml.ExitDecryptionOperation(); + } } // @@ -1062,6 +1132,11 @@ private bool CheckSignedInfo(KeyedHashAlgorithm macAlg) byte[] hashValue = GetC14NDigest(macAlg); SignedXmlDebugLog.LogVerifySignedInfo(this, macAlg, hashValue, m_signature.SignatureValue); + if (LocalAppContextSwitches.AllowUnsafeTruncatedHmacSignatureVerification) + { + return m_signature.SignatureValue.AsSpan().SequenceEqual(hashValue.AsSpan(0, m_signature.SignatureValue.Length)); + } + return CryptographicEquals(m_signature.SignatureValue, hashValue); } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Transform.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Transform.cs index 17377f2a118b01..47cd895f24c70c 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Transform.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Transform.cs @@ -153,7 +153,15 @@ internal XmlElement GetXml(XmlDocument document, string name) public virtual byte[] GetDigestedOutput(HashAlgorithm hash) { - return hash.ComputeHash((Stream)GetOutput(typeof(Stream))); + EncryptedXml.EnterDecryptionOperation(); + try + { + return hash.ComputeHash((Stream)GetOutput(typeof(Stream))); + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } // Clears internal state after output has been obtained, so intermediate data diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/TransformChain.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/TransformChain.cs index ee7b76bf2f00ca..7177da4d5f2fe0 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/TransformChain.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/TransformChain.cs @@ -85,7 +85,16 @@ internal Stream TransformToOctetStream(object? inputObject, XmlResolver? resolve XmlDocument doc = new XmlDocument(); doc.PreserveWhitespace = true; XmlReader valReader = Utils.PreProcessStreamInput(currentInputStream, resolver, baseUri); - doc.Load(valReader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(doc, valReader, maxDepth); + } + else + { + doc.Load(valReader); + } + transform.LoadInput(doc); currentInputStream.Close(); currentInput = transform.GetOutput(); diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Utils.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Utils.cs index 9cae83b813411e..86fec3278ed565 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Utils.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/Utils.cs @@ -23,6 +23,27 @@ internal static class Utils // Keeping this number low will preserve some stack space internal const int XmlDsigSearchDepth = 20; + /// + /// Adds to a canonicalization render list, enforcing + /// . + /// + /// + /// The render lists are scanned linearly for each candidate name, so leaving them unbounded + /// makes canonicalizing a single element quadratic in the number of attributes or namespace + /// declarations it carries - a cost an attacker controls on the signature verification path. + /// + internal static void AddToRenderList(SortedList list, XmlAttribute attr) + { + int maxAttributes = LocalAppContextSwitches.DangerousMaxCanonicalizationAttributesPerElement; + + if (maxAttributes > 0 && list.Count >= maxAttributes) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxCanonicalizationAttributesExceeded); + } + + list.Add(attr, null); + } + private static bool HasNamespace(XmlElement element, string prefix, string value) { if (IsCommittedNamespace(element, prefix, value)) return true; @@ -182,12 +203,237 @@ internal static XmlReaderSettings GetSecureXmlReaderSettings(XmlResolver? xmlRes { XmlReaderSettings settings = new XmlReaderSettings(); settings.XmlResolver = xmlResolver; - settings.DtdProcessing = DtdProcessing.Parse; + settings.DtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing + ? DtdProcessing.Parse + : DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = MaxCharactersFromEntities; settings.MaxCharactersInDocument = MaxCharactersInDocument; return settings; } + internal static void LoadXmlDocumentWithMaxDepthCheck(XmlDocument xmlDocument, XmlReader reader, long maxDepth) + { + if (maxDepth == 0) + { + xmlDocument.Load(reader); + return; + } + + using (XmlReader maxDepthReader = new MaxDepthXmlReader(reader, maxDepth, depthOffset: 0)) + { + xmlDocument.Load(maxDepthReader); + } + } + + internal static void ValidateInputXmlMaxDepth(XmlNode node, long maxDepth) + { + if (maxDepth == 0) + { + return; + } + + XmlNode current = node; + long depth = 0; + while (true) + { + if (depth > maxDepth) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxDepthExceeded); + } + + if (current.FirstChild != null) + { + current = current.FirstChild; + depth++; + continue; + } + + while (current != node && current.NextSibling == null) + { + current = current.ParentNode!; + depth--; + } + + if (current == node) + { + return; + } + + current = current.NextSibling!; + } + } + + internal static void ValidateInputXmlMaxDepth(XmlNode parent, string xml, long maxDepth, bool allowDtdProcessing) + { + if (maxDepth == 0 && allowDtdProcessing) + { + return; + } + + NameTable nameTable = new NameTable(); + XmlNamespaceManager nsMgr = new XmlNamespaceManager(nameTable); + for (XmlNode? ancestor = parent; ancestor != null; ancestor = ancestor.ParentNode) + { + if (ancestor.Attributes == null) + { + continue; + } + + foreach (XmlAttribute attr in ancestor.Attributes) + { + if (attr.Prefix == "xmlns") + { + if (nsMgr.LookupNamespace(attr.LocalName) == null) + { + nsMgr.AddNamespace(attr.LocalName, attr.Value); + } + } + else if (attr.Name == "xmlns") + { + if (nsMgr.LookupNamespace(string.Empty) == null) + { + nsMgr.AddNamespace(string.Empty, attr.Value); + } + } + } + } + + XmlParserContext context = new XmlParserContext(nameTable, nsMgr, null, XmlSpace.None); + + // XmlTextReader is used so general entity references surface as EntityReference + // nodes (below) instead of throwing when they aren't declared in this standalone + // parser's scope, and so DtdProcessing.Ignore skips any DOCTYPE without loading + // its entities. XmlResolver is nulled out so no external entity can be fetched. + XmlTextReader textReader; + if (parent is XmlDocument) + { + textReader = new XmlTextReader(new StringReader(xml), nameTable); + } + else + { + textReader = new XmlTextReader(xml, XmlNodeType.Element, context); + } + + textReader.EntityHandling = EntityHandling.ExpandCharEntities; + textReader.DtdProcessing = allowDtdProcessing + ? DtdProcessing.Ignore + : DtdProcessing.Prohibit; + textReader.XmlResolver = null; + + long depthOffset = 0; + + if (maxDepth > 0 && parent is not XmlDocument) + { + XmlNode? container = parent is XmlElement ? parent : parent.ParentNode; + + for (XmlNode? ancestor = container; ancestor != null; ancestor = ancestor.ParentNode) + { + if (ancestor is XmlElement) + { + depthOffset++; + } + + if (ancestor is XmlDocument) + { + break; + } + } + } + + XmlReader outerReader = textReader; + XmlReader? innerReader = null; + + if (maxDepth > 0) + { + outerReader = new MaxDepthXmlReader(textReader, maxDepth, depthOffset); + innerReader = textReader; + } + + using (innerReader) + using (outerReader) + { + try + { + while (outerReader.Read()) + { + // A general entity reference can expand into an arbitrarily deep subtree + // via the target document's DTD after this validator returns; without + // processing that DTD here (billion laughs) the depth cannot be counted, + // so reject the reference outright when DTD processing is disabled. + // Predefined character entities do not surface as EntityReference and + // are unaffected. + if (!allowDtdProcessing && outerReader.NodeType == XmlNodeType.EntityReference) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxDepthExceeded); + } + } + } + catch (XmlException e) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxDepthExceeded, e); + } + } + } + + // Close() is a no-op so this wrapper does not take ownership of the inner reader. + private sealed class MaxDepthXmlReader : XmlReader + { + private readonly XmlReader _reader; + private readonly long _maxDepth; + private readonly long _depthOffset; + + internal MaxDepthXmlReader(XmlReader reader, long maxDepth, long depthOffset) + { + _reader = reader; + _maxDepth = maxDepth; + _depthOffset = depthOffset; + } + + public override bool Read() + { + bool result = _reader.Read(); + if (_depthOffset + _reader.Depth > _maxDepth) + { + throw new CryptographicException(SR.Cryptography_Xml_MaxDepthExceeded); + } + + return result; + } + + public override XmlNodeType NodeType => _reader.NodeType; + public override string LocalName => _reader.LocalName; + public override string NamespaceURI => _reader.NamespaceURI; + public override string Prefix => _reader.Prefix; + public override bool HasValue => _reader.HasValue; + public override string Value => _reader.Value; + public override int Depth => _reader.Depth; + public override string BaseURI => _reader.BaseURI; + public override bool IsEmptyElement => _reader.IsEmptyElement; + public override bool IsDefault => _reader.IsDefault; + public override XmlSpace XmlSpace => _reader.XmlSpace; + public override string XmlLang => _reader.XmlLang; + public override Type ValueType => _reader.ValueType; + public override int AttributeCount => _reader.AttributeCount; + public override bool EOF => _reader.EOF; + public override ReadState ReadState => _reader.ReadState; + public override XmlNameTable NameTable => _reader.NameTable; + public override bool CanResolveEntity => _reader.CanResolveEntity; + public override System.Xml.Schema.IXmlSchemaInfo? SchemaInfo => _reader.SchemaInfo; + + public override string GetAttribute(int i) => _reader.GetAttribute(i); + public override string? GetAttribute(string name) => _reader.GetAttribute(name); + public override string? GetAttribute(string name, string? namespaceURI) => _reader.GetAttribute(name, namespaceURI); + public override string? LookupNamespace(string prefix) => _reader.LookupNamespace(prefix); + public override bool MoveToAttribute(string name) => _reader.MoveToAttribute(name); + public override bool MoveToAttribute(string name, string? ns) => _reader.MoveToAttribute(name, ns); + public override bool MoveToElement() => _reader.MoveToElement(); + public override bool MoveToFirstAttribute() => _reader.MoveToFirstAttribute(); + public override bool MoveToNextAttribute() => _reader.MoveToNextAttribute(); + public override bool ReadAttributeValue() => _reader.ReadAttributeValue(); + public override void ResolveEntity() => _reader.ResolveEntity(); + public override void Close() { } + } + internal static XmlDocument PreProcessDocumentInput(XmlDocument document, XmlResolver xmlResolver, string baseUri) { ArgumentNullException.ThrowIfNull(document); @@ -200,11 +446,21 @@ internal static XmlDocument PreProcessDocumentInput(XmlDocument document, XmlRes { XmlReaderSettings settings = new XmlReaderSettings(); settings.XmlResolver = xmlResolver; - settings.DtdProcessing = DtdProcessing.Parse; + settings.DtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing + ? DtdProcessing.Parse + : DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = MaxCharactersFromEntities; settings.MaxCharactersInDocument = MaxCharactersInDocument; using XmlReader reader = XmlReader.Create(stringReader, settings, baseUri); - doc.Load(reader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + LoadXmlDocumentWithMaxDepthCheck(doc, reader, maxDepth); + } + else + { + doc.Load(reader); + } } return doc; } @@ -220,11 +476,21 @@ internal static XmlDocument PreProcessElementInput(XmlElement elem, XmlResolver { XmlReaderSettings settings = new XmlReaderSettings(); settings.XmlResolver = xmlResolver; - settings.DtdProcessing = DtdProcessing.Parse; + settings.DtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing + ? DtdProcessing.Parse + : DtdProcessing.Prohibit; settings.MaxCharactersFromEntities = MaxCharactersFromEntities; settings.MaxCharactersInDocument = MaxCharactersInDocument; using XmlReader reader = XmlReader.Create(stringReader, settings, baseUri); - doc.Load(reader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + LoadXmlDocumentWithMaxDepthCheck(doc, reader, maxDepth); + } + else + { + doc.Load(reader); + } } return doc; } diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDecryptionTransform.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDecryptionTransform.cs index 9095808496fd1d..85e63db4adcd37 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDecryptionTransform.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDecryptionTransform.cs @@ -150,7 +150,16 @@ private void LoadStreamInput(Stream stream) document.PreserveWhitespace = true; XmlResolver resolver = (ResolverSet ? _xmlResolver : XmlResolverHelper.GetThrowingResolver()); XmlReader xmlReader = Utils.PreProcessStreamInput(stream, resolver, BaseURI!); - document.Load(xmlReader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(document, xmlReader, maxDepth); + } + else + { + document.Load(xmlReader); + } + _containingDocument = document; _nsm = new XmlNamespaceManager(_containingDocument.NameTable); _nsm.AddNamespace("enc", EncryptedXml.XmlEncNamespaceUrl); @@ -180,7 +189,15 @@ private void ReplaceEncryptedData(XmlElement encryptedDataElement, byte[] decryp // However, EncryptedXml.ReplaceData will preserve other top-level elements such as the XML // entity declaration and top level comments. So, in this case we must do the replacement // ourselves. - parent.InnerXml = EncryptedXml.Encoding.GetString(decrypted); + string decryptedString = EncryptedXml.Encoding.GetString(decrypted); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + bool allowDtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing; + if (maxDepth > 0 || !allowDtdProcessing) + { + Utils.ValidateInputXmlMaxDepth(parent, decryptedString, maxDepth, allowDtdProcessing); + } + + parent.InnerXml = decryptedString; } else { @@ -217,11 +234,10 @@ private void ProcessElementRecursively(XmlNodeList encryptedDatas) if (encryptedDatas == null || encryptedDatas.Count == 0) return; int maxDepth = LocalAppContextSwitches.DangerousMaxRecursionDepth; - int maxDecryptedDataElements = LocalAppContextSwitches.MaxDecryptedDataElements; - int decryptedElementsCount = 0; Queue encryptedDatasQueue = new(); foreach (XmlNode value in encryptedDatas) { + EncryptedXml.IncrementEncryptedDataCandidateCount(); encryptedDatasQueue.Enqueue(new(value, depth: 0)); } @@ -234,12 +250,6 @@ private void ProcessElementRecursively(XmlNodeList encryptedDatas) if (encryptedDataElement != null && encryptedDataElement.LocalName == "EncryptedData" && encryptedDataElement.NamespaceURI == EncryptedXml.XmlEncNamespaceUrl) { - decryptedElementsCount++; - if (maxDecryptedDataElements > 0 && decryptedElementsCount > maxDecryptedDataElements) - { - throw new CryptographicException(SR.Cryptography_Xml_MaxDecryptableDataElementsExceeded); - } - XmlNode sibling = encryptedDataElement.NextSibling!; XmlNode parent = encryptedDataElement.ParentNode!; if (ProcessEncryptedDataItem(encryptedDataElement)) @@ -253,13 +263,14 @@ private void ProcessElementRecursively(XmlNodeList encryptedDatas) XmlNodeList nodes = child.SelectNodes("//enc:EncryptedData", _nsm!)!; if (nodes.Count > 0) { - if (maxDepth > 0 && depth >= maxDepth) + if (maxDepth > 0 && depth > maxDepth) { throw new CryptographicException(SR.Cryptography_Xml_MaxDepthExceeded); } foreach (XmlNode value in nodes) { + EncryptedXml.IncrementEncryptedDataCandidateCount(); encryptedDatasQueue.Enqueue(new(value, depth + 1)); } } @@ -271,20 +282,36 @@ private void ProcessElementRecursively(XmlNodeList encryptedDatas) public override object GetOutput() { - // decrypt the encrypted sections - if (_encryptedDataList != null) - ProcessElementRecursively(_encryptedDataList); - // propagate namespaces - Utils.AddNamespaces(_containingDocument!.DocumentElement!, PropagatedNamespaces); - return _containingDocument; + EncryptedXml.EnterDecryptionOperation(); + try + { + // decrypt the encrypted sections + if (_encryptedDataList != null) + ProcessElementRecursively(_encryptedDataList); + // propagate namespaces + Utils.AddNamespaces(_containingDocument!.DocumentElement!, PropagatedNamespaces); + return _containingDocument; + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } public override object GetOutput(Type type) { - if (type == typeof(XmlDocument)) - return (XmlDocument)GetOutput(); - else - throw new ArgumentException(SR.Cryptography_Xml_TransformIncorrectInputType, nameof(type)); + EncryptedXml.EnterDecryptionOperation(); + try + { + if (type == typeof(XmlDocument)) + return (XmlDocument)GetOutput(); + else + throw new ArgumentException(SR.Cryptography_Xml_TransformIncorrectInputType, nameof(type)); + } + finally + { + EncryptedXml.ExitDecryptionOperation(); + } } internal override void ClearState() diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigEnvelopedSignatureTransform.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigEnvelopedSignatureTransform.cs index b31d7a36f60b66..d531d8ee145034 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigEnvelopedSignatureTransform.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigEnvelopedSignatureTransform.cs @@ -81,7 +81,16 @@ private void LoadStreamInput(Stream stream) doc.PreserveWhitespace = true; XmlResolver resolver = ResolverSet ? _xmlResolver : XmlResolverHelper.GetThrowingResolver(); XmlReader xmlReader = Utils.PreProcessStreamInput(stream, resolver, BaseURI!); - doc.Load(xmlReader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(doc, xmlReader, maxDepth); + } + else + { + doc.Load(xmlReader); + } + _containingDocument = doc; if (_containingDocument == null) throw new CryptographicException(SR.Cryptography_Xml_EnvelopedSignatureRequiresContext); diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigXPathTransform.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigXPathTransform.cs index 963260182ac4d9..6c5b0fc77f454f 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigXPathTransform.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlDsigXPathTransform.cs @@ -137,7 +137,15 @@ private void LoadStreamInput(Stream stream) XmlReader valReader = Utils.PreProcessStreamInput(stream, resolver, BaseURI!); _document = new XmlDocument(); _document.PreserveWhitespace = true; - _document.Load(valReader); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + if (maxDepth > 0) + { + Utils.LoadXmlDocumentWithMaxDepthCheck(_document, valReader, maxDepth); + } + else + { + _document.Load(valReader); + } } private void LoadXmlNodeListInput(XmlNodeList nodeList) diff --git a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlLicenseTransform.cs b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlLicenseTransform.cs index e794c85f91ba86..35a8e0acf3e490 100644 --- a/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlLicenseTransform.cs +++ b/src/libraries/System.Security.Cryptography.Xml/src/System/Security/Cryptography/Xml/XmlLicenseTransform.cs @@ -87,6 +87,17 @@ private void DecryptEncryptedGrants(XmlNodeList encryptedGrantList) streamReader = new StreamReader(decryptedContent); string clearContent = streamReader.ReadToEnd(); + long maxDepth = LocalAppContextSwitches.CryptoXmlMaxInputDocumentDepth; + bool allowDtdProcessing = LocalAppContextSwitches.CryptoXmlAllowDtdProcessing; + if (maxDepth > 0 || !allowDtdProcessing) + { + Utils.ValidateInputXmlMaxDepth( + encryptedGrantList[i]!.ParentNode!, + clearContent, + maxDepth, + allowDtdProcessing); + } + // red flag encryptedGrantList[i]!.ParentNode!.InnerXml = clearContent; } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/EncryptedXmlTests.cs b/src/libraries/System.Security.Cryptography.Xml/tests/EncryptedXmlTests.cs index 43f3e7f13425f9..36fe2cbf499865 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/EncryptedXmlTests.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/EncryptedXmlTests.cs @@ -17,7 +17,13 @@ public static class EncryptedXmlTests private const string AllowDangerousEncryptedXmlTransformsAppContextSwitch = "System.Security.Cryptography.Xml.AllowDangerousEncryptedXmlTransforms"; private const int DefaultMaxTransformsPerChain = 20; private const string MaxTransformsPerChainAppContextSwitch = "System.Security.Cryptography.Xml.MaxTransformsPerChain"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + private const string CryptoXmlMaxInputDocumentDepthAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlMaxInputDocumentDepth"; + internal const string MaxDecryptedDataElementsAppContextSwitch = "System.Security.Cryptography.Xml.MaxDecryptedDataElements"; + private const string DangerousMaxDecryptionCountAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount"; private const string MalformedTransformsMessage = "Malformed element Transforms."; + private const int DefaultMaxCount = 256; + private const string MaxDecryptionCountExceededMessage = "The XML document has exceeded the maximum number of elements allowed for decryption."; [Fact] [ActiveIssue("https://github.com/dotnet/runtime/issues/49871", TestPlatforms.Android)] @@ -1687,6 +1693,7 @@ public static void EncryptedData_LoadEncodedDtd_WithExactTransformChainLimit(int { RemoteExecutor.Invoke(static (string maxTransformsPerChainText, string expectTransformLimitText) => { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); AppContext.SetData(MaxTransformsPerChainAppContextSwitch, int.Parse(maxTransformsPerChainText, CultureInfo.InvariantCulture)); if (bool.Parse(expectTransformLimitText)) @@ -1707,6 +1714,7 @@ public static void EncryptedData_LoadEncodedDtd_WithUnlimitedTransformChain() { RemoteExecutor.Invoke(static () => { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); AppContext.SetData(MaxTransformsPerChainAppContextSwitch, 0); LoadEncodedDtdPayload(); }).Dispose(); @@ -1750,6 +1758,503 @@ private static EncryptedXml CreateEncryptedXmlWithEncodedDtdPayload() return encryptedXml; } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void NestedParentPlusDeepDecryptedFragment_Throws() + { + RemoteExecutor.Invoke(static () => + { + // ReplaceData mutates the containing document in-place, so the fragment-relative + // depth check must include the target parent's ancestor depth. Placing the + // EncryptedData under a wrapper chain that is already near the cap and + // decrypting to a fragment near the cap must be rejected, not silently + // produce a document deeper than the configured maximum. + const int MaxDepth = 30; + const int WrapperDepth = 20; + const int FragmentDepth = 20; + + AppContext.SetData(CryptoXmlMaxInputDocumentDepthAppContextSwitch, MaxDepth); + + using Aes aes = Aes.Create(); + + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml("" + BuildDeepMarkup(WrapperDepth, "") + ""); + XmlElement target = (XmlElement)doc.SelectSingleNode("//target"); + + XmlDocument plaintextDoc = new XmlDocument { PreserveWhitespace = true }; + plaintextDoc.LoadXml("" + BuildDeepMarkup(FragmentDepth, "y") + ""); + EncryptedXml encryptor = new EncryptedXml(); + encryptor.AddKeyNameMapping("k", aes); + EncryptedData ed = encryptor.Encrypt(plaintextDoc.DocumentElement, "k"); + EncryptedXml.ReplaceElement(target, ed, false); + + EncryptedXml decryptor = new EncryptedXml(doc); + decryptor.AddKeyNameMapping("k", aes); + Assert.Throws(() => decryptor.DecryptDocument()); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepNonRootDecryptedXml_Throws() + { + RemoteExecutor.Invoke(static () => Assert.Throws(() => DecryptDeepXml(root: false))).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepRootDecryptedXml_Throws() + { + RemoteExecutor.Invoke(static () => Assert.Throws(() => DecryptDeepXml(root: true))).Dispose(); + } + + private static void DecryptDeepXml(bool root) + { + const int Depth = 100000; + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + string plaintext = "" + BuildDeepMarkup(Depth, "x") + ""; + doc.LoadXml(root ? plaintext : "" + plaintext + ""); + XmlElement target = root ? doc.DocumentElement : (XmlElement)doc.SelectSingleNode("//target"); + using (Aes aes = Aes.Create()) + { + EncryptedXml encryptedXml = new EncryptedXml(); + encryptedXml.AddKeyNameMapping("k", aes); + EncryptedXml.ReplaceElement(target, encryptedXml.Encrypt(target, "k"), false); + + EncryptedXml decryptor = new EncryptedXml(doc); + decryptor.AddKeyNameMapping("k", aes); + decryptor.DecryptDocument(); + } + } + + private static string BuildDeepMarkup(int depth, string inner) + { + StringBuilder builder = new StringBuilder(depth * 7 + inner.Length); + for (int i = 0; i < depth; i++) + builder.Append(""); + builder.Append(inner); + for (int i = 0; i < depth; i++) + builder.Append(""); + return builder.ToString(); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(false)] + [InlineData(true)] + public static void DtdOptIn_EntityRefInDecryptedNonRoot_Resolves(bool disableDepthLimit) + { + RemoteExecutor.Invoke(static (string disableDepthLimitText) => + { + // With DTD processing opted in, entity references defined by the containing + // document's DTD must still resolve on the non-root decrypt path, matching + // the root and stream re-parse paths. The depth guard's EntityReference + // rejection is a default-safe measure and must not fire when the caller + // has opted in. + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + if (bool.Parse(disableDepthLimitText)) + { + AppContext.SetData(CryptoXmlMaxInputDocumentDepthAppContextSwitch, 0); + } + + XmlDocument wire = new XmlDocument { PreserveWhitespace = true }; + using (System.IO.StringReader sr = new System.IO.StringReader("]>")) + using (XmlReader xr = XmlReader.Create(sr, new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse })) + { + wire.Load(xr); + } + XmlElement wrap = (XmlElement)wire.SelectSingleNode("//wrap"); + + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + wrap.AppendChild(BuildEncryptedDataElement("&e;", aes, wire)); + EncryptedXml decryptor = new EncryptedXml(wire); + decryptor.AddKeyNameMapping("k", aes); + decryptor.DecryptDocument(); + Assert.Equal("x", wire.SelectSingleNode("//r").InnerText); + } + }, disableDepthLimit.ToString()).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdInDecryptedNonRoot_WithDepthDisabled_Throws() + { + RemoteExecutor.Invoke(static () => + { + // Disabling the depth cap must not also disable DTD enforcement on the + // non-root re-parse path. + AppContext.SetData(CryptoXmlMaxInputDocumentDepthAppContextSwitch, 0); + + XmlDocument wire = new XmlDocument { PreserveWhitespace = true }; + wire.LoadXml(""); + XmlElement wrap = (XmlElement)wire.SelectSingleNode("//wrap"); + + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + // Non-root replacement: EncryptedData sits under , so ReplaceData + // hits the "dummy" child branch that reparses via InnerXml. + wrap.AppendChild(BuildEncryptedDataElement("&test;", aes, wire)); + EncryptedXml decryptor = new EncryptedXml(wire); + decryptor.AddKeyNameMapping("k", aes); + Assert.ThrowsAny(() => decryptor.DecryptDocument()); + } + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdInDecryptedRoot_Throws() + { + RemoteExecutor.Invoke(static () => Assert.Throws(() => DecryptRootDtdPayload())).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdInDecryptedRoot_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + DecryptRootDtdPayload(); + }).Dispose(); + } + + private static void DecryptRootDtdPayload() + { + const string dtdContent = "x"; + XmlDocument wire = new XmlDocument { PreserveWhitespace = true }; + using (Aes aes = Aes.Create()) + { + aes.KeySize = 256; + wire.AppendChild(BuildEncryptedDataElement(dtdContent, aes, wire)); + EncryptedXml decryptor = new EncryptedXml(wire); + decryptor.AddKeyNameMapping("k", aes); + decryptor.DecryptDocument(); + } + } + + private static XmlElement BuildEncryptedDataElement(string plaintext, Aes aes, XmlDocument targetDoc) + { + EncryptedXml helper = new EncryptedXml(); + helper.AddKeyNameMapping("k", aes); + byte[] cipher = helper.EncryptData(Encoding.UTF8.GetBytes(plaintext), aes); + EncryptedData encData = new EncryptedData + { + Type = EncryptedXml.XmlEncElementUrl, + EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url), + KeyInfo = new KeyInfo(), + CipherData = new CipherData(cipher), + }; + encData.KeyInfo.AddClause(new KeyInfoName("k")); + XmlElement encXml = encData.GetXml(); + return (XmlElement)targetDoc.ImportNode(encXml, deep: true); + } + + [Fact] + public static void EncryptedXml_GetDecryptionKey_MaxCount_Default() + { + AssertKeyDecryptionCount(DefaultMaxCount, useGetDecryptionKey: true, operationCount: 2); + CryptographicException exception = Assert.Throws( + () => AssertKeyDecryptionCount(DefaultMaxCount + 1, useGetDecryptionKey: true)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void EncryptedXml_GetDecryptionKey_MaxCount_AppContext() + { + RemoteExecutor.Invoke(static () => + { + const int ConfiguredMaxCount = 1; + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, ConfiguredMaxCount); + + AssertKeyDecryptionCount(ConfiguredMaxCount, useGetDecryptionKey: true); + CryptographicException exception = Assert.Throws( + () => AssertKeyDecryptionCount(ConfiguredMaxCount + 1, useGetDecryptionKey: true)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + }).Dispose(); + } + + [Fact] + public static void EncryptedXml_DecryptEncryptedKey_MaxCount_Default() + { + AssertKeyDecryptionCount(DefaultMaxCount, useGetDecryptionKey: false, operationCount: 2); + CryptographicException exception = Assert.Throws( + () => AssertKeyDecryptionCount(DefaultMaxCount + 1, useGetDecryptionKey: false)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void EncryptedXml_DecryptEncryptedKey_MaxCount_AppContext() + { + RemoteExecutor.Invoke(static () => + { + const int ConfiguredMaxCount = 1; + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, ConfiguredMaxCount); + + AssertKeyDecryptionCount(ConfiguredMaxCount, useGetDecryptionKey: false); + CryptographicException exception = Assert.Throws( + () => AssertKeyDecryptionCount(ConfiguredMaxCount + 1, useGetDecryptionKey: false)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + }).Dispose(); + } + + [Fact] + public static void EncryptedXml_DecryptDocument_MaxCount_Default() + { + RemoteExecutor.Invoke(static () => + { + // Make sure we don't hit the decrypt data count, which is a different counter. + AppContext.SetData(MaxDecryptedDataElementsAppContextSwitch, 0); + + AssertDecryptDocumentCount(DefaultMaxCount / 2); + CryptographicException exception = Assert.Throws(() => AssertDecryptDocumentCount(DefaultMaxCount / 2 + 1)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void EncryptedXml_DecryptDocument_MaxCount_AppContext() + { + RemoteExecutor.Invoke(static () => + { + const int ConfiguredMaxCount = 2; + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, ConfiguredMaxCount); + + AssertDecryptDocumentCount(ConfiguredMaxCount / 2); + CryptographicException exception = Assert.Throws(() => AssertDecryptDocumentCount(ConfiguredMaxCount / 2 + 1)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void EncryptedXml_DecryptData_NestedDecryptionCountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 1); + + using Aes aes = Aes.Create(); + XmlDocument document = new(); + document.LoadXml(""); + + EncryptedXml encryptedXml = new(document); + byte[] cipherValue = encryptedXml.EncryptData(""u8.ToArray(), aes); + CipherReference cipherReference = new("#cipher"); + cipherReference.AddTransform(new NestedDecryptionTransform(cipherValue)); + EncryptedData encryptedData = new() + { + EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url), + CipherData = new CipherData(cipherReference), + }; + + CryptographicException exception = Assert.Throws( + () => encryptedXml.DecryptData(encryptedData, aes)); + Assert.Equal(MaxDecryptionCountExceededMessage, exception.Message); + }).Dispose(); + } + + private static void AssertKeyDecryptionCount(int count, bool useGetDecryptionKey, int operationCount = 1) + { + const string EncNamespace = "http://www.w3.org/2001/04/xmlenc#"; + const string DsigNamespace = "http://www.w3.org/2000/09/xmldsig#"; + + using Aes contentKey = Aes.Create(); + using Aes keyEncryptionKey = Aes.Create(); + using Aes outerKey = Aes.Create(); + bool useNestedKey = count > 1; + string outerCipher = Convert.ToBase64String( + EncryptedXml.EncryptKey(contentKey.Key, useNestedKey ? outerKey : keyEncryptionKey)); + string innerCipher = Convert.ToBase64String(EncryptedXml.EncryptKey(outerKey.Key, keyEncryptionKey)); + + StringBuilder builder = new(count * 200 + 4096); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + if (useNestedKey) + { + builder.Append(""); + for (int i = 0; i < count - 2; i++) + { + builder.Append(""); + builder.Append(""); + builder.Append("no-such-key"); + builder.Append("AAAA"); + builder.Append(""); + } + builder.Append(""); + builder.Append(""); + builder.Append("kek"); + builder.Append("").Append(innerCipher).Append(""); + builder.Append(""); + builder.Append(""); + } + else + { + builder.Append("kek"); + } + builder.Append("").Append(outerCipher).Append(""); + builder.Append(""); + builder.Append(""); + builder.Append("AAAA"); + builder.Append(""); + builder.Append(""); + + XmlDocument document = new() { PreserveWhitespace = true }; + document.LoadXml(builder.ToString()); + EncryptedData encryptedData = new(); + encryptedData.LoadXml((XmlElement)document.GetElementsByTagName("EncryptedData", EncNamespace)[0]); + EncryptedKey encryptedKey = new(); + encryptedKey.LoadXml((XmlElement)document.GetElementsByTagName("EncryptedKey", EncNamespace)[0]); + + EncryptedXml encryptedXml = new(document); + encryptedXml.AddKeyNameMapping("kek", keyEncryptionKey); + + for (int i = 0; i < operationCount; i++) + { + if (useGetDecryptionKey) + { + using SymmetricAlgorithm decryptionKey = encryptedXml.GetDecryptionKey(encryptedData, null); + Assert.NotNull(decryptionKey); + Assert.Equal(contentKey.Key, decryptionKey.Key); + } + else + { + Assert.Equal(contentKey.Key, encryptedXml.DecryptEncryptedKey(encryptedKey)); + } + } + } + + private static void AssertDecryptDocumentCount(int count) + { + using RSA rsa = RSA.Create(2048); + using Aes aes = Aes.Create(); + aes.KeySize = 256; + aes.GenerateKey(); + + XmlDocument seed = new(); + seed.LoadXml("secret"); + string encryptedDataCipher = Convert.ToBase64String(new EncryptedXml().EncryptData(seed.DocumentElement, aes, false)); + string encryptedKeyCipher = Convert.ToBase64String(EncryptedXml.EncryptKey(aes.Key, rsa, useOAEP: false)); + + StringBuilder builder = new(count * 400); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append("rsaKey"); + builder.Append("").Append(encryptedKeyCipher).Append(""); + builder.Append(""); + for (int i = 0; i < count; i++) + { + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append(""); + builder.Append("").Append(encryptedDataCipher).Append(""); + builder.Append(""); + } + builder.Append(""); + + XmlDocument document = new() { PreserveWhitespace = true }; + document.LoadXml(builder.ToString()); + EncryptedXml encryptedXml = new(document); + encryptedXml.AddKeyNameMapping("rsaKey", rsa); + + encryptedXml.DecryptDocument(); + Assert.Equal(0, document.GetElementsByTagName("EncryptedData", EncryptedXml.XmlEncNamespaceUrl).Count); + } + + private sealed class NestedDecryptionTransform : Transform + { + private static readonly Type[] s_inputTypes = { typeof(Stream) }; + private static readonly Type[] s_outputTypes = { typeof(Stream) }; + private readonly byte[] _cipherValue; + + public NestedDecryptionTransform(byte[] cipherValue) + { + _cipherValue = cipherValue; + Algorithm = SignedXml.XmlDecryptionTransformUrl; + } + + public override Type[] InputTypes => s_inputTypes; + + public override Type[] OutputTypes => s_outputTypes; + + public override void LoadInnerXml(XmlNodeList nodeList) + { + } + + protected override XmlNodeList GetInnerXml() + { + return null; + } + + public override void LoadInput(object obj) + { + } + + public override object GetOutput() + { + new EncryptedXml().DecryptEncryptedKey(new EncryptedKey()); + return new MemoryStream(_cipherValue); + } + + public override object GetOutput(Type type) + { + return GetOutput(); + } + } #endif + + [Fact] + public static void EncryptedKeyNameRescan_Completes() + { + using (Aes wrappingKey = Aes.Create()) + using (Aes dataKey = Aes.Create()) + { + wrappingKey.KeySize = 256; + dataKey.KeySize = 256; + string xml = BuildEncryptedKeyRescanDocument(20, dataKey, wrappingKey); + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(xml); + EncryptedXml encryptedXml = new EncryptedXml(doc); + encryptedXml.AddKeyNameMapping("wrapping-key", wrappingKey); + + encryptedXml.DecryptDocument(); + + Assert.Equal(20, doc.GetElementsByTagName("payload").Count); + } + } + + private static string BuildEncryptedKeyRescanDocument(int count, SymmetricAlgorithm dataKey, SymmetricAlgorithm wrappingKey) + { + XmlDocument scratch = new XmlDocument { PreserveWhitespace = true }; + scratch.LoadXml(""); + EncryptedXml helper = new EncryptedXml(scratch); + string encryptedElement = Convert.ToBase64String(helper.EncryptData(scratch.DocumentElement, dataKey, false)); + string encryptedKey = Convert.ToBase64String(EncryptedXml.EncryptKey(dataKey.Key, wrappingKey)); + + StringWriter writer = new StringWriter(); + writer.Write(""); + for (int i = 0; i < count; i++) + { + writer.Write("data-key"); + writer.Write(encryptedElement); + writer.Write(""); + } + writer.Write("wrapping-key"); + writer.Write(encryptedKey); + writer.Write("data-key"); + return writer.ToString(); + } } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTest.cs index 4df826a3b387a1..58e06b5d451fe3 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTest.cs @@ -38,6 +38,8 @@ public AsymmetricAlgorithm PublicGetPublicKey() public class SignedXmlTest { + private const string MaxDecryptedDataElementsAppContextSwitch = "System.Security.Cryptography.Xml.MaxDecryptedDataElements"; + private const string DangerousMaxDecryptionCountAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount"; private const string signature = "CTnnhjxUQHJmD+t1MjVXrOW+MCA=dbFt6Zw3vR+Xh7LbM/vuifyFA7gPh/NlDM2Glz/SJBsveISieuTBpZlk/zavAeuXR/Nu0Ztt4OP4tCOg09a2RNlrTP0dhkeEfL1jTzpnVaLHuQbCiwOWCgbRif7Xt7N12FuiHYb3BltP/YyXS4E12NxlGlqnDiFA1v/mkK5+C1o=hEfTJNa2idz2u+fSYDDG4Lx/xuk4aBbvOPVNqgc1l9Y8t7Pt+ZyF+kkF3uUl8Y0700BFGAsprnhwrWENK+PGdtvM5796ZKxCCa0ooKkofiT4355HqK26hpV8dvj38vq/rkJe1jHZgkTKa+c/0vjcYZOI/RT/IZv9JfXxVWLuLxk=EQ=="; @@ -2141,6 +2143,24 @@ public void VerifyHMAC_HalfLength_NullSignatureFormatValidator_DefaultRejects() Assert.False(sign.CheckSignature(new HMACSHA256("secret"u8.ToArray()))); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + public void VerifyHMAC_ZeroLength_NullSignatureFormatValidator_UnsafeSwitchRestoresInsecureBehavior() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch( + "Switch.System.Security.Cryptography.Xml.SignedXml.AllowUnsafeTruncatedHmacSignatureVerification", + true); + + using HMACSHA256 builderHmac = new HMACSHA256("secret"u8.ToArray()); + SignedXml sign = BuildHmacSignedXml(signingHmac: builderHmac, hmacOutputLengthBits: 0); + sign.SignatureFormatValidator = null; + + Assert.True(sign.CheckSignature(new HMACSHA256("no clue"u8.ToArray()))); + }).Dispose(); + } + [Fact] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] public void VerifyHMAC_EnvelopedSignature_PostProcessedToZeroLengthNonHmacMethod_Rejects() @@ -2176,6 +2196,353 @@ public void VerifyHMAC_EnvelopedSignature_PostProcessedToZeroLengthNonHmacMethod } #if NET + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void SignedXml_MultipleDecryptionReferences_CountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 3); + + using RSA rsa = RSA.Create(); + using Aes aes = Aes.Create(); + XmlDocument document = new(); + document.LoadXml("onetwo"); + + SignedXml signer = new(document) + { + SigningKey = rsa, + }; + foreach (string id in new[] { "first", "second" }) + { + Reference reference = new($"#{id}"); + reference.AddTransform(new XmlDecryptionTransform()); + signer.AddReference(reference); + } + + signer.ComputeSignature(); + document.DocumentElement!.AppendChild(signer.GetXml()); + + EncryptedXml encryptedXml = new(document); + encryptedXml.AddKeyNameMapping("aes", aes); + XmlNodeList secrets = document.GetElementsByTagName("Secret"); + XmlElement[] elementsToEncrypt = { (XmlElement)secrets[0]!, (XmlElement)secrets[1]! }; + foreach (XmlElement secret in elementsToEncrypt) + { + EncryptedData encryptedData = encryptedXml.Encrypt(secret, "aes"); + EncryptedXml.ReplaceElement(secret, encryptedData, content: false); + } + + SignedXml verifier = new(document); + verifier.LoadXml((XmlElement)document.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0]); + verifier.EncryptedXml.AddKeyNameMapping("aes", aes); + + CryptographicException exception = Assert.Throws(() => verifier.CheckSignature(rsa)); + Assert.Equal("The XML document has exceeded the maximum number of elements allowed for decryption.", exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ComputeSignature_MultipleDecryptionReferences_CountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 3); + + using RSA rsa = RSA.Create(); + using Aes aes = Aes.Create(); + XmlDocument document = new(); + document.LoadXml("onetwo"); + + EncryptedXml encryptedXml = new(document); + encryptedXml.AddKeyNameMapping("aes", aes); + XmlNodeList secrets = document.GetElementsByTagName("Secret"); + XmlElement[] elementsToEncrypt = { (XmlElement)secrets[0]!, (XmlElement)secrets[1]! }; + foreach (XmlElement secret in elementsToEncrypt) + { + EncryptedData encryptedData = encryptedXml.Encrypt(secret, "aes"); + EncryptedXml.ReplaceElement(secret, encryptedData, content: false); + } + + SignedXml signer = new(document) + { + SigningKey = rsa, + }; + signer.EncryptedXml.AddKeyNameMapping("aes", aes); + foreach (string id in new[] { "first", "second" }) + { + Reference reference = new($"#{id}"); + reference.AddTransform(new XmlDecryptionTransform()); + signer.AddReference(reference); + } + + CryptographicException exception = Assert.Throws(signer.ComputeSignature); + Assert.Equal("The XML document has exceeded the maximum number of elements allowed for decryption.", exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void ComputeSignature_MultipleDecryptionReferences_ExcludedCandidateCountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 1); + + using RSA rsa = RSA.Create(); + XmlDocument document = new(); + document.LoadXml( + $"" + + $""); + + SignedXml signer = new(document) + { + SigningKey = rsa, + }; + + foreach ((string referenceId, string candidateId) in new[] + { + ("first", "first-candidate"), + ("second", "second-candidate"), + }) + { + XmlDecryptionTransform transform = new(); + transform.AddExceptUri(candidateId); + Reference reference = new($"#{referenceId}"); + reference.AddTransform(transform); + signer.AddReference(reference); + } + + CryptographicException exception = Assert.Throws(signer.ComputeSignature); + Assert.Equal( + "The number of EncryptedData elements processed during decryption has exceeded the maximum allowed for a single operation.", + exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void CheckSignatureReturningKey_MultipleKeys_CountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 3); + + using RSA rsa = RSA.Create(); + using Aes aes = Aes.Create(); + XmlDocument document = new(); + document.LoadXml("one"); + + SignedXml signer = new(document) + { + SigningKey = rsa, + }; + Reference reference = new("#target"); + reference.AddTransform(new XmlDecryptionTransform()); + signer.AddReference(reference); + signer.ComputeSignature(); + + KeyInfo keyInfo = new(); + keyInfo.AddClause(new RSAKeyValue(rsa)); + keyInfo.AddClause(new RSAKeyValue(rsa)); + signer.KeyInfo = keyInfo; + document.DocumentElement!.AppendChild(signer.GetXml()); + + XmlElement secret = (XmlElement)document.GetElementsByTagName("Secret")[0]!; + secret.InnerText = "two"; + EncryptedXml encryptedXml = new(document); + encryptedXml.AddKeyNameMapping("aes", aes); + EncryptedData encryptedData = encryptedXml.Encrypt(secret, "aes"); + EncryptedXml.ReplaceElement(secret, encryptedData, content: false); + + SignedXml verifier = new(document); + verifier.LoadXml((XmlElement)document.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0]); + verifier.EncryptedXml.AddKeyNameMapping("aes", aes); + + CryptographicException exception = Assert.Throws( + () => verifier.CheckSignatureReturningKey(out _)); + Assert.Equal("The XML document has exceeded the maximum number of elements allowed for decryption.", exception.Message); + }).Dispose(); + } + + public static bool CanRunStoreBackedRecursiveDecryptionTest { get; } = + RemoteExecutor.IsSupported && CanModifyCurrentUserMyStore(); + + [OuterLoop("Modifies system state")] + [PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)] + [ConditionalFact(typeof(SignedXmlTest), nameof(CanRunStoreBackedRecursiveDecryptionTest))] + public void SignedXml_CheckSignature_RecursiveGuard() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(MaxDecryptedDataElementsAppContextSwitch, 2); + + const string Password = "PLACEHOLDER"; + byte[] pfx; + + using (RSA certificateKey = RSA.Create()) + { + CertificateRequest request = new( + $"CN=dotnet-runtime-SignedXml-{Guid.NewGuid():N}", + certificateKey, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + using (X509Certificate2 generatedCertificate = request.CreateSelfSigned( + DateTimeOffset.UtcNow.AddMinutes(-5), + DateTimeOffset.UtcNow.AddHours(1))) + { + pfx = generatedCertificate.Export(X509ContentType.Pfx, Password); + } + } + + try + { + using (X509Certificate2 certificate = X509CertificateLoader.LoadPkcs12(pfx, Password, X509KeyStorageFlags.Exportable)) + using (X509Certificate2 publicCertificate = X509CertificateLoader.LoadCertificate(certificate.RawData)) + using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + + // Remove the certificate if it already exists in the store to avoid duplicates + store.Remove(publicCertificate); + + try + { + store.Add(certificate); + + EncryptedData rec = CreateRecursive(publicCertificate); + string signedDoc = CreateSignedXmlWithTamperedContent(rec); + + CryptographicException ex = Assert.ThrowsAny(() => VerifySignature(signedDoc)); + Assert.Equal("The number of EncryptedData elements processed during decryption has exceeded the maximum allowed for a single operation.", ex.Message); + } + finally + { + store.Remove(publicCertificate); + } + } + } + finally + { + CryptographicOperations.ZeroMemory(pfx); + } + }).Dispose(); + } + + private static EncryptedData CreateRecursive(X509Certificate2 certificate) + { + using RSA publicKey = certificate.GetRSAPublicKey()!; + using Aes aes = Aes.Create(); + + byte[] wrapped = EncryptedXml.EncryptKey(aes.Key, publicKey, useOAEP: true); + + EncryptedKey encryptedKey = new() + { + CipherData = new CipherData(wrapped), + EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncRSAOAEPUrl), + }; + + encryptedKey.KeyInfo.AddClause(new KeyInfoX509Data(certificate)); + + EncryptedData encryptedData = new() + { + Id = "ed0", + Type = EncryptedXml.XmlEncElementUrl, + EncryptionMethod = new EncryptionMethod(EncryptedXml.XmlEncAES256Url), + }; + + encryptedData.KeyInfo.AddClause(new KeyInfoEncryptedKey(encryptedKey)); + + CipherReference self = new("#ed0"); + self.AddTransform(new XmlDecryptionTransform()); + encryptedData.CipherData = new CipherData { CipherReference = self }; + + return encryptedData; + } + + private static string CreateSignedXmlWithTamperedContent(EncryptedData content) + { + using RSA key = RSA.Create(); + + XmlDocument signingDocument = new(); + signingDocument.LoadXml(""); + + SignedXml signer = new(signingDocument) + { + SigningKey = key, + KeyInfo = new KeyInfo(), + }; + + signer.KeyInfo.AddClause(new RSAKeyValue(key)); + + Reference reference = new("#data"); + reference.AddTransform(new XmlDecryptionTransform()); + signer.AddReference(reference); + + signer.ComputeSignature(); + + XmlElement signature = signer.GetXml(); + + XmlDocument destinationDocument = new(); + destinationDocument.LoadXml(""); + + destinationDocument.DocumentElement!.AppendChild( + destinationDocument.ImportNode(signature, deep: true)); + + XmlElement data = + (XmlElement)destinationDocument.SelectSingleNode("//*[@Id='data']")!; + + data.AppendChild(destinationDocument.ImportNode(content.GetXml(), deep: true)); + + return destinationDocument.OuterXml; + } + + private static void VerifySignature(string signedDocument) + { + XmlDocument document = new(); + document.LoadXml(signedDocument); + + SignedXml verifier = new(document); + verifier.LoadXml( + (XmlElement)document.GetElementsByTagName( + "Signature", + SignedXml.XmlDsigNamespaceUrl)[0]!); + + verifier.CheckSignature(); + } + + private static bool CanModifyCurrentUserMyStore() + { + if (!PlatformDetection.IsWindows && !PlatformDetection.IsLinux) + { + return false; + } + + try + { + using (X509Store store = new(StoreName.My, StoreLocation.CurrentUser)) + { + store.Open(OpenFlags.ReadWrite); + } + + return true; + } + catch (CryptographicException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + catch (IOException) + { + return false; + } + catch (InvalidOperationException) + { + return false; + } + } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsReflectionEmitSupported))] public void SignedXml_EncryptedDataWithInfiniteXslTransform() { @@ -2431,10 +2798,13 @@ public void SignedXml_DeepXmlDocument_CheckSignature() XmlElement signatureElement = signer.GetXml(); XmlDocument deepXmlDoc = new(); deepXmlDoc.LoadXml(GenerateDeepXmlString(depth: 4500, "target")); - SignedXml verifier = new(deepXmlDoc); - verifier.LoadXml(signatureElement); - CryptographicException ex = Assert.Throws(() => verifier.CheckSignature(rsa)); + CryptographicException ex = Assert.Throws(() => + { + SignedXml verifier = new(deepXmlDoc); + verifier.LoadXml(signatureElement); + verifier.CheckSignature(rsa); + }); Assert.Equal("The XML element has exceeded the maximum nesting depth allowed for decryption.", ex.Message); static string GenerateDeepXmlString(int depth, string id) @@ -2454,6 +2824,41 @@ static string GenerateDeepXmlString(int depth, string id) return sb.ToString(); } } + + [Fact] + public void SignedXml_ManyNamespaces_CheckSignature() + { + using var rsa = RSA.Create(2048); + + XmlDocument doc = new(); + doc.LoadXml(BuildSignatureXml(count: 1000)); + var sigEl = (XmlElement)doc.GetElementsByTagName("Signature", "http://www.w3.org/2000/09/xmldsig#")[0]; + var sx = new SignedXml(doc); + sx.LoadXml(sigEl); + + Assert.Throws(() => sx.CheckSignature(rsa)); + + static string BuildSignatureXml(int count) + { + StringBuilder sb = new(); + sb.Append(""); + sb.Append(""); + sb.Append("= 0; i--) sb.Append($" xmlns:p{i:D7}=\"urn:{i}\""); + sb.Append(">"); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append(""); + sb.Append("AAAAAAAAAAAAAAAAAAAAAAAAAAA="); + sb.Append(""); + sb.Append("AAAA"); + sb.Append(""); + sb.Append(""); + return sb.ToString(); + } + } #endif } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTests.cs b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTests.cs index c9f02d221797fe..05a323a8970694 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTests.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXmlTests.cs @@ -1,8 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.IO; +using System.Text; using System.Xml; using Xunit; +#if NET +using Microsoft.DotNet.RemoteExecutor; +#endif namespace System.Security.Cryptography.Xml.Tests { @@ -342,5 +347,202 @@ public void SignedXml_DifferentDigestMethods() Assert.Contains("sha256", digestMethod.Attributes["Algorithm"].Value); } } + [Fact] + public static void CommentInsertedAfterSigning_CanLeaveSignatureValidForTruncatingConsumer() + { + using (RSA rsa = RSA.Create()) + { + XmlDocument doc = LoadPreservingWhitespace("admin@good.com.evil.com"); + SignEnvelope(doc, rsa, "#x"); + + XmlDocument attacked = LoadPreservingWhitespace(doc.OuterXml); + XmlElement nameId = (XmlElement)attacked.GetElementsByTagName("NameID")[0]; + nameId.RemoveAll(); + nameId.AppendChild(attacked.CreateTextNode("admin@good.com")); + nameId.AppendChild(attacked.CreateComment("split")); + nameId.AppendChild(attacked.CreateTextNode(".evil.com")); + + Assert.True(VerifyEnvelope(attacked, rsa)); + Assert.Equal("admin@good.com", nameId.FirstChild.Value); + Assert.Equal("admin@good.com.evil.com", nameId.InnerText); + } + } + + private static XmlDocument LoadPreservingWhitespace(string xml) + { + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(xml); + return doc; + } + + private static void SignEnvelope(XmlDocument doc, RSA rsa, string uri) + { + SignedXml signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url; + Reference reference = new Reference(uri) { DigestMethod = SignedXml.XmlDsigSHA256Url }; + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + doc.DocumentElement.AppendChild(doc.ImportNode(signedXml.GetXml(), true)); + } + + private static bool VerifyEnvelope(XmlDocument doc, RSA rsa) + { + SignedXml signedXml = new SignedXml(doc); + signedXml.LoadXml((XmlElement)doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0]); + return signedXml.CheckSignature(rsa); + } + +#if NET + private const int DeepInputDepth = 1000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepXmlDocumentTarget_ComputeSignature_Throws() + { + RemoteExecutor.Invoke(static () => + { + XmlDocument doc = BuildDeepXmlDocument(DeepInputDepth, wrapInEnvelope: false, out _); + using RSA rsa = RSA.Create(); + + Assert.Throws(() => + { + SignedXml signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url; + Reference reference = new Reference("") { DigestMethod = SignedXml.XmlDsigSHA256Url }; + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepXmlElementTarget_ComputeSignature_Throws() + { + RemoteExecutor.Invoke(static () => + { + XmlDocument doc = BuildDeepXmlDocument(DeepInputDepth, wrapInEnvelope: true, out XmlElement targetWithId); + targetWithId.SetAttribute("Id", "t"); + using RSA rsa = RSA.Create(); + + Assert.Throws(() => + { + SignedXml signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url; + Reference reference = new Reference("#t") { DigestMethod = SignedXml.XmlDsigSHA256Url }; + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + }); + }).Dispose(); + } + + private static XmlDocument BuildDeepXmlDocument(int depth, bool wrapInEnvelope, out XmlElement innermostAncestor) + { + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + XmlElement top; + if (wrapInEnvelope) + { + XmlElement envelope = doc.CreateElement("Envelope"); + doc.AppendChild(envelope); + top = doc.CreateElement("Target"); + envelope.AppendChild(top); + } + else + { + top = doc.CreateElement("Root"); + doc.AppendChild(top); + } + innermostAncestor = top; + XmlElement current = top; + for (int i = 0; i < depth; i++) + { + XmlElement child = doc.CreateElement("a"); + current.AppendChild(child); + current = child; + } + return doc; + } + + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdInXmlDocumentTarget_ComputeSignature_Throws() + { + RemoteExecutor.Invoke(static () => + Assert.Throws(() => SignDtdDocument())).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdInXmlDocumentTarget_ComputeSignature_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + SignDtdDocument(); + }).Dispose(); + } + + private static void SignDtdDocument() + { + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml("x"); + using RSA rsa = RSA.Create(); + SignedXml signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url; + Reference reference = new Reference("") { DigestMethod = SignedXml.XmlDsigSHA256Url }; + reference.AddTransform(new XmlDsigEnvelopedSignatureTransform()); + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepInnerTextInLoadXml_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + const int Depth = 100000; + StringBuilder sb = new StringBuilder(Depth * 7 + 32); + sb.Append("AA=="); + for (int i = 0; i < Depth; i++) sb.Append(""); + sb.Append('x'); + for (int i = 0; i < Depth; i++) sb.Append(""); + sb.Append(""); + + XmlDocument doc = new XmlDocument(); + doc.LoadXml(sb.ToString()); + new SignedXml().LoadXml(doc.DocumentElement); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepIdElementInReference_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + const int Depth = 100000; + StringBuilder sb = new StringBuilder(Depth * 7 + 64); + sb.Append(""); + for (int i = 0; i < Depth; i++) sb.Append(""); + sb.Append('x'); + for (int i = 0; i < Depth; i++) sb.Append(""); + sb.Append(""); + + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(sb.ToString()); + new SignedXml(doc).GetIdElement(doc, "t"); + }); + }).Dispose(); + } +#endif } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXml_SignatureMethodAlgorithm.cs b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXml_SignatureMethodAlgorithm.cs index 1486e2835d2582..76c9418557ad45 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/SignedXml_SignatureMethodAlgorithm.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/SignedXml_SignatureMethodAlgorithm.cs @@ -13,6 +13,7 @@ namespace System.Security.Cryptography.Xml.Tests public class SignedXml_SignatureMethodAlgorithm { private const string MaxDecryptedDataElementsAppContextSwitch = "System.Security.Cryptography.Xml.MaxDecryptedDataElements"; + private const string DangerousMaxDecryptionCountAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount"; [Fact] public static void TestDummySignatureAlgorithm() @@ -89,18 +90,26 @@ public void SignedXml_LargeEncryptedElementList_ThrowsCryptographicException(int Assert.Throws(() => signedXml.CheckSignature(rsa)); } - [Theory] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [InlineData(50)] [InlineData(99)] public void SignedXml_EncryptedElementListWithinDefaultLimit_DoesNotThrowCryptographicException(int encryptedElementCount) { - using RSA rsa = RSA.Create(); - using Aes aes = Aes.Create(); + RemoteExecutor.Invoke(static (string encryptedElementCountText) => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 0); - SignedXml signedXml = BuildSignedXmlWithManyEncryptedElements(aes, rsa, encryptedElementCount); + using RSA rsa = RSA.Create(); + using Aes aes = Aes.Create(); + + SignedXml signedXml = BuildSignedXmlWithManyEncryptedElements( + aes, + rsa, + int.Parse(encryptedElementCountText, CultureInfo.InvariantCulture)); - // Document is processed fully without throwing a CryptographicException - Assert.Throws(() => signedXml.CheckSignature(rsa)); + // Document is processed fully without throwing a CryptographicException. + Assert.Throws(() => signedXml.CheckSignature(rsa)); + }, encryptedElementCount.ToString(CultureInfo.InvariantCulture)).Dispose(); } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] @@ -108,6 +117,8 @@ public void SignedXml_EncryptedElementListWithinDefaultLimit_DoesNotThrowCryptog [InlineData(99, 200, false)] // raise the limit; counts well below the new cap still don't trip it [InlineData(50, 30, true)] // lower the limit; 50 elements now exceed the configured cap [InlineData(10, 5, true)] // lower the limit aggressively; even 10 elements exceed the cap + [InlineData(9, 10, false)] // boundary: count == limit - 1 + [InlineData(10, 10, true)] // boundary: count == limit public void SignedXml_EncryptedElementListLimit_RespectsAppContextSwitch( int encryptedElementCount, int maxDecryptedDataElements, @@ -121,6 +132,9 @@ public void SignedXml_EncryptedElementListLimit_RespectsAppContextSwitch( AppContext.SetData(MaxDecryptedDataElementsAppContextSwitch, limit); + // There's a separate decryption count limit, so make sure we're not hitting that. + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 0); + using RSA rsa = RSA.Create(); using Aes aes = Aes.Create(); @@ -154,7 +168,7 @@ public void XmlDecryptionTransform_EncryptedElementListLimit_Boundary( int limit = int.Parse(limitText, CultureInfo.InvariantCulture); bool shouldThrow = bool.Parse(expectThrowText); - AppContext.SetData(MaxDecryptedDataElementsAppContextSwitch, limit); + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, limit); XmlDocument document = new(); XmlElement root = document.CreateElement("root"); @@ -192,6 +206,7 @@ public void SignedXml_EncryptedElementListLimit_ZeroDisablesLimit() RemoteExecutor.Invoke(static () => { AppContext.SetData(MaxDecryptedDataElementsAppContextSwitch, 0); + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 0); using RSA rsa = RSA.Create(); using Aes aes = Aes.Create(); diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDecryptionTransformTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDecryptionTransformTest.cs index e3eb28d122bf3d..ec61e2a49a8849 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDecryptionTransformTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDecryptionTransformTest.cs @@ -9,6 +9,7 @@ // Copyright (C) 2008 Novell, Inc (http://www.novell.com) using System.IO; +using System.Text; using System.Xml; using Microsoft.DotNet.RemoteExecutor; using Xunit; @@ -31,7 +32,11 @@ public XmlNodeList UnprotectedGetInnerXml() public class XmlDecryptionTransformTest { + private const string CryptoXmlMaxInputDocumentDepthAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlMaxInputDocumentDepth"; + private const string DangerousMaxDecryptionCountAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxDecryptionCount"; private const string DangerousMaxRecursionDepthAppContextSwitch = "System.Security.Cryptography.Xml.DangerousMaxRecursionDepth"; + private const string MaxEncryptedDataElementsExceededMessage = "The number of EncryptedData elements processed during decryption has exceeded the maximum allowed for a single operation."; + private UnprotectedXmlDecryptionTransform transform; public XmlDecryptionTransformTest() @@ -224,8 +229,8 @@ public void GetOutput_XmlWithEncryptedDataAndExcept() } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [InlineData(3, false)] - [InlineData(4, true)] + [InlineData(4, false)] + [InlineData(5, true)] public void GetOutput_XmlWithNestedEncryptedData_EnforcesDepthLimit(int depth, bool shouldThrow) { RemoteExecutor.Invoke(static (string depthStr, string shouldThrowStr) => @@ -233,6 +238,8 @@ public void GetOutput_XmlWithNestedEncryptedData_EnforcesDepthLimit(int depth, b int depth = int.Parse(depthStr); bool shouldThrow = bool.Parse(shouldThrowStr); + // Isolate recursive decryption depth so the independent input-document depth limit cannot mask the 3-pass/4-fail boundary. + AppContext.SetData(CryptoXmlMaxInputDocumentDepthAppContextSwitch, 0); AppContext.SetData(DangerousMaxRecursionDepthAppContextSwitch, 3); if (shouldThrow) @@ -246,6 +253,118 @@ public void GetOutput_XmlWithNestedEncryptedData_EnforcesDepthLimit(int depth, b } }, depth.ToString(), shouldThrow.ToString()).Dispose(); } +#if NET + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void GetOutput_ExcludedEncryptedDataCandidates_ZeroDisablesLimit() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 0); + + XmlDecryptionTransform transform = CreateTransformWithExcludedCandidates(257); + Assert.NotNull(transform.GetOutput(typeof(XmlDocument))); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void GetOutput_ExcludedEncryptedDataCandidates_ResetAfterException() + { + RemoteExecutor.Invoke(static () => + { + const int MaxCandidateCount = 2; + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, MaxCandidateCount); + + CryptographicException exception = Assert.Throws( + () => CreateTransformWithExcludedCandidates(MaxCandidateCount + 1).GetOutput()); + Assert.Equal(MaxEncryptedDataElementsExceededMessage, exception.Message); + + Assert.NotNull(CreateTransformWithExcludedCandidates(MaxCandidateCount).GetOutput()); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void GetOutput_RecursivelyRevealedEncryptedDataCandidate_CountsTowardLimit() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData(DangerousMaxDecryptionCountAppContextSwitch, 2); + + using Aes aes = Aes.Create(); + XmlDocument document = new XmlDocument(); + document.LoadXml( + $"" + + $""); + EncryptedXml encryptedXml = new EncryptedXml(document); + encryptedXml.AddKeyNameMapping("aes", aes); + + XmlElement secret = (XmlElement)document.GetElementsByTagName("secret")[0]; + EncryptedData encryptedData = encryptedXml.Encrypt(secret, "aes"); + EncryptedXml.ReplaceElement(secret, encryptedData, content: false); + + XmlDecryptionTransform transform = new XmlDecryptionTransform + { + EncryptedXml = encryptedXml, + }; + transform.AddExceptUri("excluded"); + transform.LoadInput(document); + + CryptographicException exception = Assert.Throws(transform.GetOutput); + Assert.Equal(MaxEncryptedDataElementsExceededMessage, exception.Message); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void GetOutput_EncryptedDataCountExceeded() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData("System.Security.Cryptography.Xml.DangerousMaxDecryptionCount", 2); + + using Aes aes = Aes.Create(); + XmlDocument document = new XmlDocument(); + document.LoadXml("onetwo"); + EncryptedXml encryptedXml = new EncryptedXml(document); + encryptedXml.AddKeyNameMapping("aes", aes); + + foreach (string elementName in new[] { "first", "second" }) + { + XmlElement element = (XmlElement)document.GetElementsByTagName(elementName)[0]; + EncryptedData encryptedData = encryptedXml.Encrypt(element, "aes"); + EncryptedXml.ReplaceElement(element, encryptedData, content: false); + } + + XmlDecryptionTransform transform = new XmlDecryptionTransform + { + EncryptedXml = encryptedXml, + }; + transform.LoadInput(document); + + CryptographicException exception = Assert.Throws(transform.GetOutput); + Assert.Equal(MaxEncryptedDataElementsExceededMessage, exception.Message); + }).Dispose(); + } + + private static XmlDecryptionTransform CreateTransformWithExcludedCandidates(int candidateCount) + { + XmlDocument document = new XmlDocument(); + XmlElement root = document.CreateElement("root"); + document.AppendChild(root); + + XmlDecryptionTransform transform = new XmlDecryptionTransform(); + for (int i = 0; i < candidateCount; i++) + { + string id = $"candidate-{i}"; + XmlElement candidate = document.CreateElement("EncryptedData", EncryptedXml.XmlEncNamespaceUrl); + candidate.SetAttribute("Id", id); + root.AppendChild(candidate); + transform.AddExceptUri(id); + } + + transform.LoadInput(document); + return transform; + } + +#endif private XmlDocument GetTransformedOutput(XmlDocument doc, string nodeToEncrypt) { @@ -310,5 +429,66 @@ private static void DecryptNestedEncryptedDataDocument(int depth) Assert.Equal("payload", transformedDocument.DocumentElement!.InnerText); } +#if NET + private const int DeepInputDepth = 100000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + byte[] payload = Encoding.UTF8.GetBytes(BuildDeepXml(DeepInputDepth)); + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(payload); + XmlDecryptionTransform transform = new XmlDecryptionTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + private static string BuildDeepXml(int depth) + { + StringBuilder sb = new StringBuilder(depth * 7 + 1); + for (int i = 0; i < depth; i++) + sb.Append(""); + sb.Append('x'); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + private const string DtdPayload = "x"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDecryptionTransform transform = new XmlDecryptionTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDecryptionTransform transform = new XmlDecryptionTransform(); + transform.LoadInput(ms); + Assert.NotNull(transform.GetOutput()); + }).Dispose(); + } +#endif } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigC14NTransformTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigC14NTransformTest.cs index 97cc079a3b9ffb..dd8a9c83212416 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigC14NTransformTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigC14NTransformTest.cs @@ -151,6 +151,94 @@ public void UnsupportedOutput() AssertExtensions.Throws("type", () => transform.GetOutput(doc.GetType())); } +#if NET + [Fact] + public void C14N_AttributeLimit_DefaultBoundary_RendersUpToLimitThenThrows() + { + TestHelpers.ExecuteTransform(BuildElementWithAttributes(512), new XmlDsigC14NTransform()); + + Assert.Throws( + () => TestHelpers.ExecuteTransform(BuildElementWithAttributes(513), new XmlDsigC14NTransform())); + } + + [Fact] + public void C14N_NamespaceLimit_DefaultBoundary_RendersUpToLimitThenThrows() + { + TestHelpers.ExecuteTransform(BuildElementWithNamespaces(512), new XmlDsigC14NTransform()); + + Assert.Throws( + () => TestHelpers.ExecuteTransform(BuildElementWithNamespaces(513), new XmlDsigC14NTransform())); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [InlineData(0)] // 0 means no limit + [InlineData(300)] // explicit limit above the element width + public void C14N_AttributeLimit_RaisedOrUnlimited_AllowsWiderElements(int maxAttributes) + { + RemoteExecutor.Invoke(static (string maxAttributesStr) => + { + AppContext.SetData( + "System.Security.Cryptography.Xml.DangerousMaxCanonicalizationAttributesPerElement", + int.Parse(maxAttributesStr)); + + TestHelpers.ExecuteTransform(BuildElementWithAttributes(200), new XmlDsigC14NTransform()); + TestHelpers.ExecuteTransform(BuildElementWithNamespaces(200), new XmlDsigC14NTransform()); + }, maxAttributes.ToString()).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void C14N_AttributeLimit_LoweredLimit_IsEnforced() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetData("System.Security.Cryptography.Xml.DangerousMaxCanonicalizationAttributesPerElement", 10); + + TestHelpers.ExecuteTransform(BuildElementWithAttributes(10), new XmlDsigC14NTransform()); + + Assert.Throws( + () => TestHelpers.ExecuteTransform(BuildElementWithAttributes(11), new XmlDsigC14NTransform())); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void C14N_AttributeLimit_NegativeFallsBackToDefault() + { + // Negative values are rejected by GetInt32Config (allowNegative: false), leaving the + // default of 512 in force. + RemoteExecutor.Invoke(static () => + { + AppContext.SetData("System.Security.Cryptography.Xml.DangerousMaxCanonicalizationAttributesPerElement", -1); + + TestHelpers.ExecuteTransform(BuildElementWithAttributes(512), new XmlDsigC14NTransform()); + + Assert.Throws( + () => TestHelpers.ExecuteTransform(BuildElementWithAttributes(513), new XmlDsigC14NTransform())); + }).Dispose(); + } + + private static string BuildElementWithAttributes(int count) + { + StringBuilder element = new StringBuilder("").ToString(); + } + + private static string BuildElementWithNamespaces(int count) + { + StringBuilder element = new StringBuilder("").ToString(); + } +#endif + [Fact] public void C14NSpecExample1() { @@ -486,6 +574,66 @@ private static XmlDocument CreateDeepXmlDocument(int depth) doc.Load(ms); return doc; } + + private const int DeepInputDepth = 100000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + byte[] payload = Encoding.UTF8.GetBytes(BuildDeepXml(DeepInputDepth)); + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(payload); + XmlDsigC14NTransform transform = new XmlDsigC14NTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + private static string BuildDeepXml(int depth) + { + StringBuilder sb = new StringBuilder(depth * 7 + 1); + for (int i = 0; i < depth; i++) + sb.Append(""); + sb.Append('x'); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + private const string DtdPayload = "x"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigC14NTransform transform = new XmlDsigC14NTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigC14NTransform transform = new XmlDsigC14NTransform(); + transform.LoadInput(ms); + Assert.NotNull(transform.GetOutput()); + }).Dispose(); + } #endif } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigEnvelopedSignatureTransformTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigEnvelopedSignatureTransformTest.cs index cfeb35fa69c137..465fbb262966a8 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigEnvelopedSignatureTransformTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigEnvelopedSignatureTransformTest.cs @@ -9,8 +9,12 @@ // (C) 2004 Novell Inc. using System.IO; +using System.Text; using System.Xml; using Xunit; +#if NET +using Microsoft.DotNet.RemoteExecutor; +#endif namespace System.Security.Cryptography.Xml.Tests { @@ -139,5 +143,67 @@ public void LoadInputAsXmlNodeList() XmlNodeList xnl = (XmlNodeList)transform.GetOutput(); AssertEquals(doc.ChildNodes, xnl, "EnvelopedSignature result"); } + +#if NET + private const int DeepInputDepth = 100000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + byte[] payload = Encoding.UTF8.GetBytes(BuildDeepXml(DeepInputDepth)); + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(payload); + XmlDsigEnvelopedSignatureTransform envTransform = new XmlDsigEnvelopedSignatureTransform(); + envTransform.LoadInput(ms); + envTransform.GetOutput(); + }); + }).Dispose(); + } + + private static string BuildDeepXml(int depth) + { + StringBuilder sb = new StringBuilder(depth * 7 + 1); + for (int i = 0; i < depth; i++) + sb.Append(""); + sb.Append('x'); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + private const string DtdPayload = "x"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigEnvelopedSignatureTransform envTransform = new XmlDsigEnvelopedSignatureTransform(); + envTransform.LoadInput(ms); + envTransform.GetOutput(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigEnvelopedSignatureTransform envTransform = new XmlDsigEnvelopedSignatureTransform(); + envTransform.LoadInput(ms); + Assert.NotNull(envTransform.GetOutput()); + }).Dispose(); + } +#endif } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigExcC14NTransformTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigExcC14NTransformTest.cs index ff7e708a71b4ff..ac0936a9428655 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigExcC14NTransformTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigExcC14NTransformTest.cs @@ -340,6 +340,61 @@ public void ExcC14NSpecExample6() Assert.Equal(ExcC14NSpecExample6Output, res); } +#if NET + [Fact] + public void ExcC14N_AttributeLimit_DefaultBoundary_RendersUpToLimitThenThrows() + { + TestHelpers.ExecuteTransform(BuildUtilizedPrefixes(512), new XmlDsigExcC14NTransform()); + + Assert.Throws( + () => TestHelpers.ExecuteTransform(BuildUtilizedPrefixes(513), new XmlDsigExcC14NTransform())); + + static string BuildUtilizedPrefixes(int count) + { + StringBuilder element = new StringBuilder("").ToString(); + } + } + + [Fact] + public void ExcC14N_NamespaceLimit_InclusivePrefixList_RendersUpToLimitThenThrows() + { + Execute(512); + + Assert.Throws(() => Execute(513)); + + static void Execute(int count) + { + StringBuilder element = new StringBuilder(" 0) + { + prefixes.Append(' '); + } + + prefixes.Append(FormattableString.Invariant($"p{i:D4}")); + } + + element.Append(">"); + + TestHelpers.ExecuteTransform(element.ToString(), new XmlDsigExcC14NTransform(prefixes.ToString())); + } + } +#endif + private string ExecuteXmlDSigExcC14NTransform(string InputXml, XmlResolver resolver = null) { XmlDocument doc = new XmlDocument(); @@ -638,6 +693,124 @@ private static XmlDocument CreateDeepXmlDocument(int depth) doc.Load(ms); return doc; } + + private const int DeepInputDepth = 100000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + byte[] payload = Encoding.UTF8.GetBytes(BuildDeepXml(DeepInputDepth)); + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(payload); + XmlDsigExcC14NTransform transform = new XmlDsigExcC14NTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + private static string BuildDeepXml(int depth) + { + StringBuilder sb = new StringBuilder(depth * 7 + 1); + for (int i = 0; i < depth; i++) + sb.Append(""); + sb.Append('x'); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + private const string DtdPayload = "x"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigExcC14NTransform transform = new XmlDsigExcC14NTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigExcC14NTransform transform = new XmlDsigExcC14NTransform(); + transform.LoadInput(ms); + Assert.NotNull(transform.GetOutput()); + }).Dispose(); + } #endif + + [Fact] + public static void ExclusiveC14n_DoesNotCoverNamespacePrefixUsedOnlyInAttributeValue() + { + using (RSA rsa = RSA.Create()) + { + string one = SignAndDigest(rsa, QNameAttributeDocument("urn:one"), new XmlDsigExcC14NTransform(), out XmlDocument signed); + string two = SignAndDigest(rsa, QNameAttributeDocument("urn:two"), new XmlDsigExcC14NTransform(), out _); + string inclusiveOne = SignAndDigest(rsa, QNameAttributeDocument("urn:one"), new XmlDsigC14NTransform(), out _); + string inclusiveTwo = SignAndDigest(rsa, QNameAttributeDocument("urn:two"), new XmlDsigC14NTransform(), out _); + + XmlDocument rebound = LoadPreservingWhitespace(signed.OuterXml.Replace("urn:one", "urn:two")); + + Assert.Equal(one, two); + Assert.NotEqual(inclusiveOne, inclusiveTwo); + Assert.True(VerifyReboundSignature(rebound, rsa)); + Assert.Equal("{urn:two}T", ResolveQName((XmlElement)rebound.GetElementsByTagName("data")[0], "type", "http://www.w3.org/2001/XMLSchema-instance")); + } + } + + private static string QNameAttributeDocument(string uri) => + "" + + "value"; + + private static string SignAndDigest(RSA rsa, string xml, Transform transform, out XmlDocument doc) + { + doc = LoadPreservingWhitespace(xml); + SignedXml signedXml = new SignedXml(doc) { SigningKey = rsa }; + signedXml.SignedInfo.SignatureMethod = SignedXml.XmlDsigRSASHA256Url; + Reference reference = new Reference("#d1") { DigestMethod = SignedXml.XmlDsigSHA256Url }; + reference.AddTransform(transform); + signedXml.AddReference(reference); + signedXml.ComputeSignature(); + doc.DocumentElement.AppendChild(doc.ImportNode(signedXml.GetXml(), true)); + return doc.GetElementsByTagName("DigestValue", SignedXml.XmlDsigNamespaceUrl)[0].InnerText; + } + + private static bool VerifyReboundSignature(XmlDocument doc, RSA rsa) + { + SignedXml signedXml = new SignedXml(doc); + signedXml.LoadXml((XmlElement)doc.GetElementsByTagName("Signature", SignedXml.XmlDsigNamespaceUrl)[0]); + return signedXml.CheckSignature(rsa); + } + + private static string ResolveQName(XmlElement element, string localName, string namespaceUri) + { + string value = element.GetAttribute(localName, namespaceUri); + int colon = value.IndexOf(':'); + string prefix = value.Substring(0, colon); + return "{" + element.GetNamespaceOfPrefix(prefix) + "}" + value.Substring(colon + 1); + } + + private static XmlDocument LoadPreservingWhitespace(string xml) + { + XmlDocument doc = new XmlDocument { PreserveWhitespace = true }; + doc.LoadXml(xml); + return doc; + } } } diff --git a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigXPathTransformTest.cs b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigXPathTransformTest.cs index 2c0535cd8e9e52..7da5f931ba6fe1 100644 --- a/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigXPathTransformTest.cs +++ b/src/libraries/System.Security.Cryptography.Xml/tests/XmlDsigXPathTransformTest.cs @@ -10,8 +10,12 @@ // Copyright (C) 2004-2005 Novell, Inc (http://www.novell.com) using System.IO; +using System.Text; using System.Xml; using Xunit; +#if NET +using Microsoft.DotNet.RemoteExecutor; +#endif namespace System.Security.Cryptography.Xml.Tests { @@ -280,5 +284,67 @@ public void FunctionHereObsolete() nl = (XmlNodeList)t.GetOutput(); Assert.Equal(0, nl.Count); } + +#if NET + private const int DeepInputDepth = 100000; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DeepStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + byte[] payload = Encoding.UTF8.GetBytes(BuildDeepXml(DeepInputDepth)); + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(payload); + XmlDsigXPathTransform transform = new XmlDsigXPathTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + private static string BuildDeepXml(int depth) + { + StringBuilder sb = new StringBuilder(depth * 7 + 1); + for (int i = 0; i < depth; i++) + sb.Append(""); + sb.Append('x'); + for (int i = 0; i < depth; i++) + sb.Append(""); + return sb.ToString(); + } + + private const string DtdPayload = "x"; + private const string CryptoXmlAllowDtdProcessingAppContextSwitch = "System.Security.Cryptography.Xml.CryptoXmlAllowDtdProcessing"; + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_Throws() + { + RemoteExecutor.Invoke(static () => + { + Assert.Throws(() => + { + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigXPathTransform transform = new XmlDsigXPathTransform(); + transform.LoadInput(ms); + transform.GetOutput(); + }); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public static void DtdStreamInput_LoadInput_WithOptIn_Succeeds() + { + RemoteExecutor.Invoke(static () => + { + AppContext.SetSwitch(CryptoXmlAllowDtdProcessingAppContextSwitch, true); + using MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(DtdPayload)); + XmlDsigXPathTransform transform = new XmlDsigXPathTransform(); + transform.LoadInput(ms); + Assert.NotNull(transform.GetOutput()); + }).Dispose(); + } +#endif } } diff --git a/src/libraries/System.Threading/tests/MutexTests.cs b/src/libraries/System.Threading/tests/MutexTests.cs index b96dd1ad763827..d78281ad997dcc 100644 --- a/src/libraries/System.Threading/tests/MutexTests.cs +++ b/src/libraries/System.Threading/tests/MutexTests.cs @@ -1095,6 +1095,81 @@ public void NamedMutex_OtherEvent_NotCompatible() | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + private const UnixFileMode OwnerUserRwx = + UnixFileMode.UserRead + | UnixFileMode.UserWrite + | UnixFileMode.UserExecute; + + [ConditionalFact(nameof(IsRemoteExecutorAndCrossProcessNamedMutexSupported))] + [PlatformSpecific(TestPlatforms.Linux)] + [UnsupportedOSPlatform("windows")] + public void NamedMutex_UserScopedSharedMemoryFallbackPathIsCreated() + { + string homeDirectory = GetTestFilePath(); + Directory.CreateDirectory(homeDirectory); + + RemoteInvokeOptions remoteInvokeOptions = new(); + remoteInvokeOptions.StartInfo.EnvironmentVariables["HOME"] = homeDirectory; + remoteInvokeOptions.StartInfo.EnvironmentVariables["XDG_RUNTIME_DIR"] = string.Empty; + + RemoteExecutor.Invoke( + homeDirectory => + { + string sharedMemoryBaseDirectory = Path.Combine( + homeDirectory, + ".dotnet", + "corefx", + "Runtime", + "SharedMemory"); + string name = Guid.NewGuid().ToString("N"); + string sharedMemoryDirectory = Path.Combine(sharedMemoryBaseDirectory, ".dotnet", "shm", "global"); + NamedWaitHandleOptions options = + new() { CurrentUserOnly = true, CurrentSessionOnly = false }; + using Mutex mutex = new($"Global\\{name}", options); + Assert.True(File.Exists(Path.Combine(sharedMemoryDirectory, name))); + Assert.Equal(OwnerUserRwx, File.GetUnixFileMode(Path.Combine(sharedMemoryBaseDirectory, ".dotnet"))); + Assert.Equal(OwnerUserRwx, File.GetUnixFileMode(Path.Combine(sharedMemoryBaseDirectory, ".dotnet", "shm"))); + Assert.Equal(OwnerUserRwx, File.GetUnixFileMode(sharedMemoryDirectory)); + }, + homeDirectory, + remoteInvokeOptions).Dispose(); + } + + [ConditionalFact(nameof(IsRemoteExecutorAndCrossProcessNamedMutexSupported))] + [PlatformSpecific(TestPlatforms.Linux)] + [UnsupportedOSPlatform("windows")] + public void NamedMutex_SharedMemoryDirectoryReplacementDoesNotRedirectFileAccess() + { + string userRuntimeDirectory = GetTestFilePath(); + Directory.CreateDirectory(userRuntimeDirectory); + + RemoteInvokeOptions remoteInvokeOptions = new(); + remoteInvokeOptions.StartInfo.EnvironmentVariables["XDG_RUNTIME_DIR"] = userRuntimeDirectory; + + RemoteExecutor.Invoke( + userRuntimeDirectory => + { + NamedWaitHandleOptions options = + new() { CurrentUserOnly = true, CurrentSessionOnly = false }; + string firstName = Guid.NewGuid().ToString("N"); + + using Mutex firstMutex = new($"Global\\{firstName}", options); + + string runtimeDirectory = Path.Combine(userRuntimeDirectory, ".dotnet"); + string sharedMemoryDirectory = Path.Combine(runtimeDirectory, "shm"); + string originalSharedMemoryDirectory = Path.Combine(runtimeDirectory, "shm-original"); + Directory.Move(sharedMemoryDirectory, originalSharedMemoryDirectory); + Directory.CreateDirectory(Path.Combine(sharedMemoryDirectory, "global"), OwnerUserRwx); + + string secondName = Guid.NewGuid().ToString("N"); + using Mutex secondMutex = new($"Global\\{secondName}", options); + + Assert.True(File.Exists(Path.Combine(originalSharedMemoryDirectory, "global", secondName))); + Assert.False(File.Exists(Path.Combine(sharedMemoryDirectory, "global", secondName))); + }, + userRuntimeDirectory, + remoteInvokeOptions).Dispose(); + } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMobile))] [PlatformSpecific(TestPlatforms.AnyUnix)] diff --git a/src/mono/browser/debugger/BrowserDebugProxy/EvaluateExpression.cs b/src/mono/browser/debugger/BrowserDebugProxy/EvaluateExpression.cs index bbbd5759f70cb2..5b1d1ada7b7701 100644 --- a/src/mono/browser/debugger/BrowserDebugProxy/EvaluateExpression.cs +++ b/src/mono/browser/debugger/BrowserDebugProxy/EvaluateExpression.cs @@ -232,6 +232,12 @@ void AddLocalVariableWithValue(string idName, JObject value) } } + private static string FormatCSharpStringLiteral(string value) + => SyntaxFactory.Literal(value ?? string.Empty).Text; + + private static string FormatCSharpCharacterLiteral(char value) + => SyntaxFactory.Literal(value).Text; + public static string ConvertJSToCSharpLocalVariableAssignment(string idName, JToken variable) { string typeRet; @@ -243,15 +249,13 @@ public static string ConvertJSToCSharpLocalVariableAssignment(string idName, JTo { case "string": { - var str = value?.Value(); - str = str.Replace("\"", "\\\""); - valueRet = $"\"{str}\""; + valueRet = FormatCSharpStringLiteral(value?.Value()); typeRet = "string"; break; } case "symbol": { - valueRet = $"'{value?.Value()}'"; + valueRet = FormatCSharpCharacterLiteral(value.Value()); typeRet = "char"; break; } @@ -283,11 +287,11 @@ public static string ConvertJSToCSharpLocalVariableAssignment(string idName, JTo case "object": default: valueRet = "Newtonsoft.Json.Linq.JObject.FromObject(new {" - + $"type = \"{type}\"" - + $", description = \"{variable["description"].Value()}\"" - + $", className = \"{variable["className"].Value()}\"" - + (subType != null ? $", subtype = \"{subType}\"" : "") - + (objectId != null ? $", objectId = \"{objectId}\"" : "") + + $"type = {FormatCSharpStringLiteral(type)}" + + $", description = {FormatCSharpStringLiteral(variable["description"]?.Value())}" + + $", className = {FormatCSharpStringLiteral(variable["className"]?.Value())}" + + (subType != null ? $", subtype = {FormatCSharpStringLiteral(subType)}" : "") + + (objectId != null ? $", objectId = {FormatCSharpStringLiteral(objectId.ToString())}" : "") + "})"; typeRet = "object"; break; @@ -306,7 +310,7 @@ public static string ConvertJSToCSharpLocalVariableAssignment(string idName, JTo => ("Newtonsoft.Json.Linq.JObject.FromObject(new {" + $"type = \"object\"," + $"description = \"object\"," - + $"className = \"{className}\"," + + $"className = {FormatCSharpStringLiteral(className)}," + $"subtype = \"null\"" + "})", "object"); @@ -488,13 +492,13 @@ internal static async Task> GetVariableDefinitions(MemberReferenceR { if (objectId.IsValueType) { - variableDefStrings.Add($"string {definition.IdName} = \"{definition.Obj["description"].Value()}\";"); + variableDefStrings.Add($"string {definition.IdName} = {FormatCSharpStringLiteral(definition.Obj["description"]?.Value())};"); } else { var typeIds = await resolver.GetContext().SdbAgent.GetTypeIdsForObject(objectId.Value, withParents: true, token); var toString = await resolver.GetContext().SdbAgent.InvokeToStringAsync(typeIds, isValueType: false, isEnum: false, objectId.Value, BindingFlags.DeclaredOnly, invokeToStringInObject: true, token); - variableDefStrings.Add($"string {definition.IdName} = \"{toString}\";"); + variableDefStrings.Add($"string {definition.IdName} = {FormatCSharpStringLiteral(toString)};"); } } else diff --git a/src/mono/browser/debugger/Directory.Build.props b/src/mono/browser/debugger/Directory.Build.props index c13ea1ed0a66b0..e72eb602fe4160 100644 --- a/src/mono/browser/debugger/Directory.Build.props +++ b/src/mono/browser/debugger/Directory.Build.props @@ -1,3 +1,8 @@ + + + + AnyCPU + diff --git a/src/mono/mono/mini/CMakeLists.txt b/src/mono/mono/mini/CMakeLists.txt index 33623aae258757..a0aea1ea35270e 100644 --- a/src/mono/mono/mini/CMakeLists.txt +++ b/src/mono/mono/mini/CMakeLists.txt @@ -565,6 +565,10 @@ if(NOT DISABLE_EXECUTABLES) set_target_properties(mono-sgen PROPERTIES INSTALL_RPATH "$ORIGIN") endif() endif() + + if(HOST_OSX AND NOT HOST_MACCAT AND NOT MONO_CROSS_COMPILE_EXECUTABLE_NAME) + set_macos_bundle_identifier(mono-sgen com.microsoft.dotnet.mono-sgen) + endif() set(MONOSGEN_LINKABLE_LIBS "") list(APPEND MONOSGEN_LINKABLE_LIBS monoapi eglib_api monosgen-static dn-containers) diff --git a/src/mono/nuget/Directory.Build.targets b/src/mono/nuget/Directory.Build.targets index c5a434cbbf9c85..8e2f1b6a5c2e33 100644 --- a/src/mono/nuget/Directory.Build.targets +++ b/src/mono/nuget/Directory.Build.targets @@ -2,7 +2,7 @@ - $(ProductVersion) + $(ProductVersion) diff --git a/src/mono/nuget/mono-packages.proj b/src/mono/nuget/mono-packages.proj index 9c8dbbf3d74486..bf04a3cb4a48e5 100644 --- a/src/mono/nuget/mono-packages.proj +++ b/src/mono/nuget/mono-packages.proj @@ -3,9 +3,13 @@ + + + + + - @@ -18,7 +22,6 @@ - diff --git a/src/mono/wasm/host/DevServer/WebAssemblyNetDebugProxyAppBuilderExtensions.cs b/src/mono/wasm/host/DevServer/WebAssemblyNetDebugProxyAppBuilderExtensions.cs index a1430cfa054fae..354efc30fabe2b 100644 --- a/src/mono/wasm/host/DevServer/WebAssemblyNetDebugProxyAppBuilderExtensions.cs +++ b/src/mono/wasm/host/DevServer/WebAssemblyNetDebugProxyAppBuilderExtensions.cs @@ -359,7 +359,7 @@ Ensure your browser is running with debugging enabled.

This should launch a new browser window with debugging enabled..

Underlying exception:

-
{ex}
+
{WebUtility.HtmlEncode(ex.ToString())}
"); return; diff --git a/src/native/corehost/dotnet/CMakeLists.txt b/src/native/corehost/dotnet/CMakeLists.txt index dab92ad6518576..55602c0f70c18e 100644 --- a/src/native/corehost/dotnet/CMakeLists.txt +++ b/src/native/corehost/dotnet/CMakeLists.txt @@ -24,6 +24,10 @@ add_executable(dotnet ${SOURCES}) target_link_libraries(dotnet PRIVATE hostmisc fxr_resolver) +if(CLR_CMAKE_TARGET_OSX) + set_macos_bundle_identifier(dotnet com.microsoft.dotnet.dotnet) +endif() + add_sanitizer_runtime_support(dotnet) if(NOT CLR_CMAKE_TARGET_WIN32) diff --git a/src/native/corehost/hostmisc/pal.unix.cpp b/src/native/corehost/hostmisc/pal.unix.cpp index 3663716c232fcd..be613a0c688e58 100644 --- a/src/native/corehost/hostmisc/pal.unix.cpp +++ b/src/native/corehost/hostmisc/pal.unix.cpp @@ -186,9 +186,7 @@ bool pal::get_default_breadcrumb_store(string_t* recv) if (!pal::directory_exists(ext)) { trace::info(_X("Directory core breadcrumbs [%s] was not specified or found"), ext.c_str()); - ext.clear(); - append_path(&ext, _X("opt")); - append_path(&ext, _X("corebreadcrumbs")); + ext.assign(DIR_SEPARATOR_STR _X("opt") DIR_SEPARATOR_STR _X("corebreadcrumbs")); if (!pal::directory_exists(ext)) { trace::info(_X("Fallback directory core breadcrumbs at [%s] was not found"), ext.c_str()); @@ -196,9 +194,11 @@ bool pal::get_default_breadcrumb_store(string_t* recv) } } - if (access(ext.c_str(), (R_OK | W_OK)) != 0) + // Writing breadcrumbs creates files at known paths, which requires write and search permission on the directory. + if (access(ext.c_str(), (W_OK | X_OK)) != 0) { - trace::info(_X("Breadcrumb store [%s] is not ACL-ed with rw-"), ext.c_str()); + trace::info(_X("Breadcrumb store [%s] is not writable"), ext.c_str()); + return false; } recv->assign(ext); @@ -218,9 +218,7 @@ bool pal::get_default_servicing_directory(string_t* recv) if (!pal::directory_exists(ext)) { trace::info(_X("Directory core servicing at [%s] was not specified or found"), ext.c_str()); - ext.clear(); - append_path(&ext, _X("opt")); - append_path(&ext, _X("coreservicing")); + ext.assign(DIR_SEPARATOR_STR _X("opt") DIR_SEPARATOR_STR _X("coreservicing")); if (!pal::directory_exists(ext)) { trace::info(_X("Fallback directory core servicing at [%s] was not found"), ext.c_str()); @@ -228,9 +226,11 @@ bool pal::get_default_servicing_directory(string_t* recv) } } - if (access(ext.c_str(), R_OK) != 0) + // Servicing assets are looked up by known path, which requires search permission on the directory. + if (access(ext.c_str(), X_OK) != 0) { - trace::info(_X("Directory core servicing at [%s] was not ACL-ed properly"), ext.c_str()); + trace::info(_X("Directory core servicing at [%s] is not accessible"), ext.c_str()); + return false; } recv->assign(ext); diff --git a/src/native/corehost/hostpolicy/args.h b/src/native/corehost/hostpolicy/args.h index 0539e7e3d1edc4..279f158ebd7159 100644 --- a/src/native/corehost/hostpolicy/args.h +++ b/src/native/corehost/hostpolicy/args.h @@ -67,7 +67,9 @@ struct probe_config_t , probe_deps_json(nullptr) , fx_level(-1) , only_runtime_assets(false) - { } + { + assert(probe_type == type::app || pal::is_path_fully_qualified(probe_dir)); + } bool is_lookup() const { diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c index 78e08565015511..79380d874dd302 100644 --- a/src/native/libs/System.Native/entrypoints.c +++ b/src/native/libs/System.Native/entrypoints.c @@ -61,9 +61,11 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_Stat) DllImportEntry(SystemNative_LStat) DllImportEntry(SystemNative_Open) + DllImportEntry(SystemNative_OpenAt) DllImportEntry(SystemNative_Close) DllImportEntry(SystemNative_Dup) DllImportEntry(SystemNative_Unlink) + DllImportEntry(SystemNative_UnlinkAt) DllImportEntry(SystemNative_IsMemfdSupported) DllImportEntry(SystemNative_MemfdCreate) DllImportEntry(SystemNative_ShmOpen) @@ -81,6 +83,7 @@ static const Entry s_sysNative[] = DllImportEntry(SystemNative_FcntlSetIsNonBlocking) DllImportEntry(SystemNative_FcntlGetIsNonBlocking) DllImportEntry(SystemNative_MkDir) + DllImportEntry(SystemNative_MkDirAt) DllImportEntry(SystemNative_ChMod) DllImportEntry(SystemNative_FChMod) DllImportEntry(SystemNative_FSync) diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c index ebfb79d6287032..1ae712017d94cc 100644 --- a/src/native/libs/System.Native/pal_io.c +++ b/src/native/libs/System.Native/pal_io.c @@ -306,7 +306,7 @@ static int32_t ConvertOpenFlags(int32_t flags) return -1; } - if (flags & ~(PAL_O_ACCESS_MODE_MASK | PAL_O_CLOEXEC | PAL_O_CREAT | PAL_O_EXCL | PAL_O_TRUNC | PAL_O_SYNC | PAL_O_NOFOLLOW)) + if (flags & ~(PAL_O_ACCESS_MODE_MASK | PAL_O_CLOEXEC | PAL_O_CREAT | PAL_O_EXCL | PAL_O_TRUNC | PAL_O_SYNC | PAL_O_NOFOLLOW | PAL_O_DIRECTORY)) { assert_msg(false, "Unknown Open flag", (int)flags); return -1; @@ -326,18 +326,15 @@ static int32_t ConvertOpenFlags(int32_t flags) ret |= O_SYNC; if (flags & PAL_O_NOFOLLOW) ret |= O_NOFOLLOW; + if (flags & PAL_O_DIRECTORY) + ret |= O_DIRECTORY; assert(ret != -1); return ret; } -intptr_t SystemNative_Open(const char* path, int32_t flags, int32_t mode) +static int32_t ConvertOpenFlagsAndAddDefaults(int32_t flags) { -// these two ifdefs are for platforms where we dont have the open version of CLOEXEC and thus -// must simulate it by doing a fcntl with the SETFFD version after the open instead -#if !HAVE_O_CLOEXEC - int32_t old_flags = flags; -#endif flags = ConvertOpenFlags(flags); if (flags == -1) { @@ -351,15 +348,47 @@ intptr_t SystemNative_Open(const char* path, int32_t flags, int32_t mode) #ifndef TARGET_WASM flags |= O_NOCTTY; #endif + return flags; +} - int result; - while ((result = open(path, flags, (mode_t)mode)) < 0 && errno == EINTR); +static void SetCloseOnExecIfNeeded(int result, int32_t flags) +{ #if !HAVE_O_CLOEXEC - if (old_flags & PAL_O_CLOEXEC) + if (result >= 0 && (flags & PAL_O_CLOEXEC)) { fcntl(result, F_SETFD, FD_CLOEXEC); } +#else + (void)result; + (void)flags; #endif +} + +intptr_t SystemNative_Open(const char* path, int32_t flags, int32_t mode) +{ + int32_t nativeFlags = ConvertOpenFlagsAndAddDefaults(flags); + if (nativeFlags == -1) + { + return -1; + } + + int result; + while ((result = open(path, nativeFlags, (mode_t)mode)) < 0 && errno == EINTR); + SetCloseOnExecIfNeeded(result, flags); + return result; +} + +intptr_t SystemNative_OpenAt(intptr_t dirfd, const char* path, int32_t flags, int32_t mode) +{ + int32_t nativeFlags = ConvertOpenFlagsAndAddDefaults(flags); + if (nativeFlags == -1) + { + return -1; + } + + int result; + while ((result = openat(ToFileDescriptor(dirfd), path, nativeFlags, (mode_t)mode)) < 0 && errno == EINTR); + SetCloseOnExecIfNeeded(result, flags); return result; } @@ -394,6 +423,27 @@ int32_t SystemNative_Unlink(const char* path) return result; } +int32_t SystemNative_UnlinkAt(intptr_t dirfd, const char* path, int32_t flags) +{ + int nativeFlags; + switch (flags) + { + case 0: + nativeFlags = 0; + break; + case PAL_AT_REMOVEDIR: + nativeFlags = AT_REMOVEDIR; + break; + default: + errno = EINVAL; + return -1; + } + + int32_t result; + while ((result = unlinkat(ToFileDescriptor(dirfd), path, nativeFlags)) < 0 && errno == EINTR); + return result; +} + #ifdef __NR_memfd_create #ifndef MFD_CLOEXEC #define MFD_CLOEXEC 0x0001U @@ -761,6 +811,13 @@ int32_t SystemNative_MkDir(const char* path, int32_t mode) return result; } +int32_t SystemNative_MkDirAt(intptr_t dirfd, const char* path, int32_t mode) +{ + int32_t result; + while ((result = mkdirat(ToFileDescriptor(dirfd), path, (mode_t)mode)) < 0 && errno == EINTR); + return result; +} + int32_t SystemNative_ChMod(const char* path, int32_t mode) { #if HAVE_CHMOD diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h index 10332df8043230..6d62d40677166b 100644 --- a/src/native/libs/System.Native/pal_io.h +++ b/src/native/libs/System.Native/pal_io.h @@ -183,6 +183,12 @@ enum PAL_O_NOFOLLOW = 0x0200, // Fails to open the target if it's a symlink, parent symlinks are allowed PAL_O_NONBLOCK_READ = 0x0400, // Set O_NONBLOCK on the read end of a pipe PAL_O_NONBLOCK_WRITE = 0x0800, // Set O_NONBLOCK on the write end of a pipe + PAL_O_DIRECTORY = 0x1000, // Fails to open the target if it's not a directory +}; + +enum +{ + PAL_AT_REMOVEDIR = 0x0001, // Remove the entry as a directory }; /** @@ -375,6 +381,13 @@ PALEXPORT int32_t SystemNative_LStat(const char* path, FileStatus* output); */ PALEXPORT intptr_t SystemNative_Open(const char* path, int32_t flags, int32_t mode); +/** + * Open or create a file or device relative to a directory descriptor. Implemented as shim to openat(2). + * + * Returns file descriptor or -1 for failure. Sets errno on failure. + */ +PALEXPORT intptr_t SystemNative_OpenAt(intptr_t dirfd, const char* path, int32_t flags, int32_t mode); + /** * Close a file descriptor. Implemented as shim to open(2). * @@ -396,6 +409,13 @@ PALEXPORT intptr_t SystemNative_Dup(intptr_t oldfd); */ PALEXPORT int32_t SystemNative_Unlink(const char* path); +/** + * Delete an entry relative to a directory descriptor. Implemented as shim to unlinkat(2). + * + * Returns 0 for success, -1 for failure. Sets errno on failure. + */ +PALEXPORT int32_t SystemNative_UnlinkAt(intptr_t dirfd, const char* path, int32_t flags); + /** * Check if the system supports memfd_create(2). * @@ -520,6 +540,13 @@ PALEXPORT int32_t SystemNative_FcntlGetIsNonBlocking(intptr_t fd, int32_t* isNon */ PALEXPORT int32_t SystemNative_MkDir(const char* path, int32_t mode); +/** + * Create a directory relative to a directory descriptor. Implemented as a shim to mkdirat(2). + * + * Returns 0 for success, -1 for failure. Sets errno for failure. + */ +PALEXPORT int32_t SystemNative_MkDirAt(intptr_t dirfd, const char* path, int32_t mode); + /** * Change permissions of a file. Implemented as a shim to chmod(2). * diff --git a/src/native/libs/System.Native/pal_process.c b/src/native/libs/System.Native/pal_process.c index c5af90e397287e..3c138380877c93 100644 --- a/src/native/libs/System.Native/pal_process.c +++ b/src/native/libs/System.Native/pal_process.c @@ -24,7 +24,7 @@ #include #if defined(__linux__) -#if !defined(HAVE_CLOSE_RANGE) +#if !HAVE_CLOSE_RANGE #include #if !defined(__NR_close_range) // close_range was added in Linux 5.9. The syscall number is 436 for all @@ -37,7 +37,7 @@ # define __NR_close_range 436 # endif #endif // !defined(__NR_close_range) -#endif // !defined(HAVE_CLOSE_RANGE) +#endif // !HAVE_CLOSE_RANGE #endif // defined(__linux__) #if (HAVE_CLOSE_RANGE || defined(__NR_close_range)) && !defined(CLOSE_RANGE_CLOEXEC) #define CLOSE_RANGE_CLOEXEC (1U << 2) @@ -246,44 +246,16 @@ static int SetCloexecForFd(void* context, int fd) } #endif -// Used when OS-specific bulk methods (close_range, fdwalk) are unavailable or fail. -// Always sets FD_CLOEXEC rather than closing fds directly. Closing fds directly would close -// waitForChildToExecPipe[WRITE_END_OF_PIPE] (which has O_CLOEXEC), making it impossible to -// report exec() failures back to the parent when execve() fails after RestrictHandleInheritance. -static void SetCloexecForAllFdsFallback(void) -{ - struct rlimit rl; - int maxFd; - if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) - { - maxFd = (int)rl.rlim_cur; - } - else - { - maxFd = (int)sysconf(_SC_OPEN_MAX); - if (maxFd <= 0) - { - maxFd = 65536; // reasonable upper bound - } - } - - // Fallback: iterate over all file descriptors and set FD_CLOEXEC on each open one >= 3. - for (int fd = 3; fd < maxFd; fd++) - { - int flags = fcntl(fd, F_GETFD); - if (flags == -1) - { - continue; // fd not open - } - - if ((flags & FD_CLOEXEC) == 0) - { - fcntl(fd, F_SETFD, flags | FD_CLOEXEC); - } - } -} - -static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFdCount) +// Returns 0 on success, or -1 (with errno set to ENOTSUP) if this platform/kernel doesn't provide +// a performant native way (close_range()/fdwalk()) to restrict which fds a child process inherits. +// +// We intentionally do NOT fall back to manually scanning every possible fd via fcntl() in that +// case: without close_range()/fdwalk(), there is no reliable way to learn only the fds that are +// actually open, so such a scan would need to cover the entire valid fd range (up to INT_MAX) to +// be correct -- something that has been measured to take multiple minutes of CPU time per +// Process.Start call. That's not an acceptable cost, so we report failure instead and let the +// managed layer surface a clear, actionable error. +static int32_t RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFdCount) { // FDs 0-2 are stdin/stdout/stderr; this method must be called AFTER the dup2 calls. // We always set FD_CLOEXEC rather than closing fds directly. This is critical because @@ -291,29 +263,27 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd // until execve() is called so that exec failures can be reported back to the parent. // Using closefrom() or close_range() with flag 0 (direct close) would destroy this pipe. + int result; #if HAVE_CLOSE_RANGE // On systems where close_range() is available as a function (FreeBSD 12.2+, Linux glibc >= 2.34). - if (close_range(3, UINT_MAX, CLOSE_RANGE_CLOEXEC) != 0) - { - SetCloexecForAllFdsFallback(); - } + result = close_range(3, UINT_MAX, CLOSE_RANGE_CLOEXEC); #elif defined(__NR_close_range) // On Linux with older glibc that doesn't expose close_range() as a function, // use the raw syscall number if the kernel supports it (kernel >= 5.9). - if (syscall(__NR_close_range, 3, UINT_MAX, CLOSE_RANGE_CLOEXEC) != 0) - { - SetCloexecForAllFdsFallback(); - } + result = (int)syscall(__NR_close_range, 3, UINT_MAX, CLOSE_RANGE_CLOEXEC); #elif HAVE_FDWALK // On Illumos/Solaris, use fdwalk() to set FD_CLOEXEC on all open fds >= 3. - if (fdwalk(SetCloexecForFd, NULL) != 0) - { - SetCloexecForAllFdsFallback(); - } + result = fdwalk(SetCloexecForFd, NULL); #else - SetCloexecForAllFdsFallback(); + result = -1; #endif + if (result != 0) + { + errno = ENOTSUP; + return -1; + } + // Remove CLOEXEC from user-provided inherited file descriptors so they survive execve. for (int i = 0; i < inheritedFdCount; i++) { @@ -327,6 +297,8 @@ static void RestrictHandleInheritance(int32_t* inheritedFds, int32_t inheritedFd } } } + + return 0; } // Forward declaration of the internal fork+exec function @@ -907,9 +879,9 @@ static int32_t ForkAndExecProcessInternal( } } - if (inheritedFdCount >= 0) + if (inheritedFdCount >= 0 && RestrictHandleInheritance(inheritedFds, inheritedFdCount) != 0) { - RestrictHandleInheritance(inheritedFds, inheritedFdCount); + ExitChild(waitForChildToExecPipe[WRITE_END_OF_PIPE], errno); } #if HAVE_PR_SET_PDEATHSIG diff --git a/src/tasks/Microsoft.NET.WebAssembly.Webcil/Microsoft.NET.WebAssembly.Webcil.csproj b/src/tasks/Microsoft.NET.WebAssembly.Webcil/Microsoft.NET.WebAssembly.Webcil.csproj index e582aeabfe42a0..5fe4db7356ba80 100644 --- a/src/tasks/Microsoft.NET.WebAssembly.Webcil/Microsoft.NET.WebAssembly.Webcil.csproj +++ b/src/tasks/Microsoft.NET.WebAssembly.Webcil/Microsoft.NET.WebAssembly.Webcil.csproj @@ -2,6 +2,7 @@ $(NetCoreAppToolCurrent);$(NetFrameworkToolCurrent) + AnyCPU Abstractions for modifying .NET webcil binary images true true diff --git a/src/tools/ilasm/src/ilasm/ilasm.csproj b/src/tools/ilasm/src/ilasm/ilasm.csproj index 8a1e3d9fe0a6b4..16c04c3963be8a 100644 --- a/src/tools/ilasm/src/ilasm/ilasm.csproj +++ b/src/tools/ilasm/src/ilasm/ilasm.csproj @@ -11,6 +11,8 @@ true true true + com.microsoft.dotnet.ilasm + ilasm true enable