Skip to content

du, chmod, perms: avoid stack overflow on deep directory traversal - #14887

Open
vpashaiev wants to merge 1 commit into
uutils:mainfrom
vpashaiev:fix-deep-traversal-stack-overflow
Open

vpashaiev wants to merge 1 commit into
uutils:mainfrom
vpashaiev:fix-deep-traversal-stack-overflow

Conversation

@vpashaiev

Copy link
Copy Markdown
Contributor

Replace recursive directory traversal in safe_du (du), safe_traverse_dir (chmod), and safe_traverse_dir (perms, used by chown and chgrp) with an iterative heap-allocated stack.

Problem

When traversing deep directory trees (depth >= 2000), recursive function calls cause a stack overflow and crash with SIGSEGV (signal 11, code 139) because Linux has an 8MB default stack limit (ulimit -s 8192). Each stack frame holds open directory file descriptors (DirFd), Stat/Metadata, and path buffers.

Fix

  • Replaced recursion in safe_du with an iterative loop using a heap-allocated Vec<DuFrame> stack.
  • Replaced recursion in safe_traverse_dir (chmod) with an iterative loop using Vec<ChmodFrame>, maintaining cycle detection via ancestors.
  • Replaced recursion in safe_traverse_dir (perms, powering chown and chgrp) with an iterative loop using Vec<PermsFrame>, maintaining cycle detection via ancestors.
  • Call stack remains O(1) regardless of directory depth.
  • Directory descriptors (DirFd) are closed as soon as a directory frame completes and pops from the stack.

Verified on Linux with test directory trees created via openat/mkdirat up to 4000 levels deep:

  • du -s passes with code 0 (previously crashed with SIGSEGV at depth 2000 and 4000).
  • chmod -R passes with code 0.
  • chown -R and chgrp -R pass with code 0.
  • All existing unit tests pass.

Fixes #10173
Fixes: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2167204

@anastygnome

Copy link
Copy Markdown
Contributor

Please add a test

@codspeed

codspeed Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 4.57%

2 warnings, 1 note

⚠️ 122 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

ℹ️ 1 benchmark has no flamegraph

The flamegraph wasn't generated: most of the time was spent in runtime internals, which would leave nothing useful to analyze.

See affected benchmarks

⚡ 2 improved benchmarks
❌ 2 regressed benchmarks
✅ 397 untouched benchmarks
⏩ 70 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
❌ Memory du_deep_tree[(100, 3)] 76.5 KB 100.6 KB -23.97%
❌ CPU Simulation mv_single_file 146.3 ms 151.1 ms -3.15%
⚡ CPU Simulation du_deep_tree[(100, 3)] 1.9 ms 1.8 ms +7.87%
⚡ CPU Simulation du_summarize_balanced_tree[(5, 4, 10)] 11.5 ms 11 ms +4.41%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing vpashaiev:fix-deep-traversal-stack-overflow (2728039) with main (1d6ca65)2

Open in CodSpeed

Footnotes

  1. 70 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (24f90f5) during the generation of this report, so 1d6ca65 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@vpashaiev

vpashaiev commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Added regression test test_deep_directory_traversal_no_stack_overflow in tests/by-util/test_du.rs verifying nested directory traversal up to 1500 levels without stack overflow.

Also moved S_IFMT, S_IFDIR, S_IFLNK constants to module level in du.rs to resolve the clippy items-after-statements warning, and formatted changed files via cargo fmt.

@vpashaiev
vpashaiev force-pushed the fix-deep-traversal-stack-overflow branch from d664578 to 0b0b191 Compare September 27, 2026 14:46
@vpashaiev

vpashaiev commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor Author

Adjusted the regression test depth to 400 so it stays safely within the default file descriptor limit (RLIMIT_NOFILE = 1024) on WSL2 runners (depth 1500 hit EMFILE). All tests should pass cleanly now.

Comment thread src/uucore/src/lib/features/perms.rs Outdated
} else {
// Report the successful ownership change using the shared helper
self.report_ownership_change_success(&entry_path, meta.uid(), meta.gid());
return;

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.

before, this return only left the current directory. Now it stops the whole traversal. Is this intended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not intended. Fixed, now popping the current frame and continuing.

Comment thread src/uucore/src/lib/features/perms.rs Outdated
*ret = 1;
if self.verbosity.level != VerbosityLevel::Silent {
show_error!(
"cannot read directory {}: {}",

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.

please use translate!() here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, switched to translate!().

Comment thread src/uu/du/src/du.rs Outdated
top.my_stat.inodes += this_stat.inodes;
top.my_stat.latest_time =
match (top.my_stat.latest_time, this_stat.latest_time) {
(Some(a), Some(b)) => Some(a.max(b)),

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.

the same size/blocks/inodes/latest_time merge is now there 4 times, could you please move it into a helper on Stat?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, added merge() helper on Stat.

Comment thread src/uu/du/src/du.rs Outdated

while let Some(top) = stack.last_mut() {
if top.entry_idx < top.entries.len() {
let entry_name = top.entries[top.entry_idx].clone();

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.

codspeed shows an 18% regression. Could you please look at it? Cloning every entry name probably doesn't help, std::mem::take would avoid that

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed. Converted entries to IntoIter to move them directly without cloning.

Comment thread src/uu/chmod/src/chmod.rs Outdated
} else {
// Backtrack so sibling subtrees that legitimately reach the same directory
// (e.g. two symlinks to one dir) are not mistaken for cycles.
let finished = stack.pop().unwrap();

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.

please avoid the unwrap here (same in perms.rs and du.rs). while let Some(top) = stack.last_mut() already guarantees it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed across chmod, perms, and du.

}

#[cfg(target_os = "linux")]
struct PermsFrame {

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.

ChmodFrame and PermsFrame are the same struct, and the loop is almost the same too. Could this be a shared walker in uucore safe_traversal?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The frames are similar, but per-entry logic diverges quite a bit between chmod (symlink handling) and perms (preserve-root, filters, bitmask). I can extract a shared generic walker in a follow-up PR to keep this bugfix focused.

Comment thread src/uu/du/src/du.rs Outdated
}

Ok(my_stat)
unreachable!()

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.

please avoid unreachable!(). Returning at the end of the loop would work

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Returning at the end of the loop now.

Comment thread tests/by-util/test_du.rs Outdated
let mut depth = 0;
// Limit depth to 400 to stay safely below the default RLIMIT_NOFILE (1024)
// on WSL2 and macOS while testing deep traversal without stack overflow.
for _ in 0..400 {

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.

400 levels never overflowed the stack before this fix either, so this test would pass on main.
could you please make it deep enough to crash without the patch (maybe raising RLIMIT_NOFILE in the test)? and please add the same kind of test for chmod -R and chown -R

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.

Alternatively, making the test unix only could be acceptable with 1500, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Marked as unix-only, raised RLIMIT_NOFILE to 4096, set depth to 1500, and added tests for both chmod -R and chown -R.

@vpashaiev

Copy link
Copy Markdown
Contributor Author

Pushed updates addressing the review comments and added tests for chmod/chown.

@vpashaiev

Copy link
Copy Markdown
Contributor Author

Regarding the CodSpeed report:

Most of the reported regressions are in dd, which this PR does not touch (caused by different runner base environments).

The memory increase in du_deep_tree (+35 KB at depth 100) is expected. This PR moves traversal state from the call stack to a heap vector to avoid stack overflow on deep trees. Runtime speed itself did not regress.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

GNU testsuite comparison:

Skipping an intermittent issue tests/date/resolution (passes in this run but fails in the 'main' branch)

Comment thread src/uu/du/src/du.rs Outdated
Comment thread src/uucore/locales/en-US.ftl Outdated
Comment thread src/uucore/src/lib/features/perms.rs Outdated
Comment thread src/uucore/src/lib/features/perms.rs Outdated
Comment thread tests/by-util/test_chmod.rs Outdated
Comment thread tests/by-util/test_du.rs Outdated
@vpashaiev
vpashaiev force-pushed the fix-deep-traversal-stack-overflow branch 2 times, most recently from 4f09184 to c44e5cc Compare October 19, 2026 14:42
@vpashaiev

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

  1. Removed the unused parameters and dead branches in du.
  2. Removed the duplicate localization key in en-US.ftl.
  3. Replaced unwrap_err() with idiomatic match and cleaned up explicit type annotations in perms and chmod.
  4. Deduplicated the test setup across du, chmod, and chown into a common prepare_deep_traversal helper in uutests.
  5. Rebased onto latest main and squashed the branch into a single commit.

@vpashaiev
vpashaiev force-pushed the fix-deep-traversal-stack-overflow branch 5 times, most recently from 614f849 to 98966a7 Compare October 23, 2026 16:06
@vpashaiev
vpashaiev force-pushed the fix-deep-traversal-stack-overflow branch 3 times, most recently from 941747d to ffb4c0c Compare October 25, 2026 11:36
Replace recursive directory traversal in safe_du (du), safe_traverse_dir (chmod), and safe_traverse_dir (perms, used by chown and chgrp) with an iterative heap-allocated stack.

Previously, traversing deep directory structures (depth >= 2000) exhausted the default 8MB thread stack on Linux due to recursive function calls, causing a segmentation fault (core dumped).

Using heap-allocated stacks (DuFrame, ChmodFrame, PermsFrame) keeps the call stack strictly O(1) while preserving exact post-order / pre-order semantics and cycle detection via ancestors.

Fixes: uutils#10173
Fixes: https://bugs.launchpad.net/ubuntu/+source/rust-coreutils/+bug/2167204
@vpashaiev
vpashaiev force-pushed the fix-deep-traversal-stack-overflow branch from ffb4c0c to 2728039 Compare October 25, 2026 13:16

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

du overflows the stack

3 participants