CVE Patch Review

CVE-2026-32686: Decimal128 Defaults Turn Unbounded Exponent Parsing into a Root-Cause Fix

CVE-2026-32686 · GHSA-RHV4-8758-JX7V · Updated 2026-05-12 Root-cause

Summary

The patch changes ericmj/decimal from permissive, effectively unbounded parsing and context defaults to bounded IEEE 754 decimal128 defaults. It adds default limits for parse/cast and output rendering, and changes the default Decimal.Context exponent range to finite values. This directly addresses the resource-exhaustion primitive described in CVE-2026-32686 by rejecting oversized exponents and digit counts before expensive arithmetic alignment or string expansion occurs.

Analysis

Vulnerability

CVE-2026-32686 and the corresponding GHSA advisory describe a denial-of-service condition in ericmj/decimal where unbounded exponent parsing permits attacker-controlled scientific notation values to drive excessive memory allocation. The vulnerable behavior is visible in the pre-patch documentation and code paths: parsing accepted arbitrary exponent magnitude for backwards compatibility, and the default Decimal.Context used emax: :infinity and emin: :infinity, so already-created decimals could carry pathological exponents into later arithmetic.

The commit 6a523f3a73b8c9974540e21c7aa88f1258bb35ae also documents the operational impact: some operations and output formats may allocate memory or CPU proportional to the expanded size of the number. That is the core failure mode for this CVE: a small textual input can induce huge internal bignum alignment or expanded rendering work, leading to BEAM VM OOM or severe resource exhaustion.

defstruct precision: 28,
          emax: :infinity,
          emin: :infinity,

# parse/1 accepted input without default max_digits/max_exponent limits

Because the issue originates at object creation and representation boundaries, relying only on arithmetic-time overflow/underflow handling is insufficient. Inputs must be constrained before they become oversized decimals, and rendering paths that can expand exponents must also be bounded.

Patch

The patch in the upstream commit makes three material changes.

First, it introduces bounded defaults for parsing and casting. cast/1 now delegates to a bounded helper using default parse limits, and parse/1 now uses default limits rather than accepting arbitrary exponent and digit counts. The new defaults are IEEE 754 decimal128-aligned: max_digits = 34 and max_exponent = 6144. Explicit arities still allow callers to override or disable limits with :infinity.

Second, it bounds output generation by default. to_string/2 now checks a default maximum digit budget of 6178 before rendering, which the patch explains as precision + emax, sufficient for any in-range decimal128 value even in expansion-prone formats such as :normal and :xsd. This closes a related amplification path where a previously accepted decimal could trigger large output allocation.

Third, it changes the default runtime context from unbounded exponents to decimal128-style finite bounds. In Decimal.Context, the default struct becomes precision: 34, emax: 6_144, and emin: -6_143. Tests that depended on the old unbounded behavior were updated to opt back into :infinity explicitly, which is a strong signal that the new bounded behavior is intentional and systemic rather than cosmetic.

@default_max_digits 34
@default_max_exponent 6_144
@default_to_string_max_digits 6_178

def cast(term), do: cast_with_limits(term, default_parse_limits())

def parse(binary) when is_binary(binary) do
  parse_with_limits(binary, default_parse_limits())
end

defstruct precision: 34,
          emax: 6_144,
          emin: -6_143,

These changes align with the advisory guidance in GHSA-RHV4-8758-JX7V and the CVE record at CVE.org: the fix is to prevent pathological exponent values from being accepted and propagated under default usage.

Review

Pros

  • The patch addresses the vulnerability at the earliest trust boundary by changing default parse and cast behavior, which is where attacker-controlled strings first become Decimal values.
  • It also covers a second expansion surface, to_string, preventing large allocations during rendering of hostile or previously persisted values.
  • Changing Decimal.Context defaults to finite decimal128 bounds reduces the chance that arithmetic on accepted values can still drift into unbounded exponent behavior.
  • The implementation is coherent: documentation, defaults, helper functions, and tests were all updated together in the same commit 6a523f3a73b8c9974540e21c7aa88f1258bb35ae.
  • Backward-compatibility escape hatches remain available through explicit option overrides and explicit :infinity settings, which is useful for trusted internal workloads.

Cons

  • This is a behavior-changing patch. Applications that relied on permissive parsing of very large exponents or digit counts will now receive :error or rendering exceptions unless they opt out explicitly.
  • The patch cannot retroactively sanitize already-created or persisted Decimal values if an application explicitly disables limits or loads hostile values from another source.
  • Security now depends partly on callers not reintroducing the old risk by passing :infinity for untrusted input paths.
  • The review material here shows updated tests around context defaults, but not exhaustive new tests for all parse and rendering rejection edge cases; those may exist outside the provided snippet, but they are not visible in the supplied source digest.

Verdict

Root-cause.

The patch fixes the underlying design flaw by replacing unsafe unbounded defaults with bounded decimal128 defaults across the creation, operation, and rendering lifecycle. The vulnerable condition was not merely an arithmetic bug in one code path; it was the library-wide acceptance and propagation of arbitrarily large exponents. By default-limiting parse, cast, to_string, and Decimal.Context, the patch removes the primary resource-exhaustion primitive described in NVD and GHSA. The remaining risk is mainly opt-out misuse, not an unfixed default-path vulnerability.

Sources