commit: d50822c313dbbef2c65f666e003d087defdd9694
parent bc75bb35fac172c021f6caa3ee8624cbc5325c9c
Author: vaartis <vaartis@kotobank.ch>
Date: Mon, 16 Jun 2025 12:20:19 +0000
Merge branch 'bugfix/toctou-mkdir' into 'develop'
backports: Copy mkdir_p TOCTOU fix from elixir PR 14242
See merge request pleroma/pleroma!4320
Diffstat:
21 files changed, 106 insertions(+), 32 deletions(-)
diff --git a/changelog.d/toctou-mkdir.fix b/changelog.d/toctou-mkdir.fix
@@ -0,0 +1 @@
+Backport [Elixir PR 14242](https://github.com/elixir-lang/elixir/pull/14242) fixing racy mkdir and lack of error handling of parent directory creation
+\ No newline at end of file
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
@@ -271,7 +271,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
[config_dir, psql_dir, static_dir, uploads_dir]
|> Enum.reject(&File.exists?/1)
|> Enum.each(fn dir ->
- File.mkdir_p!(dir)
+ Pleroma.Backports.mkdir_p!(dir)
File.chmod!(dir, 0o700)
end)
diff --git a/lib/mix/tasks/pleroma/robots_txt.ex b/lib/mix/tasks/pleroma/robots_txt.ex
@@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.RobotsTxt do
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
if !File.exists?(static_dir) do
- File.mkdir_p!(static_dir)
+ Pleroma.Backports.mkdir_p!(static_dir)
end
robots_txt_path = Path.join(static_dir, "robots.txt")
diff --git a/lib/pleroma/backports.ex b/lib/pleroma/backports.ex
@@ -0,0 +1,72 @@
+# Copyright 2012 Plataformatec
+# Copyright 2021 The Elixir Team
+# SPDX-License-Identifier: Apache-2.0
+
+defmodule Pleroma.Backports do
+ import File, only: [dir?: 1]
+
+ # <https://github.com/elixir-lang/elixir/pull/14242>
+ # To be removed when we require Elixir 1.19
+ @doc """
+ Tries to create the directory `path`.
+
+ Missing parent directories are created. Returns `:ok` if successful, or
+ `{:error, reason}` if an error occurs.
+
+ Typical error reasons are:
+
+ * `:eacces` - missing search or write permissions for the parent
+ directories of `path`
+ * `:enospc` - there is no space left on the device
+ * `:enotdir` - a component of `path` is not a directory
+
+ """
+ @spec mkdir_p(Path.t()) :: :ok | {:error, File.posix() | :badarg}
+ def mkdir_p(path) do
+ do_mkdir_p(IO.chardata_to_string(path))
+ end
+
+ defp do_mkdir_p("/") do
+ :ok
+ end
+
+ defp do_mkdir_p(path) do
+ parent = Path.dirname(path)
+
+ if parent == path do
+ :ok
+ else
+ case do_mkdir_p(parent) do
+ :ok ->
+ case :file.make_dir(path) do
+ {:error, :eexist} ->
+ if dir?(path), do: :ok, else: {:error, :enotdir}
+
+ other ->
+ other
+ end
+
+ e ->
+ e
+ end
+ end
+ end
+
+ @doc """
+ Same as `mkdir_p/1`, but raises a `File.Error` exception in case of failure.
+ Otherwise `:ok`.
+ """
+ @spec mkdir_p!(Path.t()) :: :ok
+ def mkdir_p!(path) do
+ case mkdir_p(path) do
+ :ok ->
+ :ok
+
+ {:error, reason} ->
+ raise File.Error,
+ reason: reason,
+ action: "make directory (with -p)",
+ path: IO.chardata_to_string(path)
+ end
+ end
+end
diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex
@@ -488,7 +488,7 @@ defmodule Pleroma.Emoji.Pack do
with true <- String.contains?(file_path, "/"),
path <- Path.dirname(file_path),
false <- File.exists?(path) do
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
end
end
@@ -536,7 +536,7 @@ defmodule Pleroma.Emoji.Pack do
emoji_path = emoji_path()
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
- with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
+ with {:create_dir, :ok} <- {:create_dir, Pleroma.Backports.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
{:ok, Enum.sort(results)}
else
@@ -561,7 +561,7 @@ defmodule Pleroma.Emoji.Pack do
end
defp unzip(archive, pack_info, remote_pack, local_pack) do
- with :ok <- File.mkdir_p!(local_pack.path) do
+ with :ok <- Pleroma.Backports.mkdir_p!(local_pack.path) do
files = Enum.map(remote_pack["files"], fn {_, path} -> path end)
# Fallback cannot contain a pack.json file
files = if pack_info[:fallback], do: files, else: ["pack.json" | files]
diff --git a/lib/pleroma/frontend.ex b/lib/pleroma/frontend.ex
@@ -66,7 +66,7 @@ defmodule Pleroma.Frontend do
def unzip(zip, dest) do
File.rm_rf!(dest)
- File.mkdir_p!(dest)
+ Pleroma.Backports.mkdir_p!(dest)
case Pleroma.SafeZip.unzip_data(zip, dest) do
{:ok, _} -> :ok
@@ -90,7 +90,7 @@ defmodule Pleroma.Frontend do
defp install_frontend(frontend_info, source, dest) do
from = frontend_info["build_dir"] || "dist"
File.rm_rf!(dest)
- File.mkdir_p!(dest)
+ Pleroma.Backports.mkdir_p!(dest)
File.cp_r!(Path.join([source, from]), dest)
:ok
end
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex
@@ -19,7 +19,7 @@ defmodule Pleroma.Uploaders.Local do
[file | folders] ->
path = Path.join([upload_path()] ++ Enum.reverse(folders))
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
{path, file}
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
@@ -193,7 +193,7 @@ defmodule Pleroma.User.Backup do
backup = Repo.preload(backup, :user)
tempfile = Path.join([backup.tempdir, backup.file_name])
- with {_, :ok} <- {:mkdir, File.mkdir_p(backup.tempdir)},
+ with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)},
{_, :ok} <- {:actor, actor(backup.tempdir, backup.user)},
{_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)},
{_, :ok} <- {:likes, likes(backup.tempdir, backup.user)},
diff --git a/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex b/lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
@@ -87,7 +87,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
- File.mkdir_p(emoji_dir_path)
+ Pleroma.Backports.mkdir_p(emoji_dir_path)
new_emojis =
foreign_emojis
diff --git a/lib/pleroma/web/instance_document.ex b/lib/pleroma/web/instance_document.ex
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.InstanceDocument do
defp put_file(origin_path, destination_path) do
with destination <- instance_static_dir(destination_path),
- {_, :ok} <- {:mkdir_p, File.mkdir_p(Path.dirname(destination))},
+ {_, :ok} <- {:mkdir_p, Pleroma.Backports.mkdir_p(Path.dirname(destination))},
{_, {:ok, _}} <- {:copy, File.copy(origin_path, destination)} do
:ok
else
diff --git a/test/mix/tasks/pleroma/frontend_test.exs b/test/mix/tasks/pleroma/frontend_test.exs
@@ -11,7 +11,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
@dir "test/frontend_static_test"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
clear_config([:instance, :static_dir], @dir)
on_exit(fn ->
@@ -50,7 +50,7 @@ defmodule Mix.Tasks.Pleroma.FrontendTest do
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
previously_existing = Path.join([folder, "temp"])
- File.mkdir_p!(folder)
+ Pleroma.Backports.mkdir_p!(folder)
File.write!(previously_existing, "yey")
assert File.exists?(previously_existing)
diff --git a/test/mix/tasks/pleroma/instance_test.exs b/test/mix/tasks/pleroma/instance_test.exs
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Pleroma.InstanceTest do
use Pleroma.DataCase
setup do
- File.mkdir_p!(tmp_path())
+ Pleroma.Backports.mkdir_p!(tmp_path())
on_exit(fn ->
File.rm_rf(tmp_path())
diff --git a/test/mix/tasks/pleroma/uploads_test.exs b/test/mix/tasks/pleroma/uploads_test.exs
@@ -62,7 +62,7 @@ defmodule Mix.Tasks.Pleroma.UploadsTest do
upload_dir = Config.get([Pleroma.Uploaders.Local, :uploads])
if not File.exists?(upload_dir) || File.ls!(upload_dir) == [] do
- File.mkdir_p(upload_dir)
+ Pleroma.Backports.mkdir_p(upload_dir)
Path.join([upload_dir, "file.txt"])
|> File.touch()
diff --git a/test/pleroma/emoji/pack_test.exs b/test/pleroma/emoji/pack_test.exs
@@ -58,7 +58,7 @@ defmodule Pleroma.Emoji.PackTest do
test "skips existing emojis when adding from zip file", %{pack: pack} do
# First, let's create a test pack with a "bear" emoji
test_pack_path = Path.join(@emoji_path, "test_bear_pack")
- File.mkdir_p(test_pack_path)
+ Pleroma.Backports.mkdir_p(test_pack_path)
# Create a pack.json file
File.write!(Path.join(test_pack_path, "pack.json"), """
diff --git a/test/pleroma/frontend_test.exs b/test/pleroma/frontend_test.exs
@@ -9,7 +9,7 @@ defmodule Pleroma.FrontendTest do
@dir "test/frontend_static_test"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
clear_config([:instance, :static_dir], @dir)
on_exit(fn ->
@@ -46,7 +46,7 @@ defmodule Pleroma.FrontendTest do
folder = Path.join([@dir, "frontends", "pleroma", "fantasy"])
previously_existing = Path.join([folder, "temp"])
- File.mkdir_p!(folder)
+ Pleroma.Backports.mkdir_p!(folder)
File.write!(previously_existing, "yey")
assert File.exists?(previously_existing)
diff --git a/test/pleroma/object_test.exs b/test/pleroma/object_test.exs
@@ -156,7 +156,7 @@ defmodule Pleroma.ObjectTest do
uploads_dir = Pleroma.Config.get!([Pleroma.Uploaders.Local, :uploads])
- File.mkdir_p!(uploads_dir)
+ Pleroma.Backports.mkdir_p!(uploads_dir)
file = %Plug.Upload{
content_type: "image/jpeg",
diff --git a/test/pleroma/safe_zip_test.exs b/test/pleroma/safe_zip_test.exs
@@ -9,12 +9,12 @@ defmodule Pleroma.SafeZipTest do
setup do
# Ensure tmp directory exists
- File.mkdir_p!(@tmp_dir)
+ Pleroma.Backports.mkdir_p!(@tmp_dir)
on_exit(fn ->
# Clean up any files created during tests
File.rm_rf!(@tmp_dir)
- File.mkdir_p!(@tmp_dir)
+ Pleroma.Backports.mkdir_p!(@tmp_dir)
end)
:ok
@@ -89,7 +89,7 @@ defmodule Pleroma.SafeZipTest do
# For this test, we'll manually check if the file exists in the archive
# by extracting it and verifying it exists
extract_dir = Path.join(@tmp_dir, "extract_check")
- File.mkdir_p!(extract_dir)
+ Pleroma.Backports.mkdir_p!(extract_dir)
{:ok, files} = SafeZip.unzip_file(zip_path, extract_dir)
# Verify the root file was extracted
@@ -145,7 +145,7 @@ defmodule Pleroma.SafeZipTest do
test "can create zip with directories" do
# Create a directory structure
dir_path = Path.join(@tmp_dir, "test_dir")
- File.mkdir_p!(dir_path)
+ Pleroma.Backports.mkdir_p!(dir_path)
file_in_dir_path = Path.join(dir_path, "file_in_dir.txt")
File.write!(file_in_dir_path, "file in directory")
@@ -428,7 +428,7 @@ defmodule Pleroma.SafeZipTest do
# Create a directory and a file in it
dir_path = Path.join(@tmp_dir, "file_in_dir")
- File.mkdir_p!(dir_path)
+ Pleroma.Backports.mkdir_p!(dir_path)
file_in_dir_path = Path.join(dir_path, "test_file.txt")
File.write!(file_in_dir_path, "file in directory content")
diff --git a/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs b/test/pleroma/web/admin_api/controllers/frontend_controller_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.AdminAPI.FrontendControllerTest do
setup do
clear_config([:instance, :static_dir], @dir)
- File.mkdir_p!(Pleroma.Frontend.dir())
+ Pleroma.Backports.mkdir_p!(Pleroma.Frontend.dir())
on_exit(fn ->
File.rm_rf(@dir)
diff --git a/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs b/test/pleroma/web/admin_api/controllers/instance_document_controller_test.exs
@@ -10,7 +10,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceDocumentControllerTest do
@default_instance_panel ~s(<p>Welcome to <a href="https://pleroma.social" target="_blank">Pleroma!</a></p>)
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
diff --git a/test/pleroma/web/plugs/frontend_static_plug_test.exs b/test/pleroma/web/plugs/frontend_static_plug_test.exs
@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
@dir "test/tmp/instance_static"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/")
@@ -52,7 +52,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :admin], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/pleroma/admin/")
@@ -67,7 +67,7 @@ defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!("#{path}/proxy/rr/ss")
+ Pleroma.Backports.mkdir_p!("#{path}/proxy/rr/ss")
File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image")
ConfigMock
diff --git a/test/pleroma/web/plugs/instance_static_test.exs b/test/pleroma/web/plugs/instance_static_test.exs
@@ -8,7 +8,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
@dir "test/tmp/instance_static"
setup do
- File.mkdir_p!(@dir)
+ Pleroma.Backports.mkdir_p!(@dir)
on_exit(fn -> File.rm_rf(@dir) end)
end
@@ -34,7 +34,7 @@ defmodule Pleroma.Web.Plugs.InstanceStaticTest do
refute html_response(bundled_index, 200) == "from frontend plug"
path = "#{@dir}/frontends/#{name}/#{ref}"
- File.mkdir_p!(path)
+ Pleroma.Backports.mkdir_p!(path)
File.write!("#{path}/index.html", "from frontend plug")
index = get(conn, "/")