Skip to content

PowerRename: fix STATUS_STACK_BUFFER_OVERRUN crash with 30,000+ files#47401

Draft
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-powerrename-buffer-overflow
Draft

PowerRename: fix STATUS_STACK_BUFFER_OVERRUN crash with 30,000+ files#47401
Copilot wants to merge 3 commits into
mainfrom
copilot/fix-powerrename-buffer-overflow

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 29, 2026

PowerRename crashes with exception 0xc000027b (STATUS_STACK_BUFFER_OVERRUN) when opening with 30,000+ selected files. Four bugs combine to cause this.

Summary of the Pull Request

Four bugs fixed in the PowerRename file-loading pipeline that together cause a hard crash when 30,000+ files are selected.

PR Checklist

  • Communication: I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected
  • Tests: Added/updated and all pass
  • Localization: All end-user-facing strings can be localized
  • Dev docs: Added/updated
  • New binaries: Added on the required places
  • Documentation updated: If checked, please file a pull request on our docs repo and link it here: #xxx

Detailed Description of the Pull Request / Additional comments

Bug 1 — Pipe reading splits paths across chunk boundaries (App.xaml.cpp)

ReadFile was called in 32 KB chunks and each chunk was immediately split on ?. A path straddling two reads produced two invalid fragments in g_files. Fixed with an accumulator that carries leftover bytes across reads; entries are only emitted when a complete ?-delimited token is available. Trailing data without a terminator (sender crashed mid-write) is discarded with a Logger::warn.

// Before — splits on chunk boundary:
std::wstring inputBatch{ chBuf, dwRead / sizeof(wchar_t) };
std::wstringstream ss(inputBatch);
while (std::getline(ss, item, delimiter)) g_files.push_back(item);

// After — accumulates across reads:
accumulated.append(chBuf, dwRead / sizeof(wchar_t));
while ((found = accumulated.find(delimiter, pos)) != npos) {
    if (found > pos) g_files.push_back(accumulated.substr(pos, found - pos));
    pos = found + 1;
}
accumulated = accumulated.substr(pos); // carry remainder

Bug 2 — Null/invalid PIDLs included in shell item array (CreateShellItemArrayFromPaths)

itemsCnt was incremented unconditionally regardless of whether SHParseDisplayName succeeded, so uninitialized PIDLIST_ABSOLUTE slots could be passed to SHCreateShellItemArrayFromIDLists. Also, PIDLs were never freed on the SHCreateShellItemArrayFromIDLists failure path (memory leak). Fixed: only store and count PIDLs on SUCCEEDED(SHParseDisplayName(...)), and free them on both success and failure paths.

Bug 3 — Single SHCreateShellItemArrayFromIDLists call with 35,000+ items (MainWindow.xaml.cpp)

Passing all 35,000 PIDLs in one call triggers internal Windows Shell processing that exhausts the thread stack → STATUS_STACK_BUFFER_OVERRUN. Fixed by processing in batches of 5,000: each batch calls CreateShellItemArrayFromPaths + EnumerateShellItems independently, and all items land in the same CPowerRenameManager.

constexpr size_t BATCH_SIZE = 5000;
for (size_t batchStart = 0; batchStart < totalFiles; batchStart += BATCH_SIZE) {
    auto batch = /* slice g_files[batchStart .. batchStart+BATCH_SIZE] */;
    if (SUCCEEDED(CreateShellItemArrayFromPaths(std::move(batch), &shellItemArray)))
        if (SUCCEEDED(shellItemArray->EnumItems(&enumShellItems)))
            EnumerateShellItems(enumShellItems);
}

Bug 4 — GetFullPathName truncates long paths (CreateShellItemArrayFromPaths)

GetFullPathName was called with a fixed 4096-character stack buffer. When the required path length equals or exceeds BUFSIZE, the function returns the needed size without writing a complete path. The previous code treated any non-zero return as success, silently dropping long-path files. Fixed by re-calling GetFullPathName with a dynamically allocated buffer sized to the required length when the initial call indicates the buffer is too small. Additionally, the HRESULT from a failed allocation (E_OUTOFMEMORY) is now correctly preserved — the "no valid paths" error branch is guarded with SUCCEEDED(hr) so allocation failures are no longer silently overwritten with E_FAIL.

Validation Steps Performed

  • Code review of all four fix sites against the original crash report.
  • Confirmed the accumulator logic correctly handles paths split mid-chunk and emits no duplicate/empty entries.
  • Confirmed itemsCnt only ever indexes valid PIDLIST_ABSOLUTE pointers after the fix.
  • Batch loop verified to cover totalFiles % BATCH_SIZE != 0 (last partial batch) correctly via std::min.
  • Verified that GetFullPathName dynamic retry correctly handles paths exceeding 4096 characters.
  • Verified that E_OUTOFMEMORY from new (std::nothrow) is propagated correctly and not overwritten.

Copilot AI changed the title [WIP] Fix buffer overflow vulnerability in PowerRename PowerRename: fix STATUS_STACK_BUFFER_OVERRUN crash with 30,000+ files Apr 29, 2026
Copilot AI requested a review from MuyuanMS April 29, 2026 09:58
@niels9001 niels9001 added the Product-PowerRename Refers to the PowerRename PowerToy label Apr 29, 2026
@MuyuanMS MuyuanMS requested a review from Copilot May 14, 2026 03:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes a PowerRename crash (STATUS_STACK_BUFFER_OVERRUN) when launching with very large selections (30,000+ files) by hardening the file-ingestion pipeline and avoiding problematic large Shell API calls.

Changes:

  • Updated stdin/pipe parsing to accumulate across ReadFile chunk boundaries so ?-delimited paths aren’t split into invalid fragments.
  • Fixed CreateShellItemArrayFromPaths to only count/store valid PIDLs and to free PIDLs on both success and failure paths.
  • Batched Shell item creation/enumeration in MainWindow to avoid Windows Shell stack exhaustion with huge item arrays.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp Batches shell item enumeration; fixes PIDL counting and frees PIDLs on failure.
src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/App.xaml.cpp Makes pipe parsing delimiter-safe across read chunk boundaries; discards unterminated trailing fragment.

Comment on lines 552 to 556
else if (itemsCnt == 0)
{
Logger::error(L"Parsing path list display names failed.");
Logger::error(L"No valid file paths could be resolved; all entries either do not exist or could not be parsed by the shell.");
hr = E_FAIL;
}
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.

Fixed: Added SUCCEEDED(hr) guard before the itemsCnt == 0 check, so prior allocation failures are preserved instead of being overwritten with E_FAIL.

Comment on lines +520 to +526
const DWORD BUFSIZE = 4096;
TCHAR buffer[BUFSIZE] = TEXT("");
auto retval = GetFullPathName(file.c_str(), BUFSIZE, buffer, NULL);
if (retval != 0 && PathFileExists(buffer))
{
PIDLIST_ABSOLUTE pidl = nullptr;
if (SUCCEEDED(SHParseDisplayName(buffer, nullptr, &pidl, 0, nullptr)))
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.

Fixed: Added dynamic buffer allocation when GetFullPathName indicates the buffer is too small, so long paths are no longer truncated.

…RESULT

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@MuyuanMS
Copy link
Copy Markdown
Contributor

@copilot please re-review the latest changes.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 14, 2026

@copilot please re-review the latest changes.

Reviewed the latest changes in 31d5c8c. Both fixes look correct:

  1. GetFullPathName long-path handling: When needed >= BUFSIZE, a dynamic buffer is allocated with exactly the required size, GetFullPathName is re-called, and the result is validated before use. Short paths still use the stack buffer, so there's no overhead for the common case.

  2. HRESULT preservation: The guard is now else if (SUCCEEDED(hr) && itemsCnt == 0), so an E_OUTOFMEMORY from the initial allocation is propagated correctly instead of being overwritten with E_FAIL.

No further issues found in these changes.

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

Labels

Product-PowerRename Refers to the PowerRename PowerToy

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Buffer Overflow - PowerRename Buffer Overflow (Large Files)

4 participants