From b1eeaa2e8eb01a335074e7cb423bfdabe03d2ea4 Mon Sep 17 00:00:00 2001 From: Ben Hillis Date: Tue, 28 Apr 2026 14:41:48 -0700 Subject: [PATCH] Convert EnsureCaseSensitiveDirectoryRecursive to iterative traversal Replace unbounded recursive depth-first traversal with an iterative approach using an explicit stack. A deeply nested directory tree could overflow the thread stack during the recursive calls. The iterative version collects directories in reverse post-order during traversal, then marks them bottom-up (children before parents), preserving the ordering invariant that EnsureCaseSensitiveDirectory relies on for its early-exit optimization. Bug: 61964147, 61974992, 61911308, 61463090 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/windows/common/filesystem.cpp | 93 ++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 32 deletions(-) diff --git a/src/windows/common/filesystem.cpp b/src/windows/common/filesystem.cpp index 59236db8f..a322a63f7 100644 --- a/src/windows/common/filesystem.cpp +++ b/src/windows/common/filesystem.cpp @@ -244,43 +244,82 @@ bool HasReadAccessToDrive(wchar_t drive) void EnsureCaseSensitiveDirectoryRecursive(_In_ HANDLE Directory) { + // Use iterative depth-first traversal with explicit frames to avoid stack overflow on deeply nested + // directory trees. Only O(depth) handles are open at any time — each directory handle is closed once + // all its children have been processed and marked case-sensitive. + + struct Frame + { + wil::unique_hfile OwnedHandle; + HANDLE DirectoryHandle; + std::vector Buffer; + bool Restart; + }; + + std::vector stack; + stack.push_back( + {.OwnedHandle = {}, .DirectoryHandle = Directory, .Buffer = std::vector(sizeof(FILE_ID_BOTH_DIR_INFORMATION) + MAX_PATH), .Restart = true}); + FILE_CASE_SENSITIVE_INFORMATION CaseInfo{}; IO_STATUS_BLOCK IoStatus{}; - std::vector buffer{sizeof(FILE_ID_BOTH_DIR_INFORMATION) + MAX_PATH}; - bool restart = true; - while (true) + while (!stack.empty()) { + auto& frame = stack.back(); + + // + // Enumerate the next entry in this directory. + // + const auto result = NtQueryDirectoryFile( - Directory, + frame.DirectoryHandle, nullptr, nullptr, nullptr, &IoStatus, - buffer.data(), - static_cast(buffer.size()), + frame.Buffer.data(), + static_cast(frame.Buffer.size()), static_cast(FileIdBothDirectoryInformation), true, nullptr, - restart); + frame.Restart); WI_ASSERT(result != STATUS_PENDING); if (result == STATUS_NO_MORE_FILES || result == STATUS_NO_SUCH_FILE) { - break; + // + // Enumeration complete — mark this directory case-sensitive, then pop the frame. + // + // N.B. This is done with a retry because if the NtfsEnableDirCaseSensitivity + // flag was just changed from 3 to 1, NTFS may not have updated its + // behavior yet in which case it will fail with STATUS_DIRECTORY_NOT_EMPTY. + // + + CaseInfo.Flags = FILE_CS_FLAG_CASE_SENSITIVE_DIR; + wsl::shared::retry::RetryWithTimeout( + [&]() { + THROW_IF_NTSTATUS_FAILED(NtSetInformationFile( + frame.DirectoryHandle, &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation)); + }, + std::chrono::milliseconds{100}, + std::chrono::seconds{1}, + []() { return wil::ResultFromCaughtException() == HRESULT_FROM_NT(STATUS_DIRECTORY_NOT_EMPTY); }); + + stack.pop_back(); + continue; } else if (result == STATUS_BUFFER_OVERFLOW) { - buffer.resize(buffer.size() * 2); + frame.Buffer.resize(frame.Buffer.size() * 2); continue; } THROW_IF_NTSTATUS_FAILED(result); - restart = false; + frame.Restart = false; - const auto* information = reinterpret_cast(buffer.data()); + const auto* information = reinterpret_cast(frame.Buffer.data()); // // Only process non-reparse point directories. @@ -303,10 +342,12 @@ void EnsureCaseSensitiveDirectoryRecursive(_In_ HANDLE Directory) } UNICODE_STRING Name{}; - RtlInitUnicodeString(&Name, information->FileName); + Name.Buffer = const_cast(information->FileName); + Name.Length = static_cast(information->FileNameLength); + Name.MaximumLength = Name.Length; auto Child = wsl::windows::common::filesystem::OpenRelativeFile( - Directory, + frame.DirectoryHandle, &Name, (FILE_LIST_DIRECTORY | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE), FILE_OPEN, @@ -315,32 +356,20 @@ void EnsureCaseSensitiveDirectoryRecursive(_In_ HANDLE Directory) THROW_IF_NTSTATUS_FAILED(NtQueryInformationFile(Child.get(), &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation)); // - // Skip if the directory already has the flag. + // If the child directory is not yet case-sensitive, push a new frame to process it. // if (WI_IsFlagClear(CaseInfo.Flags, FILE_CS_FLAG_CASE_SENSITIVE_DIR)) { - EnsureCaseSensitiveDirectoryRecursive(Child.get()); + HANDLE childHandle = Child.get(); + stack.push_back( + {.OwnedHandle = std::move(Child), + .DirectoryHandle = childHandle, + .Buffer = std::vector(sizeof(FILE_ID_BOTH_DIR_INFORMATION) + MAX_PATH), + .Restart = true}); } } } - - // - // After all children are processed, mark the directory case-sensitive. - // - // N.B. This is done with a retry because if the NtfsEnableDirCaseSensitivity - // flag was just changed from 3 to 1, NTFS may not have updated its - // behavior yet in which case it will fail with STATUS_DIRECTORY_NOT_EMPTY. - // - - CaseInfo.Flags = FILE_CS_FLAG_CASE_SENSITIVE_DIR; - wsl::shared::retry::RetryWithTimeout( - [&]() { - THROW_IF_NTSTATUS_FAILED(NtSetInformationFile(Directory, &IoStatus, &CaseInfo, sizeof(CaseInfo), FileCaseSensitiveInformation)); - }, - std::chrono::milliseconds{100}, - std::chrono::seconds{1}, - []() { return wil::ResultFromCaughtException() == HRESULT_FROM_NT(STATUS_DIRECTORY_NOT_EMPTY); }); } void SetDirectoryCaseSensitive(_In_ PCWSTR Path)