Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package elixir for openSUSE:Factory checked in at 2026-06-10 16:14:11 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/elixir (Old) and /work/SRC/openSUSE:Factory/.elixir.new.2375 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "elixir" Wed Jun 10 16:14:11 2026 rev:48 rq:1358516 version:1.20.1 Changes: -------- --- /work/SRC/openSUSE:Factory/elixir/elixir.changes 2026-06-05 14:58:44.056420832 +0200 +++ /work/SRC/openSUSE:Factory/.elixir.new.2375/elixir.changes 2026-06-10 16:18:26.255585392 +0200 @@ -2 +2,7 @@ -Thu Jun 4 07:29:56 UTC 2026 - Alessio Biancalana <[email protected]> +Wed Jun 10 08:42:34 UTC 2026 - Alessio Biancalana <[email protected]> + +- Upgrade to Elixir 1.20.1: + * Changelog available at https://hexdocs.pm/elixir/1.20.1/changelog.html + +------------------------------------------------------------------- +Thu Jun 4 07:29:56 UTC 2026 - Alessio Biancalana <[email protected]> Old: ---- elixir-1.20.0-doc.zip elixir-1.20.0.tar.gz New: ---- elixir-1.20.1-doc.zip elixir-1.20.1.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ elixir.spec ++++++ --- /var/tmp/diff_new_pack.AKbFkw/_old 2026-06-10 16:18:27.391632471 +0200 +++ /var/tmp/diff_new_pack.AKbFkw/_new 2026-06-10 16:18:27.395632637 +0200 @@ -18,7 +18,7 @@ %define elixirdir %{_prefix}/lib/elixir Name: elixir -Version: 1.20.0 +Version: 1.20.1 Release: 0 Summary: Functional meta-programming aware language built atop Erlang License: Apache-2.0 ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.AKbFkw/_old 2026-06-10 16:18:27.459635289 +0200 +++ /var/tmp/diff_new_pack.AKbFkw/_new 2026-06-10 16:18:27.463635455 +0200 @@ -1,6 +1,6 @@ -mtime: 1780558328 -commit: 1df83241ccf5af42b9acba4f760cdcdb88630521d67cc62256e92853b2dfc7b8 +mtime: 1781083285 +commit: 4c13afaf16a26b18750f65ca479305232b7167320cc27a1c129018bce9657f6a url: https://src.opensuse.org/erlang/elixir -revision: 1df83241ccf5af42b9acba4f760cdcdb88630521d67cc62256e92853b2dfc7b8 +revision: 4c13afaf16a26b18750f65ca479305232b7167320cc27a1c129018bce9657f6a projectscmsync: https://src.opensuse.org/erlang/_ObsPrj.git ++++++ build.specials.obscpio ++++++ ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-06-10 11:21:25.000000000 +0200 @@ -0,0 +1 @@ +.osc ++++++ elixir-1.20.0-doc.zip -> elixir-1.20.1-doc.zip ++++++ /work/SRC/openSUSE:Factory/elixir/elixir-1.20.0-doc.zip /work/SRC/openSUSE:Factory/.elixir.new.2375/elixir-1.20.1-doc.zip differ: char 11, line 1 ++++++ elixir-1.20.0.tar.gz -> elixir-1.20.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/CHANGELOG.md new/elixir-1.20.1/CHANGELOG.md --- old/elixir-1.20.0/CHANGELOG.md 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/CHANGELOG.md 2026-06-09 15:36:40.000000000 +0200 @@ -192,6 +192,27 @@ You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`. +## v1.20.1 (2026-06-09) + +### 1. Security + +#### Elixir + + * [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p) + +### 2. Bug fixes + +#### Elixir + + * [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters + * [Code] Ensure `Code.require_file` releases the file if compilation fails + * [Kernel] Fix documentation generation to use the correct version in search + +#### Mix + + * [mix archive.install] Validate paths and files when extracting archives + * [mix format] Honor `--no-compile` option when loading plugins + ## v1.20.0 (2026-06-03) This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/VERSION new/elixir-1.20.1/VERSION --- old/elixir-1.20.0/VERSION 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/VERSION 2026-06-09 15:36:40.000000000 +0200 @@ -1 +1 @@ -1.20.0 +1.20.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/bin/elixir new/elixir-1.20.1/bin/elixir --- old/elixir-1.20.0/bin/elixir 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/bin/elixir 2026-06-09 15:36:40.000000000 +0200 @@ -6,7 +6,7 @@ set -e -ELIXIR_VERSION=1.20.0 +ELIXIR_VERSION=1.20.1 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <<USAGE >&2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/bin/elixir.bat new/elixir-1.20.1/bin/elixir.bat --- old/elixir-1.20.0/bin/elixir.bat 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/bin/elixir.bat 2026-06-09 15:36:40.000000000 +0200 @@ -4,7 +4,7 @@ :: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2012 Plataformatec -set ELIXIR_VERSION=1.20.0 +set ELIXIR_VERSION=1.20.1 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/lib/calendar.ex new/elixir-1.20.1/lib/elixir/lib/calendar.ex --- old/elixir-1.20.0/lib/elixir/lib/calendar.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/lib/calendar.ex 2026-06-09 15:36:40.000000000 +0200 @@ -3,6 +3,8 @@ # SPDX-FileCopyrightText: 2012 Plataformatec defmodule Calendar do + @strftime_max_width 1024 + @moduledoc """ This module defines the responsibilities for working with calendars, dates, times and datetimes in Elixir. @@ -529,6 +531,7 @@ * `%`: indicates the start of a formatted section * `<padding>`: set the padding (see below) * `<width>`: a number indicating the minimum size of the formatted section + (maximum #{@strftime_max_width}) * `<format>`: the format itself (see below) ### Accepted padding options @@ -667,9 +670,13 @@ end defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do - new_width = (width || 0) * 10 + (digit - ?0) + width = (width || 0) * 10 + (digit - ?0) + + if width > @strftime_max_width do + raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}" + end - parse_modifiers(rest, new_width, pad, parser_data) + parse_modifiers(rest, width, pad, parser_data) end # set default padding if none was specified diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/lib/code/fragment.ex new/elixir-1.20.1/lib/elixir/lib/code/fragment.ex --- old/elixir-1.20.0/lib/elixir/lib/code/fragment.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/lib/code/fragment.ex 2026-06-09 15:36:40.000000000 +0200 @@ -204,6 +204,7 @@ | {:local_arity, charlist} | {:local_call, charlist} | {:anonymous_call, inside_caller} + | {:capture_arg, charlist} | {:module_attribute, charlist} | {:operator, charlist} | {:operator_arity, charlist} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/lib/code.ex new/elixir-1.20.1/lib/elixir/lib/code.ex --- old/elixir-1.20.0/lib/elixir/lib/code.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/lib/code.ex 2026-06-09 15:36:40.000000000 +0200 @@ -1634,13 +1634,19 @@ nil :proceed -> - loaded = - Module.ParallelChecker.verify(fn -> - :elixir_compiler.string(charlist, file, fn _, _ -> :ok end) - end) + try do + loaded = + Module.ParallelChecker.verify(fn -> + :elixir_compiler.string(charlist, file, fn _, _ -> :ok end) + end) - :elixir_code_server.cast({:required, file}) - loaded + :elixir_code_server.cast({:required, file}) + loaded + catch + kind, reason -> + :elixir_code_server.call({:release, file}) + :erlang.raise(kind, reason, __STACKTRACE__) + end end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/lib/uri.ex new/elixir-1.20.1/lib/elixir/lib/uri.ex --- old/elixir-1.20.0/lib/elixir/lib/uri.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/lib/uri.ex 2026-06-09 15:36:40.000000000 +0200 @@ -24,6 +24,12 @@ [scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment] + The fields contain the encoded URI components as they appear in the URI + itself. For example, a slash inside the userinfo must be stored as `%2F`, + not as `/`. Functions such as `parse/1` and `new/1` preserve existing + percent-encoded sequences in those fields, and functions such as `to_string/1` + expects those fields to already be encoded as needed. Whenever setting or + modifying the fields directly, you must encode them accordingly. Note the `authority` field is deprecated. `parse/1` will still populate it for backwards compatibility but you should generally @@ -897,6 +903,9 @@ @doc """ Returns the string representation of the given [URI struct](`t:t/0`). + This function assembles the URI components into a string, assuming each + field is valid and escaped as done by `parse/1` and `new/1`. + ## Examples iex> uri = URI.parse("http://google.com") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/lib/version.ex new/elixir-1.20.1/lib/elixir/lib/version.ex --- old/elixir-1.20.0/lib/elixir/lib/version.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/lib/version.ex 2026-06-09 15:36:40.000000000 +0200 @@ -18,12 +18,16 @@ MAJOR.MINOR.PATCH + Each numeric component is limited to at most 14 digits. + Pre-releases are supported by optionally appending a hyphen and a series of period-separated identifiers immediately following the patch version. Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): "1.0.0-alpha.3" + Numeric pre-release identifiers are also limited to at most 14 digits. + Build information can be added by appending a plus sign and a series of dot-separated identifiers immediately following the patch or pre-release version. Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`): @@ -520,6 +524,8 @@ defmodule Parser do @moduledoc false + @max_numeric_component_digits 14 + operators = [ {">=", :>=}, {"<=", :<=}, @@ -621,7 +627,9 @@ defp require_digits(nil), do: :error defp require_digits(string) do - if leading_zero?(string), do: :error, else: parse_digits(string, "") + if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits, + do: :error, + else: parse_digits(string, "") end defp leading_zero?(<<?0, _, _::binary>>), do: true @@ -649,6 +657,11 @@ end end + defp convert_parts_to_integer([part | rest], acc) + when byte_size(part) > @max_numeric_component_digits do + if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc]) + end + defp convert_parts_to_integer([part | rest], acc) do case parse_digits(part, "") do {:ok, integer} -> @@ -667,6 +680,10 @@ {:ok, Enum.reverse(acc)} end + defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest) + defp all_digits?(<<>>), do: true + defp all_digits?(_other), do: false + defp valid_identifier?(<<char, rest::binary>>) when char in ?0..?9 when char in ?a..?z diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/scripts/elixir_docs.exs new/elixir-1.20.1/lib/elixir/scripts/elixir_docs.exs --- old/elixir-1.20.0/lib/elixir/scripts/elixir_docs.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/scripts/elixir_docs.exs 2026-06-09 15:36:40.000000000 +0200 @@ -3,7 +3,11 @@ # SPDX-FileCopyrightText: 2012 Plataformatec # Returns config for Elixir docs (exclusively) -canonical = System.fetch_env!("CANONICAL") +canonical = + case System.fetch_env!("CANONICAL") do + "" -> System.version() <> "/" + canonical -> canonical + end [ search: [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/scripts/mix_docs.exs new/elixir-1.20.1/lib/elixir/scripts/mix_docs.exs --- old/elixir-1.20.0/lib/elixir/scripts/mix_docs.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/scripts/mix_docs.exs 2026-06-09 15:36:40.000000000 +0200 @@ -2,7 +2,11 @@ # SPDX-FileCopyrightText: 2021 The Elixir Team # Returns config for other apps except Elixir -canonical = System.fetch_env!("CANONICAL") +canonical = + case System.fetch_env!("CANONICAL") do + "" -> System.version() <> "/" + canonical -> canonical + end [ search: [ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/src/elixir_code_server.erl new/elixir-1.20.1/lib/elixir/src/elixir_code_server.erl --- old/elixir-1.20.0/lib/elixir/src/elixir_code_server.erl 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/src/elixir_code_server.erl 2026-06-09 15:36:40.000000000 +0200 @@ -59,6 +59,9 @@ handle_call(required, _From, Config) -> {reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config}; +handle_call({release, Path}, _From, Config) -> + {reply, ok, release(Path, Config)}; + handle_call(retrieve_compiler_module, _From, Config) -> case Config#elixir_code_server.mod_pool of {Used, [Mod | Unused], Counter} -> @@ -140,6 +143,20 @@ code_change(_Old, Config, _Extra) -> {ok, Config}. +release(Path, Config) -> + Current = Config#elixir_code_server.required, + case maps:find(Path, Current) of + {ok, []} -> + Released = maps:remove(Path, Current), + Config#elixir_code_server{required=Released}; + {ok, [Next | Waiting]} -> + _ = gen_server:reply(Next, proceed), + Released = maps:put(Path, Waiting, Current), + Config#elixir_code_server{required=Released}; + error -> + Config + end. + compiler_module(I) -> list_to_atom("elixir_compiler_" ++ integer_to_list(I)). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/src/elixir_utils.erl new/elixir-1.20.1/lib/elixir/src/elixir_utils.erl --- old/elixir-1.20.0/lib/elixir/src/elixir_utils.erl 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/src/elixir_utils.erl 2026-06-09 15:36:40.000000000 +0200 @@ -233,6 +233,9 @@ returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _, _]}) when Fun == function_exported; Fun == is_record -> true; +returns_boolean({{'.', _, [lists, member]}, _, [_, _]}) -> + true; + returns_boolean({'case', _, [_, [{do, Clauses}]]}) -> lists:all(fun ({'->', _, [_, Expr]}) -> returns_boolean(Expr) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/test/elixir/calendar_test.exs new/elixir-1.20.1/lib/elixir/test/elixir/calendar_test.exs --- old/elixir-1.20.0/lib/elixir/test/elixir/calendar_test.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/test/elixir/calendar_test.exs 2026-06-09 15:36:40.000000000 +0200 @@ -340,6 +340,32 @@ assert Calendar.strftime(~N[2019-08-15 17:07:57], "%010A") == "00Thursday" end + test "limits width to at most 1024 characters" do + assert Calendar.strftime(~D[2019-08-15], "%1024d") |> byte_size() == 1024 + + assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn -> + Calendar.strftime(~D[2019-08-15], "%1025d") + end + + assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn -> + Calendar.strftime(~D[2019-08-15], "%10000d") + end + end + + test "limits width in preferred formats" do + assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn -> + Calendar.strftime(~N[2019-08-15 17:07:57], "%c", preferred_datetime: "%1025d") + end + + assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn -> + Calendar.strftime(~N[2019-08-15 17:07:57], "%x", preferred_date: "%1025d") + end + + assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn -> + Calendar.strftime(~N[2019-08-15 17:07:57], "%X", preferred_time: "%1025H") + end + end + test "formats Epoch time with %s" do assert Calendar.strftime(~N[2019-08-15 17:07:57], "%s") == "1565888877" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/test/elixir/code_test.exs new/elixir-1.20.1/lib/elixir/test/elixir/code_test.exs --- old/elixir-1.20.0/lib/elixir/test/elixir/code_test.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/test/elixir/code_test.exs 2026-06-09 15:36:40.000000000 +0200 @@ -514,6 +514,25 @@ Code.unrequire_files([fixture_path("code_sample.exs")]) end + test "require_file/1 releases the file when compilation fails" do + path = tmp_path("bad_require_#{System.unique_integer([:positive])}.ex") + + try do + File.write!(path, ~s|raise "boom"|) + + assert_raise RuntimeError, "boom", fn -> + Code.require_file(path) + end + + assert_raise RuntimeError, "boom", fn -> + Code.require_file(path) + end + after + File.rm(path) + Code.unrequire_files([path]) + end + end + test "string_to_quoted!/2 errors take lines/columns/indentation into account" do assert_exception( SyntaxError, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/elixir/test/elixir/version_test.exs new/elixir-1.20.1/lib/elixir/test/elixir/version_test.exs --- old/elixir-1.20.0/lib/elixir/test/elixir/version_test.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/elixir/test/elixir/version_test.exs 2026-06-09 15:36:40.000000000 +0200 @@ -84,9 +84,15 @@ assert {:ok, %Version{major: 1, minor: 4, patch: 5, pre: [6, 7, "eight"]}} = Version.parse("1.4.5-6.7.eight") + assert {:ok, %Version{major: 99_999_999_999_999, minor: 0, patch: 0}} = + Version.parse("99999999999999.0.0") + assert {:ok, %Version{major: 1, minor: 4, patch: 5, pre: ["6-g3318bd5"]}} = Version.parse("1.4.5-6-g3318bd5+ignore") + assert {:ok, %Version{major: 1, minor: 0, patch: 0, pre: ["100000000000000-alpha"]}} = + Version.parse("1.0.0-100000000000000-alpha") + assert Version.parse("foobar") == :error assert Version.parse("2") == :error assert Version.parse("2.") == :error @@ -105,6 +111,13 @@ assert Version.parse("02.3.0") == :error assert Version.parse("0. 0.0") == :error assert Version.parse("0.1.0-&&pre") == :error + assert Version.parse("100000000000000.0.0") == :error + assert Version.parse("1.100000000000000.0") == :error + assert Version.parse("1.0.100000000000000") == :error + assert Version.parse("1.0.0-100000000000000") == :error + + assert Version.parse("1.0.0+100000000000000") == + {:ok, %Version{major: 1, minor: 0, patch: 0, build: "100000000000000"}} end test "to_string/1" do @@ -338,6 +351,7 @@ assert Version.parse_requirement("1.2.3 and or 4.5.6") == :error assert Version.parse_requirement(">= 1") == :error assert Version.parse_requirement("1.2.3 >=") == :error + assert Version.parse_requirement("100000000000000.0.0") == :error end test "inspect/1" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/lib/mix/local/installer.ex new/elixir-1.20.1/lib/mix/lib/mix/local/installer.ex --- old/elixir-1.20.0/lib/mix/lib/mix/local/installer.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/lib/mix/local/installer.ex 2026-06-09 15:36:40.000000000 +0200 @@ -181,15 +181,17 @@ message = case previous_files do [] -> - "Are you sure you want to install #{inspect(src)}?" + "Do you trust and want to install #{inspect(src)}?" [file] -> "Found existing entry: #{file}\n" <> - "Are you sure you want to replace it with #{inspect(src)}?" + "The existing entry will be replaced.\n" <> + "Do you trust and want to install #{inspect(src)}?" files -> "Found existing entries: #{Enum.map_join(files, ", ", &Path.basename/1)}\n" <> - "Are you sure you want to replace them with #{inspect(src)}?" + "The existing entries will be replaced.\n" <> + "Do you trust and want to install #{inspect(src)}?" end Mix.shell().yes?(message) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/lib/mix/tasks/archive.install.ex new/elixir-1.20.1/lib/mix/lib/mix/tasks/archive.install.ex --- old/elixir-1.20.0/lib/mix/lib/mix/tasks/archive.install.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/lib/mix/tasks/archive.install.ex 2026-06-09 15:36:40.000000000 +0200 @@ -40,6 +40,15 @@ Note that installing via Git, GitHub, or Hex fetches the source of the archive and builds it, while using local path uses a pre-built archive. + ## Security + + Archives must be installed only from sources you trust. + + Installing an archive from Git, GitHub, or Hex executes code from the source + during installation, unless a pre-built archive is given. Once an archive is + installed, Mix may load code from it as a plugin on any Mix command, even if + no archive command is executed. + ## Command line options * `--sha512` - checks the archive matches the given SHA-512 checksum. Only @@ -119,7 +128,8 @@ @impl true def install(basename, contents, previous) do ez_path = Path.join(Mix.path_for(:archives), basename) - dir_dest = resolve_destination(ez_path, contents) + archive_name = archive_name!(contents) + dir_dest = Path.join(Path.dirname(ez_path), archive_name) remove_previous_versions(previous) @@ -149,17 +159,62 @@ ### Private helpers - defp resolve_destination(ez_path, contents) do - with {:ok, [_comment, zip_first_file | _]} <- :zip.list_dir(contents), - {:zip_file, zip_first_path, _, _, _, _} = zip_first_file, - [zip_root_dir | _] = Path.split(zip_first_path) do - Path.join(Path.dirname(ez_path), zip_root_dir) + defp archive_name!(contents) do + with {:ok, files} <- :zip.list_dir(contents), + zip_files = Enum.filter(files, &match?({:zip_file, _, _, _, _, _}, &1)), + true <- zip_files != [] do + Enum.reduce(zip_files, nil, fn zip_file, root -> + validate_archive_path!(zip_file, root) + end) else _ -> - Mix.raise("Installation failed: invalid archive file") + Mix.raise("Installation failed: invalid archive file, no files found") + end + end + + defp validate_archive_path!({:zip_file, path, file_info, _, _, _}, root) do + type = elem(file_info, 2) + path = zip_path_to_string(path) + + unless type in [:regular, :directory] do + Mix.raise( + "Installation failed: invalid archive file, #{inspect(path)} is not a regular file or directory" + ) + end + + cond do + Path.type(path) != :relative -> + Mix.raise( + "Installation failed: invalid archive file, #{inspect(path)} is an absolute path" + ) + + String.contains?(path, ["..", "\\", <<0>>]) -> + Mix.raise("Installation failed: invalid archive file, #{inspect(path)} is an unsafe path") + + true -> + :ok + end + + case String.split(path, "/", trim: true) do + [new_root | _] -> + cond do + root && root != new_root -> + Mix.raise( + "Installation failed: invalid archive file, #{inspect(path)} is outside archive root #{inspect(root)}" + ) + + true -> + new_root + end + + [] -> + Mix.raise("Installation failed: invalid archive file, #{inspect(path)} is empty") end end + defp zip_path_to_string(path) when is_list(path), do: List.to_string(path) + defp zip_path_to_string(path) when is_binary(path), do: path + defp archives(name) do Mix.path_for(:archives) |> Path.join(name) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/lib/mix/tasks/escript.install.ex new/elixir-1.20.1/lib/mix/lib/mix/tasks/escript.install.ex --- old/elixir-1.20.0/lib/mix/lib/mix/tasks/escript.install.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/lib/mix/tasks/escript.install.ex 2026-06-09 15:36:40.000000000 +0200 @@ -40,6 +40,14 @@ `$PATH` environment variable. For more information, check the wikipedia article on PATH: https://en.wikipedia.org/wiki/PATH_(variable) + ## Security + + Escripts must be installed only from sources you trust. + + Installing an escript from Git, GitHub, or Hex executes code from the source + during installation, unless a pre-built escript is given. Once an escript is + installed, running it executes code on your machine. + ## Command line options * `--sha512` - checks the escript matches the given SHA-512 checksum. Only diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/lib/mix/tasks/format.ex new/elixir-1.20.1/lib/mix/lib/mix/tasks/format.ex --- old/elixir-1.20.0/lib/mix/lib/mix/tasks/format.ex 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/lib/mix/tasks/format.ex 2026-06-09 15:36:40.000000000 +0200 @@ -78,6 +78,10 @@ * `--dry-run` - does not save files after formatting. + * `--no-compile` - does not compile, even if compilation is required + to load formatter plugins. If a plugin cannot be loaded, an error + is raised. + * `--verbose` - prints the names of files that were formatted. * `--dot-formatter` - path to the file with formatter configuration. @@ -152,7 +156,7 @@ ] Notice that, when running the formatter with plugins, your code will be - compiled first. + compiled first, unless the `--no-compile` flag is given. In addition, the order by which you input your plugins is the format order. So, in the above `.formatter.exs`, the `MixMarkdownFormatter` will format @@ -316,7 +320,7 @@ plugins = if plugins != [] do - Keyword.get(opts, :plugin_loader, &plugin_loader/1).(plugins) + Keyword.get(opts, :plugin_loader, &plugin_loader(&1, opts)).(plugins) else [] end @@ -357,12 +361,12 @@ end)} end - defp plugin_loader(plugins) do + defp plugin_loader(plugins, opts) do if plugins != [] do - Mix.Task.run("loadpaths", []) + Mix.Task.run("loadpaths", if(opts[:no_compile], do: ["--no-compile"], else: [])) end - if not Enum.all?(plugins, &Code.ensure_loaded?/1) do + if !opts[:no_compile] and not Enum.all?(plugins, &Code.ensure_loaded?/1) do Mix.Task.run("compile", []) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/test/mix/tasks/archive_test.exs new/elixir-1.20.1/lib/mix/test/mix/tasks/archive_test.exs --- old/elixir-1.20.0/lib/mix/test/mix/tasks/archive_test.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/test/mix/tasks/archive_test.exs 2026-06-09 15:36:40.000000000 +0200 @@ -119,6 +119,45 @@ end) end + test "archive install rejects parent directory entries" do + in_tmp("archive install rejects parent directory entries", fn -> + assert {:ok, _} = + :zip.create(~c"bad-0.1.0.ez", [ + {~c"../outside", "bad"}, + {~c"bad-0.1.0/ebin/bad", "bad"} + ]) + + send(self(), {:mix_shell_input, :yes?, true}) + + assert_raise Mix.Error, ~r/invalid archive file/, fn -> + Mix.Tasks.Archive.Install.run(["bad-0.1.0.ez"]) + end + + refute File.exists?(tmp_path("userhome/outside")) + refute File.exists?(tmp_path("userhome/.mix/outside")) + refute File.dir?(tmp_path("userhome/.mix/archives/bad-0.1.0")) + end) + end + + test "archive install rejects entries outside the archive root" do + in_tmp("archive install rejects entries outside the archive root", fn -> + assert {:ok, _} = + :zip.create(~c"bad-0.1.0.ez", [ + {~c"bad-0.1.0/ebin/bad", "bad"}, + {~c"other-0.1.0/ebin/bad", "bad"} + ]) + + send(self(), {:mix_shell_input, :yes?, true}) + + assert_raise Mix.Error, ~r/invalid archive file/, fn -> + Mix.Tasks.Archive.Install.run(["bad-0.1.0.ez"]) + end + + refute File.dir?(tmp_path("userhome/.mix/archives/bad-0.1.0")) + refute File.dir?(tmp_path("userhome/.mix/archives/other-0.1.0")) + end) + end + test "archive install missing file" do message = ~r[Expected "./unlikely-to-exist-0.1.0.ez" to be a local file path] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.0/lib/mix/test/mix/tasks/format_test.exs new/elixir-1.20.1/lib/mix/test/mix/tasks/format_test.exs --- old/elixir-1.20.0/lib/mix/test/mix/tasks/format_test.exs 2026-06-03 19:38:11.000000000 +0200 +++ new/elixir-1.20.1/lib/mix/test/mix/tasks/format_test.exs 2026-06-09 15:36:40.000000000 +0200 @@ -597,6 +597,51 @@ end) end + defmodule FormatWithPluginApp do + def project do + [app: :format_with_plugin, version: "0.1.0"] + end + end + + test "doesn't compile plugins with --no-compile", context do + in_tmp(context.test, fn -> + Mix.Project.push(__MODULE__.FormatWithPluginApp) + on_exit(fn -> purge([UncompiledPlugin]) end) + + File.write!(".formatter.exs", """ + [ + inputs: ["a.ex"], + plugins: [UncompiledPlugin] + ] + """) + + File.mkdir_p!("lib") + + File.write!("lib/uncompiled_plugin.ex", """ + defmodule UncompiledPlugin do + @behaviour Mix.Tasks.Format + + def features(_opts), do: [extensions: [".ex"]] + def format(contents, _opts), do: "# formatted\\n" <> contents + end + """) + + File.write!("a.ex", """ + foo bar + """) + + assert_raise Mix.Error, "Formatter plugin UncompiledPlugin cannot be found", fn -> + Mix.Tasks.Format.run(["--no-compile"]) + end + + refute_received {:mix_shell, :info, ["Compiling" <> _]} + + assert File.read!("a.ex") == """ + foo bar + """ + end) + end + test "uses extension plugins with --stdin-filename", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """
