Skip to content

File.cp_r! regression when copy from read-only fs #15308

@delitrem

Description

@delitrem

Elixir and Erlang/OTP versions

Erlang/OTP 27 [erts-15.2.7.4] [source] [64-bit] [smp:16:16] [ds:16:16:10] [async-threads:1] [jit:ns]

Elixir 1.20.0-rc.4 (9e4eb3d) (compiled with Erlang/OTP 27)

Operating system

Guix System (NixOS-like Linux distro)

Current behavior

Read-only directories can't be copied recursively because destination directories inherit permissions from the source directory without postpone.

How to reproduce:

% mkdir src-dir
% touch src-dir/test
% chmod -w -R src-dir
% ~/src/elixir/elixir/bin/iex
iex(1)> File.cp_r!("src-dir", "dst-dir")
** (File.CopyError) could not copy recursively from "src-dir" to "dst-dir". src-dir/test: permission denied
    (elixir 1.20.0-rc.4) lib/file.ex:1252: File.cp_r!/3
    iex:1: (file)

As I've mentioned before, I use Guix System (and I'm a maintainer of Erlang and Elixir packages), which is immutable, just like a little bit more popular NixOS, so everything there is in read-only "store", i.e.:

% ls -ld /gnu/store/vwif63v4d5hbna1isr1iaamxqlqjiwdb-erlang-27.3.4.6/lib/erlang/erts-15.2.7.4/bin
dr-xr-xr-x 2 root root 4096 Jan  1  1970 /gnu/store/vwif63v4d5hbna1isr1iaamxqlqjiwdb-erlang-27.3.4.6/lib/erlang/erts-15.2.7.4/bin

Even with fix below applied, some tests fail:

  1) test copy_erts/1 copies to directory (Mix.ReleaseTest)
     test/mix/release_test.exs:664
     ** (File.Error) could not write to file "/home/igor/src/elixir/elixir/lib/mix/tmp/mix_release/_build/dev/rel/demo/erts-15.2.7.4/bin/erl": permission denied
     code: assert copy_erts(release(include_erts: true))
     stacktrace:
       (elixir 1.20.0-rc.4) lib/file.ex:1477: File.write!/3
       (mix 1.20.0-rc.4) lib/mix/release.ex:804: Mix.Release.copy_erts/1
       test/mix/release_test.exs:665: (test)

  2) test copy_app/2 does not copy OTP app if include_erts is false (Mix.ReleaseTest)
     test/mix/release_test.exs:755
     ** (File.Error) could not remove files and directories recursively from "/home/igor/src/elixir/elixir/lib/mix/tmp/mix_release": file already exists
     stacktrace:
       (elixir 1.20.0-rc.4) lib/file.ex:1751: File.rm_rf!/1
       test/mix/release_test.exs:30: Mix.ReleaseTest.__ex_unit_setup_1/1
       test/mix/release_test.exs:7: Mix.ReleaseTest.__ex_unit__/2
...
64) test validates compile_env (Mix.Tasks.ReleaseTest)
     /home/igor/src/elixir/elixir/lib/mix/test/mix/tasks/release_test.exs:542
     ** (EXIT from #PID<0.15884.0>) an exception was raised:
         ** (File.Error) could not write to file "/home/igor/src/elixir/elixir/lib/mix/tmp/Mix.Tasks.ReleaseTest/test validates compile_env/_build/dev/rel/compile_env_config/erts-15.2.7.4/bin/erl": permission denied
             (elixir 1.20.0-rc.4) lib/file.ex:1477: File.write!/3
             (mix 1.20.0-rc.4) lib/mix/release.ex:804: Mix.Release.copy_erts/1
             (mix 1.20.0-rc.4) lib/mix/tasks/release.ex:1391: Mix.Tasks.Release.copy/2
             (elixir 1.20.0-rc.4) lib/task/supervised.ex:105: Task.Supervised.invoke_mfa/2
             (elixir 1.20.0-rc.4) lib/task/supervised.ex:40: Task.Supervised.reply/4
...

Expected behavior

iex(1)> File.cp_r!("src-dir", "dst-dir")
["dst-dir/test", "dst-dir"]

I guess, the problem itself is in lib/elixir/lib/file.ex, function do_cp_r/5. The following changes fix the behaviour:

...
              success when success in [:ok, {:error, :eexist}] ->
                case get_dir_mode(src, dest) do
                  {:ok, dest_fileinfo} ->
                    result =
                      Enum.reduce_while(files, [dest | acc], fn x, acc ->
                        case do_cp_r(
                               Path.join(src, x),
                               Path.join(dest, x),
                               on_conflict,
                               dereference,
                               acc
                             ) do
                          {:error, _, _} = error -> {:halt, error}
                          acc -> {:cont, acc}
                        end
                      end)

                    write_stat(dest, dest_fileinfo)
                    result
...
  defp get_dir_mode(src, dest) do
    with {:ok, dest_fileinfo} <- stat(dest),
         {:ok, src_fileinfo} <- stat(src) do
      {:ok, %{dest_fileinfo | mode: src_fileinfo.mode}}
    end
  end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions