ghsa-9hj4-r449-hfvc

osv_rubygems
Description

### Summary Ruby'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. When `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. An 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. The issue was reproduced in the native C extension from the official RubyGems releases: * JSON 2.20.0 * JSON 2.21.0 * JSON 2.21.1 The attached evidence demonstrates: * an AddressSanitizer-confirmed heap-use-after-free; * a native `SIGSEGV` using the official JSON 2.21.1 RubyGem; * an end-to-end loopback TCP attacker/victim reproduction; * four differential controls; * successful execution after applying a tested patch control. This 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. No code execution or information disclosure is claimed. ### Details The affected source is: ```text ext/json/ext/parser/parser.c ``` The vulnerable sequence in JSON 2.21.1 is: 1. `cResumableParser_parse` reaches the end of the current input buffer. 2. It calls `json_str_clear(parser->buffer)`. 3. It sets `parser->buffer = Qfalse`. 4. The parser-state pointers into the released buffer are not reset. 5. `partial_value` makes a shallow copy of the parser state. 6. Reconstructing an incomplete object containing duplicate keys reaches the duplicate-key warning path. 7. `cursor_position` walks through the stale input pointers and reads released memory. Relevant source locations: * Buffer release: https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L2562-L2569 * Parser-state copy: https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L2647-L2654 * Stale-pointer read in `cursor_position`: https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L590-L628 * Duplicate-key handling path: https://github.com/ruby/json/blob/fd61def38b9bb859fee7eec8e7d3143600e5b347/ext/json/ext/parser/parser.c#L1196-L1255 When input is supplied to the resumable parser, the parser state stores direct pointers into the backing Ruby string: ```c RSTRING_GETMEM(parser->buffer, start, len); parser->state.start = start; parser->state.end = start + len; parser->state.cursor = parser->state.start + offset; ``` After the current buffer has been consumed, `cResumableParser_parse` clears the string and removes the parser's reference to it: ```c if (eos(&parser->state)) { json_str_clear(parser->buffer); parser->buffer = Qfalse; } ``` This path does not invalidate or replace: ```text parser->state.start parser->state.cursor parser->state.end ``` `JSON::ResumableParser#partial_value` subsequently makes a shallow copy of the parser structure: ```c JSON_ResumableParser *original_parser = cResumableParser_get(self); JSON_ResumableParser parser = *original_parser; ``` When the partial object contains duplicate keys, reconstruction follows this call path: ```text cResumableParser_partial_value_body -> json_decode_object -> json_on_duplicate_key -> emit_duplicate_key_warning -> emit_parse_warning -> cursor_position ``` `cursor_position` then reads through pointers that may refer to released storage. AddressSanitizer reports: ```text ERROR: AddressSanitizer: heap-use-after-free cursor_position at parser.c:604 freed by cResumableParser_parse at parser.c:2567 ``` The reproducer follows the normal resumable-parser API sequence: ```ruby parser << chunk parser.parse parser << next_chunk parser.parse parser.partial_value ``` The issue does not require: * an application-defined callback; * explicit garbage collection; * parser reentrancy; * custom parser options; * an attacker-supplied Ruby object; * manual modification of native parser state. The release-build crash reproduced on JSON 2.20.0, 2.21.0, and 2.21.1. This report covers the native C-extension implementation. The separate Java-platform implementation was not tested and is not claimed to be affected. ### PoC The complete evidence bundle is attached as: ```text ruby-json-resumable-partial-value-uaf-evidence-20260716.zip ``` SHA-256: ```text 07bf8d47b115e45d6145d0447ab6c1c0255e4a7e9b2fb55c3c9a0e24406134ac ``` #### Requirements * Linux * Ruby with development headers * C compiler * `make` * RubyGems #### Release-build, network, and differential reproduction Extract the attachment: ```sh unzip ruby-json-resumable-partial-value-uaf-evidence-20260716.zip cd ruby-json-resumable-partial-value-uaf-evidence-20260716 ``` Run the official JSON 2.21.1 release-build proof, loopback network proof, and differential controls: ```sh ./run_exact_2211.sh ``` Expected primary results: ```text release_exit=139 network_victim_exit=139 network_result=PASS result=PASS ``` The following four differential controls must also report `result=PASS`: ```text unique_key duplicate_allowed no_partial complete_document ``` The release-build crash stack includes: ```text cursor_position emit_parse_warning emit_duplicate_key_warning json_decode_object cResumableParser_partial_value_body ``` #### AddressSanitizer reproduction Run: ```sh ./run_asan.sh ``` Expected vulnerable result: ```text asan_vulnerable_exit=134 ERROR: AddressSanitizer: heap-use-after-free cursor_position at parser.c:604 freed by cResumableParser_parse at parser.c:2567 ``` Expected patched-control result: ```text asan_patched_exit=0 asan_result=PASS ``` #### Affected-version matrix The release-build crash was reproduced three times for each of the following official RubyGems releases: ```text json 2.20.0 json 2.21.0 json 2.21.1 ``` Additional evidence is included in: ```text artifacts/exact-2211-e2e.txt artifacts/asan-and-patched-control.txt artifacts/version-matrix.txt artifacts/source-and-release-verification.txt source-slices.txt prior-art.md patch-control.diff ``` ### Impact This is a use-after-free that can result in native Ruby process termination. An attacker must be able to supply JSON stream data to an application that: 1. uses `JSON::ResumableParser`; 2. processes attacker-controlled streaming input; 3. calls `partial_value` after parsing an incomplete document containing duplicate object keys. In network-facing deployments meeting these conditions, an attacker can cause process termination and denial of service. The 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. The demonstrated impact is: ```text Denial of service through native process termination ``` No confidentiality impact, integrity impact, arbitrary code execution, or information disclosure is claimed. ### Suggested remediation Before clearing or releasing the resumable parser's input buffer, invalidate or replace every parser-state pointer that refers to the buffer's backing storage. Delayed code paths such as duplicate-key warning generation must not calculate cursor positions using pointers after the corresponding buffer has been released. The attached `patch-control.diff` demonstrates one tested control and is provided for validation rather than as a required final implementation.

Timeline
Published
unknown
Last Modified
unknown
CVSS Details

CVSS details not available.

Affected Products

No product information available.

Weaknesses (CWE)
References
Linked Vulnerabilities

No linked vulnerabilities found.

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "<= 2.21.1",
        "source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-9hj4-r449-hfvc/GHSA-9hj4-r449-hfvc.json"
      },
      "package": {
        "ecosystem": "RubyGems",
        "name": "json",
        "purl": "pkg:gem/json"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.20.0"
            },
            {
              "fixed": "2.21.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ],
      "versions": [
        "2.20.0",
        "2.21.0",
        "2.21.1"
      ]
    }
  ],
  "aliases": [
    "CVE-2026-71847"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-416"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-08-07T18:33:12Z",
    "nvd_published_at": null,
    "severity": "LOW"
  },
  "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.",
  "id": "GHSA-9hj4-r449-hfvc",
  "modified": "2026-09-10T03:51:13.782635464Z",
  "published": "2026-08-07T18:33:12Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/ruby/json/security/advisories/GHSA-9hj4-r449-hfvc"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/ruby/json"
    }
  ],
  "schema_version": "1.9.0",
  "summary": "Ruby JSON: JSON::ResumableParser#partial_value dereferences a freed input buffer and crashes on truncated duplicate-key streams"
}
View JSON API Download JSON