GHSA-9hj4-r449-hfvc
Ruby JSON: JSON::ResumableParser#partial_value dereferences a freed input buffer and crashes on truncated duplicate-key streams
Quick fix
GHSA-9hj4-r449-hfvc — json: upgrade to the fixed version with the command below.
bundle update json Details
### 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.
Are you affected?
Enter the version of the package you're using.