From be72901c68540ad0e499a6797ad2a34ff905214e Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Tue, 31 Mar 2026 16:55:24 +0200 Subject: [PATCH 1/8] fix windows tar symlink --- .../src/System/Formats/Tar/TarEntry.cs | 4 ++-- ...e.ExtractToDirectory.File.Tests.Windows.cs | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) 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 6e8382552e4d4b..a8bd41ae559abf 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 @@ -350,7 +350,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b string name = ArchivingUtils.SanitizeEntryFilePath(Name, preserveDriveRoot: true); string? fileDestinationPath = GetFullDestinationPath( destinationDirectoryPath, - Path.IsPathFullyQualified(name) ? name : Path.Join(destinationDirectoryPath, name)); + Path.IsPathRooted(name) ? Path.GetFullPath(name) : Path.Join(destinationDirectoryPath, name)); if (fileDestinationPath == null) { throw new IOException(SR.Format(SR.TarExtractingResultsFileOutside, name, destinationDirectoryPath)); @@ -364,7 +364,7 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: true); string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, - Path.IsPathFullyQualified(linkName) ? linkName : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); + Path.IsPathRooted(linkName) ? Path.GetFullPath(linkName) : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); if (linkDestination is null) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs index 19d5dc19627db2..2f1f301f8d3cb4 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs @@ -27,5 +27,28 @@ public void Extract_SpecialFiles_Windows_ThrowsInvalidOperation() Assert.Equal(0, Directory.GetFileSystemEntries(destination).Count()); } + + [ConditionalFact(typeof(MountHelper), nameof(MountHelper.CanCreateSymbolicLinks))] + public void ExtractToDirectory_RejectsSymlinkWithRootedTargetOutsideDestination() + { + using TempDirectory root = new TempDirectory(); + string destDir = Path.Combine(root.Path, "dest"); + Directory.CreateDirectory(destDir); + + // A rooted path that points outside destDir (the target doesn't need to exist). + string rootedLinkTarget = @"\Temp\temp.ini"; + + string tarPath = Path.Combine(root.Path, "windows_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.SymbolicLink, "outside.txt") { LinkName = rootedLinkTarget }); + } + + Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); + + string symlinkPath = Path.Combine(destDir, "outside.txt"); + Assert.False(File.Exists(symlinkPath) || Directory.Exists(symlinkPath), "outside.txt should not have been created."); + } } } From c23ad6abd3f90362ca138cb795c5ebc4eea63d1f Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 6 Apr 2026 14:39:36 +0200 Subject: [PATCH 2/8] address feedback comments --- .../System.Formats.Tar/src/System/Formats/Tar/TarEntry.cs | 7 ++++--- .../TarFile.ExtractToDirectory.File.Tests.Windows.cs | 4 +--- 2 files changed, 5 insertions(+), 6 deletions(-) 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 a8bd41ae559abf..5c88ac4963b912 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 @@ -362,15 +362,16 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // LinkName is an absolute path, or path relative to the fileDestinationPath directory. // We don't check if the LinkName is empty. In that case, creation of the link will fail because link targets can't be empty. string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: true); + bool isRooted = Path.IsPathRooted(linkName); string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, - Path.IsPathRooted(linkName) ? Path.GetFullPath(linkName) : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); + isRooted ? Path.GetFullPath(linkName) : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); if (linkDestination is null) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); } - // Use the linkName for creating the symbolic link. - linkTargetPath = linkName; + // For rooted targets, use the fully-resolved path so the created symlink points to exactly the path that was validated otherwise preserve the original relative path + linkTargetPath = isRooted ? linkDestination : linkName; } else if (EntryType is TarEntryType.HardLink) { diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs index 2f1f301f8d3cb4..a05d4c2de245a4 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs @@ -46,9 +46,7 @@ public void ExtractToDirectory_RejectsSymlinkWithRootedTargetOutsideDestination( } Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); - - string symlinkPath = Path.Combine(destDir, "outside.txt"); - Assert.False(File.Exists(symlinkPath) || Directory.Exists(symlinkPath), "outside.txt should not have been created."); + Assert.Empty(Directory.EnumerateFileSystemEntries(destDir)); } } } From 6d49ee78c3d627de32976e0d12d7e37994c92e35 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 6 Apr 2026 14:44:45 +0200 Subject: [PATCH 3/8] add extra assert --- .../TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs index a05d4c2de245a4..4003d7c4312425 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs @@ -47,6 +47,7 @@ public void ExtractToDirectory_RejectsSymlinkWithRootedTargetOutsideDestination( Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); Assert.Empty(Directory.EnumerateFileSystemEntries(destDir)); + Assert.DoesNotContain("outside.txt", Directory.EnumerateFileSystemEntries(destDir).Select(Path.GetFileName)); } } } From 465dc4acef47a9408ea0128181cda2bcc0d1ff03 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Mon, 6 Apr 2026 14:51:18 +0200 Subject: [PATCH 4/8] remove redundant check --- .../TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs index 4003d7c4312425..a05d4c2de245a4 100644 --- a/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs +++ b/src/libraries/System.Formats.Tar/tests/TarFile/TarFile.ExtractToDirectory.File.Tests.Windows.cs @@ -47,7 +47,6 @@ public void ExtractToDirectory_RejectsSymlinkWithRootedTargetOutsideDestination( Assert.Throws(() => TarFile.ExtractToDirectory(tarPath, destDir, overwriteFiles: true)); Assert.Empty(Directory.EnumerateFileSystemEntries(destDir)); - Assert.DoesNotContain("outside.txt", Directory.EnumerateFileSystemEntries(destDir).Select(Path.GetFileName)); } } } From feff4f661c3e7f48fe1e309c780fc3fa62059fd2 Mon Sep 17 00:00:00 2001 From: Irem Yuksel Date: Wed, 8 Apr 2026 17:38:25 +0000 Subject: [PATCH 5/8] Merged PR 59737: [internal/release/9.0] Add check for negative ExtentedAttributes size in TarHeader Adds check for the "size" attribute in the ExdendedAttributes section of a tar file to prevent infinite loop with negative size. Follows the same throw logic for TarHeader's size. The negative sized tar file cannot be reproduced using .NET, hence the lack of tests. ---- #### AI description (iteration 1) #### PR Classification Bug fix to add validation for negative size values in TAR extended attributes. #### PR Summary Adds a safety check to prevent negative size values when reading TAR extended attributes from the PaxEaSize field, throwing an `InvalidDataException` if a negative size is encountered. - `TarHeader.Read.cs`: Added validation to check if the extended attributes size is negative before assignment, throwing `InvalidDataException` with `TarSizeFieldNegative` error message if true. --- .../src/System/Formats/Tar/TarHeader.Read.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs index 281812c11f6f21..a5a1a69dac08f6 100644 --- a/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs +++ b/src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarHeader.Read.cs @@ -135,6 +135,11 @@ internal void ReplaceNormalAttributesWithExtended(Dictionary? di // The 'size' header field only fits 12 bytes, so the data section length that surpases that limit needs to be retrieved if (TarHelpers.TryGetStringAsBaseTenLong(ExtendedAttributes, PaxEaSize, out long size)) { + if (size < 0) + { + throw new InvalidDataException(SR.Format(SR.TarSizeFieldNegative)); + } + _size = size; } From ca5a4ec523409f7723dafa503abb2a4d2016a556 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 9 Apr 2026 09:34:00 +0200 Subject: [PATCH 6/8] reject ambigous windows files --- .../src/System/Formats/Tar/TarEntry.cs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) 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 5c88ac4963b912..ad96b7ec17c84c 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 @@ -348,9 +348,15 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); string name = ArchivingUtils.SanitizeEntryFilePath(Name, preserveDriveRoot: true); + // On Windows, reject rooted but not fully qualified paths ("\Windows\win.ini", "C:foo"). + // They resolve ambiguously based on the current drive or working directory. + if (OperatingSystem.IsWindows() && Path.IsPathRooted(name) && !Path.IsPathFullyQualified(name)) + { + throw new IOException(SR.Format(SR.TarExtractingResultsFileOutside, name, destinationDirectoryPath)); + } string? fileDestinationPath = GetFullDestinationPath( destinationDirectoryPath, - Path.IsPathRooted(name) ? Path.GetFullPath(name) : Path.Join(destinationDirectoryPath, name)); + Path.IsPathFullyQualified(name) ? name : Path.Join(destinationDirectoryPath, name)); if (fileDestinationPath == null) { throw new IOException(SR.Format(SR.TarExtractingResultsFileOutside, name, destinationDirectoryPath)); @@ -362,22 +368,29 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // LinkName is an absolute path, or path relative to the fileDestinationPath directory. // We don't check if the LinkName is empty. In that case, creation of the link will fail because link targets can't be empty. string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: true); - bool isRooted = Path.IsPathRooted(linkName); + if (OperatingSystem.IsWindows() && Path.IsPathRooted(linkName) && !Path.IsPathFullyQualified(linkName)) + { + throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); + } string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, - isRooted ? Path.GetFullPath(linkName) : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); + Path.IsPathFullyQualified(linkName) ? linkName : Path.Join(Path.GetDirectoryName(fileDestinationPath), linkName)); if (linkDestination is null) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); } - // For rooted targets, use the fully-resolved path so the created symlink points to exactly the path that was validated otherwise preserve the original relative path - linkTargetPath = isRooted ? linkDestination : linkName; + // Use the linkName for creating the symbolic link. + linkTargetPath = linkName; } else if (EntryType is TarEntryType.HardLink) { // LinkName is path relative to the destinationDirectoryPath. // We don't check if the LinkName is empty. In that case, creation of the link will fail because a hard link can't target a directory. string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: false); + if (OperatingSystem.IsWindows() && Path.IsPathRooted(linkName) && !Path.IsPathFullyQualified(linkName)) + { + throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); + } string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, Path.Join(destinationDirectoryPath, linkName)); @@ -592,10 +605,10 @@ private FileStreamOptions CreateFileStreamOptions(bool isAsync) if (!OperatingSystem.IsWindows()) { - const UnixFileMode OwnershipPermissions = - UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | - UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | - UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; + const UnixFileMode OwnershipPermissions = + UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | + UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | + UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute; // Restore permissions. // For security, limit to ownership permissions, and respect umask (through UnixCreateMode). From eb52dc2573f613c85cd3da52f50a84777af40798 Mon Sep 17 00:00:00 2001 From: alinpahontu2912 Date: Thu, 9 Apr 2026 16:24:35 +0200 Subject: [PATCH 7/8] only reject ambigous symlink paths --- .../src/System/Formats/Tar/TarEntry.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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 ad96b7ec17c84c..8191f0bacd18fe 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 @@ -348,12 +348,6 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b Debug.Assert(Path.IsPathFullyQualified(destinationDirectoryPath)); string name = ArchivingUtils.SanitizeEntryFilePath(Name, preserveDriveRoot: true); - // On Windows, reject rooted but not fully qualified paths ("\Windows\win.ini", "C:foo"). - // They resolve ambiguously based on the current drive or working directory. - if (OperatingSystem.IsWindows() && Path.IsPathRooted(name) && !Path.IsPathFullyQualified(name)) - { - throw new IOException(SR.Format(SR.TarExtractingResultsFileOutside, name, destinationDirectoryPath)); - } string? fileDestinationPath = GetFullDestinationPath( destinationDirectoryPath, Path.IsPathFullyQualified(name) ? name : Path.Join(destinationDirectoryPath, name)); @@ -368,6 +362,9 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // LinkName is an absolute path, or path relative to the fileDestinationPath directory. // We don't check if the LinkName is empty. In that case, creation of the link will fail because link targets can't be empty. string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: true); + // On Windows, reject rooted-but-not-fully-qualified symlink targets (e.g., "\Windows\win.ini"). + // Unlike files, symlink targets are resolved at access time, not extraction time, + // so Path.GetFullPath here cannot reliably predict what drive the OS will resolve them against. if (OperatingSystem.IsWindows() && Path.IsPathRooted(linkName) && !Path.IsPathFullyQualified(linkName)) { throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); @@ -387,10 +384,6 @@ internal Task ExtractRelativeToDirectoryAsync(string destinationDirectoryPath, b // LinkName is path relative to the destinationDirectoryPath. // We don't check if the LinkName is empty. In that case, creation of the link will fail because a hard link can't target a directory. string linkName = ArchivingUtils.SanitizeEntryFilePath(LinkName, preserveDriveRoot: false); - if (OperatingSystem.IsWindows() && Path.IsPathRooted(linkName) && !Path.IsPathFullyQualified(linkName)) - { - throw new IOException(SR.Format(SR.TarExtractingResultsLinkOutside, linkName, destinationDirectoryPath)); - } string? linkDestination = GetFullDestinationPath( destinationDirectoryPath, Path.Join(destinationDirectoryPath, linkName)); From a1e6809fb8318884882ceff057000654f558738a Mon Sep 17 00:00:00 2001 From: "Sean Reeser (CSI Interfusion Inc)" Date: Wed, 29 Apr 2026 22:59:19 +0000 Subject: [PATCH 8/8] Merged PR 60607: Updated Versions.props - MicrosoftNativeQuicMsQuicSchannelVersion 2.4.18 Updated Versions.props - MicrosoftNativeQuicMsQuicSchannelVersion 2.4.18 ---- #### AI description (iteration 1) #### PR Classification Dependency version update to upgrade the MsQuic Schannel package from version 2.4.17 to 2.4.18. #### PR Summary This pull request updates the MsQuic Schannel dependency to a newer patch version. - `/eng/Versions.props`: Updated `MicrosoftNativeQuicMsQuicSchannelVersion` from 2.4.17 to 2.4.18 --- eng/Versions.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eng/Versions.props b/eng/Versions.props index dedee1ab5ac105..7221cec9c4c75d 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -229,7 +229,7 @@ 9.0.0-rtm.25627.1 9.0.0-rtm.24466.4 - 2.4.17 + 2.4.18 19.1.0-alpha.1.26202.3 19.1.0-alpha.1.26202.3