{"id":"CVE-2026-71847","aliases":["GHSA-9hj4-r449-hfvc"],"url":"https://o3.security/vulnerability/CVE-2026-71847","summary":"Ruby JSON: JSON::ResumableParser#partial_value dereferences a freed input buffer and crashes on truncated duplicate-key streams","details":"### Summary\n\nRuby's JSON native C extension clears the consumed `JSON::ResumableParser` input buffer but leaves `state.start`, `state.cursor`, and `state.end` pointing into released storage.\n\nWhen `partial_value` reconstructs an incomplete object containing duplicate keys, the duplicate-key warning path calls `cursor_position`, which dereferences those stale pointers. This results in a heap-use-after-free and can terminate the Ruby process.\n\nAn attacker who can supply JSON stream data to an application using `JSON::ResumableParser` may cause process termination when the application calls `partial_value` on incomplete attacker-controlled input containing duplicate object keys.\n\nThe issue was reproduced in the native C extension from the official RubyGems releases:\n\n* JSON 2.20.0\n* JSON 2.21.0\n* JSON 2.21.1\n\nThe attached evidence demonstrates:\n\n* an AddressSanitizer-confirmed heap-use-after-free;\n* a native `SIGSEGV` using the official JSON 2.21.1 RubyGem;\n* an end-to-end loopback TCP attacker/victim reproduction;\n* four differential controls;\n* successful execution after applying a tested patch control.\n\nThis was originally reported privately through Ruby's HackerOne program as report `#3867755`. A Ruby maintainer independently confirmed reproduction of the ASan failure and requested that further coordination continue through this private advisory.\n\nNo code execution or information disclosure is claimed.\n\n### Details\n\nThe affected source is:\n\n```text\next/json/ext/parser/parser.c\n```\n\nThe vulnerable sequence in JSON 2.21.1 is:\n\n1. `cResumableParser_parse` reaches the end of the current input buffer.\n2. It calls `json_str_clear(parser->buffer)`.\n3. It sets `parser->buffer = Qfalse`.\n4. The parser-state pointers into the released buffer are not reset.\n5. `partial_value` makes a shallow copy of the parser state.\n6. Reconstructing an incomplete object containing duplicate keys reaches the duplicate-key warning path.\n7. `cursor_position` walks through the stale input pointers and reads released memory.\n\nRelevant source locations:\n\n* Buffer release:\n  https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L2562-L2569\n\n* Parser-state copy:\n  https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L2647-L2654\n\n* Stale-pointer read in `cursor_position`:\n  https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L590-L628\n\n* Duplicate-key handling path:\n  https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L1196-L1255\n\nWhen input is supplied to the resumable parser, the parser state stores direct pointers into the backing Ruby string:\n\n```c\nRSTRING_GETMEM(parser->buffer, start, len);\nparser->state.start = start;\nparser->state.end = start + len;\nparser->state.cursor = parser->state.start + offset;\n```\n\nAfter the current buffer has been consumed, `cResumableParser_parse` clears the string and removes the parser's reference to it:\n\n```c\nif (eos(&parser->state)) {\n    json_str_clear(parser->buffer);\n    parser->buffer = Qfalse;\n}\n```\n\nThis path does not invalidate or replace:\n\n```text\nparser->state.start\nparser->state.cursor\nparser->state.end\n```\n\n`JSON::ResumableParser#partial_value` subsequently makes a shallow copy of the parser structure:\n\n```c\nJSON_ResumableParser *original_parser = cResumableParser_get(self);\nJSON_ResumableParser parser = *original_parser;\n```\n\nWhen the partial object contains duplicate keys, reconstruction follows this call path:\n\n```text\ncResumableParser_partial_value_body\n  -> json_decode_object\n  -> json_on_duplicate_key\n  -> emit_duplicate_key_warning\n  -> emit_parse_warning\n  -> cursor_position\n```\n\n`cursor_position` then reads through pointers that may refer to released storage.\n\nAddressSanitizer reports:\n\n```text\nERROR: AddressSanitizer: heap-use-after-free\ncursor_position at parser.c:604\nfreed by cResumableParser_parse at parser.c:2567\n```\n\nThe reproducer follows the normal resumable-parser API sequence:\n\n```ruby\nparser << chunk\nparser.parse\nparser << next_chunk\nparser.parse\nparser.partial_value\n```\n\nThe issue does not require:\n\n* an application-defined callback;\n* explicit garbage collection;\n* parser reentrancy;\n* custom parser options;\n* an attacker-supplied Ruby object;\n* manual modification of native parser state.\n\nThe release-build crash reproduced on JSON 2.20.0, 2.21.0, and 2.21.1.\n\nThis report covers the native C-extension implementation. The separate Java-platform implementation was not tested and is not claimed to be affected.\n\n### PoC\n\nThe complete evidence bundle is attached as:\n\n```text\nruby-json-resumable-partial-value-uaf-evidence-20260716.zip\n```\n\nSHA-256:\n\n```text\n07bf8d47b115e45d6145d0447ab6c1c0255e4a7e9b2fb55c3c9a0e24406134ac\n```\n\n#### Requirements\n\n* Linux\n* Ruby with development headers\n* C compiler\n* `make`\n* RubyGems\n\n#### Release-build, network, and differential reproduction\n\nExtract the attachment:\n\n```sh\nunzip ruby-json-resumable-partial-value-uaf-evidence-20260716.zip\ncd ruby-json-resumable-partial-value-uaf-evidence-20260716\n```\n\nRun the official JSON 2.21.1 release-build proof, loopback network proof, and differential controls:\n\n```sh\n./run_exact_2211.sh\n```\n\nExpected primary results:\n\n```text\nrelease_exit=139\nnetwork_victim_exit=139\nnetwork_result=PASS\nresult=PASS\n```\n\nThe following four differential controls must also report `result=PASS`:\n\n```text\nunique_key\nduplicate_allowed\nno_partial\ncomplete_document\n```\n\nThe release-build crash stack includes:\n\n```text\ncursor_position\nemit_parse_warning\nemit_duplicate_key_warning\njson_decode_object\ncResumableParser_partial_value_body\n```\n\n#### AddressSanitizer reproduction\n\nRun:\n\n```sh\n./run_asan.sh\n```\n\nExpected vulnerable result:\n\n```text\nasan_vulnerable_exit=134\nERROR: AddressSanitizer: heap-use-after-free\ncursor_position at parser.c:604\nfreed by cResumableParser_parse at parser.c:2567\n```\n\nExpected patched-control result:\n\n```text\nasan_patched_exit=0\nasan_result=PASS\n```\n\n#### Affected-version matrix\n\nThe release-build crash was reproduced three times for each of the following official RubyGems releases:\n\n```text\njson 2.20.0\njson 2.21.0\njson 2.21.1\n```\n\nAdditional evidence is included in:\n\n```text\nartifacts/exact-2211-e2e.txt\nartifacts/asan-and-patched-control.txt\nartifacts/version-matrix.txt\nartifacts/source-and-release-verification.txt\nsource-slices.txt\nprior-art.md\npatch-control.diff\n```\n\n### Impact\n\nThis is a use-after-free that can result in native Ruby process termination.\n\nAn attacker must be able to supply JSON stream data to an application that:\n\n1. uses `JSON::ResumableParser`;\n2. processes attacker-controlled streaming input;\n3. calls `partial_value` after parsing an incomplete document containing duplicate object keys.\n\nIn network-facing deployments meeting these conditions, an attacker can cause process termination and denial of service.\n\nThe release-build crash was reproduced consistently in the tested Linux environment. The AddressSanitizer result confirms the underlying heap-use-after-free independently of normal allocator behavior.\n\nThe demonstrated impact is:\n\n```text\nDenial of service through native process termination\n```\n\nNo confidentiality impact, integrity impact, arbitrary code execution, or information disclosure is claimed.\n\n### Suggested remediation\n\nBefore clearing or releasing the resumable parser's input buffer, invalidate or replace every parser-state pointer that refers to the buffer's backing storage.\n\nDelayed code paths such as duplicate-key warning generation must not calculate cursor positions using pointers after the corresponding buffer has been released.\n\nThe attached `patch-control.diff` demonstrates one tested control and is provided for validation rather than as a required final implementation.","published":"2026-08-07T18:31:28.507Z","modified":"2026-09-20T11:46:12.179053654Z","cvss":null,"epss":{"score":0.00329,"percentile":0.26125,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"RubyGems","name":"json","fixedVersion":"2.21.2"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/71xxx/CVE-2026-71847.json"},{"type":"ADVISORY","url":"https://github.com/ruby/json/security/advisories/GHSA-9hj4-r449-hfvc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-71847"},{"type":"PACKAGE","url":"https://github.com/ruby/json"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-20T11:46:12.179053654Z"}}