From 7a05dbc47831a655a1ef8a1635f88292acd325da Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Mon, 8 Sep 2025 13:30:44 +0200 Subject: [PATCH 1/9] File.dirname: return consistent encoding for `"."` [Bug #21561] It's preferable if the method is consistent in the encoding in the returned string. --- file.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/file.c b/file.c index ff755b4ac312d7..4fc2fec75f59a6 100644 --- a/file.c +++ b/file.c @@ -5060,8 +5060,11 @@ rb_file_dirname_n(VALUE fname, int n) break; } } - if (p == name) - return rb_usascii_str_new2("."); + if (p == name) { + dirname = rb_str_new(".", 1); + rb_enc_copy(dirname, fname); + return dirname; + } #ifdef DOSISH_DRIVE_LETTER if (has_drive_letter(name) && isdirsep(*(name + 2))) { const char *top = skiproot(name + 2, end, enc); From d39fdee9e5653cf00971b4e77d8e14e10d012bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Thu, 21 Aug 2025 16:52:38 +0200 Subject: [PATCH 2/9] Remove redefined method warnings from TestRubyOptimization#test_opt_new --- test/ruby/test_optimization.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index e39eafa5e50bb9..90ef0a102f8b80 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -1256,6 +1256,9 @@ def initialize a, **kw insn = iseq.disasm assert_match(/opt_new/, insn) assert_match(/OptNewFoo:.+@a=1, @b=2/, iseq.eval.inspect) + # clean up to avoid warnings + Object.send :remove_const, :OptNewFoo + Object.remove_method :optnew_foo if defined?(optnew_foo) end [ 'def optnew_foo(&) = OptNewFoo.new(&)', From 1902c42b1f3d81874e1d8503a1f2d373c1d5590a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Thu, 21 Aug 2025 16:53:44 +0200 Subject: [PATCH 3/9] Remove block may be ignored warnings from TestRubyOptimization#test_block_given_aset_aref --- test/ruby/test_optimization.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/ruby/test_optimization.rb b/test/ruby/test_optimization.rb index 90ef0a102f8b80..089c5fbd1d0dc6 100644 --- a/test/ruby/test_optimization.rb +++ b/test/ruby/test_optimization.rb @@ -606,11 +606,11 @@ def foo(n) end class Bug10557 - def [](_) + def [](_, &) block_given? end - def []=(_, _) + def []=(_, _, &) block_given? end end From af210467b5d513fa7444cd64023f207aa2aaea14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 22 Aug 2025 10:38:37 +0200 Subject: [PATCH 4/9] Suppress warnings when testing RubyVM::AbstractSyntaxTree --- test/ruby/test_ast.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ruby/test_ast.rb b/test/ruby/test_ast.rb index 55e5915d821546..c7a946dec868bb 100644 --- a/test/ruby/test_ast.rb +++ b/test/ruby/test_ast.rb @@ -48,7 +48,7 @@ def initialize(path, src: nil) @path = path @errors = [] @debug = false - @ast = RubyVM::AbstractSyntaxTree.parse(src) if src + @ast = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse(src) } if src end def validate_range @@ -67,7 +67,7 @@ def validate_not_cared def ast return @ast if defined?(@ast) - @ast = RubyVM::AbstractSyntaxTree.parse_file(@path) + @ast = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse_file(@path) } end private @@ -135,7 +135,7 @@ def validate_not_cared0(node) Dir.glob("test/**/*.rb", base: SRCDIR).each do |path| define_method("test_all_tokens:#{path}") do - node = RubyVM::AbstractSyntaxTree.parse_file("#{SRCDIR}/#{path}", keep_tokens: true) + node = EnvUtil.suppress_warning { RubyVM::AbstractSyntaxTree.parse_file("#{SRCDIR}/#{path}", keep_tokens: true) } tokens = node.all_tokens.sort_by { [_1.last[0], _1.last[1]] } tokens_bytes = tokens.map { _1[2]}.join.bytes source_bytes = File.read("#{SRCDIR}/#{path}").bytes From 38ec296ce5e7e4cf0ac49b2fa43eec9e6a53e269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Barri=C3=A9?= Date: Fri, 22 Aug 2025 11:38:24 +0200 Subject: [PATCH 5/9] Remove unused variable warning --- test/ruby/test_variable.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_variable.rb b/test/ruby/test_variable.rb index df0c6f1f094578..68434e0b6c479b 100644 --- a/test/ruby/test_variable.rb +++ b/test/ruby/test_variable.rb @@ -512,7 +512,7 @@ def test_genivar_cache instance.instance_variable_set(:@a3, 3) instance.instance_variable_set(:@a4, 4) end.resume - assert_equal 4, instance.instance_variable_get(:@a4) + assert_equal 4, instance.instance_variable_get(:@a4), bug21547 end private From 2687ecaf6fc3e07ce3cbe089d0537eb94518c082 Mon Sep 17 00:00:00 2001 From: Peter Zhu Date: Thu, 11 Sep 2025 10:51:00 -0400 Subject: [PATCH 6/9] Fix use of uninitialized memory in strings Strings created from the C API with a len but no ptr have a buffer allocated and the length set, but the buffer is not zero'd. This causes use of uninitialized memory and allows reading memory that previously existed there. For example, the rb_str_tmp_new spec fails when we create a string with a large length greater than 24 bytes (since we zero the first 24 bytes of the slot). --- spec/ruby/optional/capi/string_spec.rb | 18 +++++++++++++----- string.c | 3 +++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/spec/ruby/optional/capi/string_spec.rb b/spec/ruby/optional/capi/string_spec.rb index be9cb9015f0d34..605c43769ddb0b 100644 --- a/spec/ruby/optional/capi/string_spec.rb +++ b/spec/ruby/optional/capi/string_spec.rb @@ -191,11 +191,19 @@ def inspect end it "returns a new String object filled with \\0 bytes" do - s = @s.rb_str_tmp_new(4) - s.encoding.should == Encoding::BINARY - s.bytesize.should == 4 - s.size.should == 4 - s.should == "\x00\x00\x00\x00" + lens = [4] + + ruby_version_is "3.5" do + lens << 100 + end + + lens.each do |len| + s = @s.rb_str_tmp_new(len) + s.encoding.should == Encoding::BINARY + s.bytesize.should == len + s.size.should == len + s.should == "\x00" * len + end end end diff --git a/string.c b/string.c index 20873a35a5579a..7b8a55a5358e7a 100644 --- a/string.c +++ b/string.c @@ -1066,6 +1066,9 @@ str_enc_new(VALUE klass, const char *ptr, long len, rb_encoding *enc) if (ptr) { memcpy(RSTRING_PTR(str), ptr, len); } + else { + memset(RSTRING_PTR(str), 0, len); + } STR_SET_LEN(str, len); TERM_FILL(RSTRING_PTR(str) + len, termlen); From aca0faf5fd34b1494c417bec75fe8f3baa05db0e Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Thu, 4 Jul 2024 23:35:27 +0900 Subject: [PATCH 7/9] [ruby/openssl] pkey: add more tests for OpenSSL::PKey.read Add tests covering edge cases in the current behavior to prevent accidental regressions. The next patches will update the OpenSSL 3.x path. https://github.com/ruby/openssl/commit/468f8ceea2 --- test/openssl/test_pkey.rb | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/openssl/test_pkey.rb b/test/openssl/test_pkey.rb index 71f5da81d145e7..9fe4afc9f2709f 100644 --- a/test/openssl/test_pkey.rb +++ b/test/openssl/test_pkey.rb @@ -60,6 +60,31 @@ def test_s_generate_key assert_not_equal nil, pkey.private_key end + def test_s_read_pem_unknown_block + # A PEM-encoded certificate and a PEM-encoded private key are combined. + # Check that OSSL_STORE doesn't stop after the first PEM block. + orig = Fixtures.pkey("rsa-1") + subject = OpenSSL::X509::Name.new([["CN", "test"]]) + cert = issue_cert(subject, orig, 1, [], nil, nil) + + input = cert.to_text + cert.to_pem + orig.to_text + orig.private_to_pem + pkey = OpenSSL::PKey.read(input) + assert_equal(orig.private_to_der, pkey.private_to_der) + end + + def test_s_read_der_then_pem + # If the input is valid as both DER and PEM (which allows garbage data + # before and after the block), it is read as DER + # + # TODO: Garbage data after DER should not be allowed, but it is currently + # ignored + orig1 = Fixtures.pkey("rsa-1") + orig2 = Fixtures.pkey("rsa-2") + pkey = OpenSSL::PKey.read(orig1.public_to_der + orig2.private_to_pem) + assert_equal(orig1.public_to_der, pkey.public_to_der) + assert_not_predicate(pkey, :private?) + end + def test_hmac_sign_verify pkey = OpenSSL::PKey.generate_key("HMAC", { "key" => "abcd" }) From 8af8582d4c3baf0ba41f8b54b43839ec8ba3dc3d Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sun, 3 Aug 2025 19:50:04 +0900 Subject: [PATCH 8/9] [ruby/openssl] pkey: pass pem_password_cb to OSSL_DECODER only when it is needed Specify OSSL_DECODER_CTX_set_pem_password_cb() only when we expect a passphrase-protected private key. OSSL_DECODER appears to try to decrypt every PEM block in the input even when the PEM header does not match the requested selection. This can cause repeated prompts for a passphrase in a single OpenSSL::PKey.read call. https://github.com/ruby/openssl/commit/933503f49f --- ext/openssl/ossl_pkey.c | 3 +- test/openssl/test_pkey.rb | 80 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c index e88074ddf2582c..0fed03332fc31e 100644 --- a/ext/openssl/ossl_pkey.c +++ b/ext/openssl/ossl_pkey.c @@ -94,7 +94,8 @@ ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass) selection, NULL, NULL); if (!dctx) goto out; - if (OSSL_DECODER_CTX_set_pem_password_cb(dctx, ossl_pem_passwd_cb, + if (selection == EVP_PKEY_KEYPAIR && + OSSL_DECODER_CTX_set_pem_password_cb(dctx, ossl_pem_passwd_cb, ppass) != 1) goto out; while (1) { diff --git a/test/openssl/test_pkey.rb b/test/openssl/test_pkey.rb index 9fe4afc9f2709f..8066c4dc190fa9 100644 --- a/test/openssl/test_pkey.rb +++ b/test/openssl/test_pkey.rb @@ -85,6 +85,86 @@ def test_s_read_der_then_pem assert_not_predicate(pkey, :private?) end + def test_s_read_passphrase + orig = Fixtures.pkey("rsa-1") + encrypted_pem = orig.private_to_pem("AES-256-CBC", "correct_passphrase") + assert_match(/\A-----BEGIN ENCRYPTED PRIVATE KEY-----/, encrypted_pem) + + # Correct passphrase passed as the second argument + pkey1 = OpenSSL::PKey.read(encrypted_pem, "correct_passphrase") + assert_equal(orig.private_to_der, pkey1.private_to_der) + + # Correct passphrase returned by the block. The block gets false + called = 0 + flag = nil + pkey2 = OpenSSL::PKey.read(encrypted_pem) { |f| + called += 1 + flag = f + "correct_passphrase" + } + assert_equal(orig.private_to_der, pkey2.private_to_der) + assert_equal(1, called) + assert_false(flag) + + # Incorrect passphrase passed. The block is not called + called = 0 + assert_raise(OpenSSL::PKey::PKeyError) { + OpenSSL::PKey.read(encrypted_pem, "incorrect_passphrase") { + called += 1 + } + } + assert_equal(0, called) + + # Incorrect passphrase returned by the block. The block is called only once + called = 0 + assert_raise(OpenSSL::PKey::PKeyError) { + OpenSSL::PKey.read(encrypted_pem) { + called += 1 + "incorrect_passphrase" + } + } + assert_equal(1, called) + end + + def test_s_read_passphrase_tty + omit "https://github.com/aws/aws-lc/pull/2555" if aws_lc? + + orig = Fixtures.pkey("rsa-1") + encrypted_pem = orig.private_to_pem("AES-256-CBC", "correct_passphrase") + + # Correct passphrase passed to OpenSSL's prompt + script = <<~"end;" + require "openssl" + Process.setsid + OpenSSL::PKey.read(#{encrypted_pem.dump}) + puts "ok" + end; + assert_in_out_err([*$:.map { |l| "-I#{l}" }, "-e#{script}"], + "correct_passphrase\n") { |stdout, stderr| + assert_equal(["Enter PEM pass phrase:"], stderr) + assert_equal(["ok"], stdout) + } + + # Incorrect passphrase passed to OpenSSL's prompt + script = <<~"end;" + require "openssl" + Process.setsid + begin + OpenSSL::PKey.read(#{encrypted_pem.dump}) + rescue OpenSSL::PKey::PKeyError + puts "ok" + else + puts "expected OpenSSL::PKey::PKeyError" + end + end; + stdin = "incorrect_passphrase\n" * 5 + assert_in_out_err([*$:.map { |l| "-I#{l}" }, "-e#{script}"], + stdin) { |stdout, stderr| + assert_equal(1, stderr.count("Enter PEM pass phrase:")) + assert_equal(["ok"], stdout) + } + end if ENV["OSSL_TEST_ALL"] == "1" && Process.respond_to?(:setsid) + def test_hmac_sign_verify pkey = OpenSSL::PKey.generate_key("HMAC", { "key" => "abcd" }) From c0820058243842d1391d896baf67914a8ea50e13 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Wed, 13 Aug 2025 03:08:22 +0900 Subject: [PATCH 9/9] [ruby/openssl] pkey: stop retrying after non-retryable error from OSSL_DECODER Continue processing only when OSSL_DECODER_from_bio() returns the error code ERR_R_UNSUPPORTED. Otherwise, raise an exception without retrying decoding the input in another format. This fixes another case where OpenSSL::PKey.read prompts for a passphrase multiple times when the input contains multiple passphrase-protected PEM blocks and the first one cannot be decoded. I am not entirely sure if the error code ERR_R_UNSUPPORTED is considered part of the public interface of OpenSSL, but this seems to be the only option available and is the approach used internally by the PEM_read_bio_*() functions. Fixes https://github.com/ruby/openssl/issues/927 https://github.com/ruby/openssl/commit/985ba27d63 --- ext/openssl/ossl_pkey.c | 38 ++++++++++++++++++++++++-------------- test/openssl/test_pkey.rb | 11 +++++++++++ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/ext/openssl/ossl_pkey.c b/ext/openssl/ossl_pkey.c index 0fed03332fc31e..481bd8a8ee09e3 100644 --- a/ext/openssl/ossl_pkey.c +++ b/ext/openssl/ossl_pkey.c @@ -83,13 +83,15 @@ ossl_pkey_wrap(EVP_PKEY *pkey) # include static EVP_PKEY * -ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass) +ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass, + int *retryable) { void *ppass = (void *)pass; OSSL_DECODER_CTX *dctx; EVP_PKEY *pkey = NULL; int pos = 0, pos2; + *retryable = 0; dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, input_type, NULL, NULL, selection, NULL, NULL); if (!dctx) @@ -100,17 +102,22 @@ ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass) goto out; while (1) { if (OSSL_DECODER_from_bio(dctx, bio) == 1) - goto out; - if (BIO_eof(bio)) break; + if (ERR_GET_REASON(ERR_peek_error()) != ERR_R_UNSUPPORTED) + break; + if (BIO_eof(bio) == 1) { + *retryable = 1; + break; + } pos2 = BIO_tell(bio); - if (pos2 < 0 || pos2 <= pos) + if (pos2 < 0 || pos2 <= pos) { + *retryable = 1; break; + } ossl_clear_error(); pos = pos2; } out: - OSSL_BIO_reset(bio); OSSL_DECODER_CTX_free(dctx); return pkey; } @@ -118,7 +125,6 @@ ossl_pkey_read(BIO *bio, const char *input_type, int selection, VALUE pass) EVP_PKEY * ossl_pkey_read_generic(BIO *bio, VALUE pass) { - EVP_PKEY *pkey = NULL; /* First check DER, then check PEM. */ const char *input_types[] = {"DER", "PEM"}; int input_type_num = (int)(sizeof(input_types) / sizeof(char *)); @@ -167,18 +173,22 @@ ossl_pkey_read_generic(BIO *bio, VALUE pass) EVP_PKEY_PUBLIC_KEY }; int selection_num = (int)(sizeof(selections) / sizeof(int)); - int i, j; - for (i = 0; i < input_type_num; i++) { - for (j = 0; j < selection_num; j++) { - pkey = ossl_pkey_read(bio, input_types[i], selections[j], pass); - if (pkey) { - goto out; + for (int i = 0; i < input_type_num; i++) { + for (int j = 0; j < selection_num; j++) { + if (i || j) { + ossl_clear_error(); + BIO_reset(bio); } + + int retryable; + EVP_PKEY *pkey = ossl_pkey_read(bio, input_types[i], selections[j], + pass, &retryable); + if (pkey || !retryable) + return pkey; } } - out: - return pkey; + return NULL; } #else EVP_PKEY * diff --git a/test/openssl/test_pkey.rb b/test/openssl/test_pkey.rb index 8066c4dc190fa9..24a595333b4ea3 100644 --- a/test/openssl/test_pkey.rb +++ b/test/openssl/test_pkey.rb @@ -124,6 +124,17 @@ def test_s_read_passphrase } } assert_equal(1, called) + + # Incorrect passphrase returned by the block. The input contains two PEM + # blocks. + called = 0 + assert_raise(OpenSSL::PKey::PKeyError) { + OpenSSL::PKey.read(encrypted_pem + encrypted_pem) { + called += 1 + "incorrect_passphrase" + } + } + assert_equal(1, called) end def test_s_read_passphrase_tty