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('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", '''))
+ <_HostBundleNameXml>$(HostBundleName.Replace('&', '&').Replace('<', '<').Replace('>', '>').Replace("'", '''))
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
-
<_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
+
+
+
+
+
-
@@ -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