From 26a0f55add0a71cf29272e5a52a7b8dac7f67df8 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Thu, 27 Aug 2026 22:30:07 -0700 Subject: [PATCH] Autoload IPAddr instead of requiring it eagerly openssl/ssl.rb requires ipaddr at the top of the file. The only use is in OpenSSL::SSL.verify_certificate_identity, and only for certificates carrying an iPAddress SAN: return true if san.value == IPAddr.new(hostname).hton rescue IPAddr::InvalidAddressError Autoload matches the shape of the change net/http is making for the same constant and keeps IPAddr resolvable, so anything downstream that relied on require "openssl" defining it is unaffected. Note this does not save the socket require on the next line, which is unconditional and unrelated. The saving is ipaddr.rb itself, 857 lines. Measured with socket already loaded, which is openssl's actual situation, on Ruby 4.0.6 (arm64-darwin), 15 runs: marginal cost of require "ipaddr" min 2.47 ms / median 2.95 ms, 1 file End-to-end require "openssl" moves from a min of 26.42 ms to 24.88 ms, but that figure carries a lot of variance from loading openssl.so, so the marginal number above is the honest one. Small in absolute terms, but openssl is loaded in a large share of Ruby processes, and the change is one line with no behavior difference. The rescue clause still resolves correctly: IPAddr.new triggers the autoload before it can raise, so IPAddr::InvalidAddressError is defined by the time the rescue is evaluated. Test suite: 630 tests, 0 failures, unchanged. The existing coverage already exercises the IPAddr path directly, asserting verify_certificate_identity against a cert with an IP:127.0.0.1 SAN. The three added tests cover that requiring openssl does not load ipaddr, that IPAddr still resolves afterwards, and that referencing it pulls the library in. The first fails against the previous code. Signed-off-by: Tim Smith --- lib/openssl/ssl.rb | 6 +++++- test/openssl/test_require.rb | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/openssl/test_require.rb diff --git a/lib/openssl/ssl.rb b/lib/openssl/ssl.rb index dccc11a55..9b1539840 100644 --- a/lib/openssl/ssl.rb +++ b/lib/openssl/ssl.rb @@ -15,7 +15,11 @@ if defined?(OpenSSL::SSL) require "io/nonblock" -require "ipaddr" +# Only OpenSSL::SSL.verify_certificate_identity uses IPAddr, and only for +# certificates carrying an iPAddress SAN. Autoloading keeps ipaddr (and the +# socket library it pulls in) off the require path for everyone else, while +# leaving the IPAddr constant resolvable as before. +autoload :IPAddr, "ipaddr" require "socket" module OpenSSL diff --git a/test/openssl/test_require.rb b/test/openssl/test_require.rb new file mode 100644 index 000000000..06e407867 --- /dev/null +++ b/test/openssl/test_require.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +require_relative "utils" + +class OpenSSL::TestRequire < OpenSSL::TestCase + IPADDR_LOADED = '$LOADED_FEATURES.any? { |f| File.basename(f) == "ipaddr.rb" }' + + def subprocess(script) + lib = File.expand_path("../../lib", __dir__) + IO.popen([RbConfig.ruby, "-I", lib, "-e", script], &:read) + end + + def test_requiring_openssl_does_not_load_ipaddr + assert_equal "false", subprocess("require 'openssl'; print #{IPADDR_LOADED}") + end + + def test_ipaddr_is_still_reachable_after_requiring_openssl + assert_equal "127.0.0.1", subprocess("require 'openssl'; print IPAddr.new('127.0.0.1').to_s") + end + + def test_referencing_ipaddr_loads_it + script = "require 'openssl'; IPAddr.new('127.0.0.1'); print #{IPADDR_LOADED}" + assert_equal "true", subprocess(script) + end +end