Skip to content

RUBY-3840 Keep MemoryPointer alive in Binary.wrap_string#3063

Merged
comandeo-mongo merged 1 commit into
mongodb:masterfrom
comandeo-mongo:3840-wrap-string-uaf
Jun 17, 2026
Merged

RUBY-3840 Keep MemoryPointer alive in Binary.wrap_string#3063
comandeo-mongo merged 1 commit into
mongodb:masterfrom
comandeo-mongo:3840-wrap-string-uaf

Conversation

@comandeo-mongo

Copy link
Copy Markdown
Contributor

Problem

Mongo::Crypt::Binary.wrap_string passed FFI::MemoryPointer.from_string(str) directly into mongocrypt_binary_new_from_data as an anonymous temporary. That C API does not copy the buffer — the binding doc states "the data is not copied and must outlive the mongocrypt_binary_t object". With no Ruby-side reference, the MemoryPointer became GC-eligible the moment the call returned. If GC ran during the yield, its finalizer freed the buffer while libmongocrypt still pointed into it — a use-after-free.

Impact is intermittent (GC-timing dependent under MRI): crashes under GC pressure, garbled BSON / spurious libmongocrypt errors, or silent local corruption of the BSON fed to libmongocrypt (KMS providers, schema maps, key IDs/material, KMS network bytes). Not attacker-controlled, so a reliability bug rather than a security vulnerability.

Fix

Hold the MemoryPointer in a local for the lifetime of the method frame, mirroring how Binary#initialize retains its pointer in @data_p.

Files changed

  • lib/mongo/crypt/binary.rb — retain the pointer in a local across the wrapped binary's lifetime.
  • spec/mongo/crypt/binary_spec.rb — add a .wrap_string group, including a GC-pressure regression test that reads the wrapped buffer back through libmongocrypt.

Test plan

  • spec/mongo/crypt/binary_spec.rb — new GC-pressure test fails on the old code (libmongocrypt reads freed/reused bytes), passes with the fix. 11 examples, 0 failures.
  • spec/mongo/crypt/handle_spec.rb + binary_spec.rb — 81 examples, 0 failures (cloud-KMS cases pending without credentials).
  • CSFLE integration (data_key_spec.rb, explicit_encryption_spec.rb) — remaining failures are pre-existing and environment-only (AWS credentials / KMIP mock server); identical with and without this change.
  • bundle exec rubocop — clean.

RUBY-3840

mongocrypt_binary_new_from_data does not copy the buffer, so the
FFI::MemoryPointer backing the wrapped binary must outlive the
mongocrypt_binary_t. The pointer was an anonymous temporary with no
Ruby-side reference, so GC could free its buffer while libmongocrypt
still pointed into it - a use-after-free that manifests as crashes or
corrupted BSON under GC pressure.

Hold the pointer in a local for the lifetime of the method frame.
Add a GC-pressure regression spec that reads the wrapped buffer back
through libmongocrypt.
@comandeo-mongo comandeo-mongo marked this pull request as ready for review June 17, 2026 13:54
@comandeo-mongo comandeo-mongo requested a review from a team as a code owner June 17, 2026 13:54
@comandeo-mongo comandeo-mongo requested review from Copilot and jamis June 17, 2026 13:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 fixes a GC-related use-after-free in Mongo::Crypt::Binary.wrap_string by ensuring the FFI::MemoryPointer backing the wrapped mongocrypt_binary_t remains strongly referenced for the duration of the yielded block.

Changes:

  • Retain the FFI::MemoryPointer in a local in Binary.wrap_string to keep the underlying buffer alive while libmongocrypt references it.
  • Add specs for .wrap_string, including a regression test that applies GC/allocation pressure and validates the wrapped bytes can be read back via libmongocrypt.

Reviewed changes

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

File Description
lib/mongo/crypt/binary.rb Keeps the wrapped string buffer alive across the wrap_string yield to avoid GC-triggered UAF.
spec/mongo/crypt/binary_spec.rb Adds .wrap_string coverage and a GC-pressure regression test validating buffer stability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +103
described_class.wrap_string(str) do |binary_p|
GC.start(full_mark: true, immediate_sweep: true)
# Allocate garbage to reuse any freed buffer in this tick.
Array.new(1000) { 'y' * 64 }
Comment thread lib/mongo/crypt/binary.rb
@comandeo-mongo comandeo-mongo merged commit 0766092 into mongodb:master Jun 17, 2026
222 of 223 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants