From a324484ef04f36f7b44055e2ab53be9ed9acdf8a Mon Sep 17 00:00:00 2001 From: Raimund Andree Date: Thu, 8 Oct 2026 09:57:18 +0000 Subject: [PATCH] fix: write non-terminating errors from the hard link cmdlets on shares and for folders Windows can't list the names of a file on a network share and answers with (50) The request is not supported. The new live tests found that Get-NTFSHardLink then stopped with a terminating error, so that it skipped the remaining paths, and that New-NTFSHardLink -PassThru did so after it had created the link. A folder stopped Get-NTFSHardLink the same way. Both cmdlets now write a non-terminating GetHardLinkError and go on. The tests reach the sandbox over the administrative share of its drive, which behaves like the share of a file server, and skip where that share isn't available, such as for a basic user. The pages describe the limit on shares. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: AI Assistant --- CHANGELOG.md | 5 ++ Docs/Cmdlets/Get-NTFSHardLink.md | 4 +- Docs/Cmdlets/New-NTFSHardLink.md | 2 + NTFSSecurity/LinkCmdlets/GetHardLink.cs | 16 +++++- NTFSSecurity/LinkCmdlets/NewHardLink.cs | 15 +++++- NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml | 4 +- Tests/Links.Tests.ps1 | 55 ++++++++++++++++++++ 7 files changed, 97 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bc6d7..c820fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -295,5 +295,10 @@ The format is based on write `DestinationFileAlreadyExists` and an error that names the missing folder. `Copy-Item2` no longer creates the missing folders of the destination when it copies a folder, which the prereleases of 5.0.0 did +- Fix `Get-NTFSHardLink`, which stopped for all remaining paths at a folder + and at a file on a network share, where Windows can't list the names of + a file, and `New-NTFSHardLink -PassThru`, which stopped with a + terminating error on a share after it had created the link; both now + write a non-terminating `GetHardLinkError` [Unreleased]: https://github.com/raandree/NTFSSecurity/compare/4.2.6...HEAD diff --git a/Docs/Cmdlets/Get-NTFSHardLink.md b/Docs/Cmdlets/Get-NTFSHardLink.md index 5febacc..b1aefef 100644 --- a/Docs/Cmdlets/Get-NTFSHardLink.md +++ b/Docs/Cmdlets/Get-NTFSHardLink.md @@ -23,7 +23,7 @@ On an NTFS volume, a file is a block of data that one or more directory entries, A file that has only one name returns a single object. A file that has additional hard links returns one object per name, which lets you find all the places on the volume from which the same data is reachable. The file system reports the links relative to the root of the volume, and the cmdlet combines them with the root of the path you specify, so the result contains full paths. All hard links of a file are always on the same volume as the file. -`-Path` must point to a file. A folder causes an error, because NTFS does not support hard links to folders. If you omit `-Path`, the cmdlet falls back to the current location, which is a folder and therefore produces the same error, so always pass the path of a file. +`-Path` must point to a file. A folder causes a non-terminating `GetHardLinkError`, because NTFS does not support hard links to folders, and the cmdlet continues with the next path. If you omit `-Path`, the cmdlet falls back to the current location, which is a folder and therefore produces the same error, so always pass the path of a file. The parameter accepts an array of paths and takes pipeline input by value and by property name through its `FullName` alias. `Get-ChildItem2` adds a `HardLinkCount` property to each file as long as the `IdentifyHardLinks` entry in the `PrivateData` section of the module manifest is `$true`, which lets you select the files that have more than one name before you resolve them. @@ -102,6 +102,8 @@ The cmdlet never writes folder objects, because it rejects folders with the erro Hard links exist only within a single NTFS volume. Every object that this cmdlet returns therefore refers to a path on the volume of the file that you passed in. +Windows can't list the names of a file on a network share, also when the share lies on an NTFS volume of the file server. For such a file the cmdlet writes a non-terminating `GetHardLinkError` with the message "The request is not supported" and continues with the next path; run the cmdlet on the file server itself instead. Before 5.0.0, a file on a network share and a folder stopped the cmdlet with a terminating error, so that it skipped the remaining paths. + Because all hard links of a file share the same data, they also share the file content, the file size, and the time stamps. The security descriptor is stored with the file as well, so changing permissions through one name changes them for every name. The cmdlet resolves paths through the AlphaFS library and therefore also works with paths that exceed the 260-character `MAX_PATH` limit. diff --git a/Docs/Cmdlets/New-NTFSHardLink.md b/Docs/Cmdlets/New-NTFSHardLink.md index 184f5a7..3bcf7df 100644 --- a/Docs/Cmdlets/New-NTFSHardLink.md +++ b/Docs/Cmdlets/New-NTFSHardLink.md @@ -134,6 +134,8 @@ The cmdlet never writes folder objects, because hard links are supported for fil Windows supports hard links only for files on the same NTFS volume. A link that points to a file on another volume, or a target on a file system that does not implement hard links, cannot be created. +The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link. + The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged. Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain. diff --git a/NTFSSecurity/LinkCmdlets/GetHardLink.cs b/NTFSSecurity/LinkCmdlets/GetHardLink.cs index 7f96b1a..3804e7b 100644 --- a/NTFSSecurity/LinkCmdlets/GetHardLink.cs +++ b/NTFSSecurity/LinkCmdlets/GetHardLink.cs @@ -48,8 +48,12 @@ namespace NTFSSecurity //access the path to make sure it exists and is a file var item = GetFileSystemInfo2(path); + // An error for this path only; before 5.0.0-rc6, a folder stopped the cmdlet for all paths. if (item is DirectoryInfo) - throw new ArgumentException("The item must be a file"); + { + WriteError(new ErrorRecord(new ArgumentException("The item must be a file"), "GetHardLinkError", ErrorCategory.InvalidArgument, path)); + continue; + } var links = File.EnumerateHardlinks(item.FullName); @@ -64,6 +68,16 @@ namespace NTFSSecurity { WriteError(new ErrorRecord(ex, "FileNotFound", ErrorCategory.ObjectNotFound, path)); } + // Windows can't list the names of a file on a network share: (50) The request is not supported. + // Before 5.0.0-rc6, this stopped the cmdlet for all paths. + catch (System.IO.IOException ex) + { + WriteError(new ErrorRecord(ex, "GetHardLinkError", ErrorCategory.ReadError, path)); + } + catch (UnauthorizedAccessException ex) + { + WriteError(new ErrorRecord(ex, "GetHardLinkError", ErrorCategory.PermissionDenied, path)); + } } } diff --git a/NTFSSecurity/LinkCmdlets/NewHardLink.cs b/NTFSSecurity/LinkCmdlets/NewHardLink.cs index 3ce89c6..93ad344 100644 --- a/NTFSSecurity/LinkCmdlets/NewHardLink.cs +++ b/NTFSSecurity/LinkCmdlets/NewHardLink.cs @@ -1,5 +1,7 @@ using Alphaleonis.Win32.Filesystem; using System; +using System.Collections.Generic; +using System.Linq; using System.Management.Automation; namespace NTFSSecurity @@ -74,7 +76,18 @@ namespace NTFSSecurity if (passThru) { - var links = File.EnumerateHardlinks(path); + IEnumerable links; + try + { + links = File.EnumerateHardlinks(path).ToList(); + } + // Windows can't list the names of a file on a network share: (50) The request is not supported. + // The link exists; before 5.0.0-rc6, this stopped the cmdlet with a terminating error. + catch (System.IO.IOException ex) + { + WriteError(new ErrorRecord(ex, "GetHardLinkError", ErrorCategory.ReadError, path)); + return; + } foreach (var link in links) { diff --git a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml index 5276fae..85576db 100644 --- a/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml +++ b/NTFSSecurity/en-US/NTFSSecurity.dll-Help.xml @@ -5224,7 +5224,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd On an NTFS volume, a file is a block of data that one or more directory entries, called hard links, refer to. The `Get-NTFSHardLink` cmdlet asks the file system for every hard link of the file that `-Path` points to and writes a file object for each of them, including the name that you passed in. A file that has only one name returns a single object. A file that has additional hard links returns one object per name, which lets you find all the places on the volume from which the same data is reachable. The file system reports the links relative to the root of the volume, and the cmdlet combines them with the root of the path you specify, so the result contains full paths. All hard links of a file are always on the same volume as the file. - `-Path` must point to a file. A folder causes an error, because NTFS does not support hard links to folders. If you omit `-Path`, the cmdlet falls back to the current location, which is a folder and therefore produces the same error, so always pass the path of a file. + `-Path` must point to a file. A folder causes a non-terminating `GetHardLinkError`, because NTFS does not support hard links to folders, and the cmdlet continues with the next path. If you omit `-Path`, the cmdlet falls back to the current location, which is a folder and therefore produces the same error, so always pass the path of a file. The parameter accepts an array of paths and takes pipeline input by value and by property name through its `FullName` alias. `Get-ChildItem2` adds a `HardLinkCount` property to each file as long as the `IdentifyHardLinks` entry in the `PrivateData` section of the module manifest is `$true`, which lets you select the files that have more than one name before you resolve them. @@ -5289,6 +5289,7 @@ PS C:\> Get-NTFSAudit -SecurityDescriptor $sd Hard links exist only within a single NTFS volume. Every object that this cmdlet returns therefore refers to a path on the volume of the file that you passed in. + Windows can't list the names of a file on a network share, also when the share lies on an NTFS volume of the file server. For such a file the cmdlet writes a non-terminating `GetHardLinkError` with the message "The request is not supported" and continues with the next path; run the cmdlet on the file server itself instead. Before 5.0.0, a file on a network share and a folder stopped the cmdlet with a terminating error, so that it skipped the remaining paths. Because all hard links of a file share the same data, they also share the file content, the file size, and the time stamps. The security descriptor is stored with the file as well, so changing permissions through one name changes them for every name. The cmdlet resolves paths through the AlphaFS library and therefore also works with paths that exceed the 260-character `MAX_PATH` limit. @@ -7160,6 +7161,7 @@ PS C:\Data> Get-NTFSSecurityDescriptor Windows supports hard links only for files on the same NTFS volume. A link that points to a file on another volume, or a target on a file system that does not implement hard links, cannot be created. + The cmdlet creates hard links on a network share as well, but Windows can't list the names of a file there. With `-PassThru` on a share, the cmdlet creates the link and writes a non-terminating `GetHardLinkError` with the message "The request is not supported" instead of the objects. Before 5.0.0, it stopped with a terminating error after it had created the link. The cmdlet does not overwrite anything. If `-Path` already exists, or if `-Target` is missing or is a folder, the cmdlet reports an error and leaves the file system unchanged. Because all names of a file share the same data, the number of hard links is a property of the file, not of an individual name. Use `Get-NTFSHardLink` to list them, and delete a link with `Remove-Item2` or `Remove-Item`, which removes only that name as long as other names remain. Before 5.0.0, the error for a missing `-Target` said "The target path exist", the opposite of the cause. diff --git a/Tests/Links.Tests.ps1 b/Tests/Links.Tests.ps1 index 8d69a92..43f1d0b 100644 --- a/Tests/Links.Tests.ps1 +++ b/Tests/Links.Tests.ps1 @@ -9,6 +9,10 @@ param () BeforeDiscovery { Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1') -Force $canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege' + # The administrative share of the drive of the sandboxes reaches them over SMB, like a share of a file server. + $tempPath = [IO.Path]::GetTempPath() + $canUseAdminShare = (Test-IsElevated) -and + (Test-Path -LiteralPath ('\\localhost\{0}$\' -f $tempPath.Substring(0, 1)) -ErrorAction SilentlyContinue) } BeforeAll { @@ -17,6 +21,13 @@ BeforeAll { Import-Module -Name $modulePath -Force -ErrorAction Stop $sandbox = New-TestSandbox -Name 'Links' Push-Location -LiteralPath $sandbox + + function ConvertTo-AdminSharePath { + # The path of a sandbox item on the administrative share of its drive, such as \\localhost\C$\... + param ([string] $Path) + + '\\localhost\{0}${1}' -f $Path.Substring(0, 1), $Path.Substring(2) + } } AfterAll { @@ -110,6 +121,21 @@ Describe 'New-NTFSHardLink' { $link | Should -Not -Exist } + + # Windows can't list the names of a file on a network share. Before 5.0.0-rc6, the cmdlet stopped with a + # terminating error after it had created the link. + It 'Should create the link on a network share and write an error for -PassThru, which cannot list the names there' -Skip:(-not $canUseAdminShare) { + $target = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareTarget' + $link = Join-Path -Path $sandbox -ChildPath 'ShareLink.txt' + Assert-TestSandboxPath -Sandbox $sandbox -Path $link + + $result = @(New-NTFSHardLink -Path (ConvertTo-AdminSharePath -Path $link) -Target (ConvertTo-AdminSharePath -Path $target) -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue) + + $link | Should -Exist + $linkErrors | Should -HaveCount 1 + $linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' + $result | Should -BeNullOrEmpty + } } Describe 'Get-NTFSHardLink' { @@ -159,6 +185,35 @@ Describe 'Get-NTFSHardLink' { $linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*' $result.FullName | Should -Be $file } + + # Before 5.0.0-rc6, a folder stopped the cmdlet with a terminating error, so that it skipped the remaining paths. + It 'Should write an error for a folder and continue with the next path' { + $folder = New-TestSandboxItem -Sandbox $sandbox -Name 'HardLinkFolder' -Directory + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'AfterFolder' + + $result = @(Get-NTFSHardLink -Path $folder, $file -ErrorVariable linkErrors -ErrorAction SilentlyContinue) + + $linkErrors | Should -HaveCount 1 + $linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' + $linkErrors[0].TargetObject | Should -Be $folder + $linkErrors[0].Exception.Message | Should -Be 'The item must be a file' + $result.FullName | Should -Be $file + } + + # Windows can't list the names of a file on a network share. Before 5.0.0-rc6, the cmdlet stopped with a + # terminating error, so that it skipped the remaining paths. + It 'Should write an error for a file on a network share and continue with the next path' -Skip:(-not $canUseAdminShare) { + $file = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareFile' + $other = New-TestSandboxItem -Sandbox $sandbox -Name 'AfterShare' + $sharePath = ConvertTo-AdminSharePath -Path $file + + $result = @(Get-NTFSHardLink -Path $sharePath, $other -ErrorVariable linkErrors -ErrorAction SilentlyContinue) + + $linkErrors | Should -HaveCount 1 + $linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*' + $linkErrors[0].TargetObject | Should -Be $sharePath + $result.FullName | Should -Be $other + } } Describe 'New-NTFSSymbolicLink' {