-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
mkdir: Fix stack overflow with deeply nested directories #8947
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
365711a
cae4988
8dbd99f
3bf5ab6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,7 @@ dequeue | |
dev | ||
devs | ||
discoverability | ||
dotdot | ||
duplicative | ||
dsync | ||
endianness | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -216,7 +216,8 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> { | |||||||||||||||||||
Ok(()) | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Return true if the directory at `path` has been created by this call. | ||||||||||||||||||||
// Create a directory at the given path. | ||||||||||||||||||||
// Uses iterative approach instead of recursion to avoid stack overflow with deep nesting. | ||||||||||||||||||||
// `is_parent` argument is not used on windows | ||||||||||||||||||||
#[allow(unused_variables)] | ||||||||||||||||||||
Comment on lines
221
to
222
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment should be moved to
Suggested change
|
||||||||||||||||||||
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { | ||||||||||||||||||||
|
@@ -231,15 +232,52 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { | |||||||||||||||||||
return Ok(()); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Iterative implementation: collect all directories to create, then create them | ||||||||||||||||||||
// This avoids stack overflow with deeply nested directories | ||||||||||||||||||||
if config.recursive { | ||||||||||||||||||||
match path.parent() { | ||||||||||||||||||||
Some(p) => create_dir(p, true, config)?, | ||||||||||||||||||||
None => { | ||||||||||||||||||||
USimpleError::new(1, translate!("mkdir-error-failed-to-create-tree")); | ||||||||||||||||||||
// Collect all parent directories that need to be created | ||||||||||||||||||||
let mut dirs_to_create = Vec::new(); | ||||||||||||||||||||
let mut current = path; | ||||||||||||||||||||
|
||||||||||||||||||||
// Walk up the tree collecting non-existent directories | ||||||||||||||||||||
while let Some(parent) = current.parent() { | ||||||||||||||||||||
if parent == Path::new("") { | ||||||||||||||||||||
break; | ||||||||||||||||||||
} | ||||||||||||||||||||
// Only check exists() for paths without ".." to avoid false positives | ||||||||||||||||||||
// ("test_dir/.." may return true even when the logical parent doesn't exist) | ||||||||||||||||||||
// Use as_os_str() to avoid string allocation which can use stack space | ||||||||||||||||||||
let has_dotdot = parent | ||||||||||||||||||||
.as_os_str() | ||||||||||||||||||||
.as_encoded_bytes() | ||||||||||||||||||||
.windows(2) | ||||||||||||||||||||
.any(|w| w == b".."); | ||||||||||||||||||||
Comment on lines
+250
to
+254
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be clearer to use
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
if !has_dotdot && parent.exists() { | ||||||||||||||||||||
break; | ||||||||||||||||||||
} | ||||||||||||||||||||
dirs_to_create.push(parent); | ||||||||||||||||||||
current = parent; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Reverse to create from root to leaf | ||||||||||||||||||||
dirs_to_create.reverse(); | ||||||||||||||||||||
|
||||||||||||||||||||
// Create each parent directory | ||||||||||||||||||||
for dir in dirs_to_create { | ||||||||||||||||||||
create_single_dir(dir, true, config)?; | ||||||||||||||||||||
} | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Create the target directory | ||||||||||||||||||||
create_single_dir(path, is_parent, config) | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Helper function to create a single directory with appropriate permissions | ||||||||||||||||||||
#[allow(unused_variables)] | ||||||||||||||||||||
fn create_single_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { | ||||||||||||||||||||
let path_exists = path.exists(); | ||||||||||||||||||||
|
||||||||||||||||||||
match std::fs::create_dir(path) { | ||||||||||||||||||||
Ok(()) => { | ||||||||||||||||||||
if config.verbose { | ||||||||||||||||||||
|
@@ -287,7 +325,24 @@ fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> { | |||||||||||||||||||
Ok(()) | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
Err(_) if path.is_dir() => Ok(()), | ||||||||||||||||||||
Err(_) if path.is_dir() => { | ||||||||||||||||||||
// Directory already exists - still need to print verbose message if requested | ||||||||||||||||||||
// in recursive mode, but only for logical directory names, not parent references | ||||||||||||||||||||
// (i.e., paths ending in ".." like "test_dir/.." shouldn't print) | ||||||||||||||||||||
let ends_with_dotdot = path | ||||||||||||||||||||
.components() | ||||||||||||||||||||
.next_back() | ||||||||||||||||||||
.map(|c| matches!(c, std::path::Component::ParentDir)) | ||||||||||||||||||||
.unwrap_or(false); | ||||||||||||||||||||
Comment on lines
+332
to
+336
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can re-arrange things a bit and get rid of the
Suggested change
|
||||||||||||||||||||
|
||||||||||||||||||||
if config.verbose && is_parent && config.recursive && !ends_with_dotdot { | ||||||||||||||||||||
println!( | ||||||||||||||||||||
"{}", | ||||||||||||||||||||
translate!("mkdir-verbose-created-directory", "util_name" => uucore::util_name(), "path" => path.quote()) | ||||||||||||||||||||
); | ||||||||||||||||||||
} | ||||||||||||||||||||
Ok(()) | ||||||||||||||||||||
} | ||||||||||||||||||||
Err(e) => Err(e.into()), | ||||||||||||||||||||
} | ||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -430,3 +430,28 @@ fn test_selinux_invalid() { | |||||||||||||||||||||||
// invalid context, so, no directory | ||||||||||||||||||||||||
assert!(!at.dir_exists(dest)); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||
fn test_mkdir_deep_nesting() { | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this test supposed to fail without the changes made to |
||||||||||||||||||||||||
// Regression test for stack overflow with deeply nested directories. | ||||||||||||||||||||||||
// The iterative implementation should handle arbitrary depth without stack overflow. | ||||||||||||||||||||||||
let scene = TestScenario::new(util_name!()); | ||||||||||||||||||||||||
let at = &scene.fixtures; | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Create a path with 350 levels of nesting | ||||||||||||||||||||||||
let depth = 350; | ||||||||||||||||||||||||
let dir_name = "d"; | ||||||||||||||||||||||||
let mut path = String::new(); | ||||||||||||||||||||||||
for i in 0..depth { | ||||||||||||||||||||||||
if i > 0 { | ||||||||||||||||||||||||
path.push('/'); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
path.push_str(dir_name); | ||||||||||||||||||||||||
} | ||||||||||||||||||||||||
Comment on lines
+444
to
+450
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can omit the condition if you use a
Suggested change
Or you could use a functional approach: let path = Path::new(dir_name).join(std::iter::repeat(dir_name).take(depth).collect::<PathBuf>()); |
||||||||||||||||||||||||
|
||||||||||||||||||||||||
// This should succeed without stack overflow | ||||||||||||||||||||||||
scene.ucmd().arg("-p").arg(&path).succeeds(); | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
// Verify the deepest directory exists | ||||||||||||||||||||||||
assert!(at.dir_exists(&path)); | ||||||||||||||||||||||||
} |
There was a problem hiding this comment.
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