Skip to content

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

Draft
comandeo-mongo wants to merge 1 commit into
mongodb:masterfrom
comandeo-mongo:3840-wrap-string-uaf
Draft

RUBY-3840 Keep MemoryPointer alive in Binary.wrap_string#3063
comandeo-mongo wants to merge 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.
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.

1 participant