CVE-2026-75758 Overview
CVE-2026-75758 is an Uncontrolled Recursion vulnerability [CWE-674] in the Elixir standard library. An attacker who controls a list passed to inspect/1, List.to_string/1, or List.to_charlist/1 can exhaust a BEAM node's memory. The flaw resides in Inspect.List's charlist branch in lib/elixir/lib/inspect.ex, where a mis-classified list triggers infinite non-tail recursion inside a rescue clause. Each recursion cycle re-walks the list and retains stack frames until the process is killed by max_heap_size or the node runs out of memory. The issue affects Elixir versions from 1.15.0-rc.0 before 1.18.5, from 1.19.0-rc.0 before 1.19.6, and from 1.20.0-rc.0 before 1.20.4.
Critical Impact
An attacker-controlled list crafted to bypass charlist printability detection can exhaust BEAM node memory, resulting in denial of service across every Elixir process and application hosted on that node.
Affected Products
- Elixir 1.15.0-rc.0 through versions before 1.18.5
- Elixir 1.19.0-rc.0 through versions before 1.19.6
- Elixir 1.20.0-rc.0 through versions before 1.20.4
Discovery Timeline
- 2026-08-28 - CVE-2026-75758 published to NVD
- 2026-09-01 - Last updated in NVD database
Technical Details for CVE-2026-75758
Vulnerability Analysis
The defect lives in the charlist inspection path of Inspect.List. Elixir classifies a list as a charlist using List.ascii_printable?/2, which examines only the first :printable_limit elements (4096 by default). After classification, Elixir calls IO.chardata_to_string/1 on the entire list. When the printable prefix exceeds the limit but the tail contains a non-code-point element such as an atom, an out-of-range integer, or an improper tail, the conversion raises ArgumentError.
List.to_string/1 catches this error in a rescue clause that builds its message by interpolating inspect(list). That interpolation re-enters the same charlist branch and raises again. The nested inspect/1 call is an argument to raise, so recursion does not occur in tail position. Stack frames accumulate, each cycle re-walks the list, and the BEAM process consumes memory until the node terminates. List.to_charlist/1 shares the same rescue shape and is affected identically.
Below the printable limit, the inner inspect/1 observes the invalid element within its counter and renders the list in ordinary bracket form, producing a single ArgumentError without recursion. Exploitation therefore requires a list larger than :printable_limit with a poisoned element after that boundary.
Root Cause
The root cause is a mismatch between the bounded scan in List.ascii_printable?/2 and the unbounded conversion in IO.chardata_to_string/1, combined with an error-handling path that calls back into the same faulty inspection logic without a base case.
Attack Vector
The attack vector is local. Any Elixir or Erlang service that accepts external data and passes a resulting list into inspect/1, List.to_string/1, or List.to_charlist/1 can be forced into the recursion. Logging pipelines, formatters, and error handlers are common triggers because they routinely inspect untrusted terms.
# Patch diff — lib/elixir/lib/list.ex
Please check the given list or call inspect/1 to get the list representation, got:
- #{inspect(list)}
+ #{inspect(list, charlists: :as_lists)}
"""
else
result when is_binary(result) ->
Source: Elixir commit 0bba588. The fix forces the error message to render the list as a plain list, bypassing the charlist branch and preventing re-entry into the faulty code path.
Detection Methods for CVE-2026-75758
Indicators of Compromise
- BEAM node processes terminated by max_heap_size violations or system OOM killer entries referencing beam.smp.
- Rapidly growing process heap sizes correlated with calls to List.to_string/1, List.to_charlist/1, or Inspect.List frames in crash dumps.
- Repeated ArgumentError exceptions originating from IO.chardata_to_string/1 in application logs shortly before node crashes.
Detection Strategies
- Inventory Elixir runtimes across servers and containers, and flag any deployment running versions earlier than 1.18.5, 1.19.6, or 1.20.4.
- Use Software Composition Analysis (SCA) tools with feeds that include GHSA-jf5q-v438-665c to detect vulnerable Elixir versions in build manifests.
- Review application code paths that pass externally-controlled data into inspect/1, List.to_string/1, or List.to_charlist/1.
Monitoring Recommendations
- Alert on sudden BEAM VM memory growth or repeated erlang:system_info(:memory) spikes on production nodes.
- Ship crash dumps and erl_crash.dump files to a central log store and search for Inspect.List or chardata_to_string frames.
- Track process restart rates in supervision trees to catch DoS attempts that force sequential worker termination.
How to Mitigate CVE-2026-75758
Immediate Actions Required
- Upgrade Elixir to 1.18.5, 1.19.6, or 1.20.4 or later depending on the release branch in use.
- Rebuild and redeploy release artifacts to ensure the patched standard library ships to every runtime node.
- Audit application code that inspects or converts externally-supplied lists and add explicit type validation before calling the affected functions.
Patch Information
The Elixir team fixed the recursion by rendering the offending list with charlists: :as_lists inside the rescue message, preventing re-entry into the charlist branch. Fixes were shipped across the supported branches in commits 0bba588, 1eff1ac, 5230d73, and a983c8c. Additional context is available in the CNA report from the Erlang Ecosystem Foundation and the OSV entry EEF-CVE-2026-75758.
Workarounds
- Set a per-process max_heap_size on workers that handle untrusted input so a single poisoned list cannot exhaust the entire node.
- Validate incoming lists for length and element type before passing them to inspect/1, List.to_string/1, or List.to_charlist/1.
- Where source changes are possible, replace inspect(list) calls in error paths with inspect(list, charlists: :as_lists) to match the upstream patch behavior.
# Verify the installed Elixir version on each node
elixir --version
# Example mix.exs constraint enforcing a patched release
# {:elixir, "~> 1.18.5 or ~> 1.19.6 or ~> 1.20.4"}
# Enforce a per-process heap ceiling when spawning workers that touch untrusted input
# spawn_opt(fun, [{:max_heap_size, %{size: 10_000_000, kill: true, error_logger: true}}])
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.