Skip to content

Commit c6820a1

Browse files
authored
Fix File.cp_r/3 infinite loop with symlink cycles (#15152)
When `dereference_symlinks: true` was set and there was a symlink cycle (e.g., a -> b -> a), the function would infinitely recurse. This fix tracks visited paths using a `MapSet` and returns `{:error, :eloop, path}` when a cycle is detected.
1 parent 93734a5 commit c6820a1

2 files changed

Lines changed: 41 additions & 6 deletions

File tree

lib/elixir/lib/file.ex

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,7 +1199,9 @@ defmodule File do
11991199
if source_parts != dest_parts and List.starts_with?(dest_parts, source_parts) do
12001200
{:error, :einval, destination}
12011201
else
1202-
case do_cp_r(source, destination, on_conflict, dereference?, []) do
1202+
dereference = if dereference?, do: MapSet.new(), else: nil
1203+
1204+
case do_cp_r(source, destination, on_conflict, dereference, []) do
12031205
{:error, _, _} = error -> error
12041206
res -> {:ok, res}
12051207
end
@@ -1240,7 +1242,7 @@ defmodule File do
12401242
end
12411243
end
12421244

1243-
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
1245+
defp do_cp_r(src, dest, on_conflict, dereference, acc) when is_list(acc) do
12441246
case :elixir_utils.read_link_type(src) do
12451247
{:ok, :regular} ->
12461248
case do_cp_file(src, dest, on_conflict, acc) do
@@ -1253,8 +1255,15 @@ defmodule File do
12531255

12541256
{:ok, :symlink} ->
12551257
case :file.read_link(src) do
1256-
{:ok, link} when dereference? ->
1257-
do_cp_r(Path.expand(link, Path.dirname(src)), dest, on_conflict, dereference?, acc)
1258+
{:ok, link} when dereference != nil ->
1259+
resolved = Path.expand(link, Path.dirname(src))
1260+
1261+
if MapSet.member?(dereference, resolved) do
1262+
{:error, :eloop, src}
1263+
else
1264+
dereference = MapSet.put(dereference, resolved)
1265+
do_cp_r(resolved, dest, on_conflict, dereference, acc)
1266+
end
12581267

12591268
{:ok, link} ->
12601269
do_cp_link(link, src, dest, on_conflict, acc)
@@ -1268,8 +1277,17 @@ defmodule File do
12681277
{:ok, files} ->
12691278
case mkdir(dest) do
12701279
success when success in [:ok, {:error, :eexist}] ->
1271-
Enum.reduce(files, [dest | acc], fn x, acc ->
1272-
do_cp_r(Path.join(src, x), Path.join(dest, x), on_conflict, dereference?, acc)
1280+
Enum.reduce_while(files, [dest | acc], fn x, acc ->
1281+
case do_cp_r(
1282+
Path.join(src, x),
1283+
Path.join(dest, x),
1284+
on_conflict,
1285+
dereference,
1286+
acc
1287+
) do
1288+
{:error, _, _} = error -> {:halt, error}
1289+
acc -> {:cont, acc}
1290+
end
12731291
end)
12741292

12751293
{:error, reason} ->

lib/elixir/test/elixir/file_test.exs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,23 @@ defmodule FileTest do
678678
end
679679
end
680680

681+
@tag :unix
682+
test "cp_r with dereference symlink cycle returns eloop error" do
683+
src = tmp_path("tmp/src")
684+
dest = tmp_path("tmp/dest")
685+
686+
File.mkdir_p!(src)
687+
:ok = :file.make_symlink(Path.join(src, "b"), Path.join(src, "a"))
688+
:ok = :file.make_symlink(Path.join(src, "a"), Path.join(src, "b"))
689+
690+
try do
691+
assert {:error, :eloop, _} = File.cp_r(src, dest, dereference_symlinks: true)
692+
after
693+
File.rm_rf(src)
694+
File.rm_rf(dest)
695+
end
696+
end
697+
681698
test "cp_r with dir and file conflict" do
682699
src = fixture_path("cp_r")
683700
dest = tmp_path("tmp")

0 commit comments

Comments
 (0)