Skip to content

CVE-2026-69664: Verified Reproduction

CVE-2026-69664: Erlang/OTP inets httpd parks request worker indefinitely on malformed chunk size sent after headers unauthenticated remote DoS

CVE-2026-69664 is verified against erlang/otp · github. Affected versions: OTP >= 18.1.4 < 27.3.4.17; >= 28.0 < 28.5.0.6; >= 29.0 < 29.0.6 (inets >= 6.0.3 < 9.3.2.7; >= 9.4 < 9.6.2.3; >= 9.7 < 9.7.2). Vulnerability class: DoS. This high reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00370.

REPRO-2026-00370 erlang/otp · github DoS Sep 24, 2026 CVE entry .txt
Severity
HIGH
Confidence
HIGH
Reproduced in
323m 56s
Tool calls
507
Spend
$10.56
01 · Overview

What Is CVE-2026-69664?

CVE-2026-69664 is a high-severity DoS vulnerability affecting erlang/otp OTP >= 18.1.4 < 27.3.4.17; >= 28.0 < 28.5.0.6; >= 29.0 < 29.0.6 (inets >= 6.0.3 < 9.3.2.7; >= 9.4 < 9.6.2.3; >= 9.7 < 9.7.2). Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00370).

02 · Severity & CVSS

CVE-2026-69664 Severity

CVE-2026-69664 is rated high severity.

HIGH threat level
Weakness CWE-772

High — serious impact or readily exploitable. Prioritize remediation.

03 · Affected Versions

Affected erlang/otp Versions

erlang/otp · github versions OTP >= 18.1.4 < 27.3.4.17; >= 28.0 < 28.5.0.6; >= 29.0 < 29.0.6 (inets >= 6.0.3 < 9.3.2.7; >= 9.4 < 9.6.2.3; >= 9.7 < 9.7.2) are affected.

How to Reproduce CVE-2026-69664

$ pruva-verify REPRO-2026-00370
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00370/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-69664

Denial of service — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

HTTP/1.1 POST with Transfer-Encoding: chunked; headers (ending CRLFCRLF) in one TCP write, then a separate TCP write with non-hex chunk-size line 'ZZZ\r\n', socket held open with no further bytes

Attack chain
  1. inets httpd TCP listener
  2. httpd_manager accept
  3. httpd_request_handler handle_info({tcp,...}) bare-catch decoder continuation
  4. error tuple stored as NewMFA
  5. socket re-armed {active,once} with no timeout
  6. worker parked indefinitely; repeated across >max_clients (150) connections
  7. 503 heavy-load denial for legitimate clients
How the agent worked 1,192 events · 507 tool calls · 30h 0m
30h 0mDuration
507Tool calls
255Reasoning steps
1,192Events
50Dead-ends
Agent activity over 30h 0m
Policy
2
Support
8
Repro
443
Judge
25
Variant
708
Verify
1
0:001800:05

Root Cause and Exploit Chain for CVE-2026-69664

Versions: Erlang/OTP >= 18.1.4 < 27.3.4.17 | >= 28.0 < 28.5.0.6 |

In Erlang/OTP's built-in inets HTTP server (httpd), a request that uses Transfer-Encoding: chunked and whose chunk-size line arrives in a TCP write separate from the headers is decoded through a continuation invoked via a bare catch in httpd_request_handler:handle_info/2. When the chunk-size line is not valid hexadecimal (e.g. ZZZ\r\n), http_chunk:decode_size/4 throws {error, {chunk_size, Line}}; the bare catch flattens this throw into a plain term that matches none of the error clauses, so it falls into the catch-all NewMFA clause: the error tuple is stored as the next decoder continuation and the socket is re-armed with {active, once}. Because the request timeout was already cancelled when the headers were accepted and minimum_bytes_per_second is disabled by default, no timer ever reclaims the worker — it is parked indefinitely while the attacker keeps the TCP connection open with zero further bytes. Repeating this across connections exhausts the request-worker pool (max_clients, documented default 150) and denies service to legitimate clients (CWE-772, Missing Release of Resource after Effective Lifetime).

  • Package/component affected: lib/inets http_server/httpd_request_handler.erl (interaction with http_server/http_chunk.erl decode_size/4)
  • Affected versions: Erlang/OTP >= 18.1.4 < 27.3.4.17 | >= 28.0 < 28.5.0.6 |

    = 29.0 < 29.0.6 (inets >= 6.0.3 < 9.3.2.7 | >= 9.4 < 9.6.2.3 | >= 9.7 < 9.7.2). Introduced by commit 77acb473d8f056f6f534395f131c6e45693797f0. Tested here: OTP-27 maintenance line, vulnerable commit e9f49f57cef6e38fd13c4b0cee1eb5509ef471e8 (parent of the fix; inets 9.3.2.6).

  • Risk level and consequences: High (CVSS 8.7, AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H). Unauthenticated remote attacker, default configuration, availability-only impact: each parked connection permanently consumes one request-handler process and one socket until the attacker voluntarily disconnects; enough such connections make the server answer every new client with 503 Service Unavailable ("heavy load").

Impact Parity

  • Disclosed/claimed maximum impact: Unauthenticated remote denial of service against the default inets httpd configuration (dos).
  • Reproduced impact from this run: dos — full remote denial of service demonstrated end-to-end over the real TCP listener:
    • a single segmented request parks a httpd_request_handler process indefinitely (the same process pid observed alive with {message_queue_len,0},{status,waiting} and zero traffic for the full 165 s observation window, beyond the 150 s default keep-alive timeout; no response is ever sent to the parked socket);
    • a 300-connection exhaustion wave of identical parked requests fills the worker pool at the documented max_clients default of 150 (confirmed against the server-side census before the probe is fired), after which fresh legitimate GET requests are denied with 503 Service Unavailable ("heavy load"); the denial persists while the attacker holds the sockets (re-verified after a 25 s wait, census still pinned at 150) and disappears only after the attacker disconnects (census drops to 0, legit 200);
    • the fixed build (fix commit a3adf630...) responds 400 Bad Request immediately on the exact same segmented input, closes the connection, reclaims the worker (census 0), and the same 300-connection wave cannot deny service to legitimate clients (200 throughout).
  • Parity: full (claimed DoS fully demonstrated; no code execution was claimed or observed).
  • Not demonstrated: none — no impact beyond availability was claimed.

Root Cause

httpd_request_handler.erl, handle_info({Proto, Socket, Data}, ...) clause (vulnerable code, line 241 of the tested checkout):

PROCESSED = (catch Module:Function([Data | Args])),
...
case PROCESSED of
    {ok, Result} -> ...;
    {error, {size_error, ...}, Version} -> ...;
    {error, {version_error, ...}, Version} -> ...;
    {error, {bad_request, ...}, Version} -> ...;
    {http_chunk = Module, Function, Args} when ChunkState =/= undefined -> ...;
    NewMFA ->
        setopts(Socket, SockType, [{active, once}]),
        {noreply, State#state{mfa = NewMFA}}   %% <-- parks here
end

Flow for a chunked request:

  1. Headers arrive; httpd_request:parse/2 returns {ok, {continue, http_chunk, decode_size, Args}}; handle_msg/2 sets mfa = {http_chunk, decode_size, Args} after the request timeout was cancelled in the {ok, ...} branch (cancel_request_timeout/1). No new timer is armed; minimum_bytes_per_second is false by default, so data_receive_counter/2 arms nothing either.
  2. The chunk-size line arrives in a later TCP write, so handle_info({tcp, Socket, Data}, ...) runs with Module = http_chunk, Function = decode_size.
  3. http_chunk:decode_size(<<"ZZZ\r\n">>, ...) throws {error, {chunk_size, Line}} for a non-hex chunk size.
  4. The bare catch converts the throw into the plain term {error, {chunk_size, Line}}, which matches no clause of the case (the {error, ...} clauses expect 3-tuples with a Version), so the catch-all NewMFA clause stores the error tuple as the next continuation MFA and re-arms the socket with {active, once}.
  5. The attacker sends no more bytes; no timeout exists; the worker waits forever (only tcp_closed would free it). Each such connection leaks one handler process + one socket until client disconnect.

Note the contrast that proves segmentation matters: when the body arrives together with the headers, handle_body/3 calls http_chunk:decode/... inside a proper try ... catch and returns 400 Bad Request — that path was never vulnerable. The trigger requires the chunk-size line to arrive in a later write, exactly as the advisory states.

  • Fix: OTP maintenance commits a3adf63078438c86527d704e23282b7721d8ca12 (OTP 27), df1a9ca4666e2fdfc44886bfaae76de086d803f6 (OTP 28), bd4e74348c6be8a49f060da6fd48d43f3a960292 (OTP 29). The fix wraps the decoder call as catch throw:{error, Error} when Module =:= http_chunk -> (line 287 in the fixed tree) so a thrown chunk-decode error is routed to the error handling that sends 400 Bad Request and terminates the worker, plus a regression test in httpd_SUITE for the segmented-write case.

Reproduction Steps

  1. Script: bundle/repro/reproduction_steps.sh (self-contained; run twice consecutively — both runs must exit 0).
  2. What the script does:
    • Resolves the prepared project cache (bundle/project_cache_context.json); with no cached repo present it clones/fetches OTP fix commit a3adf63078438c86527d704e23282b7721d8ca12 (depth 2) from https://github.com/erlang/otp.git into bundle/artifacts/otp.
    • Builds the real Erlang/OTP from source at the vulnerable commit e9f49f57cef6e38fd13c4b0cee1eb5509ef471e8 (parent of the fix; the top-level make tolerates the wx-disabled debugger failure, then make local_setup + targeted lib/stdlib/lib/inets builds complete the system) and verifies the vulnerable checkout lacks the fix hunk (records the bare catch at line 241 in evidence/vuln_bare_catch.txt).
    • For each of two clean vulnerable attempts: starts a fresh erl node running the real inets httpd on an ephemeral port with default configuration (keep_alive_timeout 150 s and minimum_bytes_per_second false left at their defaults on purpose; max_clients set to its documented default of 150 — httpd_conf only persists the key when the user supplies it, and the manager's handle_new_connection/4 looks it up without a default, so an unset key means no bound at all), plus a 1 Hz server-side census of httpd_request_handler processes. Then attack_client.py: (a) healthchecks with GET (Connection: close), (b) opens a POST with Transfer-Encoding: chunked, sends headers in one TCP write, waits 0.3 s, sends ZZZ\r\n as a separate write, and holds the socket open with no further bytes for 165 s (beyond the 150 s default keep-alive timeout), (c) sends a legit request mid-park (server still appears healthy), (d) opens a 300-connection wave of identical parked requests, waits until the server-side census confirms the pool is actually full (>= 150 parked handlers) before probing (this removes the accept-backlog race where a probe fired while the pool was still filling could be served 200), then shows fresh legitimate requests are denied (503 Service Unavailable, "heavy load"), still denied after a 25 s wait, (e) disconnects the attacker and shows the server recovers (200, census back to 0).
    • Checks out the fix commit, rebuilds lib/inets in the same tree, verifies the fix hunk is present (line 287 recorded in evidence/fix_hunk.txt), and runs two clean fixed attempts with the same attack: the bad chunk-size line must produce 400 Bad Request + server close on the parked socket, census back to 0, and legit requests still served 200 during the full exhaustion wave.
    • Evaluates every attempt against the criteria above (evidence/summary.json, pass must be true), writes runtime_manifest.json, and exits 0 = confirmed / 1 = not reproduced.
  3. Expected evidence of reproduction (all produced by the script):
    • evidence/summary.json with pass: true and every per-attempt check true for two vulnerable and two fixed attempts;
    • evidence/vulnerable_attempt{1,2}/census.log: PATH vsn=9.3.2.6 with the inets beams loaded from the build tree, then CENSUS lines showing one parked handler for the whole 165 s window and 150 parked handlers during the wave;
    • evidence/vulnerable_attempt{1,2}/result.json: parked_socket_status: "timeout", parked_socket_recv: "", legit_after_exhaustion → HTTP/1.1 503 Service Unavailable, exhausted_recheck → still 503, after_close_legit → 200;
    • evidence/fixed_attempt{1,2}/result.json: parked_socket_recv starting with HTTP/1.1 400 Bad Request, parked_socket_status: "closed", census 0, legit 200 during the wave.

Evidence

  • Environment: Linux x86_64, 4 cores; OTP built from source at bundle/artifacts/otp (fallback location — the prepared project cache contained no repo); vulnerable checkout e9f49f57cef6e38fd13c4b0cee1eb5509ef471e8, fixed checkout a3adf63078438c86527d704e23282b7721d8ca12, both verified by hunk inspection before their respective attempts.
  • Primary logs and artifacts (all under bundle/):
    • bundle/logs/reproduction_steps.log — full transcript of the final passing run (run 2); bundle/logs/reproduction_steps_run1.log — the first consecutive passing run. Both end with RESULT: CVE-2026-69664 CONFIRMED on vulnerable build; fixed build fails closed.
    • bundle/repro/evidence/summary.json — machine-readable pass/fail per attempt and per criterion (pass: true, 4/4 attempts pass).
    • bundle/repro/evidence/vulnerable_attempt{1,2}/census.log — server-side process census, 1 Hz, 196 samples per attempt (PATH line + CENSUS lines; final entry after attacker disconnect shows count 0).
    • bundle/repro/evidence/vulnerable_attempt{1,2}/result.json, client_output.log — client-side observations of each phase.
    • bundle/repro/evidence/fixed_attempt{1,2}/... — same for the fixed build.
    • bundle/repro/evidence/vuln_bare_catch.txt (241: PROCESSED = (catch Module:Function([Data | Args])),), fix_hunk.txt (287: catch throw:{error, Error} when Module =:= http_chunk ->) — source-line proof that the vulnerable tree has the bare catch and the fixed tree has the corrected clause.
    • bundle/repro/runtime_manifest.json — runtime evidence manifest with per-artifact SHA-256 (all verified) and target identity git:https://github.com/erlang/otp@e9f49f57cef6e38fd13c4b0cee1eb5509ef471e8 (digest fac886b1b27ab179e45c1c7bf166b06ff9e1f3800dd1334f5f510e6a5cf67449).
  • Key excerpts (from the final run's result.json files):
    • Vulnerable, parked worker census (both attempts): CENSUS ... 1 <0.NNN.0>:[{message_queue_len,0},{status,waiting}] at t=3/45/95/162 s — the same pid for the entire 165 s window; parked_socket_status: "timeout", parked_socket_recv: "".
    • Vulnerable, exhaustion: parked=300/300; pool_full=True (census=150 ...); legit_after_exhaustion → HTTP/1.1 503 Service Unavailable (heavy load); exhausted_recheck after 25 s → still HTTP/1.1 503 Service Unavailable, census 150; after_close_legit → HTTP/1.1 200 OK, census 0.
    • Fixed: parked_socket_recv = HTTP/1.1 400 Bad Request ..., parked_socket_status: "closed", census count 0, and legit_after_exhaustion → HTTP/1.1 200 OK during the same wave.
  • Note on version labels: the fixed checkout still reports vsn=9.3.2.6 in its PATH line because the maintenance fix commit does not bump the OTP/inets version file; the authoritative discriminators between the two builds are the recorded fix-hunk lines (241 bare catch vs 287 catch throw:{error, Error}) and the behavioral divergence (indefinite park + 503 exhaustion vs immediate 400 + close + no denial).

Recommendations / Next Steps

  • Upgrade guidance: upgrade to OTP 27.3.4.17 / 28.5.0.6 / 29.0.6 (inets 9.3.2.7 / 9.6.2.3 / 9.7.2) or later, which contain the catch throw:{error, Error} fix.
  • Mitigations for interim: enable minimum_bytes_per_second in the httpd config so slow/stalled body receives are reaped; reduce exposure of inets httpd to untrusted networks. Be aware that leaving max_clients unset does not fall back to the documented default of 150 — httpd_conf only stores the value when the user supplies it, so an unset value effectively disables the connection cap (an even easier unbounded DoS); explicitly set it, ideally below 150.
  • Suggested fix approach: the merged fix wraps the decoder continuation call so chunk-decode throws are converted into a proper 400 Bad Request response and handler termination; keep that structure for every continuation invoked from handle_info.
  • Testing recommendations: the fix commit adds an httpd_SUITE case (invalid chunk size in a separate write). Keep regression coverage for the segmented-write case specifically — the single-write case was never vulnerable (handle_body/3 already had a proper try/catch), so a test that coalesces headers and body will not catch this class of bug. Consider also auditing other handle_info continuation consumers for bare catch patterns, asserting that a request timeout is always armed while a partially-received request is outstanding, and giving handle_new_connection/4's max_clients lookup the documented default.

Additional Notes

  • Idempotency: reproduction_steps.sh is safe to re-run; it reuses the in-tree build under bundle/artifacts/otp, re-checks out each commit (git checkout -f), and incrementally rebuilds only the changed inets modules. Two consecutive full runs were executed in this session and both passed (exit 0, summary.json pass: true); transcripts: bundle/logs/reproduction_steps_run1.log and bundle/logs/reproduction_steps_run2.log.
  • Negative/scope controls included in the proof:
    • fixed-commit build fails closed on the exact same segmented input (400 + close, worker reclaimed, no denial of service) — confirms the fix, not an environmental artifact;
    • the single-write variant (headers + ZZZ\r\n in one TCP write) returns 400 even on the vulnerable build, matching the advisory's note that the handle_body/3 path was never vulnerable (verified during development).
  • Deterministic exhaustion probe: the attack client polls the server-side census until the pool is observed full (>= 150 handlers) before firing the post-exhaustion legitimate request; an earlier revision with a fixed 2-second sleep could, under scheduling pressure, fire the probe while the accept backlog was still draining and observe a misleading 200.
  • Limitations: The DoS demonstration binds max_clients at its documented default (150); deployments that raised it need proportionally more parked connections, and deployments that left it unset are unbounded (see Recommendations). The census samples at 1 Hz from inside the Erlang node, so a handler that lived for only a fraction of a second could in principle be missed — not an issue here since the parked handler is verified alive across the whole 165 s observation window with the same pid. Background orchestration note: the full script takes ~10 minutes; execution environments with per-command time limits must run it detached (e.g. setsid nohup ... &).

CVE-2026-69664 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:002:20
0:00
session startedaccounts/fireworks/models/glm-5p3 · CVE-2026-69664 · REPRO-20
0:05
0:07
web search
0:33
0:33
extract_facts
no facts extracted
0:34
0:34
supportclaim_contract
0:40
0:40
0:40
0:43
0:43
0:43
1:02
1:03
$ls /pruva/project-cache/ 2>/dev/null; ls /pruva/project-cache/repo-mirrors/ 2>/dev/null; cat /pruva/project-cache/.pruva/cache_manifest.json 2>/dev/null; ls bundle/repro bundle/logs/repro bundle/support 2>/dev/null; cat bundle/exploit_knowledge_preflight.json 2>/dev/null; cat bundle/learning/exploit_knowledge_preflight.json
0.5s✓
{"cwd":"/workspace","duration_ms":76,"exit_code":0,"stderr":"","stdout":"repo-mirrors\n{\n  \"schema_version\": 1,\n  \"entries\": [\n    {\n      \"path\": \"repo-mirrors\",\n      \"reuse_class\": \"repo\",\n      \"provenance\": \"run_generated\"\n    }\n  ]\n}bundle/logs/repro:\n\nbundle/repro:\n\nbundle/support:\n{\n  \"schema_version\": 1,\n  \"generated_from\": [\n    \"bundle/claim_contract/report.json\",\n    \"bundle/project_cache_context.json\"\n  ],\n  \"reference_only\": true,\n  \"query\": {\n    \"objective\": \"claim[0] surface=network_protocol trigger=network_protocol entrypoint_kind=tcp_peer expected_impact=dos\",\n    \"primitive_kinds\": [],\n    \"target\": {\n      \"project_id\": \"82ee934f-5b47-4f99-8a8d-a9a9263441a5\",\n      \"mitigations\": {\n        \"aslr\": \"unknown\",\n        \"pie\": \"unknown\",\n        \"nx\": \"unknown\",\n        \"stack_canary\": \"unknown\",\n        \"relro\": \"unknown\"\n      }\n    },\n    \"project_id\": \"82ee934f-5b47-4f99-8a8d-a9a9263441a5\",\n    \"include_states\": [\n      \"validated\",\n      \"observed\",\n      \"candidate\"\n    ],\n    \"limit\": 25\n  },\n  \"total_result_count\": 0,\n  \"attached_result_… [truncated]
1:09
1:09
1:09
$ls -la /pruva/project-cache/repo-mirrors/; for d in /pruva/project-cache/repo-mirrors/*/; do echo "== $d"; git -C "$d" remote -v 2>/dev/null | head -2; git -C "$d" log --oneline -3 2>/dev/null; done
0.5s✗
{"cwd":"/workspace","duration_ms":77,"exit_code":128,"stderr":"","stdout":"total 0\ndrwx------ 2 vscode vscode 40 Sep  2 06:56 .\ndrwx------ 4 vscode vscode 80 Sep  2 06:56 ..\n== /pruva/project-cache/repo-mirrors/*/\n","success":false,"timeout_secs":120}
1:09
✗ error

Unknown error

1:17
1:18
$which erl erlc docker podman gcc make perl python3; erl -noshell -eval 'io:format("~s~n",[erlang:system_info(otp_release)]), halt().' 2>&1 | head -2; nproc; df -h /pruva /tmp | tail -3; free -g | head -2
0.5s✓
/usr/bin/docker
/usr/bin/gcc
/usr/bin/make
/usr/bin/perl
/usr/bin/python3
pruva-command: 1: erl: not found
4
Filesystem      Size  Used Avail Use% Mounted on
1:18
1:59
2:08
$cd /tmp && rm -rf otptest && git init -q otptest && cd otptest && git remote add origin https://github.com/erlang/otp.git && timeout 100 git fetch --depth 1 origin a3adf63078438c86527d704e23282b7721d8ca12 2>&1 | tail -3; echo "EXIT: $?"
8.8s✓
From https://github.com/erlang/otp
 * branch            a3adf63078438c86527d704e23282b7721d8ca12 -> FETCH_HEAD
EXIT: 0
2:09
$docker info 2>&1 | head -5; echo ---; timeout 30 curl -s https://api.github.com/repos/erlang/otp/commits/a3adf63078438c86527d704e23282b7721d8ca12 2>&1 | head -30
0.8s✓
{"cwd":"/workspace","duration_ms":413,"exit_code":0,"stderr":"","stdout":"Client:\n Version:    29.1.3\n Context:    default\n Debug Mode: false\n Plugins:\n---\n{\n  \"sha\": \"a3adf63078438c86527d704e23282b7721d8ca12\",\n  \"node_id\": \"C_kwDOAAW4j9oAKGEzYWRmNjMwNzg0MzhjODY1MjdkNzA0ZTIzMjgyYjc3MjFkOGNhMTI\",\n  \"commit\": {\n    \"author\": {\n      \"name\": \"Lukas Backström\",\n      \"email\": \"lukas@erlang.org\",\n      \"date\": \"2026-06-01T08:26:29Z\"\n    },\n    \"committer\": {\n      \"name\": \"Konrad Pietrzak\",\n      \"email\": \"konrad@erlang.org\",\n      \"date\": \"2026-08-21T14:21:26Z\"\n    },\n    \"message\": \"inets: Fix rejection of invalid chunk sizes\\n\\nBefore this fix the server would hang indefinitely without the need\\nto send any bytes to keep it running. This causes it to leak a process\\nwhich could be used to DOS the server.\",\n    \"tree\": {\n      \"sha\": \"90d23a65db45036bc7c393faa8546b01e9ea8cc9\",\n      \"url\": \"https://api.github.com/repos/erlang/otp/git/trees/90d23a65db45036bc7c393faa8546b01e9ea8cc9\"\n    },\n    \"url\": \"https://api.github.com/repos/erlang/otp/git/commits/a3adf63078438c86527d704e23282b7721d8ca12\",\n    \"c… [truncated]
2:14
2:16
$cd /tmp/otptest && git show FETCH_HEAD --stat | head -20 && git show FETCH_HEAD -- lib/inets | head -200
1.0s✓
{"cwd":"/workspace","duration_ms":700,"exit_code":0,"stderr":"","stdout":"commit a3adf63078438c86527d704e23282b7721d8ca12\nAuthor: Lukas Backström <lukas@erlang.org>\nDate:   Mon Jun 1 10:26:29 2026 +0200\n\n    inets: Fix rejection of invalid chunk sizes\n    \n    Before this fix the server would hang indefinitely without the need\n    to send any bytes to keep it running. This causes it to leak a process\n    which could be used to DOS the server.\n\n .devcontainer/devcontainer.json                    |    32 +\n .dir-locals.el                                     |    14 +\n .editorconfig                                      |    22 +\n .elp.toml                                          |    16 +\n .gitattributes                                     |     2 +\n .github/ISSUE_TEMPLATE/bug_report.md               |    23 +\n .github/ISSUE_TEMPLATE/feature_request.md          |    19 +\n .github/actions/build-base-image/action.yaml       |    84 +\n .github/dockerfiles/Dockerfile.32-bit              |    25 +\n .github/dockerfiles/Dockerfile.64-bit              |    35 +\ncommit a3adf63078438c86527d704e23282b7721d8ca12\nAuthor: Lukas Backström <lukas@erlang.org>\nDate:   Mon Jun 1 1… [truncated]
2:20

Artifacts and Evidence for CVE-2026-69664

Scripts, logs, diffs, and output captured during the reproduction.

bundle/logs/reproduction_steps.log7.3 KB
bundle/logs/reproduction_steps_run1.log7.1 KB
bundle/repro/attack_client.py10.2 KB
bundle/repro/evidence/fix_hunk.txt0.1 KB
bundle/repro/evidence/fixed_attempt1/census.log3.7 KB
bundle/repro/evidence/fixed_attempt1/client_output.log0.7 KB
bundle/repro/evidence/fixed_attempt1/port.txt0.0 KB
bundle/repro/evidence/fixed_attempt1/result.json3.0 KB
bundle/repro/evidence/fixed_attempt1/stdout.log0.1 KB
bundle/repro/evidence/fixed_attempt2/census.log1.6 KB
bundle/repro/evidence/fixed_attempt2/client_output.log0.7 KB
bundle/repro/evidence/fixed_attempt2/port.txt0.0 KB
bundle/repro/evidence/fixed_attempt2/result.json3.0 KB
bundle/repro/evidence/fixed_attempt2/stdout.log0.1 KB
bundle/repro/evidence/summary.json1.8 KB
bundle/repro/evidence/vuln_bare_catch.txt0.1 KB
bundle/repro/evidence/vulnerable_attempt1/census.log206.6 KB
bundle/repro/evidence/vulnerable_attempt1/client_output.log0.9 KB
bundle/repro/evidence/vulnerable_attempt1/port.txt0.0 KB
bundle/repro/evidence/vulnerable_attempt1/result.json18.4 KB
bundle/repro/evidence/vulnerable_attempt1/stdout.log0.1 KB
bundle/repro/evidence/vulnerable_attempt2/census.log206.6 KB
bundle/repro/evidence/vulnerable_attempt2/client_output.log0.9 KB
bundle/repro/evidence/vulnerable_attempt2/port.txt0.0 KB
bundle/repro/evidence/vulnerable_attempt2/result.json18.4 KB
bundle/repro/evidence/vulnerable_attempt2/stdout.log0.1 KB
bundle/repro/rca_report.md16.9 KB
bundle/repro/reproduction_steps.sh15.6 KB
bundle/repro/runtime_manifest.json5.1 KB
bundle/repro/server_node.erl3.1 KB
bundle/repro/validation_verdict.json1.5 KB
08 · How to Fix

How to Fix CVE-2026-69664

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-69664 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-69664

Is CVE-2026-69664 exploitable?

Yes. Pruva independently reproduced CVE-2026-69664 in erlang/otp and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00370).

How severe is CVE-2026-69664?

CVE-2026-69664 is rated high severity.

What type of vulnerability is CVE-2026-69664?

CVE-2026-69664 is classified as CWE-772, a DoS vulnerability.

Which versions of erlang/otp are affected by CVE-2026-69664?

erlang/otp OTP >= 18.1.4 < 27.3.4.17; >= 28.0 < 28.5.0.6; >= 29.0 < 29.0.6 (inets >= 6.0.3 < 9.3.2.7; >= 9.4 < 9.6.2.3; >= 9.7 < 9.7.2) is affected by CVE-2026-69664.

How can I reproduce CVE-2026-69664?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-69664 reproduction verified?

Yes. Pruva reproduced CVE-2026-69664 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-69664

Authoritative sources for CVE-2026-69664 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.