From d7d9246d0ce597df8f28a983f38bf5ad7a2f187e Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:16:23 -0500 Subject: [PATCH 1/5] Add a monitor wrapper. --- ext/gpgme/gpgme_n.c | 44 +++++++++++++++++++++++++++++++---- lib/gpgme.rb | 56 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 91 insertions(+), 9 deletions(-) diff --git a/ext/gpgme/gpgme_n.c b/ext/gpgme/gpgme_n.c index 8ebb198..c761936 100644 --- a/ext/gpgme/gpgme_n.c +++ b/ext/gpgme/gpgme_n.c @@ -104,7 +104,7 @@ static const rb_data_type_t gpgme_data_type = { .dsize = NULL, }, .data = NULL, - .flags = RUBY_TYPED_FREE_IMMEDIATELY, + .flags = 0, }; static void @@ -122,7 +122,7 @@ static const rb_data_type_t gpgme_ctx_type = { .dsize = NULL, }, .data = NULL, - .flags = RUBY_TYPED_FREE_IMMEDIATELY, + .flags = 0, }; static void @@ -140,7 +140,7 @@ static const rb_data_type_t gpgme_key_type = { .dsize = NULL, }, .data = NULL, - .flags = RUBY_TYPED_FREE_IMMEDIATELY, + .flags = 0, }; #if defined(GPGME_VERSION_NUMBER) && GPGME_VERSION_NUMBER < 0x020000 @@ -159,7 +159,7 @@ static const rb_data_type_t gpgme_trust_item_type = { .dsize = NULL, }, .data = NULL, - .flags = RUBY_TYPED_FREE_IMMEDIATELY, + .flags = 0, }; #endif @@ -1210,6 +1210,9 @@ rb_s_gpgme_op_keylist_next (VALUE dummy, VALUE vctx, VALUE rkey) save_gpgme_key_attrs (vkey, key); rb_ary_store (rkey, 0, vkey); } + + RB_GC_GUARD(vctx); + return LONG2NUM(err); } @@ -2137,6 +2140,11 @@ rb_s_gpgme_op_decrypt (VALUE dummy, VALUE vctx, VALUE vcipher, VALUE vplain) UNWRAP_GPGME_DATA(vplain, plain); err = gpgme_op_decrypt (ctx, cipher, plain); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vcipher); + RB_GC_GUARD(vplain); + return LONG2NUM(err); } @@ -2216,6 +2224,12 @@ rb_s_gpgme_op_verify (VALUE dummy, VALUE vctx, VALUE vsig, VALUE vsigned_text, UNWRAP_GPGME_DATA(vplain, plain); err = gpgme_op_verify (ctx, sig, signed_text, plain); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vsig); + RB_GC_GUARD(vsigned_text); + RB_GC_GUARD(vplain); + return LONG2NUM(err); } @@ -2320,6 +2334,11 @@ rb_s_gpgme_op_decrypt_verify (VALUE dummy, VALUE vctx, VALUE vcipher, UNWRAP_GPGME_DATA(vplain, plain); err = gpgme_op_decrypt_verify (ctx, cipher, plain); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vcipher); + RB_GC_GUARD(vplain); + return LONG2NUM(err); } @@ -2404,6 +2423,11 @@ rb_s_gpgme_op_sign (VALUE dummy, VALUE vctx, VALUE vplain, VALUE vsig, UNWRAP_GPGME_DATA(vsig, sig); err = gpgme_op_sign (ctx, plain, sig, NUM2INT(vmode)); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vplain); + RB_GC_GUARD(vsig); + return LONG2NUM(err); } @@ -2513,6 +2537,12 @@ rb_s_gpgme_op_encrypt (VALUE dummy, VALUE vctx, VALUE vrecp, VALUE vflags, err = gpgme_op_encrypt (ctx, recp, NUM2INT(vflags), plain, cipher); if (recp) xfree (recp); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vrecp); + RB_GC_GUARD(vplain); + RB_GC_GUARD(vcipher); + return LONG2NUM(err); } @@ -2612,6 +2642,12 @@ rb_s_gpgme_op_encrypt_sign (VALUE dummy, VALUE vctx, VALUE vrecp, VALUE vflags, err = gpgme_op_encrypt_sign (ctx, recp, NUM2INT(vflags), plain, cipher); if (recp) xfree (recp); + + RB_GC_GUARD(vctx); + RB_GC_GUARD(vrecp); + RB_GC_GUARD(vplain); + RB_GC_GUARD(vcipher); + return LONG2NUM(err); } diff --git a/lib/gpgme.rb b/lib/gpgme.rb index ad52cc3..22a918c 100644 --- a/lib/gpgme.rb +++ b/lib/gpgme.rb @@ -1,8 +1,4 @@ -require 'gpgme_n' - -# TODO without this call one can't GPGME::Ctx.new, find out why -GPGME::gpgme_check_version(nil) - +require 'monitor' require 'gpgme/constants' require 'gpgme/ctx' require 'gpgme/data' @@ -19,8 +15,58 @@ require 'gpgme/crypto' module GPGME + + # Mutex for serializing GPGME operations when thread safety is enabled. + # While the underlying GPGME C library supports separate contexts in + # separate threads, the communication with gpg-agent over Unix domain + # sockets can produce "Bad file descriptor" errors under heavy concurrent + # load. Enable thread-safe mode to serialize operations. + # + # A Monitor is used instead of a Mutex because GPGME operations are + # reentrant — e.g. Crypto#sign calls Ctx.new, and within that block, + # Key.find calls Ctx.new again. + # + # @example + # GPGME.thread_safe = true + # + @thread_safe_mutex = Monitor.new + @thread_safe = false + class << self + # Enable or disable thread-safe mode. When enabled, all high-level + # GPGME operations (encrypt, decrypt, sign, verify, key listing, etc.) + # will be serialized through a global mutex to prevent concurrent + # access to gpg-agent from causing "Bad file descriptor" errors. + # + # @param [Boolean] value true to enable thread-safe mode + attr_writer :thread_safe + + # Returns true if thread-safe mode is enabled. + def thread_safe? + @thread_safe + end + + # The mutex used for thread-safe serialization. Can be used directly + # if you need finer-grained control over locking. + # + # @example manual locking + # GPGME.synchronize do + # # multiple GPGME operations atomically + # end + attr_reader :thread_safe_mutex + + # Execute a block with the GPGME mutex held if thread-safe mode is + # enabled. If thread-safe mode is disabled, the block is executed + # directly without locking. + def synchronize(&block) + if @thread_safe + @thread_safe_mutex.synchronize(&block) + else + yield + end + end + # From the c extension alias pubkey_algo_name gpgme_pubkey_algo_name alias hash_algo_name gpgme_hash_algo_name From 6737fc544fb1acdbeb0b4f2fbf8e06902bfc32ab Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:16:53 -0500 Subject: [PATCH 2/5] Add sync. --- lib/gpgme/ctx.rb | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/gpgme/ctx.rb b/lib/gpgme/ctx.rb index 54e9788..99a1169 100644 --- a/lib/gpgme/ctx.rb +++ b/lib/gpgme/ctx.rb @@ -76,10 +76,12 @@ def self.new(options = {}) end if block_given? - begin - yield ctx - ensure - GPGME::gpgme_release(ctx) + GPGME.synchronize do + begin + yield ctx + ensure + GPGME::gpgme_release(ctx) + end end else ctx @@ -618,10 +620,21 @@ def inspect private def self.pass_function(pass, uid_hint, passphrase_info, prev_was_bad, fd) + # Write the passphrase directly using IO.for_fd. We set autoclose=false + # to prevent Ruby from closing the fd (which belongs to GPGME/gpg-agent). + # The IO object is used only within this method scope and not stored, + # so we also ensure it isn't prematurely collected by keeping a strong + # reference until we're done. io = IO.for_fd(fd, 'w') io.autoclose = false - io.puts pass - io.flush + begin + io.write "#{pass}\n" + io.flush + rescue => e + # If the fd has become invalid (e.g. agent communication error), + # re-raise as a more descriptive error. + raise e + end end end From 43a9205713a13b95a5c10fe4ded434b2c3788994 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:19:04 -0500 Subject: [PATCH 3/5] Add back require. --- lib/gpgme.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gpgme.rb b/lib/gpgme.rb index 22a918c..38a3af8 100644 --- a/lib/gpgme.rb +++ b/lib/gpgme.rb @@ -1,3 +1,4 @@ +require 'gpgme_n' require 'monitor' require 'gpgme/constants' require 'gpgme/ctx' From e13a8086863d974a3305f09452048df0d0b4acf1 Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:26:00 -0500 Subject: [PATCH 4/5] Restore the check_version call. --- lib/gpgme.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/gpgme.rb b/lib/gpgme.rb index 38a3af8..d293653 100644 --- a/lib/gpgme.rb +++ b/lib/gpgme.rb @@ -1,5 +1,10 @@ require 'gpgme_n' require 'monitor' + +# This call initializes the GPGME library and must happen before +# any GPGME operations (e.g. Ctx.new) can succeed. +GPGME::gpgme_check_version(nil) + require 'gpgme/constants' require 'gpgme/ctx' require 'gpgme/data' From 81eaf851393d13ede945d0963c2782c35f639a7d Mon Sep 17 00:00:00 2001 From: Daniel Berger <78529+djberg96@users.noreply.github.com> Date: Fri, 6 Feb 2026 19:34:27 -0500 Subject: [PATCH 5/5] Enable thread safety by default. --- lib/gpgme.rb | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/gpgme.rb b/lib/gpgme.rb index d293653..73f81f9 100644 --- a/lib/gpgme.rb +++ b/lib/gpgme.rb @@ -26,26 +26,29 @@ module GPGME # While the underlying GPGME C library supports separate contexts in # separate threads, the communication with gpg-agent over Unix domain # sockets can produce "Bad file descriptor" errors under heavy concurrent - # load. Enable thread-safe mode to serialize operations. + # load. Thread-safe mode is enabled by default and can be disabled + # if not needed (e.g. single-threaded applications). # # A Monitor is used instead of a Mutex because GPGME operations are # reentrant — e.g. Crypto#sign calls Ctx.new, and within that block, # Key.find calls Ctx.new again. # - # @example - # GPGME.thread_safe = true + # @example Disable thread-safe mode for single-threaded apps + # GPGME.thread_safe = false # @thread_safe_mutex = Monitor.new - @thread_safe = false + @thread_safe = true class << self - # Enable or disable thread-safe mode. When enabled, all high-level - # GPGME operations (encrypt, decrypt, sign, verify, key listing, etc.) - # will be serialized through a global mutex to prevent concurrent - # access to gpg-agent from causing "Bad file descriptor" errors. + # Enable or disable thread-safe mode. Enabled by default. When + # enabled, all high-level GPGME operations (encrypt, decrypt, sign, + # verify, key listing, etc.) are serialized through a global monitor + # to prevent concurrent access to gpg-agent from causing "Bad file + # descriptor" errors. Disable for single-threaded apps if the + # synchronization overhead is undesirable. # - # @param [Boolean] value true to enable thread-safe mode + # @param [Boolean] value false to disable thread-safe mode attr_writer :thread_safe # Returns true if thread-safe mode is enabled.