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-10-30 18:17:43 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/elixir (Old) and /work/SRC/openSUSE:Factory/.elixir.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "elixir" Fri Oct 30 18:17:43 2026 rev:52 rq:1393858 version:1.20.5 Changes: -------- --- /work/SRC/openSUSE:Factory/elixir/elixir.changes 2026-08-29 17:44:57.942571014 +0200 +++ /work/SRC/openSUSE:Factory/.elixir.new.1255/elixir.changes 2026-10-30 18:23:42.129905594 +0100 @@ -1,0 +2,6 @@ +Fri Oct 30 08:45:18 UTC 2026 - Alessio Biancalana <[email protected]> + +- Upgrade to Elixir 1.20.5: + * Changelog available at https://hexdocs.pm/elixir/1.20.5/changelog.html + +------------------------------------------------------------------- Old: ---- elixir-1.20.4-doc.zip elixir-1.20.4.tar.gz New: ---- elixir-1.20.5-doc.zip elixir-1.20.5.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ elixir.spec ++++++ --- /var/tmp/diff_new_pack.cWCvdK/_old 2026-10-30 18:23:44.554007071 +0100 +++ /var/tmp/diff_new_pack.cWCvdK/_new 2026-10-30 18:23:44.557007197 +0100 @@ -18,7 +18,7 @@ %define elixirdir %{_prefix}/lib/elixir Name: elixir -Version: 1.20.4 +Version: 1.20.5 Release: 0 Summary: Functional meta-programming aware language built atop Erlang License: Apache-2.0 ++++++ _scmsync.obsinfo ++++++ --- /var/tmp/diff_new_pack.cWCvdK/_old 2026-10-30 18:23:44.808017705 +0100 +++ /var/tmp/diff_new_pack.cWCvdK/_new 2026-10-30 18:23:44.826018458 +0100 @@ -1,6 +1,6 @@ -mtime: 1788014578 -commit: bbf3086d78dde159067fb606ba70d13c01bcd172ee420ae2d7223d1b33d320ff +mtime: 1793350029 +commit: 2aaa9b559189c77e17ad558a3a83acb93e5f0d8ca519f0709db323036259c931 url: https://src.opensuse.org/erlang/elixir -revision: bbf3086d78dde159067fb606ba70d13c01bcd172ee420ae2d7223d1b33d320ff +revision: 2aaa9b559189c77e17ad558a3a83acb93e5f0d8ca519f0709db323036259c931 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-10-30 09:47:09.000000000 +0100 @@ -0,0 +1 @@ +.osc ++++++ elixir-1.20.4-doc.zip -> elixir-1.20.5-doc.zip ++++++ /work/SRC/openSUSE:Factory/elixir/elixir-1.20.4-doc.zip /work/SRC/openSUSE:Factory/.elixir.new.1255/elixir-1.20.5-doc.zip differ: char 11, line 1 ++++++ elixir-1.20.4.tar.gz -> elixir-1.20.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/CHANGELOG.md new/elixir-1.20.5/CHANGELOG.md --- old/elixir-1.20.4/CHANGELOG.md 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/CHANGELOG.md 2026-10-29 20:24:50.000000000 +0100 @@ -192,6 +192,47 @@ You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`. +## v1.20.5 (2026-10-29) + +### 1. Security + +#### Elixir + +* [URI] Bind port limits during `URI.parse` (CVE-2026-109821) +* [JSON] Raise when decoding integers in JSON beyond float64 ranges (CVE-2026-115274) + +#### Mix + +* [mix deps.partition] Validate `deps.partition` configuration with checksum on multi-user systems (CVE-2026-97209) + +### 2. Enhancements + +#### Elixir + + * [Calendar.ISO] Optimize `Calendar.ISO.parse_duration/1` + * [Kernel] Improve boot time by parsing the Erlang/OTP release with `list_to_integer/1` + +### 3. Bug fixes + +#### Elixir + + * [Date] Fix `Date.from_gregorian_days/2` passing an invalid day fraction to non-ISO calendars + * [Duration] Fix `Duration.from_iso8601/1` accepting a fractional seconds component after whole seconds, such as `PT1S2.5S` + * [Duration] Reject inputs longer than 192 bytes in `Duration.from_iso8601/1` + * [Regex] Raise `SystemLimitError` when a match reaches the regex engine match limit, instead of returning no match + * [Registry] Do not crash on timeouts on `Registry.lock/3` + * [String] Skip U+055F (Armenian abbreviation mark) when applying the Greek final sigma rule in `String.downcase/2` and `String.capitalize/2` + * [Task] Do not leak timeout messages on `Task.async_stream/3` + +#### IEx + + * [IEx] Do not halt the remote node when the remote shell receives EOF with `--remsh` + * [IEx] Fix crash on tab completion when shell is not found + +#### Mix + + * [mix xref] Fix outgoing compile-connected counts in `mix xref graph --format stats` + ## v1.20.4 (2026-08-28) ### 1. Security diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/VERSION new/elixir-1.20.5/VERSION --- old/elixir-1.20.4/VERSION 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/VERSION 2026-10-29 20:24:50.000000000 +0100 @@ -1 +1 @@ -1.20.4 +1.20.5 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/bin/elixir new/elixir-1.20.5/bin/elixir --- old/elixir-1.20.4/bin/elixir 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/bin/elixir 2026-10-29 20:24:50.000000000 +0100 @@ -6,7 +6,7 @@ set -e -ELIXIR_VERSION=1.20.4 +ELIXIR_VERSION=1.20.5 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.4/bin/elixir.bat new/elixir-1.20.5/bin/elixir.bat --- old/elixir-1.20.4/bin/elixir.bat 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/bin/elixir.bat 2026-10-29 20:24:50.000000000 +0100 @@ -4,7 +4,7 @@ :: SPDX-FileCopyrightText: 2021 The Elixir Team :: SPDX-FileCopyrightText: 2012 Plataformatec -set ELIXIR_VERSION=1.20.4 +set ELIXIR_VERSION=1.20.5 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.4/lib/elixir/lib/calendar/date.ex new/elixir-1.20.5/lib/elixir/lib/calendar/date.ex --- old/elixir-1.20.4/lib/elixir/lib/calendar/date.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/calendar/date.ex 2026-10-29 20:24:50.000000000 +0100 @@ -508,7 +508,7 @@ @doc since: "1.11.0" @spec from_gregorian_days(integer(), Calendar.calendar()) :: t def from_gregorian_days(days, calendar \\ Calendar.ISO) when is_integer(days) do - from_iso_days({days, 0}, calendar) + from_iso_days({days, {0, 86_400}}, calendar) end @doc """ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/calendar/duration.ex new/elixir-1.20.5/lib/elixir/lib/calendar/duration.ex --- old/elixir-1.20.4/lib/elixir/lib/calendar/duration.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/calendar/duration.ex 2026-10-29 20:24:50.000000000 +0100 @@ -353,6 +353,9 @@ Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`. + Inputs longer than 192 bytes are rejected, which is enough to parse each unit as a maximum signed + 64-bit integer (although it also allows parsing a single unit with 190 digits). + ## Examples iex> Duration.from_iso8601("P1Y2M3DT4H5M6S") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/calendar/iso.ex new/elixir-1.20.5/lib/elixir/lib/calendar/iso.ex --- old/elixir-1.20.4/lib/elixir/lib/calendar/iso.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/calendar/iso.ex 2026-10-29 20:24:50.000000000 +0100 @@ -689,69 +689,67 @@ """ @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom} - def parse_duration("P" <> string) when byte_size(string) > 0 do - parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) + def parse_duration("P" <> string) when byte_size(string) in 1..191 do + parse_duration_date(string, 1, [], 0) end - def parse_duration("+P" <> string) when byte_size(string) > 0 do - parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) + def parse_duration("+P" <> string) when byte_size(string) in 1..190 do + parse_duration_date(string, 1, [], 0) end - def parse_duration("-P" <> string) when byte_size(string) > 0 do - with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do - {:ok, - Enum.map(fields, fn - {:microsecond, {value, precision}} -> {:microsecond, {-value, precision}} - {unit, value} -> {unit, -value} - end)} - end + def parse_duration("-P" <> string) when byte_size(string) in 1..190 do + parse_duration_date(string, -1, [], 0) end def parse_duration(_) do {:error, :invalid_duration} end - defp parse_duration_date("", acc, _allowed), do: {:ok, acc} + defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc} - defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do - parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S) + defp parse_duration_date("T" <> string, sign, acc, _min_position) + when byte_size(string) > 0 do + parse_duration_time(string, sign, acc, 0) end - defp parse_duration_date(string, acc, allowed) do - with {integer, <<next, rest::binary>>} <- Integer.parse(string), - {key, allowed} <- find_unit(allowed, next) do - parse_duration_date(rest, [{key, integer} | acc], allowed) + defp parse_duration_date(string, sign, acc, min_position) do + with {integer, <<unit, rest::binary>>} <- Integer.parse(string), + {key, next_min_position} <- find_date_unit(min_position, unit) do + parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position) else _ -> {:error, :invalid_date_component} end end - defp parse_duration_time("", acc, _allowed), do: {:ok, acc} + defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc} - defp parse_duration_time(string, acc, allowed) do + defp parse_duration_time(string, sign, acc, min_position) do case Integer.parse(string) do {second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] -> - case parse_microsecond(rest) do - {{ms, precision}, "S"} -> - ms = - case string do - "-" <> _ -> - -ms - - _ -> - ms - end - - {:ok, [second: second, microsecond: {ms, precision}] ++ acc} + with {:second, _next_min_position} <- find_time_unit(min_position, ?S), + {{ms, precision}, "S"} <- parse_microsecond(rest) do + ms = + case string do + "-" <> _ -> + -ms + + _ -> + ms + end + {:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc} + else _ -> {:error, :invalid_time_component} end - {integer, <<next, rest::binary>>} -> - case find_unit(allowed, next) do - {key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed) - false -> {:error, :invalid_time_component} + {integer, <<unit, rest::binary>>} -> + case find_time_unit(min_position, unit) do + {key, next_min_position} -> + parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position) + + false -> + {:error, :invalid_time_component} end _ -> @@ -759,9 +757,17 @@ end end - defp find_unit([{key, unit} | rest], unit), do: {key, rest} - defp find_unit([_ | rest], unit), do: find_unit(rest, unit) - defp find_unit([], _unit), do: false + # The minimum position is the earliest unit still allowed, or one past the end. + defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1} + defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2} + defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3} + defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4} + defp find_date_unit(_min_position, _unit), do: false + + defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1} + defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2} + defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3} + defp find_time_unit(_min_position, _unit), do: false @doc """ Returns the `t:Calendar.iso_days/0` format of the specified date. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/dynamic_supervisor.ex new/elixir-1.20.5/lib/elixir/lib/dynamic_supervisor.ex --- old/elixir-1.20.4/lib/elixir/lib/dynamic_supervisor.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/dynamic_supervisor.ex 2026-10-29 20:24:50.000000000 +0100 @@ -930,12 +930,9 @@ defp wait_children(_pids, 0, timers, stacks) do for {timer, _} <- timers do - _ = :erlang.cancel_timer(timer) - - receive do - {:timeout, ^timer, :kill} -> :ok - after - 0 -> :ok + # If the timer has already expired, its message is guaranteed to arrive + if :erlang.cancel_timer(timer) == false do + receive do: ({:timeout, ^timer, :kill} -> :ok) end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/json.ex new/elixir-1.20.5/lib/elixir/lib/json.ex --- old/elixir-1.20.4/lib/elixir/lib/json.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/json.ex 2026-10-29 20:24:50.000000000 +0100 @@ -332,6 +332,7 @@ {:unexpected_end, non_neg_integer()} | {:invalid_byte, non_neg_integer(), byte()} | {:unexpected_sequence, non_neg_integer(), binary()} + | {:integer_too_large, pos_integer()} @typedoc """ Decoders for customizing JSON decoding behavior. @@ -366,6 +367,8 @@ * `{:unexpected_end, offset}` if `binary` contains incomplete JSON value * `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte * `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape + * `{:integer_too_large, digits}` if `binary` contains an integer with more than + 310 digits (enough to cover all 64-bit floats) """ @spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()} def decode(binary) when is_binary(binary) do @@ -378,6 +381,8 @@ end end + @max_integer_digits 310 + @doc ~S""" Decodes the given JSON with the given decoders. @@ -396,7 +401,9 @@ * for `object_push`: `fn key, value, acc -> [{key, value} | acc] end` * for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end` * for `float`: `&String.to_float/1` - * for `integer`: `&String.to_integer/1` + * for `integer`: `&String.to_integer/1`, except integers with more than + 310 digits (enough to cover all 64-bit floats) are rejected with + `{:integer_too_large, digits}` * for `string`: `&Function.identity/1` * for `null`: the atom `nil` @@ -405,7 +412,10 @@ @spec decode(binary(), term(), decoders()) :: {term(), term(), binary()} | {:error, decode_error_reason()} def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do - decoders = Keyword.put_new(decoders, :null, nil) + decoders = + decoders + |> Keyword.put_new(:null, nil) + |> Keyword.put_new(:integer, &decode_integer/1) try do :json.decode(binary, acc, Map.new(decoders)) @@ -418,9 +428,25 @@ :error, {:unexpected_sequence, bytes} -> {:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}} + + :throw, {__MODULE__, :integer_too_large, digits} -> + {:error, {:integer_too_large, digits}} end end + defp decode_integer(binary) when byte_size(binary) <= @max_integer_digits, + do: :erlang.binary_to_integer(binary) + + defp decode_integer(<<?-, digits::binary>> = binary) + when byte_size(digits) <= @max_integer_digits, + do: :erlang.binary_to_integer(binary) + + defp decode_integer(<<?-, digits::binary>>), + do: throw({__MODULE__, :integer_too_large, byte_size(digits)}) + + defp decode_integer(digits), + do: throw({__MODULE__, :integer_too_large, byte_size(digits)}) + defp offset(stacktrace) do with [{_, _, _, opts} | _] <- stacktrace, %{cause: %{position: position}} <- opts[:error_info] do @@ -463,6 +489,12 @@ message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}", data: binary, offset: offset + + {:error, {:integer_too_large, digits}} -> + raise JSON.DecodeError, + message: + "integer with #{digits} digits exceeds the limit of #{@max_integer_digits} digits", + data: binary end end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/kernel/parallel_compiler.ex new/elixir-1.20.5/lib/elixir/lib/kernel/parallel_compiler.ex --- old/elixir-1.20.4/lib/elixir/lib/kernel/parallel_compiler.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/kernel/parallel_compiler.ex 2026-10-29 20:24:50.000000000 +0100 @@ -367,12 +367,9 @@ purge_compiler_modules: purge_compiler_modules }) - Process.cancel_timer(state.timer_ref) - - receive do - :threshold_check -> :ok - after - 0 -> :ok + # If the timer has already expired, its message is guaranteed to arrive + if Process.cancel_timer(state.timer_ref) == false do + receive do: (:threshold_check -> :ok) end outcome diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/regex.ex new/elixir-1.20.5/lib/elixir/lib/regex.ex --- old/elixir-1.20.4/lib/elixir/lib/regex.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/regex.ex 2026-10-29 20:24:50.000000000 +0100 @@ -173,6 +173,14 @@ iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-") "Unicode-spaces" + ## Match limits + + The underlying regex engine limits how much backtracking a single match + may perform. Patterns with nested quantifiers, such as `~r/(a+)+$/`, may + reach this limit on certain inputs (known as catastrophic backtracking). + When that happens, the functions in this module raise `SystemLimitError` + (since Elixir v1.21) instead of returning no match, since the outcome of + the match is unknown. """ defstruct re_pattern: nil, source: "", opts: [] @@ -612,7 +620,19 @@ end end - defp safe_run(%Regex{re_pattern: re_pattern} = regex, string, options) do + defp safe_run(regex, string, options) do + case do_run(regex, string, [:report_errors | options]) do + {:error, reason} -> + raise SystemLimitError, + "regex #{inspect(regex)} could not complete the match because it " <> + "reached #{inspect(reason)}, see the \"Match limits\" section in the Regex docs" + + result -> + result + end + end + + defp do_run(%Regex{re_pattern: re_pattern} = regex, string, options) do # TODO: Remove me when Erlang/OTP 28+ is required # This allows regexes precompiled on Erlang/OTP 27- to work on Erlang/OTP 28+ with true <- :erlang.system_info(:otp_release) >= [?2, ?8], diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/registry.ex new/elixir-1.20.5/lib/elixir/lib/registry.ex --- old/elixir-1.20.4/lib/elixir/lib/registry.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/registry.ex 2026-10-29 20:24:50.000000000 +0100 @@ -1813,7 +1813,7 @@ Runs function with a lock. """ def lock(pid, key, lock) do - ref = GenServer.call(pid, {:lock, key}) + ref = GenServer.call(pid, {:lock, key}, :infinity) try do lock.() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/task.ex new/elixir-1.20.5/lib/elixir/lib/task.ex --- old/elixir-1.20.4/lib/elixir/lib/task.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/task.ex 2026-10-29 20:24:50.000000000 +0100 @@ -997,26 +997,29 @@ Process.send_after(self(), timeout_ref, timeout) end - try do - await_many(tasks, timeout, awaiting, %{}, timeout_ref) - after - timer_ref && Process.cancel_timer(timer_ref) - receive do: (^timeout_ref -> :ok), after: (0 -> :ok) - end + await_many(tasks, timeout, awaiting, %{}, timeout_ref, timer_ref) end - defp await_many(tasks, _timeout, awaiting, replies, _timeout_ref) + defp await_many(tasks, _timeout, awaiting, replies, timeout_ref, timer_ref) when map_size(awaiting) == 0 do + if is_reference(timer_ref) and Process.cancel_timer(timer_ref) == false do + receive do: (^timeout_ref -> :ok) + end + for %{ref: ref} <- tasks, do: Map.fetch!(replies, ref) end - defp await_many(tasks, timeout, awaiting, replies, timeout_ref) do + defp await_many(tasks, timeout, awaiting, replies, timeout_ref, timer_ref) do receive do ^timeout_ref -> demonitor_pending_tasks(awaiting) exit({:timeout, {__MODULE__, :await_many, [tasks, timeout]}}) {:DOWN, ref, _, proc, reason} when is_map_key(awaiting, ref) -> + if is_reference(timer_ref) and Process.cancel_timer(timer_ref) == false do + receive do: (^timeout_ref -> :ok) + end + demonitor_pending_tasks(awaiting) exit({reason(reason, proc), {__MODULE__, :await_many, [tasks, timeout]}}) @@ -1028,7 +1031,8 @@ timeout, Map.delete(awaiting, ref), Map.put(replies, ref, reply), - timeout_ref + timeout_ref, + timer_ref ) end end @@ -1283,8 +1287,11 @@ end defp yield_many(0, refs, timeout_ref, timer_ref) do - timer_ref && Process.cancel_timer(timer_ref) - receive do: (^timeout_ref -> :ok), after: (0 -> :ok) + # If the timer has already expired, its message is guaranteed to arrive + if is_reference(timer_ref) and Process.cancel_timer(timer_ref) == false do + receive do: (^timeout_ref -> :ok) + end + {false, refs} end @@ -1296,6 +1303,10 @@ {:DOWN, ref, _, proc, reason} when is_map_key(refs, ref) -> if reason == :noconnection do + if is_reference(timer_ref) and Process.cancel_timer(timer_ref) == false do + receive do: (^timeout_ref -> :ok) + end + throw({:noconnection, reason(:noconnection, proc)}) else yield_many(limit - 1, %{refs | ref => {:exit, reason}}, timeout_ref, timer_ref) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/lib/uri.ex new/elixir-1.20.5/lib/elixir/lib/uri.ex --- old/elixir-1.20.4/lib/elixir/lib/uri.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/lib/uri.ex 2026-10-29 20:24:50.000000000 +0100 @@ -854,7 +854,24 @@ {authority, userinfo, host, port} = split_authority(authority_with_slashes) scheme = scheme && String.downcase(scheme) - port = port || (scheme && default_port(scheme)) + + # Leading zeros are removed and then ports with more than + # 6 digits or outside of the valid range are discarded + port = + case port && String.trim_leading(port, "0") do + nil -> + scheme && default_port(scheme) + + "" -> + 0 + + port when byte_size(port) <= 6 -> + port = String.to_integer(port) + if port in 0..65535, do: port + + _ -> + nil + end %URI{ scheme: scheme, @@ -890,7 +907,7 @@ destructure [_, _, userinfo, host, _, port], components userinfo = nilify(userinfo) host = if nilify(host), do: host |> String.trim_leading("[") |> String.trim_trailing("]") - port = if nilify(port), do: String.to_integer(port) + port = nilify(port) {authority, userinfo, host, port} end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/src/elixir.erl new/elixir-1.20.5/lib/elixir/src/elixir.erl --- old/elixir-1.20.4/lib/elixir/src/elixir.erl 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/src/elixir.erl 2026-10-29 20:24:50.000000000 +0100 @@ -126,8 +126,8 @@ parse_otp_release() -> %% Whenever we change this check, we should also change Makefile. - case string:to_integer(erlang:system_info(otp_release)) of - {Num, _} when Num >= 27 -> + case list_to_integer(erlang:system_info(otp_release)) of + Num when Num >= 27 -> case Num == 28 andalso (code:ensure_loaded(re) == {module, re}) andalso not erlang:function_exported(re, import, 1) of true -> io:format(standard_error, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/src/iex.erl new/elixir-1.20.5/lib/elixir/src/iex.erl --- old/elixir-1.20.4/lib/elixir/src/iex.erl 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/src/iex.erl 2026-10-29 20:24:50.000000000 +0100 @@ -53,7 +53,7 @@ case get_remsh(Args) of nil -> - start_mfa(Args, {elixir, start_cli, []}); + start_mfa(Args, {elixir, start_cli, []}, halt); Remote -> Ref = make_ref(), @@ -67,15 +67,15 @@ end end), - {remote, Remote, start_mfa(Args, {?MODULE, sync_remote, [Parent, Ref]})} + {remote, Remote, start_mfa(Args, {?MODULE, sync_remote, [Parent, Ref]}, stop_evaluator)} end. sync_remote(Parent, Ref) -> Parent ! {'begin', Ref, self()}, receive {done, Ref} -> ok end. -start_mfa(Args, MFA) -> - Opts = [{dot_iex, get_dot_iex(Args)}, {on_eof, halt}], +start_mfa(Args, MFA, OnEof) -> + Opts = [{dot_iex, get_dot_iex(Args)}, {on_eof, OnEof}], {?MODULE, start, [Opts, MFA]}. get_dot_iex(["--dot-iex", H | _]) -> elixir_utils:characters_to_binary(H); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/calendar/date_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/calendar/date_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/calendar/date_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/calendar/date_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -194,6 +194,19 @@ fn -> Date.convert!(~D[2016-02-03], FakeCalendar) end end + test "from_gregorian_days/2" do + assert Date.from_gregorian_days(730_485, Calendar.Holocene) == + Calendar.Holocene.date(12000, 01, 01) + + assert Date.from_gregorian_days(-1, Calendar.Holocene) == + Calendar.Holocene.date(9999, 12, 31) + + for days <- [-1_000_000, 0, 1, 1_000_000] do + date = Date.from_gregorian_days(days, Calendar.Holocene) + assert Date.to_gregorian_days(date) == days + end + end + test "add/2" do assert Date.add(~D[0000-01-01], 3_652_424) == ~D[9999-12-31] assert Date.add(~D[0000-01-01], 3_652_425) == Date.new!(10000, 1, 1) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/calendar/duration_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/calendar/duration_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/calendar/duration_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/calendar/duration_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -242,9 +242,22 @@ assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component} assert Duration.from_iso8601("P0.5Y") == {:error, :invalid_date_component} assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component} + assert Duration.from_iso8601("PT1S2.5S") == {:error, :invalid_time_component} assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component} assert Duration.from_iso8601("PT0.5H") == {:error, :invalid_time_component} assert Duration.from_iso8601("invalid") == {:error, :invalid_duration} + + # Inputs are capped at 192 bytes + digits = String.duplicate("9", 189) + assert {:ok, %Duration{}} = Duration.from_iso8601("P9#{digits}D") + assert {:ok, %Duration{}} = Duration.from_iso8601("-P#{digits}D") + assert {:ok, %Duration{}} = Duration.from_iso8601("+P#{digits}D") + assert Duration.from_iso8601("P99#{digits}D") == {:error, :invalid_duration} + assert Duration.from_iso8601("-P9#{digits}D") == {:error, :invalid_duration} + assert Duration.from_iso8601("+P9#{digits}D") == {:error, :invalid_duration} + + assert Duration.from_iso8601("P#{String.duplicate("9", 4_000_000)}D") == + {:error, :invalid_duration} end test "from_iso8601!/1" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/calendar/iso_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/calendar/iso_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/calendar/iso_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/calendar/iso_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -125,6 +125,25 @@ Calendar.ISO.date_from_iso_days(iso_days) end + describe "parse_duration/1" do + test "combines duration and component signs" do + assert {:ok, fields} = Calendar.ISO.parse_duration("-P1Y-2MT3H-4.5S") + + assert Map.new(fields) == %{ + year: -1, + month: 2, + hour: -3, + second: 4, + microsecond: {500_000, 1} + } + end + + test "combines duration and negative zero fractional second signs" do + assert Calendar.ISO.parse_duration("-PT-0.6S") == + {:ok, [second: 0, microsecond: {600_000, 1}]} + end + end + describe "parse_date/1" do test "supports both only extended format by default" do assert Calendar.ISO.parse_date("20150123") == {:error, :invalid_format} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/json_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/json_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/json_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/json_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -187,6 +187,19 @@ fn -> JSON.decode!("123o") end end + test "integer too large" do + digits = String.duplicate("9", 310) + assert JSON.decode(digits) == {:ok, String.to_integer(digits)} + assert JSON.decode("-" <> digits) == {:ok, String.to_integer("-" <> digits)} + assert JSON.decode(digits <> "9") == {:error, {:integer_too_large, 311}} + assert JSON.decode("[1, -#{digits}9]") == {:error, {:integer_too_large, 311}} + assert JSON.decode(digits <> "9", :ok, integer: & &1) == {digits <> "9", :ok, ""} + + assert_raise JSON.DecodeError, + "integer with 311 digits exceeds the limit of 310 digits", + fn -> JSON.decode!(digits <> "9") end + end + test "unexpected sequence" do assert JSON.decode("\"\\ud8aa\\udcxx\"") == {:error, {:unexpected_sequence, 1, "\\ud8aa\\udcxx"}} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/regex_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/regex_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/regex_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/regex_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -351,6 +351,22 @@ assert Regex.escape("[foo]") == "\\[foo\\]" end + test "raises when the match limit is reached" do + regex = ~r/(a+)+$/ + string = String.duplicate("a", 30) <> "b" + message = ~r/could not complete the match because it reached :match_limit/ + + assert_raise SystemLimitError, message, fn -> Regex.match?(regex, string) end + assert_raise SystemLimitError, message, fn -> Regex.run(regex, string) end + assert_raise SystemLimitError, message, fn -> Regex.scan(regex, string) end + assert_raise SystemLimitError, message, fn -> Regex.split(regex, string) end + assert_raise SystemLimitError, message, fn -> Regex.replace(regex, string, "") end + + assert_raise SystemLimitError, message, fn -> + Regex.named_captures(~r/(?<all>(a+)+$)/, string) + end + end + defp matches_escaped?(string) do matches_escaped?(string, string) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/test/elixir/uri_test.exs new/elixir-1.20.5/lib/elixir/test/elixir/uri_test.exs --- old/elixir-1.20.4/lib/elixir/test/elixir/uri_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/test/elixir/uri_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -969,6 +969,14 @@ } assert URI.parse("http://foo.com:4444") == expected_uri + + assert URI.parse("http://foo.com:65535").port == 65535 + assert URI.parse("http://foo.com:065535").port == 65535 + assert URI.parse("http://foo.com:65536").port == nil + assert URI.parse("http://foo.com:0000000065535").port == 65535 + assert URI.parse("http://foo.com:0000000065536").port == nil + assert URI.parse("http://foo.com:0000000000").port == 0 + assert URI.parse("http://foo.com:#{String.duplicate("9", 1000)}").port == nil end test "can parse bad URIs" do diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/elixir/unicode/unicode.ex new/elixir-1.20.5/lib/elixir/unicode/unicode.ex --- old/elixir-1.20.4/lib/elixir/unicode/unicode.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/elixir/unicode/unicode.ex 2026-10-29 20:24:50.000000000 +0100 @@ -94,6 +94,7 @@ 0xFF0E, 0x00B7, 0x0387, + 0x055F, 0x05F4, 0x2027, 0x003A, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/iex/lib/iex/broker.ex new/elixir-1.20.5/lib/iex/lib/iex/broker.ex --- old/elixir-1.20.4/lib/iex/lib/iex/broker.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/iex/lib/iex/broker.ex 2026-10-29 20:24:50.000000000 +0100 @@ -35,7 +35,7 @@ """ @spec evaluator(shell()) :: {evaluator :: pid, server :: pid} | nil def evaluator(pid \\ shell()) do - if pid do + if is_pid(pid) do {:dictionary, dictionary} = Process.info(pid, :dictionary) {dictionary[:evaluator], pid} end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/iex/test/iex/autocomplete_test.exs new/elixir-1.20.5/lib/iex/test/iex/autocomplete_test.exs --- old/elixir-1.20.4/lib/iex/test/iex/autocomplete_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/iex/test/iex/autocomplete_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -594,6 +594,14 @@ assert expand(~c"\"#{dir}/dir/file") == dir |> Path.join("dir") |> path_autocompletion("file") end + test "expansion without a shell" do + # expand/1 is what IEx.Server installs as the expand_fun, and it defaults + # to :shell.whereis/0, which is :undefined with no shell to report + assert :undefined == :shell.whereis() + assert {:yes, [], list} = IEx.Autocomplete.expand(~c"rtS") + assert ~c"String" in list + end + defp path_autocompletion(dir, hint \\ "") do dir |> File.ls!() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/mix/lib/mix/tasks/deps.partition.ex new/elixir-1.20.5/lib/mix/lib/mix/tasks/deps.partition.ex --- old/elixir-1.20.4/lib/mix/lib/mix/tasks/deps.partition.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/mix/lib/mix/tasks/deps.partition.ex 2026-10-29 20:24:50.000000000 +0100 @@ -8,7 +8,8 @@ ## Server def server(deps, count, force?) do - {:ok, socket} = :gen_tcp.listen(0, [:binary, packet: :line, active: false]) + {:ok, socket} = + :gen_tcp.listen(0, [:binary, packet: :line, active: false, ip: {127, 0, 0, 1}]) try do server(socket, deps, count, force?) @@ -29,15 +30,20 @@ config = Mix.ProjectStack.peek() |> :erlang.term_to_binary() |> Base.url_encode64() tmp_dir = System.tmp_dir() - {config_flag, config_value} = + config_args = with true <- is_binary(tmp_dir), partition_dir = Path.join(tmp_dir, "mix_deps_partition"), :ok <- File.mkdir_p(partition_dir), partition_file = Path.join(partition_dir, Base.encode32(:crypto.strong_rand_bytes(10))), :ok <- File.write(partition_file, config) do - {~c"--config-file", String.to_charlist(partition_file)} + [ + ~c"--config-file", + String.to_charlist(partition_file), + ~c"--config-sha512", + String.to_charlist(checksum(config)) + ] else - _ -> {~c"--config", String.to_charlist(config)} + _ -> [~c"--config", String.to_charlist(config)] end args = @@ -50,10 +56,8 @@ ~c"--port", Integer.to_charlist(port), ~c"--host", - ~c"127.0.0.1", - config_flag, - config_value - ] + ~c"127.0.0.1" + ] ++ config_args options = [ :exit_status, @@ -239,7 +243,8 @@ force: :boolean, index: :string, config: :string, - config_file: :string + config_file: :string, + config_sha512: :string ] def client do @@ -254,7 +259,16 @@ peek = if config_file = Keyword.get(opts, :config_file) do - File.read!(config_file) + config = File.read!(config_file) + + expected = checksum(config) + given = Keyword.fetch!(opts, :config_sha512) + + if byte_size(expected) != byte_size(given) or not :crypto.hash_equals(expected, given) do + Mix.raise("mismatched --config-file checksum") + end + + config else Keyword.fetch!(opts, :config) end @@ -297,6 +311,10 @@ end end + defp checksum(config) do + Base.encode64(:crypto.hash(:sha512, config)) + end + def client_loop(socket, deps, force?, config) do case :gen_tcp.recv(socket, 0, :infinity) do {:ok, app} -> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/mix/lib/mix/tasks/xref.ex new/elixir-1.20.5/lib/mix/lib/mix/tasks/xref.ex --- old/elixir-1.20.4/lib/mix/lib/mix/tasks/xref.ex 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/mix/lib/mix/tasks/xref.ex 2026-10-29 20:24:50.000000000 +0100 @@ -1184,7 +1184,7 @@ |> :digraph.out_neighbours(file) |> Enum.count(fn v -> {_edge, _v1, _v2, label} = :digraph.edge(graph, {file, v}) - filter_fn.({file, label}) + filter_fn.({v, label}) end) end diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/elixir-1.20.4/lib/mix/test/mix/tasks/xref_test.exs new/elixir-1.20.5/lib/mix/test/mix/tasks/xref_test.exs --- old/elixir-1.20.4/lib/mix/test/mix/tasks/xref_test.exs 2026-08-28 12:02:55.000000000 +0200 +++ new/elixir-1.20.5/lib/mix/test/mix/tasks/xref_test.exs 2026-10-29 20:24:50.000000000 +0100 @@ -488,10 +488,10 @@ Top 5 files with most outgoing dependencies: * lib/c.ex (1) - * lib/b.ex (1) * lib/a.ex (1) * lib/e.ex (0) * lib/d.ex (0) + * lib/b.ex (0) Top 5 files with most incoming dependencies: * lib/d.ex (1)
