Skip to content

Do not use alloca for a buffer that the document sizes - #63

Open
jeremy wants to merge 1 commit into
ruby:masterfrom
jeremy:no-document-sized-alloca
Open

Do not use alloca for a buffer that the document sizes#63
jeremy wants to merge 1 commit into
ruby:masterfrom
jeremy:no-document-sized-alloca

Conversation

@jeremy

@jeremy jeremy commented Aug 6, 2026

Copy link
Copy Markdown

Two functions call S_ALLOCA_N with a size that comes from the document. S_ALLOCA_N is alloca (syck.h:52). The size has no limit.

  • syck_set_ivars — the size is an instance variable name. Syck.load calls this function one time for each key.
  • rb_syck_compile — the size is the full compiled buffer. Syck.compile calls this function.
require "syck"
name = "n" * 8_000_000
# Run on a Thread. A thread stack is small. A web server and a job worker use this stack.
Thread.new { Syck.load("--- !ruby/object:Object\n#{name}: 1\n") }.join

Change

Both functions now use ALLOCV_N. ALLOCV_N uses alloca for a small size. It uses a Ruby heap buffer for a large size. It frees the buffer if the function raises. rb_iv_set and rb_str_new can raise, so this behavior is necessary.

A document that was correct before is still correct.

Severity

This is a robustness change, not a security fix. On Ruby 3.4 and 4.0, the stack limit stops the large alloca first. The result is a SystemStackError, and you can rescue it.

input before after
large instance variable name SystemStackError ok
large Syck.compile input SystemStackError ok

I could not cause a hard crash on syck 1.6.1, on arm64-darwin or x86_64-linux, from 1 MiB to 64 MiB. But the protection is from Ruby, not from syck. An alloca with an attacker-controlled size is a stack-clash pattern. The result depends on the platform and the memory layout.

Test

test_large_input.rb runs both functions on a Thread. Each test fails with SystemStackError on the current code. Each test passes with this change. Full suite: 123 tests, 0 failures.

S_ALLOCA_N is plain alloca (syck.h:52), and two call sites size it from data
the parsed document controls, with no upper bound:

  * rubyext.c syck_set_ivars -- sized by an ivar NAME, one alloca per key,
    reached from Syck.load via `!ruby/object:Foo` plus a mapping.
  * rubyext.c rb_syck_compile -- sized by the whole compiled buffer, reached
    from the public Syck.compile.

Both become ALLOCV_N, which uses alloca for small sizes and a Ruby-managed
heap buffer once the size is worth it, and frees on unwind -- so the two
callers stay exception-safe across rb_iv_set and rb_str_new, which the plain
malloc/free version would not be.

Measured on a Thread (1 MiB stack -- the shape a web or job worker actually
has), ruby 3.4.10 arm64-darwin23, 3 runs per cell, against two separately
built .bundles:

                          before            after
  ivar name    4 MiB      SystemStackError  ok
  ivar name   16 MiB      SystemStackError  ok
  Syck.compile 4 MiB      SystemStackError  ok
  Syck.compile 16 MiB     SystemStackError  ok

Ruby's own stack-limit check is what currently turns these into a rescuable
SystemStackError rather than a fault, on both platforms I could test -- so
this is a robustness fix, not a security fix, and I would rather say so than
overstate it. The reason to make it anyway is that the protection is Ruby's
and not syck's: alloca with an attacker-controlled size is a stack-clash
primitive, and whether it lands in the guard page or in something writable
depends on mapping layout, platform, thread stack size and frame layout, none
of which this code controls or tests.

No behaviour change for any document that worked before. A large name or a
large document now allocates on the heap and raises NoMemoryError on
exhaustion instead of consuming stack.

test_large_input.rb covers both, on a Thread. Against the unpatched build both
error with SystemStackError; with this change both pass. Full suite: 123 tests,
394 assertions, 0 failures.
Copilot AI lite review requested due to automatic review settings August 6, 2026 20:17

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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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