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..73f81f9 100644 --- a/lib/gpgme.rb +++ b/lib/gpgme.rb @@ -1,6 +1,8 @@ require 'gpgme_n' +require 'monitor' -# TODO without this call one can't GPGME::Ctx.new, find out why +# 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' @@ -19,8 +21,61 @@ 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. 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 Disable thread-safe mode for single-threaded apps + # GPGME.thread_safe = false + # + @thread_safe_mutex = Monitor.new + @thread_safe = true + class << self + # 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 false to disable 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 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