Skip to content

Fix heap corruption when a YAML anchor references itself - #61

Merged
hsbt merged 1 commit into
masterfrom
claude/syck-segv-fix-78868d
Jul 28, 2026
Merged

Fix heap corruption when a YAML anchor references itself#61
hsbt merged 1 commit into
masterfrom
claude/syck-segv-fix-78868d

Conversation

@hsbt

@hsbt hsbt commented Jul 28, 2026

Copy link
Copy Markdown
Member

A self-referential anchor such as --- &a\n- *a\n is aliased before the anchored node exists, so the parser hands out a BadAlias placeholder and fuses the two afterwards by copying the real object over the placeholder's heap slot. That slot is sized for the placeholder, the copied flags carry the source object's age bits, no write barrier fires, and zeroing the source turns a live object into T_NONE behind the GC's back. The heap then stays corrupted until an unrelated allocation crashes, which is why the CI backtraces point at newobj_init with no syck frames anywhere in them.

test_circular_references triggers this on every run and leaves the live slot count off by one per load.

$ ruby -Ilib -e 'require "syck"
  a = []; a[0] = a; a[1] = a; y = a.to_yaml
  100.times { Syck.load(y) }
  GC.start(full_mark: true, immediate_sweep: true)
  GC.verify_internal_consistency'
[BUG] inconsistent live slot number: expect 20491, but 20391.

Six or more self-references make it deterministic. ObjectSpace.dump shows an array claiming "length":6, "embedded":true inside a "slot_size":40 slot, so the tail of the element list is whatever sits in the adjacent slots.

This walks the finished subtree and rewrites references to the placeholder instead, covering arrays, hash keys and values, structs, and instance variables. Verified on Ruby 2.7, 3.1, 3.2, 3.3.11, 3.4, and 4.0. The new test segfaults on the current master build and passes with the fix.

A self-referential anchor is aliased before the anchored node exists, so
the parser hands out a BadAlias placeholder and fused the two afterwards
by copying the real object over the placeholder's slot. That slot is
sized for the placeholder, the copied flags carry the source object's age
bits, no write barrier fires, and zeroing the source turns a live object
into T_NONE behind the GC's back. Rewrite the references instead.

test_circular_references alone left the live slot count off by one per
load, and the resulting crash surfaced at an unrelated allocation:

    $ ruby -Ilib -e 'require "syck"
      a = []; a[0] = a; a[1] = a; y = a.to_yaml
      100.times { Syck.load(y) }
      GC.start(full_mark: true, immediate_sweep: true)
      GC.verify_internal_consistency'
    [BUG] inconsistent live slot number: expect 20491, but 20391.
@hsbt
hsbt merged commit 1ef8220 into master Jul 28, 2026
48 checks passed
@hsbt
hsbt deleted the claude/syck-segv-fix-78868d branch July 28, 2026 05:51
@hsbt hsbt mentioned this pull request Jul 28, 2026
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.

1 participant