Skip to content

Fix three defects in Psych::Parser#parse - #817

Merged
hsbt merged 4 commits into
masterfrom
claude/kind-burnell-81c502
Aug 21, 2026
Merged

Fix three defects in Psych::Parser#parse#817
hsbt merged 4 commits into
masterfrom
claude/kind-burnell-81c502

Conversation

@hsbt

@hsbt hsbt commented Aug 20, 2026

Copy link
Copy Markdown
Member

An exception raised in Psych::Handler#event_location never reaches the caller. Every branch of the event switch overwrites the rb_protect state, so only the second result is tested.

Calling Psych::Parser#parse from inside a handler callback reinitialises the parser the outer loop is still driving. The outer loop then reads the inner document's freed buffer, or resumes with no input and trips libyaml's read_handler assertion, aborting the process. parse now raises Psych::Exception when the parser is already parsing, and rb_ensure clears the flag so the parser stays reusable.

The loop also spun forever on an event with no type, which libyaml keeps returning once stream_end_produced is set.

None of this is reachable through Psych.load and friends, which build a fresh parser each time. The libfyaml backend shared all three and is fixed alongside.

hsbt and others added 3 commits August 19, 2026 10:59
Every case of the event switch overwrote the rb_protect state, so only
the second result was ever tested and the state from the event_location
call was dropped. The parse then returned normally with the exception
silently discarded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Once libyaml has set stream_end_produced, every further
yaml_parser_parse succeeds with a zeroed event, so YAML_STREAM_END_EVENT
can no longer be reached and the loop calls handler#empty forever at
full CPU. A parser left in that state should terminate instead.

No test covers this directly because the loop only reaches it through a
reentrant parse, which the parser now rejects outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The parser calls back into Ruby for every event, so a handler can call
#parse again on the same object. That reinitialised the parser and
re-pointed it at new input while the outer loop was still driving it,
which left the outer loop reading the inner document's freed buffer, or
resuming on a parser with no input at all and tripping libyaml's
read_handler assertion.

Psych::Exception is used so the caller can rescue it, and the in-use
flag is cleared with rb_ensure so a parser stays reusable afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 20, 2026 03:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens Psych::Parser#parse against three low-level correctness defects in both the libyaml and libfyaml backends: (1) exceptions raised from Handler#event_location not reliably propagating, (2) unsafe reentrant parse calls from within callbacks corrupting parser state, and (3) a potential infinite loop when the backend returns an event with no type.

Changes:

  • Ensure exceptions from event_location are immediately surfaced (and associated event memory is released before raising).
  • Add a per-parser “in use” guard to reject reentrant parse calls, and clear it via rb_ensure to keep parsers reusable.
  • Stop the parse loop when the backend returns a “no event” type to avoid spinning indefinitely.
  • Add tests for exception propagation and reentrancy behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
test/psych/test_parser.rb Adds regression tests covering event_location exception propagation and reentrant parse rejection/reuse.
ext/psych/psych_parser.c Wraps yaml_parser_t in a struct with an in-use flag; fixes exception propagation and loop termination edge cases.
ext/psych/psych_parser_fy.c Mirrors the same correctness fixes for the libfyaml backend (event_location propagation, non-reentrancy, loop termination).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ext/psych/psych_parser.c
Comment on lines 91 to 98
static VALUE allocate(VALUE klass)
{
yaml_parser_t * parser;
VALUE obj = TypedData_Make_Struct(klass, yaml_parser_t, &psych_parser_type, parser);
psych_parser_t * parser;
VALUE obj = TypedData_Make_Struct(klass, psych_parser_t, &psych_parser_type, parser);

yaml_parser_initialize(parser);
yaml_parser_initialize(&parser->yaml_parser);

return obj;
Comment on lines 39 to 49
typedef struct {
struct fy_parser *fyp;
size_t mark_line;
size_t mark_column;
size_t mark_index;
/* The parser calls back into Ruby for every event, so a handler can call
* Psych::Parser#parse again on the same object. parse() destroys and
* recreates fyp, which would pull the parser out from under the loop still
* driving it, so keep a flag to reject a reentrant call. */
int parsing;
} psych_fy_parser_t;
@hsbt
hsbt merged commit 182f803 into master Aug 21, 2026
234 of 235 checks passed
@hsbt
hsbt deleted the claude/kind-burnell-81c502 branch August 21, 2026 01:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants