Skip to content

Fix File.cp_r/3 infinite loop when copying into subdirectory of source#15148

Merged
josevalim merged 1 commit intoelixir-lang:mainfrom
pnezis:fix-cp_r-infinite-loop
Mar 8, 2026
Merged

Fix File.cp_r/3 infinite loop when copying into subdirectory of source#15148
josevalim merged 1 commit intoelixir-lang:mainfrom
pnezis:fix-cp_r-infinite-loop

Conversation

@pnezis
Copy link
Copy Markdown
Contributor

@pnezis pnezis commented Mar 7, 2026

When destination is a subdirectory of source, cp_r would enter an infinite loop because the newly created destination directory would be included in the file listing during recursion. This can cause disk exhaustion if large files are present under source.

This adds a validation check that returns {:error, :einval, destination} when the expanded destination path starts with the expanded source path followed by a path separator.

Also document typical error reasons including the new :einval case.

Simple reproduction:

iex> File.mkdir_p!("/tmp/src/subdir")
iex> File.write!("/tmp/src/file.txt", "hello")
iex> File.cp_r("/tmp/src", "/tmp/src/subdir/dest")
{:error, :enametoolong,
"/tmp/src/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/...file.txt"}

Symlinks

Notice symlinks are not resolved by Path.expand, so the issue is still there if you are using a symlinked subdir:

iex> File.mkdir_p!("/tmp/src/subdir")
iex> File.write!("/tmp/src/file.txt", "hello")
iex> File.mkdir_p!("/tmp/outside")
iex> :file.make_symlink("/tmp/src/subdir", "/tmp/outside/link")
iex> File.cp_r("/tmp/src", "/tmp/outside/link/dest")
{:error, :enametoolong,
"/tmp/outside/link/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/subdir/dest/...dest"}
  • I could not find a stdlib elixir/erlang function that resolves symlinks, should we add a realpath ?
  • I have found another issue with infinite symlink cycles, that may handle this. Will handle in another PR.

Comment thread lib/elixir/lib/file.ex
expanded_destination = Path.expand(destination)

if String.starts_with?(expanded_destination, expanded_source <> "/") do
{:error, :einval, destination}
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.

Looks that :einval is the most appropriate here, any better suggestion?

Comment thread lib/elixir/lib/file.ex Outdated
expanded_source = Path.expand(source)
expanded_destination = Path.expand(destination)

if String.starts_with?(expanded_destination, expanded_source <> "/") do
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.

A Path.parent? would be useful here.

@josevalim
Copy link
Copy Markdown
Member

Giveni the symlinks behaviour, I am not sure if it is something we should try to solve codewise or if we should just warn users of the problematic behaviour. In any case, if we do want to check for parent/child relationship, the best is to split call Path.split and then List.start_with?

@pnezis pnezis force-pushed the fix-cp_r-infinite-loop branch from 85a1014 to 06791f8 Compare March 7, 2026 14:15
@pnezis
Copy link
Copy Markdown
Contributor Author

pnezis commented Mar 7, 2026

Pushed the fix with Path.split and List.starts_with, I think it is better to be defensive than just warn, since you can easily end up with a filled disk if by mistake you copy a big dir, to a subdir of it.

@josevalim
Copy link
Copy Markdown
Member

Tests are failing :)

When destination is a subdirectory of source, `cp_r` would enter an
infinite loop because the newly created destination directory would
be included in the file listing during recursion. This can **cause disk
exhaustion** if large files are present under source.

This adds a validation check that returns `{:error, :einval, destination}`
when copying on a subdir of the source path.

Also document typical error reasons including the new `:einval` case.
@pnezis pnezis force-pushed the fix-cp_r-infinite-loop branch from 06791f8 to b6b4d94 Compare March 7, 2026 22:19
@pnezis
Copy link
Copy Markdown
Contributor Author

pnezis commented Mar 7, 2026

Fixed, there was a test for copying src to src, that was failing with the List.starts_with

@josevalim josevalim merged commit dd1f529 into elixir-lang:main Mar 8, 2026
15 checks passed
@josevalim
Copy link
Copy Markdown
Member

💚 💙 💜 💛 ❤️

@pnezis pnezis deleted the fix-cp_r-infinite-loop branch March 11, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants