Skip to content

Conversation

naoNao89
Copy link
Contributor

@naoNao89 naoNao89 commented Oct 18, 2025

Fixes #8945

mkdir -p with 200+ nested directories? Segfault. The recursive implementation ate stack frames like candy.

Fix
Swapped recursion for iteration. Collect parent directories into a vector (heap), create them root-to-leaf. Constant stack space, infinite depth.

Proof It Works

# Before: Segmentation fault: 11
$ ulimit -s 128
$ ./target/release/coreutils mkdir -p `python3 -c 'print("a/" * 250)'`
Segmentation fault: 11

AFTER THE FIX:

$ ulimit -s 128  
$ ./target/release/coreutils mkdir -p `python3 -c 'print("a/" * 250)'`
(success - directories created)

$ ulimit -s 128
$ ./target/release/coreutils mkdir -p `python3 -c 'print("a/" * 400)'`  
(success - even deeper nesting works)

Our changes touch code originally by 8ccc45c (2022)

Replaced recursive parent directory creation with iterative approach to prevent stack overflow when creating directories with 200+ nesting levels.

The previous recursive implementation consumed one stack frame per directory level, causing crashes on systems with limited stack size. The new implementation collects parent directories into a vector and creates them iteratively, using constant stack space regardless of depth.

All existing functionality preserved: verbose output, permissions, SELinux context, and ACLs.

Includes regression test for 350-level directory nesting.
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/tee (fails in this run but passes in the 'main' branch)

…h .. components

When creating directories with -p -v and paths containing .. components (like test_dir/../test_dir_a), the verbose output wasn't printing messages for logical parent directories that already existed. This caused test_recursive_reporting to fail on Windows.

The fix ensures verbose messages are printed for existing parent directories in recursive mode, but only for logical directory names (not parent references ending in ..). This matches GNU mkdir behavior where all logical path components are reported even if they resolve to existing directories.

Fixes Windows CI test failures for test_mkdir::test_recursive_reporting.
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 18, 2025

CodSpeed Performance Report

Merging #8947 will not alter performance

Comparing naoNao89:fix/mkdir-stack-overflow (3bf5ab6) with main (85a7812)

Summary

✅ 106 untouched
⏩ 73 skipped1

Footnotes

  1. 73 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.

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/timeout/timeout (fails in this run but passes in the 'main' branch)

- Format long method chains across multiple lines
- Remove trailing whitespace
- Use next_back() instead of last() to avoid needless iterator traversal
@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/tee (fails in this run but passes in the 'main' branch)

@github-actions
Copy link

GNU testsuite comparison:

Skip an intermittent issue tests/misc/tee (fails in this run but passes in the 'main' branch)

dev
devs
discoverability
dotdot
Copy link
Contributor

Choose a reason for hiding this comment

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

please ignore it on top of the file
don't need to be global

}

#[test]
fn test_mkdir_deep_nesting() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this test supposed to fail without the changes made to mkdir.rs?

Comment on lines +444 to +450
let mut path = String::new();
for i in 0..depth {
if i > 0 {
path.push('/');
}
path.push_str(dir_name);
}
Copy link
Contributor

@cakebaker cakebaker Oct 19, 2025

Choose a reason for hiding this comment

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

You can omit the condition if you use a PathBuf instead of a String:

Suggested change
let mut path = String::new();
for i in 0..depth {
if i > 0 {
path.push('/');
}
path.push_str(dir_name);
}
let mut path = PathBuf::new();
for _ in 0..depth {
path.push(dir_name);
}

Or you could use a functional approach:

    let path = Path::new(dir_name).join(std::iter::repeat(dir_name).take(depth).collect::<PathBuf>());

Comment on lines +250 to +254
let has_dotdot = parent
.as_os_str()
.as_encoded_bytes()
.windows(2)
.any(|w| w == b"..");
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be clearer to use components():

Suggested change
let has_dotdot = parent
.as_os_str()
.as_encoded_bytes()
.windows(2)
.any(|w| w == b"..");
let has_dotdot = parent.components().any(|c| c.as_os_str() == "..");

Comment on lines 221 to 222
// `is_parent` argument is not used on windows
#[allow(unused_variables)]
Copy link
Contributor

Choose a reason for hiding this comment

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

The comment should be moved to create_single_dir as this function no longer contains any Windows-specific code.

Suggested change
// `is_parent` argument is not used on windows
#[allow(unused_variables)]

Comment on lines +332 to +336
let ends_with_dotdot = path
.components()
.next_back()
.map(|c| matches!(c, std::path::Component::ParentDir))
.unwrap_or(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

You can re-arrange things a bit and get rid of the unwrap_or:

Suggested change
let ends_with_dotdot = path
.components()
.next_back()
.map(|c| matches!(c, std::path::Component::ParentDir))
.unwrap_or(false);
let ends_with_dotdot = matches!(
path.components().next_back(),
Some(std::path::Component::ParentDir)
);

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.

mkdir -p uses unbounded stack space

3 participants