CVE-2026-32686
Description
Uncontrolled Resource Consumption vulnerability in ericmj decimal allows unauthenticated remote Denial of Service.
The decimal library does not bound the exponent on parsed input. Storing a decimal with a very large exponent (e.g. Decimal.new("1e1000000000")) is accepted without error. Subsequent calls to arithmetic functions (Decimal.add/2, Decimal.sub/2, Decimal.div/2), Decimal.to_string/2 with :normal or :xsd format, Decimal.to_integer/1, Decimal.round/3, or Decimal.compare/3 with a threshold allocate memory proportional to the exponent value, which can exhaust available memory and crash the BEAM VM.
Any application that accepts user-supplied decimal input and subsequently performs arithmetic, rounding, conversion to integer, or string formatting on it is exposed. A single malicious request is sufficient to cause an out-of-memory crash.
This issue affects decimal: from 0.1.0 before 3.0.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
decimalHex | >= 0.1.0, < 3.0.0 | 3.0.0 |
Affected products
1Patches
16a523f3a73b8Apply IEEE 754 decimal128 defaults
4 files changed · +157 −187
lib/decimal/context.ex+10 −10 modified@@ -6,14 +6,14 @@ defmodule Decimal.Context do The context is kept in the process dictionary. It can be accessed with `get/0` and `set/1`. - The default context has a precision of 28, the rounding algorithm is - `:half_up`, and unbounded `emax` and `emin`. The set trap enablers are - `:invalid_operation` and `:division_by_zero`. + The default context follows IEEE 754 decimal128: precision is 34, `emax` is + 6 144, and `emin` is -6 143. The rounding algorithm is `:half_up` and the + set trap enablers are `:invalid_operation` and `:division_by_zero`. - Finite `emax` and `emin` values limit operation results. They do not validate - values that have already been created, so applications that parse untrusted - input should still use `Decimal.parse/2` or `Decimal.cast/2` with - `:max_digits` and `:max_exponent`. + `emax` and `emin` limit operation results. They do not validate values that + have already been created, so applications that parse untrusted input should + still rely on the default `Decimal.parse/2` and `Decimal.cast/2` limits or + pass explicit `:max_digits` and `:max_exponent` options. ## Fields @@ -86,10 +86,10 @@ defmodule Decimal.Context do traps: [Decimal.signal()] } - defstruct precision: 28, + defstruct precision: 34, rounding: :half_up, - emax: :infinity, - emin: :infinity, + emax: 6_144, + emin: -6_143, flags: [], traps: [:invalid_operation, :division_by_zero]
lib/decimal.ex+81 −134 modified@@ -35,28 +35,24 @@ defmodule Decimal do 10 ^ exponent` and will refer to the sign in documentation as either *positive* or *negative*. - By default there is no maximum or minimum value for the exponent because - `Decimal.Context` defaults `emax` and `emin` to `:infinity`. Because of that - all numbers are "normal". This means that when an operation should, according - to the specification, return a number that "underflows" 0 is returned instead - of Etiny. This may happen when dividing a number with infinity. When `emax` - or `emin` are finite, overflow and underflow may be signalled. Clamped is - still not signalled. + The default `Decimal.Context` follows IEEE 754 decimal128: `precision` is + 34, `emax` is 6 144, and `emin` is -6 143. Operation results whose adjusted + exponent leaves that band signal overflow or underflow. Clamped is still + not signalled. ## Large exponents and untrusted input Decimal can represent compact values with very large exponents, such as `1e1000000`. These values are valid decimals, but some APIs may need memory - or CPU proportional to the expanded size of the number. This is especially - important for decimals parsed from user input, JSON payloads, form fields, - database fields, or other external data. + or CPU proportional to the expanded size of the number. - Use `parse/2` or `cast/2` with `:max_digits` and `:max_exponent` when parsing - untrusted input. Use `to_string/3` with `:max_digits` when rendering output - formats that may expand the exponent, such as `:normal` or `:xsd`. - Finite `Decimal.Context` `emax` and `emin` values can limit operation - results, but they do not validate already-created decimals and should not - replace parse/cast limits for untrusted input. + `parse/1`, `parse/2`, `cast/1`, `cast/2`, `to_string/2`, and `to_string/3` + apply IEEE 754 decimal128 limits by default: `:max_digits` of 34, + `:max_exponent` of 6 144, and a `:max_digits` for output of 6 178 + (precision + emax — large enough to render any in-range decimal128 in any + format). These defaults reject the pathological inputs described in + CVE-2026-32686 without materializing them. Pass options on the explicit + arities to override; pass `:infinity` to disable a limit entirely. ## Protocol Implementations @@ -164,6 +160,14 @@ defmodule Decimal do @type to_string_option :: {:max_digits, non_neg_integer | :infinity} + # IEEE 754 decimal128 defaults: precision = 34, emax = 6_144, emin = -6_143. + # The to_string default is precision + emax (34 + 6_144), which is the + # worst-case `:normal` digit-character count for any in-range decimal128 + # value. + @default_max_digits 34 + @default_max_exponent 6_144 + @default_to_string_max_digits 6_178 + # Below 10^2000 the BIF `:erlang.integer_to_binary/1` is fast enough; for # larger integers `integer_to_decimal_iodata/3` recursively splits on a # power of 10 (down to chunks of `@decimal_conversion_leaf_digits` digits) @@ -1538,18 +1542,7 @@ defmodule Decimal do """ @spec cast(term) :: {:ok, t} | :error - def cast(integer) when is_integer(integer), do: {:ok, Decimal.new(integer)} - def cast(%Decimal{} = decimal), do: {:ok, decimal} - def cast(float) when is_float(float), do: {:ok, from_float(float)} - - def cast(binary) when is_binary(binary) do - case parse(binary) do - {decimal, ""} -> {:ok, decimal} - _ -> :error - end - end - - def cast(_), do: :error + def cast(term), do: cast_with_limits(term, default_parse_limits()) @doc """ Creates a new decimal number from an integer, string, float, or existing decimal @@ -1560,8 +1553,10 @@ defmodule Decimal do doc_since("2.4.0") @spec cast(term, [parse_option]) :: {:ok, t} | :error def cast(term, opts) when is_list(opts) do - limits = parse_limits!(opts) + cast_with_limits(term, parse_limits!(opts)) + end + defp cast_with_limits(term, limits) do cond do is_integer(term) -> decimal = Decimal.new(term) @@ -1591,6 +1586,10 @@ defmodule Decimal do If successful, returns a tuple in the form of `{decimal, remainder_of_binary}`, otherwise `:error`. + Inputs whose digit count or exponent magnitude exceed the default limits + (`#{@default_max_digits}` digits, `#{@default_max_exponent}` absolute + exponent) return `:error`. Use `parse/2` to override the limits. + ## Examples iex> Decimal.parse("3.14") @@ -1607,34 +1606,21 @@ defmodule Decimal do """ @spec parse(binary()) :: {t(), binary()} | :error - def parse("+" <> rest) do - parse_unsign(rest) - end - - def parse("-" <> rest) do - case parse_unsign(rest) do - {%Decimal{} = num, rest} -> {%{num | sign: -1}, rest} - :error -> :error - end - end - def parse(binary) when is_binary(binary) do - parse_unsign(binary) + parse_with_limits(binary, default_parse_limits()) end @doc """ - Parses a binary into a decimal with optional limits. - - Use this function instead of `parse/1` for untrusted input. Without explicit - limits, decimal parsing accepts any exponent and digit count for backwards - compatibility. + Parses a binary into a decimal with explicit limits. The following options are supported: * `:max_digits` - maximum number of decimal digits consumed from the input, - including leading and trailing zeros. + including leading and trailing zeros. Defaults to `#{@default_max_digits}`. + Pass `:infinity` to disable. * `:max_exponent` - maximum absolute value of the parsed decimal exponent, - after fractional digits are accounted for. + after fractional digits are accounted for. Defaults to + `#{@default_max_exponent}`. Pass `:infinity` to disable. Returns `:error` when a parsed number exceeds the configured limits. """ @@ -1663,10 +1649,11 @@ defmodule Decimal do @doc """ Converts given number to its string representation. - The default `:scientific` format is compact for large positive exponents. - The `:normal` and `:xsd` formats may allocate output proportional to the - expanded size of the decimal. Use `to_string/3` with `:max_digits` when - rendering decimals from untrusted input. + Output is bounded to `#{@default_to_string_max_digits}` digit characters by + default; pass options via `to_string/3` to override. `:scientific` is compact + for large positive exponents and rarely hits the limit; `:normal` and `:xsd` + expand proportional to the exponent and will raise `ArgumentError` when the + limit would be exceeded. ## Options @@ -1696,15 +1683,21 @@ defmodule Decimal do @spec to_string(t, :scientific | :normal | :xsd | :raw) :: String.t() def to_string(num, type \\ :scientific) - def to_string(%Decimal{sign: sign, coef: :NaN}, _type) do + def to_string(%Decimal{} = num, type) + when type in [:scientific, :normal, :xsd, :raw] do + check_to_string_max_digits!(num, type, @default_to_string_max_digits) + do_to_string(num, type) + end + + defp do_to_string(%Decimal{sign: sign, coef: :NaN}, _type) do if sign == 1, do: "NaN", else: "-NaN" end - def to_string(%Decimal{sign: sign, coef: :inf}, _type) do + defp do_to_string(%Decimal{sign: sign, coef: :inf}, _type) do if sign == 1, do: "Infinity", else: "-Infinity" end - def to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :normal) do + defp do_to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :normal) do digits = integer_to_decimal_binary(coef) length = byte_size(digits) @@ -1725,7 +1718,7 @@ defmodule Decimal do IO.iodata_to_binary(iodata) end - def to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :scientific) do + defp do_to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :scientific) do digits = integer_to_decimal_binary(coef) length = byte_size(digits) adjusted = exp + length - 1 @@ -1762,16 +1755,16 @@ defmodule Decimal do IO.iodata_to_binary(iodata) end - def to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :raw) do + defp do_to_string(%Decimal{sign: sign, coef: coef, exp: exp}, :raw) do str = integer_to_decimal_binary(coef) str = if sign == -1, do: [?- | str], else: str str = if exp != 0, do: [str, "E", :erlang.integer_to_binary(exp)], else: str IO.iodata_to_binary(str) end - def to_string(%Decimal{} = decimal, :xsd) do - decimal |> canonical_xsd() |> to_string(:normal) + defp do_to_string(%Decimal{} = decimal, :xsd) do + decimal |> canonical_xsd() |> do_to_string(:normal) end defp zeroes(0), do: "" @@ -1845,25 +1838,25 @@ defmodule Decimal do defp byte_bit_length(_byte), do: 1 @doc """ - Converts given number to its string representation with optional limits. - - Use this function when rendering decimals from untrusted input, especially - with `:normal` or `:xsd`, because those formats may otherwise allocate output - proportional to the expanded size of the decimal. + Converts given number to its string representation with explicit limits. The following options are supported: * `:max_digits` - maximum number of digit characters in the output. Sign, - decimal point, and exponent markers are not counted. + decimal point, and exponent markers are not counted. Defaults to + `#{@default_to_string_max_digits}`. Pass `:infinity` to disable. Raises `ArgumentError` when the configured limit would be exceeded. """ doc_since("2.4.0") @spec to_string(t, :scientific | :normal | :xsd | :raw, [to_string_option]) :: String.t() - def to_string(%Decimal{} = num, type, opts) when is_list(opts) do - max_digits = limit!(:max_digits, Keyword.get(opts, :max_digits, :infinity)) + def to_string(%Decimal{} = num, type, opts) + when is_list(opts) and type in [:scientific, :normal, :xsd, :raw] do + max_digits = + limit!(:max_digits, Keyword.get(opts, :max_digits, @default_to_string_max_digits)) + check_to_string_max_digits!(num, type, max_digits) - to_string(num, type) + do_to_string(num, type) end defp canonical_xsd(%Decimal{coef: 0} = decimal), do: %{decimal | exp: -1} @@ -2566,16 +2559,24 @@ defmodule Decimal do ## PARSING ## defp parse_limits!(opts) do - Enum.reduce(opts, %{max_digits: :infinity, max_exponent: :infinity}, fn - {:max_digits, value}, acc -> - %{acc | max_digits: limit!(:max_digits, value)} - - {:max_exponent, value}, acc -> - %{acc | max_exponent: limit!(:max_exponent, value)} + Enum.reduce( + opts, + %{max_digits: @default_max_digits, max_exponent: @default_max_exponent}, + fn + {:max_digits, value}, acc -> + %{acc | max_digits: limit!(:max_digits, value)} + + {:max_exponent, value}, acc -> + %{acc | max_exponent: limit!(:max_exponent, value)} + + {key, _value}, _acc -> + raise ArgumentError, "unknown option #{inspect(key)}" + end + ) + end - {key, _value}, _acc -> - raise ArgumentError, "unknown option #{inspect(key)}" - end) + defp default_parse_limits do + %{max_digits: @default_max_digits, max_exponent: @default_max_exponent} end defp limit!(_key, :infinity), do: :infinity @@ -2587,66 +2588,12 @@ defmodule Decimal do "#{inspect(key)} must be a non-negative integer or :infinity, got: #{inspect(value)}" end - defp parse_unsign(<<first, remainder::size(7)-binary, rest::binary>>) when first in [?i, ?I] do - if String.downcase(remainder) == "nfinity" do - {%Decimal{coef: :inf}, rest} - else - :error - end - end - - defp parse_unsign(<<first, remainder::size(2)-binary, rest::binary>>) when first in [?i, ?I] do - if String.downcase(remainder) == "nf" do - {%Decimal{coef: :inf}, rest} - else - :error - end - end - - defp parse_unsign(<<first, remainder::size(2)-binary, rest::binary>>) when first in [?n, ?N] do - if String.downcase(remainder) == "an" do - {%Decimal{coef: :NaN}, rest} - else - :error - end - end - - defp parse_unsign(bin) do - {int_rev, int_size, after_int} = parse_digits_count(bin, [], 0) - - case after_int do - <<?., after_dot::binary>> -> - {coef_rev, total_size, after_float} = parse_digits_count(after_dot, int_rev, int_size) - - if total_size == 0 do - :error - else - {exp, rest} = parse_exp(after_float) - coef = digits_acc_to_integer(coef_rev, total_size) - float_size = total_size - int_size - {%Decimal{coef: coef, exp: parse_exp_int(exp) - float_size}, rest} - end - - _ -> - if int_size == 0 do - :error - else - {exp, rest} = parse_exp(after_int) - coef = digits_acc_to_integer(int_rev, int_size) - {%Decimal{coef: coef, exp: parse_exp_int(exp)}, rest} - end - end - end - defp parse_digits_count(<<digit, rest::binary>>, acc, count) when digit in ?0..?9 do parse_digits_count(rest, [digit | acc], count + 1) end defp parse_digits_count(rest, acc, count), do: {acc, count, rest} - defp parse_exp_int([]), do: 0 - defp parse_exp_int(chars), do: List.to_integer(chars) - defp digits_acc_to_integer([], _size), do: 0 defp digits_acc_to_integer(acc, _size), do: :erlang.list_to_integer(:lists.reverse(acc)) @@ -2876,21 +2823,21 @@ end defimpl Inspect, for: Decimal do def inspect(dec, _opts) do - "Decimal.new(\"" <> Decimal.to_string(dec) <> "\")" + "Decimal.new(\"" <> Decimal.to_string(dec, :scientific, max_digits: :infinity) <> "\")" end end defimpl String.Chars, for: Decimal do def to_string(dec) do - Decimal.to_string(dec) + Decimal.to_string(dec, :scientific, max_digits: :infinity) end end # TODO: remove when we require Elixir 1.18 if Code.ensure_loaded?(JSON.Encoder) and function_exported?(JSON.Encoder, :encode, 2) do defimpl JSON.Encoder, for: Decimal do def encode(decimal, _encoder) do - [?", Decimal.to_string(decimal), ?"] + [?", Decimal.to_string(decimal, :scientific, max_digits: :infinity), ?"] end end end
test/decimal/context_test.exs+23 −19 modified@@ -97,23 +97,26 @@ defmodule Decimal.ContextTest do floor: d(1, 100, 99_998), ceiling: d(1, 101, 99_998) ] do - Context.with(%Context{precision: 3, rounding: rounding}, fn -> - assert Decimal.add(num, one) == result - assert :inexact in Context.get().flags - assert :rounded in Context.get().flags - end) + Context.with( + %Context{precision: 3, rounding: rounding, emax: :infinity, emin: :infinity}, + fn -> + assert Decimal.add(num, one) == result + assert :inexact in Context.get().flags + assert :rounded in Context.get().flags + end + ) end end test "with_context/2: large exponent gap addition with zero" do num = d(1, 1, 100_000) - Context.with(%Context{precision: 3}, fn -> + Context.with(%Context{precision: 3, emax: :infinity, emin: :infinity}, fn -> assert Decimal.add(d(1, 0, -100_000), num) == d(1, 100, 99_998) assert Context.get().flags == [:rounded] end) - Context.with(%Context{precision: 3}, fn -> + Context.with(%Context{precision: 3, emax: :infinity, emin: :infinity}, fn -> assert Decimal.add(d(1, 0, 100_000), d(1, 1, 0)) == d(1, 1, 0) assert Context.get().flags == [] end) @@ -132,11 +135,14 @@ defmodule Decimal.ContextTest do floor: d(1, 999, 99_997), ceiling: d(1, 1000, 99_997) ] do - Context.with(%Context{precision: 3, rounding: rounding}, fn -> - assert Decimal.sub(num, one) == result - assert :inexact in Context.get().flags - assert :rounded in Context.get().flags - end) + Context.with( + %Context{precision: 3, rounding: rounding, emax: :infinity, emin: :infinity}, + fn -> + assert Decimal.sub(num, one) == result + assert :inexact in Context.get().flags + assert :rounded in Context.get().flags + end + ) end end @@ -145,13 +151,13 @@ defmodule Decimal.ContextTest do num = %Decimal{sign: 1, coef: 1, exp: @bounded_smoke_exp} one = d(1, 1, 0) - Context.with(%Context{precision: 3}, fn -> + Context.with(%Context{precision: 3, emax: :infinity, emin: :infinity}, fn -> assert_runs_quickly("add/2 large exponent gap", fn -> assert Decimal.add(num, one) == %Decimal{sign: 1, coef: 100, exp: @bounded_smoke_exp - 2} end) end) - Context.with(%Context{precision: 3}, fn -> + Context.with(%Context{precision: 3, emax: :infinity, emin: :infinity}, fn -> assert_runs_quickly("sub/2 large exponent gap", fn -> assert Decimal.sub(num, one) == %Decimal{sign: 1, coef: 1000, exp: @bounded_smoke_exp - 3} end) @@ -163,7 +169,7 @@ defmodule Decimal.ContextTest do zero = %Decimal{sign: 1, coef: 0, exp: -@bounded_smoke_exp} num = %Decimal{sign: 1, coef: 1, exp: @bounded_smoke_exp} - Context.with(%Context{precision: 3}, fn -> + Context.with(%Context{precision: 3, emax: :infinity, emin: :infinity}, fn -> assert_runs_quickly("add/2 large exponent gap with zero", fn -> assert Decimal.add(zero, num) == %Decimal{sign: 1, coef: 100, exp: @bounded_smoke_exp - 2} end) @@ -184,10 +190,8 @@ defmodule Decimal.ContextTest do Context.with(%Context{precision: 111}, fn -> assert [] = Context.get().flags - Decimal.div( - ~d"10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - ~d"17" - ) + coef = :erlang.binary_to_integer("1" <> String.duplicate("0", 106)) + Decimal.div(Decimal.new(1, coef, 0), ~d"17") assert [:rounded] = Context.get().flags end)
test/decimal_test.exs+43 −24 modified@@ -117,36 +117,39 @@ defmodule DecimalTest do end) end - test "parse/1 with very long digit strings" do + test "parse/2 with very long digit strings under explicit limits" do digits = String.duplicate("9", 50_000) coef = :erlang.binary_to_integer(digits) - assert Decimal.parse(digits) == {%Decimal{coef: coef, exp: 0}, ""} + opts = [max_digits: :infinity, max_exponent: :infinity] - assert Decimal.parse("0." <> digits) == {%Decimal{coef: coef, exp: -50_000}, ""} + assert Decimal.parse(digits, opts) == {%Decimal{coef: coef, exp: 0}, ""} + + assert Decimal.parse("0." <> digits, opts) == {%Decimal{coef: coef, exp: -50_000}, ""} int = String.duplicate("1", 30_000) frac = String.duplicate("5", 20_000) expected_coef = :erlang.binary_to_integer(int <> frac) - assert Decimal.parse(int <> "." <> frac) == {%Decimal{coef: expected_coef, exp: -20_000}, ""} - # Trailing non-digit boundary still parses correctly. - assert Decimal.parse(digits <> "x") == {%Decimal{coef: coef, exp: 0}, "x"} + assert Decimal.parse(int <> "." <> frac, opts) == + {%Decimal{coef: expected_coef, exp: -20_000}, ""} + + assert Decimal.parse(digits <> "x", opts) == {%Decimal{coef: coef, exp: 0}, "x"} end test "parse/2 enforces digit count on very long strings" do digits = String.duplicate("9", 50_000) - assert Decimal.parse(digits, max_digits: 50_000) == + assert Decimal.parse(digits, max_digits: 50_000, max_exponent: :infinity) == {%Decimal{coef: :erlang.binary_to_integer(digits), exp: 0}, ""} - assert Decimal.parse(digits, max_digits: 49_999) == :error + assert Decimal.parse(digits, max_digits: 49_999, max_exponent: :infinity) == :error fractional = "0." <> digits - assert Decimal.parse(fractional, max_digits: 50_001) == + assert Decimal.parse(fractional, max_digits: 50_001, max_exponent: :infinity) == {%Decimal{coef: :erlang.binary_to_integer("0" <> digits), exp: -50_000}, ""} - assert Decimal.parse(fractional, max_digits: 50_000) == :error + assert Decimal.parse(fractional, max_digits: 50_000, max_exponent: :infinity) == :error end test "nan?/1" do @@ -336,7 +339,7 @@ defmodule DecimalTest do assert Decimal.compare("Inf", "Inf") == :eq - assert Decimal.compare(~d"5e10000000000", ~d"0") == :gt + assert Decimal.compare(Decimal.new(1, 5, 10_000_000_000), ~d"0") == :gt assert_raise Error, fn -> Decimal.compare(~d"nan", ~d"0") @@ -713,20 +716,30 @@ defmodule DecimalTest do assert Decimal.to_string(~d"-inf", :raw) == "-Infinity" end - test "to_string/2 with large coefficients" do + test "to_string/3 with large coefficients under explicit max_digits" do digits = String.duplicate("9", 2_500) coef = String.to_integer(digits) - assert Decimal.to_string(%Decimal{sign: 1, coef: coef, exp: 0}, :normal) == digits + assert Decimal.to_string(%Decimal{sign: 1, coef: coef, exp: 0}, :normal, max_digits: 2_500) == + digits - assert Decimal.to_string(%Decimal{sign: -1, coef: coef, exp: -2_500}, :normal) == - "-0." <> digits + assert Decimal.to_string( + %Decimal{sign: -1, coef: coef, exp: -2_500}, + :normal, + max_digits: 2_501 + ) == "-0." <> digits - assert Decimal.to_string(%Decimal{sign: 1, coef: coef, exp: -2_499}, :scientific) == - "9." <> String.duplicate("9", 2_499) + assert Decimal.to_string( + %Decimal{sign: 1, coef: coef, exp: -2_499}, + :scientific, + max_digits: 2_500 + ) == "9." <> String.duplicate("9", 2_499) - assert Decimal.to_string(%Decimal{sign: 1, coef: coef, exp: -1}, :raw) == - digits <> "E-1" + assert Decimal.to_string( + %Decimal{sign: 1, coef: coef, exp: -1}, + :raw, + max_digits: 2_501 + ) == digits <> "E-1" end test "to_string/2 xsd" do @@ -758,7 +771,9 @@ defmodule DecimalTest do assert Decimal.to_string(~d"123", :scientific, max_digits: 3) == "123" assert Decimal.to_string(~d"1e2", :normal, max_digits: 3) == "100" assert Decimal.to_string(~d"1e2", :xsd, max_digits: 4) == "100.0" - assert Decimal.to_string(~d"1e100000", :scientific, max_digits: 7) == "1E+100000" + + assert Decimal.to_string(Decimal.new(1, 1, 100_000), :scientific, max_digits: 7) == + "1E+100000" assert_raise ArgumentError, ~r/:scientific representation requires 3 digits/, fn -> Decimal.to_string(~d"123", :scientific, max_digits: 2) @@ -773,11 +788,11 @@ defmodule DecimalTest do end assert_raise ArgumentError, ~r/:normal representation requires 100001 digits/, fn -> - Decimal.to_string(~d"1e100000", :normal, max_digits: 1_000) + Decimal.to_string(Decimal.new(1, 1, 100_000), :normal, max_digits: 1_000) end assert_raise ArgumentError, ~r/:xsd representation requires 100002 digits/, fn -> - Decimal.to_string(~d"1e100000", :xsd, max_digits: 1_000) + Decimal.to_string(Decimal.new(1, 1, 100_000), :xsd, max_digits: 1_000) end end @@ -1256,13 +1271,17 @@ defmodule DecimalTest do assert_raise Decimal.Error, ": number bigger than DBL_MAX: Decimal.new(\"9.999999999999999999E+1000000000000000000000017\")", fn -> - Decimal.to_float(Decimal.new("9999999999999999999e999999999999999999999999")) + Decimal.to_float( + Decimal.new(1, 9_999_999_999_999_999_999, 999_999_999_999_999_999_999_999) + ) end assert_raise Decimal.Error, ": number smaller than DBL_MIN: Decimal.new(\"9.9999999999999E-999999999999999999999986\")", fn -> - Decimal.to_float(Decimal.new("99999999999999e-999999999999999999999999")) + Decimal.to_float( + Decimal.new(1, 99_999_999_999_999, -999_999_999_999_999_999_999_999) + ) end end
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
7- github.com/advisories/GHSA-rhv4-8758-jx7vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-32686ghsaADVISORY
- cna.erlef.org/cves/CVE-2026-32686.htmlnvdWEB
- github.com/ericmj/decimal/commit/6a523f3a73b8c9974540e21c7aa88f1258bb35aenvdWEB
- github.com/ericmj/decimal/releases/tag/v3.0.0ghsaWEB
- github.com/ericmj/decimal/security/advisories/GHSA-rhv4-8758-jx7vnvdWEB
- osv.dev/vulnerability/EEF-CVE-2026-32686nvdWEB
News mentions
1- Siemens SIMATICCISA Alerts