dep-protobuf/ruby
Jeremy Daer 7d7d6ab836 Ruby: fix use-after-free of map keys aliasing a temporary String (#29026)
Fixes #29023.

`Map#[]=` and `Message.new(map_field: {...})` build the map key as a `upb_StringView`
aliasing a Ruby String, then convert the value before `upb_Map_Set` copies the key. The
value conversion allocates, so it can trigger GC inside that window.

The aliased String is frequently a **temporary**: `Convert_RubyToUpb` replaces the caller's
object when the key is a Symbol (via `to_s`) or a String not already tagged UTF-8 (via
`Convert_CheckStringUtf8`), and nothing references the result once it returns. When GC
collects it, the freed block goes straight back to the next `upb_Arena_Malloc`, which
memcpys the *value* into it — leaving a silently corrupted key holding unrelated heap bytes,
tagged UTF-8 while containing invalid UTF-8, which then propagates into `encode`/`to_json`.

## The fix

Pass the arena at both insertion sites, so the key is copied before anything can allocate.

The lookup paths (`Map_index`, `Map_has_key`, `Map_delete`) keep the `NULL` fast path — they
consume the key immediately with no allocation in between, which is exactly the precondition
`Convert_StringData`'s comment describes. I reworded that comment, since it read as though
the aliasing were unconditionally safe; it holds for three of its five callers and not for
the two that insert.

Cost is one arena allocation per insert for string-typed keys. Non-string keys don't reach
`Convert_StringData` at all.

## Trigger

Needs both:

- a key that is a **Symbol**, or a String not already tagged UTF-8 — `ASCII-8BIT` is the
  common case for anything read from a socket, a file, `Marshal`, or `String#pack`; and
- a value whose conversion allocates (a Symbol, or a non-UTF-8 String).

Plain UTF-8 keys are unaffected, which is presumably why this has gone unnoticed.

## Verification

Reproduces under **ordinary GC**, no `GC.stress` required — one corrupted key across 150k
iterations (0/50k, 0/50k, 1/50k), versus 100/100 with stress. That second number is an
existence proof rather than a rate.

Added regression tests to `ruby/tests/gc_test.rb` covering string keys, Symbol keys, and the
map-field kwarg path. Verified red/green against the same tree:

| ext build | new tests |
|---|---|
| unpatched `main` | 3 tests, **3 failures** |
| with this change | 3 tests, 300 assertions, **0 failures** |

Full Ruby suite green with the change on ruby 4.0.6 / arm64-darwin — `basic.rb` (133 tests,
157,864 assertions), `basic_proto2.rb` (93), `repeated_field_test.rb` (40),
`encode_decode_test.rb`, `memory_test.rb`, `object_cache_test.rb`, `well_known_types_test.rb`,
`service_test.rb`, `oom_test.rb`, `multi_level_nesting_test.rb` — 0 failures, 0 errors.

Reported separately via the channel in `SECURITY.md`, since this is a memory-safety issue in
an OT0 repository.

Closes #29026

COPYBARA_INTEGRATE_REVIEW=https://github.com/protocolbuffers/protobuf/pull/29026 from jeremy:ruby-map-key-use-after-free e11cc7dfe2
PiperOrigin-RevId: 961236703
2026-08-07 19:44:44 -07:00
..
ext/google/protobuf_c Ruby: fix use-after-free of map keys aliasing a temporary String (#29026) 2026-08-07 19:44:44 -07:00
lib Add test for RepeatedField concatenation and arena lifetime. 2026-07-07 05:59:28 -07:00
src/main/java Ruby: clamp len in RepeatedField#[beg, len] to fix out-of-bounds read (#27191) 2026-05-18 08:40:57 -07:00
tests Ruby: fix use-after-free of map keys aliasing a temporary String (#29026) 2026-08-07 19:44:44 -07:00
.gitignore Ignore files for doc (#22449) 2025-07-01 00:20:48 -07:00
.yardopts Add Ruby files for documentation purposes (#21212) 2025-06-06 19:11:48 -07:00
BUILD.bazel Add json_options.proto and json_enumvalue_options.proto and/or gencode derived from them to release artifacts 2026-07-31 16:20:59 -07:00
defs.bzl Switch rules_ruby to the supported version from bazel-contrib (#19808) 2025-03-06 13:42:11 -08:00
Gemfile ruby: make bigdecimal a development dependency (#28928) 2026-07-31 19:47:08 -07:00
Gemfile.lock Drop references to x64-mingw32 since we no longer support Ruby 3.0. 2026-01-30 20:36:36 -08:00
generate_stubs.rb Add Ruby files for documentation purposes (#21212) 2025-06-06 19:11:48 -07:00
google-protobuf.gemspec ruby: make bigdecimal a development dependency (#28928) 2026-07-31 19:47:08 -07:00
pom.xml Updating version.json and repo version numbers to: 37.0-dev (#28425) 2026-07-10 06:22:59 -07:00
Rakefile Drop support for Ruby 3.1 2026-07-07 08:11:48 -07:00
README.md Add Ruby files for documentation purposes (#21212) 2025-06-06 19:11:48 -07:00

This directory contains the Ruby extension that implements Protocol Buffers functionality in Ruby.

The Ruby extension makes use of generated Ruby code that defines message and enum types in a Ruby DSL. You may write definitions in this DSL directly, but we recommend using protoc's Ruby generation support with .proto files. The build process in this directory only installs the extension; you need to install protoc as well to have Ruby code generation functionality. You can build protoc from source using bazel build //:protoc.

Installation from Gem

In Gemfile (Please check which version of Protocol Buffers you need: RubyGems):

gem 'google-protobuf'

Or for using this pre-packaged gem, simply install it as you would any other gem:

$ gem install [--prerelease] google-protobuf

Once the gem is installed, you may or may not need protoc. If you write your message type descriptions directly in the Ruby DSL, you do not need it. However, if you wish to generate the Ruby DSL from a .proto file, you will also want to install Protocol Buffers itself, as described in this repository's main README file. The version of protoc included in the latest release supports the --ruby_out option to generate Ruby code.

A simple example of using the Ruby extension follows. More extensive documentation may be found on rubydoc.info.

require 'google/protobuf'

# generated from my_proto_types.proto with protoc:
#  $ protoc --ruby_out=. my_proto_types.proto
require 'my_proto_types'

mymessage = MyTestMessage.new(:field1 => 42, :field2 => ["a", "b", "c"])
mymessage.field1 = 43
mymessage.field2.push("d")
mymessage.field3 = SubMessage.new(:foo => 100)

encoded_data = MyTestMessage.encode(mymessage)
decoded = MyTestMessage.decode(encoded_data)
assert_equal mymessage, decoded
puts "JSON:"
puts MyTestMessage.encode_json(mymessage)

Installation from Source (Building Gem)

Protocol Buffers has a new experimental backend that uses the ffi gem to provide a unified C-based implementation across Ruby interpreters based on UPB. For now, use of the FFI implementation is opt-in. If any of the following are true, the traditional platform-native implementations (MRI-ruby based on CRuby, Java based on JRuby) are used instead of the new FFI-based implementation: 1. ffi and ffi-compiler gems are not installed 2. PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION environment variable has a value other than FFI (case-insensitive). 3. FFI is unable to load the native library at runtime.

To build this Ruby extension, you will need:

  • Rake
  • Bundler
  • Ruby development headers
  • a C compiler

To Build the JRuby extension, you will need:

  • Maven
  • The latest version of the protobuf java library (see ../java/README.md)
  • Install JRuby via rbenv or RVM

First switch to the desired platform with rbenv or RVM.

Then install the required Ruby gems:

$ gem install bundler
$ bundle

Then build the Gem:

$ rake
$ rake clobber_package gem
$ gem install `ls pkg/google-protobuf-*.gem`

If you intend to debug the protobuf_c Ruby bindings with gdb, you can also build a version with debug symbols enabled by setting the PROTOBUF_CONFIG enviroment variable when you build the native extension:

$ PROTOBUF_CONFIG=dbg rake

To run the specs:

$ rake test

To run the specs while using the FFI-based implementation:

$ PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI rake test

This gem includes the upb parsing and serialization library as a single-file amalgamation. It is up-to-date with upb git commit 535bc2fe2f2b467f59347ffc9449e11e47791257.

Alternatively, you can use Bazel to build and to run tests.

From the project root (rather than the ruby directory):

$ bazel test //ruby/tests/...

To run tests against the FFI implementation:

$ bazel test //ruby/tests/... //ruby:ffi_enabled --test_env=PROTOCOL_BUFFERS_RUBY_IMPLEMENTATION=FFI

Version Number Scheme

We are using a version number scheme that is a hybrid of Protocol Buffers' overall version number and some Ruby-specific rules. Gem does not allow re-uploads of a gem with the same version number, so we add a sequence number ("upload version") to the version. We also format alphabetical tags (alpha, pre, ...) slightly differently, and we avoid hyphens. In more detail:

  • First, we determine the prefix: a Protocol Buffers version "3.0.0-alpha-2" becomes "3.0.0.alpha.2". When we release 3.0.0, this prefix will be simply "3.0.0".
  • We then append the upload version: "3.0.0.alpha.2.0" or "3.0.0.0". If we need to upload a new version of the gem to fix an issue, the version becomes "3.0.0.alpha.2.1" or "3.0.0.1".
  • If we are working on a prerelease version, we append a prerelease tag: "3.0.0.alpha.3.0.pre". The prerelease tag comes at the end so that when version numbers are sorted, any prerelease builds are ordered between the prior version and current version.

These rules are designed to work with the sorting rules for Gem::Version: release numbers should sort in actual release order.