Skip to content

Handle STATUS_OBJECT_NAME_NOT_FOUND in Windows directory enumeration#130196

Open
jeffhandley wants to merge 1 commit into
dotnet:mainfrom
jeffhandley:jeffhandley/directory-getfiles-status-object
Open

Handle STATUS_OBJECT_NAME_NOT_FOUND in Windows directory enumeration#130196
jeffhandley wants to merge 1 commit into
dotnet:mainfrom
jeffhandley:jeffhandley/directory-getfiles-status-object

Conversation

@jeffhandley

@jeffhandley jeffhandley commented Jul 4, 2026

Copy link
Copy Markdown
Member

Fixes #130134

Summary

Directory.GetFiles/Directory.EnumerateFiles (and the DirectoryInfo/Directory.GetFileSystemEntries variants) with a search pattern that matches zero files throws System.IO.FileNotFoundException -- naming the directory itself -- on some read-only / virtual file systems, instead of returning an empty result. This was reported on an Azure App Service Windows app deployed with WEBSITE_RUN_FROM_PACKAGE, where the content root is a read-only mounted zip. It is a regression in 10.0.9, introduced by #127974 (backport of #122947 / #123695).

Root cause

#127974 began passing the search pattern to NtQueryDirectoryFile as an OS-level FileName filter. When zero entries match, FileSystemEnumerator<T>.GetData()'s switch only handles STATUS_NO_SUCH_FILE (0xC000000F). Some file system drivers report the same "no matching entries" condition as STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034), which fell through to the default case → ERROR_FILE_NOT_FOUNDthrow.

Evidence that the culprit is STATUS_OBJECT_NAME_NOT_FOUND

The default arm of GetData() turns the raw NTSTATUS into the thrown exception in two hops:

// FileSystemEnumerator.Windows.cs, default case
int error = (int)Interop.NtDll.RtlNtStatusToDosError(status);      // hop 1: NTSTATUS  -> Win32 error
...
throw Win32Marshal.GetExceptionForWin32Error(error, _currentPath); // hop 2: Win32 error -> Exception

Hop 2Win32Marshal.GetExceptionForWin32Error (src/libraries/Common/src/System/IO/Win32Marshal.cs) selects the exception type and message purely from the Win32 error code:

Win32 error Exception Message resource / text
ERROR_FILE_NOT_FOUND (2) FileNotFoundException IO_FileNotFound_FileName = "Could not find file '{0}'."
ERROR_PATH_NOT_FOUND (3) DirectoryNotFoundException IO_PathNotFound_Path = "Could not find a part of the path '{0}'."

The report is System.IO.FileNotFoundException: Could not find file '<dir>' — exactly the ERROR_FILE_NOT_FOUND (2) branch. So the raw NTSTATUS must be one that hop 1 (RtlNtStatusToDosError, an ntdll OS function — the mapping is defined by Windows, not .NET) translates to 2, and that is not already handled by the switch.

RtlNtStatusToDosError translations for the relevant statuses (values verified by calling it directly):

NTSTATUS Value RtlNtStatusToDosError In switch today?
STATUS_SUCCESS 0x00000000 ERROR_SUCCESS (0) handled
STATUS_NO_MORE_FILES 0x80000006 ERROR_NO_MORE_FILES (18) handled
STATUS_NO_SUCH_FILE (aka STATUS_FILE_NOT_FOUND) 0xC000000F ERROR_FILE_NOT_FOUND (2) handled
STATUS_OBJECT_NAME_INVALID 0xC0000033 ERROR_INVALID_NAME (123) handled
STATUS_OBJECT_NAME_NOT_FOUND 0xC0000034 ERROR_FILE_NOT_FOUND (2) NOT handled → throws
STATUS_OBJECT_PATH_NOT_FOUND 0xC000003A ERROR_PATH_NOT_FOUND (3) n/a

Only two NTSTATUS values map to ERROR_FILE_NOT_FOUND (2): STATUS_NO_SUCH_FILE (already handled, so it can't produce the throw) and STATUS_OBJECT_NAME_NOT_FOUND. Therefore 0xC0000034 is the only possible culprit. STATUS_OBJECT_PATH_NOT_FOUND (0xC000003A) is ruled out: it maps to ERROR_PATH_NOT_FOUND (3), which hop 2 turns into a DirectoryNotFoundException with "Could not find a part of the path '{0}'." — a different exception type and message than the report.

Empirically, a direct NtQueryDirectoryFile P/Invoke with a zero-match filter returns the handled STATUS_NO_SUCH_FILE (0xC000000F) on NTFS, ReFS, UDF, and CDFS — so the built-in drivers don't hit this, and it takes a driver that returns 0xC0000034 (as reported for the read-only zip mount) to trigger it. The affected file systems do honor the filter -- a matching pattern returns results while only the zero-match case reports 0xC0000034 -- so the OS-level filtering added by #127974 is working; the runtime simply needs to treat this status as "no matches."

Change

Add case Interop.StatusOptions.STATUS_OBJECT_NAME_NOT_FOUND to the existing no-matches branch in GetData() (Windows), alongside STATUS_FILE_NOT_FOUND. The directory handle was already opened successfully in Init(), so this status can only mean the filter matched nothing -- consistent with how the sibling STATUS_FILE_NOT_FOUND / STATUS_OBJECT_NAME_INVALID statuses are already handled. The STATUS_OBJECT_NAME_NOT_FOUND constant already exists in Interop.NtStatus.cs.

Testing

Reproducing this requires a file system driver that returns 0xC0000034 for a zero-match filtered NtQueryDirectoryFile; the built-in drivers (NTFS, ReFS, UDF, CDFS) all return the already-handled 0xC000000F, so there is no in-repo regression test. The fix was validated out-of-band by hooking ntdll!NtQueryDirectoryFile in-process to rewrite the zero-match status 0xC000000F -> 0xC0000034 for a single directory, faithfully mirroring the reported driver behavior:

  • The zero-match Directory.GetFiles(dir, "__no_such_file__*.dll") probe throws on the shipped 10.0.9 runtime and on an unmodified build of this branch, and returns an empty array with this fix, while a matching pattern (*.dll) continues to return results in all cases.
  • Confirmed across dotnet-installed 10.0.7 / 10.0.8 (return empty; pre-regression), 10.0.9 (throws), and this branch (throws without the fix, empty with it) on both x64 and Arm64.
  • Full System.IO.FileSystem.Tests shows no regressions from this change.

Alternatives considered

Two alternative approaches were prototyped and rejected (prototype PRs on my fork: error-mapping, unfiltered-fallback):

  • Treat any ERROR_FILE_NOT_FOUND in the default arm as no-matches -- broader than necessary; keys off the translated Win32 error rather than the specific status, so it could absorb a file-not-found produced for an unrelated reason.
  • Fall back to an unfiltered enumeration on 0xC0000034 -- because the affected drivers honor the filter, this would discard the driver's result and re-enumerate every entry into managed code for the zero-match case, reintroducing the O(all-entries) scan that [release/10.0] Optimize Directory.GetFiles by passing safe patterns to NtQueryDirectoryFile #127974 set out to eliminate, precisely in the failing scenario (a selective pattern matching nothing in a large directory).

Note

This pull request was authored with assistance from GitHub Copilot.

Add STATUS_OBJECT_NAME_NOT_FOUND (0xC0000034) to the no-matches branch of
FileSystemEnumerator<T>.GetData() on Windows, alongside STATUS_FILE_NOT_FOUND.
A filtered NtQueryDirectoryFile that matches zero entries can return this
status instead of STATUS_NO_SUCH_FILE on some file systems (for example,
certain read-only or virtual file systems); it is now treated as an empty
result instead of throwing FileNotFoundException.

Fixes dotnet#130134

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 4, 2026 08:15
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates Windows directory enumeration in System.Private.CoreLib to treat STATUS_OBJECT_NAME_NOT_FOUND from NtQueryDirectoryFile as a “no matches / end of enumeration” condition (like existing STATUS_FILE_NOT_FOUND handling), avoiding an incorrect FileNotFoundException when an OS-level filter matches zero entries on certain filesystems.

Changes:

  • Handle Interop.StatusOptions.STATUS_OBJECT_NAME_NOT_FOUND in FileSystemEnumerator<TResult>.GetData() as “no results” and finish the directory.

@JeremyKuhne JeremyKuhne left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Interesting. I'll look at the error translations to see what else might come back with the same Win32 error, but the analysis looks pretty thorough.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants