Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce1544e2d7 | ||
|
|
d4586a2e5e |
@ -1 +0,0 @@
|
||||
1
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@ -1,6 +1,3 @@
|
||||
/*/
|
||||
/ruby-*.tar.xz
|
||||
/*.rpm
|
||||
/mysql2-*.gem
|
||||
/pg-*.gem
|
||||
!/plans/
|
||||
mysql2-0.5.7.gem
|
||||
pg-1.6.3.gem
|
||||
ruby-4.0.3.tar.xz
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
--- !Policy
|
||||
product_versions:
|
||||
- rhel-10
|
||||
decision_context: osci_compose_gate
|
||||
rules:
|
||||
- !PassingTestCaseRule {test_case_name: osci.brew-build.tier0.functional}
|
||||
@ -1,26 +0,0 @@
|
||||
summary: Public (CentOS) beakerlib tests
|
||||
|
||||
adjust+:
|
||||
- when: distro == centos-stream
|
||||
because: Update all packages and reboot
|
||||
discover+<:
|
||||
- name: update-and-reboot
|
||||
how: shell
|
||||
tests:
|
||||
- name: update
|
||||
test: dnf update --refresh -y
|
||||
- name: reboot
|
||||
test: "[[ $TMT_REBOOT_COUNT == 0 ]] && tmt-reboot || echo Already rebooted"
|
||||
|
||||
environment+:
|
||||
RUBY: "ruby4.0"
|
||||
GEM: "gem4.0"
|
||||
BUNDLE: "bundle4.0"
|
||||
|
||||
discover:
|
||||
- name: centos
|
||||
how: fmf
|
||||
url: https://gitlab.com/redhat/centos-stream/tests/ruby
|
||||
filter: 'component:ruby4.0'
|
||||
execute:
|
||||
how: tmt
|
||||
@ -1,21 +0,0 @@
|
||||
badfuncs:
|
||||
# Ruby implements the functionality for compatibility.
|
||||
# Modern counterparts are also available and preferred.
|
||||
allowed:
|
||||
'/usr/lib*/ruby*/socket.so':
|
||||
- gethostbyaddr
|
||||
- gethostbyname
|
||||
|
||||
patches:
|
||||
# These patches are applied manually with the `patch` binary.
|
||||
# The contents are unpacked with Ruby itself in %build.
|
||||
# %build does not have %patch macros available for use.
|
||||
ignore_list:
|
||||
- rubygem-pg-1.3.0-remove-rpath.patch
|
||||
- rubygem-mysql2-0.5.7-Disable-RPATH-completely-in-extconf.rb.patch
|
||||
|
||||
rpmdeps:
|
||||
ignore:
|
||||
# The package will be pulled in transitively. Ruby takes care for the
|
||||
# ABI compatibility in specific major.minor solib version.
|
||||
requires: ^libruby.*\.so.*\(\)\(64bit\)$
|
||||
@ -14,9 +14,9 @@ index 8f9729ef28..a05c5e9657 100644
|
||||
--- a/ext/json/parser/parser.c
|
||||
+++ b/ext/json/parser/parser.c
|
||||
@@ -400,14 +400,9 @@ static void emit_parse_warning(const char *message, JSON_ParserState *state)
|
||||
|
||||
|
||||
#define PARSE_ERROR_FRAGMENT_LEN 32
|
||||
|
||||
|
||||
-#ifdef RBIMPL_ATTR_NORETURN
|
||||
-RBIMPL_ATTR_NORETURN()
|
||||
-#endif
|
||||
@ -26,7 +26,7 @@ index 8f9729ef28..a05c5e9657 100644
|
||||
unsigned char buffer[PARSE_ERROR_FRAGMENT_LEN + 3];
|
||||
- long line, column;
|
||||
- cursor_position(state, &line, &column);
|
||||
|
||||
|
||||
const char *ptr = "EOF";
|
||||
if (state->cursor && state->cursor < state->end) {
|
||||
@@ -442,11 +437,23 @@ static void raise_parse_error(const char *format, JSON_ParserState *state)
|
||||
@ -35,7 +35,7 @@ index 8f9729ef28..a05c5e9657 100644
|
||||
RB_GC_GUARD(msg);
|
||||
+ return message;
|
||||
+}
|
||||
|
||||
|
||||
+static VALUE parse_error_new(VALUE message, long line, long column)
|
||||
+{
|
||||
VALUE exc = rb_exc_new_str(rb_path2class("JSON::ParserError"), message);
|
||||
@ -52,12 +52,12 @@ index 8f9729ef28..a05c5e9657 100644
|
||||
+ VALUE message = build_parse_error_message(format, state, line, column);
|
||||
+ rb_exc_raise(parse_error_new(message, line, column));
|
||||
}
|
||||
|
||||
|
||||
#ifdef RBIMPL_ATTR_NORETURN
|
||||
@@ -896,6 +903,11 @@ static void raise_duplicate_key_error(JSON_ParserState *state, VALUE duplicate_k
|
||||
rb_inspect(duplicate_key)
|
||||
);
|
||||
|
||||
|
||||
+ long line, column;
|
||||
+ cursor_position(state, &line, &column);
|
||||
+ rb_str_concat(message, build_parse_error_message("", state, line, column)) ;
|
||||
@ -73,7 +73,7 @@ index ec9391909d..61ea35d1f9 100644
|
||||
@@ -411,6 +411,13 @@ def test_parse_duplicate_key
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+ def test_parse_duplicate_key_escape
|
||||
+ error = assert_raise(ParserError) do
|
||||
+ JSON.parse('{"%s%s%s%s":1,"%s%s%s%s":2}', allow_duplicate_key: false)
|
||||
|
||||
@ -1,61 +0,0 @@
|
||||
# Keep matching patterns enough not to hide unintended errors and warnings.
|
||||
|
||||
# There is no way to implement this with `%{SOURCE0}` without `%{_sourcedir}`.
|
||||
# The order in the .spec file could be possibly different.
|
||||
addFilter(r'ruby\.(spec|src):20: E: use-of-RPM_SOURCE_DIR$')
|
||||
|
||||
# The used version is not obvious.
|
||||
addFilter(r'ruby\.(spec|src):\d+: W: unversioned-explicit-provides bundled\(ccan-build_assert\)$')
|
||||
addFilter(r'ruby\.(spec|src):\d+: W: unversioned-explicit-provides bundled\(ccan-check_type\)$')
|
||||
addFilter(r'ruby\.(spec|src):\d+: W: unversioned-explicit-provides bundled\(ccan-container_of\)$')
|
||||
addFilter(r'ruby\.(spec|src):\d+: W: unversioned-explicit-provides bundled\(ccan-list\)$')
|
||||
|
||||
# The template files do not have to have executable bits.
|
||||
addFilter(r'^rubygem-bundler\.noarch: E: non-executable-script /usr/share/gems/gems/bundler-[\d\.]+/lib/bundler/templates/[\w/\.]+ 644 /usr/bin/env ')
|
||||
|
||||
# The bundled gem files permissions are overridden as 644 by `make install`.
|
||||
# https://bugs.ruby-lang.org/issues/17840
|
||||
# https://github.com/rubygems/rubygems/issues/5255
|
||||
# https://github.com/ruby/debug/pull/481
|
||||
# https://github.com/ruby/net-ftp/pull/12
|
||||
# https://github.com/ruby/net-imap/pull/53
|
||||
# https://github.com/ruby/net-pop/pull/7
|
||||
# https://github.com/ruby/prime/pull/16
|
||||
addFilter(r'^.*: E: non-executable-script /usr/share/gems/gems/(debug|net-(ftp|imap|pop)|prime)-[\d\.]+/bin/\w+ 644 ')
|
||||
|
||||
# Ruby provides API to set the cipher list.
|
||||
addFilter(r'^ruby-libs\.\w+: W: crypto-policy-non-compliance-openssl /usr/lib(64)?/ruby/openssl.so SSL_CTX_set_cipher_list$')
|
||||
|
||||
# `gethostbyname` is part of deprecated Ruby API. There is also request to drop the API altogether:
|
||||
# https://bugs.ruby-lang.org/issues/13097
|
||||
# https://bugs.ruby-lang.org/issues/17944
|
||||
addFilter(r'^ruby-libs\.\w+: W: binary-or-shlib-calls-gethostbyname /usr/lib(64)?/ruby/socket.so$')
|
||||
|
||||
# Rake ships some examples.
|
||||
addFilter(r'^rubygem-rake.noarch: W: devel-file-in-non-devel-package /usr/share/gems/gems/rake-[\d\.]+/doc/example/\w+.c$')
|
||||
|
||||
# Some executables don't have their manual pages. Is it worth of use help2man?
|
||||
addFilter(r'^.+: W: no-manual-page-for-binary (bundler|gem|racc|rbs|rdbg|rdoc|ruby-mri|syntax_suggest|typeprof)$')
|
||||
|
||||
# Default gems does not come with any documentation.
|
||||
addFilter(r'^rubygem-(bigdecimal|io-console|json|psych)\.\w+: W: no-documentation$')
|
||||
|
||||
# rubygems-devel ships only RPM macros and generators. Their placement is given
|
||||
# by RPM and can't be modified.
|
||||
addFilter(r'rubygems-devel.noarch: W: only-non-binary-in-usr-lib$')
|
||||
|
||||
# Ignore some spelling false positives.
|
||||
# Ignore spelling of technical terms
|
||||
addFilter(r'^ruby-default-gems.noarch: E: spelling-error \(\'gemspec\'')
|
||||
addFilter(r'^ruby-libs.x86_64: E: spelling-error \(\'libruby\'')
|
||||
addFilter(r'^rubygem-test-unit.noarch: E: spelling-error \(\'xUnit\'')
|
||||
addFilter(r'^rubygem-psych.x86_64: E: spelling-error \(\'libyaml\'')
|
||||
addFilter(r'^rubygem-io-console.x86_64: E: spelling-error \(\'readline\'')
|
||||
# `pyaml` is part of URL
|
||||
addFilter(r'^rubygem-psych.x86_64: E: spelling-error \(\'pyyaml\'')
|
||||
# `de-` is actually prefix
|
||||
addFilter(r'^rubygem-psych.x86_64: E: spelling-error \(\'de\'')
|
||||
|
||||
# It does not seemt to be worth of changing rubygems to archful package due to
|
||||
# single directory, unless it causes some real troubles.
|
||||
addFilter(r'^rubygems.noarch: E: noarch-with-lib64$')
|
||||
50
ruby4.0.spec
50
ruby4.0.spec
@ -202,7 +202,7 @@
|
||||
Summary: An interpreter of object-oriented scripting language
|
||||
Name: ruby4.0
|
||||
Version: %{ruby_version}%{?development_release}
|
||||
Release: 34%{?dist}
|
||||
Release: 35%{?dist}
|
||||
# Licenses, which are likely not included in binary RPMs:
|
||||
# Apache-2.0:
|
||||
# benchmark/gc/redblack.rb
|
||||
@ -335,12 +335,33 @@ Patch10: ruby-4.0.1-test_box-avoid-failure-with-program-suffix.patch
|
||||
# option in Ruby's main build.
|
||||
# https://github.com/brianmario/mysql2/issues/1201
|
||||
Patch11: rubygem-mysql2-0.5.7-Disable-RPATH-completely-in-extconf.rb.patch
|
||||
# CVE-2026-33210
|
||||
# Fix for Denial of Service or Information Disclosure
|
||||
# CVE-2026-33210
|
||||
# Fix for Denial of Service or Information Disclosure
|
||||
# via format string injection
|
||||
# in Ruby JSON
|
||||
# https://github.com/ruby/json/commit/393b41c3e5f87491e1e34fa59fa78ff6fa179a74
|
||||
Patch12: ruby-4.0.3-Fix-a-format-string-injection-vulnerability.patch
|
||||
# CVE-2026-42245
|
||||
# Fix Net::IMAP ResponseReader quadratic complexity vulnerability
|
||||
# Advisory: https://github.com/advisories/GHSA-q2mw-fvj9-vvcw
|
||||
# Sourced from:
|
||||
# - https://github.com/ruby/net-imap/commit/341ab628
|
||||
# - https://github.com/ruby/net-imap/commit/49c516d6
|
||||
# - https://github.com/ruby/net-imap/commit/6f82e28f
|
||||
Patch13: rubygem-net-imap-0.6.2-ResponseReader-quadratic-complexity-CVE-2026-42245.patch
|
||||
# CVE-2026-42246
|
||||
# Fix Net::IMAP STARTTLS stripping vulnerability
|
||||
# Advisory: https://github.com/advisories/GHSA-vcgp-9326-pqcp
|
||||
# Sourced from:
|
||||
# - https://github.com/ruby/net-imap/commit/62eea6ff
|
||||
# - https://github.com/ruby/net-imap/commit/24d5c773
|
||||
Patch14: rubygem-net-imap-0.6.2-STARTTLS-stripping-vulnerability-CVE-2026-42246.patch
|
||||
# CVE-2026-42258
|
||||
# Fix Net::IMAP command injection vulnerability via unvalidated Symbol arguments
|
||||
# Advisory: https://github.com/advisories/GHSA-75xq-5h9v-w6px
|
||||
# Sourced from:
|
||||
# - https://github.com/ruby/net-imap/commit/9db3e9d
|
||||
Patch15: rubygem-net-imap-0.6.2-flag-symbol-validation-CVE-2026-42258.patch
|
||||
|
||||
%{?with_rubypick:Suggests: rubypick}
|
||||
|
||||
@ -506,6 +527,12 @@ popd
|
||||
%patch 10 -p1
|
||||
%patch 12 -p1
|
||||
|
||||
pushd .bundle/gems/net-imap-%{net_imap_version}
|
||||
%patch 13 -p1
|
||||
%patch 14 -p1
|
||||
%patch 15 -p1
|
||||
popd
|
||||
|
||||
# Provide an example of usage of the tapset:
|
||||
cp -a %{SOURCE3} .
|
||||
|
||||
@ -1646,15 +1673,24 @@ make -C %{_vpath_builddir} runruby TESTRUN_SCRIPT=" \
|
||||
%{_libdir}/pkgconfig/%{pkgname}-%{major_minor_version}.pc
|
||||
|
||||
%changelog
|
||||
* Wed Jun 10 2026 Tomas Juhasz <tjuhasz@redhat.com> - 4.0.3-35
|
||||
- Fix Net::IMAP ResponseReader quadratic complexity vulnerability (CVE-2026-42245)
|
||||
Includes core fix plus additional performance optimizations
|
||||
Resolves: RHEL-181675
|
||||
- Fix Net::IMAP STARTTLS stripping vulnerability (CVE-2026-42246)
|
||||
Resolves: RHEL-181767
|
||||
- Fix Net::IMAP command injection vulnerability via unvalidated Symbol arguments (CVE-2026-42258)
|
||||
Resolves: RHEL-181793
|
||||
|
||||
* Wed Apr 29 2026 Tomas Juhasz <tjuhasz@redhat.com> - 4.0.3-34
|
||||
- Upgrade to Ruby 4.0.3.
|
||||
Resolves: RHEL-170933
|
||||
- Fix ERB: Arbitrary code execution via deserialization bypass
|
||||
Resolves: RHEL-171239
|
||||
- Fix ERB: Arbitrary code execution via bypass
|
||||
(CVE-2026-41316)
|
||||
Resolves: RHEL-170911
|
||||
Resolves: RHEL-170910
|
||||
- Fix JSON: Denial of Service or Information Disclosure via format string injection
|
||||
(CVE-2026-33210)
|
||||
Resolves: RHEL-169964
|
||||
Resolves: RHEL-173457
|
||||
|
||||
* Thu Feb 05 2026 Jarek Prokop <jprokop@redhat.com> - 4.0.1-33
|
||||
- Initial package.
|
||||
|
||||
@ -0,0 +1,343 @@
|
||||
From 341ab628167da41f33db69e5c240918aa65b60cd Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Tue, 31 Mar 2026 19:28:20 -0400
|
||||
Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=8D=92=20pick=20341ab6281:=20?=
|
||||
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=F0=9F=94=92=EF=B8=8F=20Fix=20non-linear=20pe?=
|
||||
=?UTF-8?q?rformance=20in=20ResponseReader?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
A very large response with many small repeated literals can trigger
|
||||
super-linear time. This happens because the regular expression that
|
||||
checks for literal continuation matches from the beginning of the buffer
|
||||
every time.
|
||||
|
||||
This could be mitigated by searching from an offset, based on what has
|
||||
already been processed, or only searching the most recent line (before
|
||||
merging it with the buffer), but that is still `O(n)` on line length.
|
||||
|
||||
The regexp is anchored to the end of the string, so searching in reverse
|
||||
from the end of the string should be `O(1)`. This is accomplished by
|
||||
converting `=~` to `rindex`.
|
||||
|
||||
Note that this _does_ slow down the "no literals" scenario.
|
||||
|
||||
```
|
||||
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
|
||||
Warming up --------------------------------------
|
||||
1KiB with no literals 143.564k i/s - 153.197k times in 1.067099s (6.97μs/i)
|
||||
10KiB with no literals 27.394k i/s - 28.864k times in 1.053670s (36.50μs/i)
|
||||
100KiB with no literals 2.926k i/s - 3.157k times in 1.079109s (341.81μs/i)
|
||||
1KiB of 25B literals 2.786k i/s - 2.970k times in 1.066159s (358.98μs/i)
|
||||
10KiB of 25B literals 263.498 i/s - 286.000 times in 1.085396s (3.80ms/i)
|
||||
100KiB of 25B literals 19.470 i/s - 20.000 times in 1.027203s (51.36ms/i)
|
||||
1KiB of 0B literals 530.014 i/s - 530.000 times in 0.999974s (1.89ms/i)
|
||||
10KiB of 0B literals 45.239 i/s - 50.000 times in 1.105233s (22.10ms/i)
|
||||
100KiB of 0B literals 3.075 i/s - 4.000 times in 1.300721s (325.18ms/i)
|
||||
Calculating -------------------------------------
|
||||
local YJIT
|
||||
1KiB with no literals 137.049k 159.971k i/s - 430.691k times in 3.142607s 2.692304s
|
||||
10KiB with no literals 27.272k 28.101k i/s - 82.181k times in 3.013413s 2.924470s
|
||||
100KiB with no literals 2.941k 2.937k i/s - 8.776k times in 2.984095s 2.988129s
|
||||
1KiB of 25B literals 2.803k 4.136k i/s - 8.357k times in 2.981249s 2.020772s
|
||||
10KiB of 25B literals 262.978 385.394 i/s - 790.000 times in 3.004055s 2.049850s
|
||||
100KiB of 25B literals 18.355 22.549 i/s - 58.000 times in 3.159962s 2.572152s
|
||||
1KiB of 0B literals 505.733 759.572 i/s - 1.590k times in 3.143953s 2.093285s
|
||||
10KiB of 0B literals 45.414 67.569 i/s - 135.000 times in 2.972648s 1.997962s
|
||||
100KiB of 0B literals 2.722 3.510 i/s - 9.000 times in 3.306786s 2.564007s
|
||||
|
||||
Comparison:
|
||||
1KiB with no literals
|
||||
YJIT: 159971.1 i/s
|
||||
local: 137049.0 i/s - 1.17x slower
|
||||
|
||||
10KiB with no literals
|
||||
YJIT: 28101.2 i/s
|
||||
local: 27271.7 i/s - 1.03x slower
|
||||
|
||||
100KiB with no literals
|
||||
local: 2940.9 i/s
|
||||
YJIT: 2937.0 i/s - 1.00x slower
|
||||
|
||||
1KiB of 25B literals
|
||||
YJIT: 4135.5 i/s
|
||||
local: 2803.2 i/s - 1.48x slower
|
||||
|
||||
10KiB of 25B literals
|
||||
YJIT: 385.4 i/s
|
||||
local: 263.0 i/s - 1.47x slower
|
||||
|
||||
100KiB of 25B literals
|
||||
YJIT: 22.5 i/s
|
||||
local: 18.4 i/s - 1.23x slower
|
||||
|
||||
1KiB of 0B literals
|
||||
YJIT: 759.6 i/s
|
||||
local: 505.7 i/s - 1.50x slower
|
||||
|
||||
10KiB of 0B literals
|
||||
YJIT: 67.6 i/s
|
||||
local: 45.4 i/s - 1.49x slower
|
||||
|
||||
100KiB of 0B literals
|
||||
YJIT: 3.5 i/s
|
||||
local: 2.7 i/s - 1.29x slower
|
||||
```
|
||||
|
||||
For responses that are larger than 10KiB, the benchmarks do take another
|
||||
dip. Despite that, I believe the algorithm _is_ still linear, and that
|
||||
the performance hit on large responses is probably due to the large
|
||||
strings inducing memory locality (paging/caching) bottlenecks.
|
||||
---
|
||||
lib/net/imap/response_reader.rb | 2 +-
|
||||
1 file changed, 1 insertion(+), 1 deletion(-)
|
||||
|
||||
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
|
||||
index d3e819c..71f1980 100644
|
||||
--- a/lib/net/imap/response_reader.rb
|
||||
+++ b/lib/net/imap/response_reader.rb
|
||||
@@ -32,7 +32,7 @@ module Net
|
||||
def empty? = buff.empty?
|
||||
def done? = line_done? && !get_literal_size
|
||||
def line_done? = buff.end_with?(CRLF)
|
||||
- def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
|
||||
+ def get_literal_size = buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
||||
|
||||
def read_line
|
||||
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
||||
|
||||
|
||||
From 49c516d621bbffeaf4472e8ff23b85703fc83034 Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Wed, 15 Apr 2026 10:00:24 -0400
|
||||
Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8D=92=20pick=2049c516d62:=20?=
|
||||
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Faster=20ResponseParser:=20short-circuit?=
|
||||
=?UTF-8?q?=20no=20literal?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
I was suprised at how much slower `buff.rindex` is (vs `=~`) when it
|
||||
doesn't match. This speeds up that case significantly (it's now faster
|
||||
than it was prior to the `rindex` change), with only a small impact in
|
||||
the case when it does match.
|
||||
|
||||
```
|
||||
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
|
||||
Warming up --------------------------------------
|
||||
1KiB with no literals 202.754k i/s - 210.144k times in 1.036449s (4.93μs/i)
|
||||
10KiB with no literals 55.683k i/s - 57.541k times in 1.033362s (17.96μs/i)
|
||||
100KiB with no literals 6.654k i/s - 7.176k times in 1.078491s (150.29μs/i)
|
||||
1KiB of 25B literals 2.780k i/s - 2.959k times in 1.064363s (359.70μs/i)
|
||||
10KiB of 25B literals 260.357 i/s - 286.000 times in 1.098491s (3.84ms/i)
|
||||
100KiB of 25B literals 19.485 i/s - 20.000 times in 1.026445s (51.32ms/i)
|
||||
1KiB of 0B literals 506.675 i/s - 550.000 times in 1.085508s (1.97ms/i)
|
||||
10KiB of 0B literals 44.384 i/s - 45.000 times in 1.013872s (22.53ms/i)
|
||||
100KiB of 0B literals 3.063 i/s - 4.000 times in 1.305939s (326.48ms/i)
|
||||
Calculating -------------------------------------
|
||||
local YJIT
|
||||
1KiB with no literals 194.355k 247.756k i/s - 608.261k times in 3.129645s 2.455086s
|
||||
10KiB with no literals 55.733k 58.585k i/s - 167.049k times in 2.997311s 2.851414s
|
||||
100KiB with no literals 6.553k 6.453k i/s - 19.961k times in 3.045870s 3.093461s
|
||||
1KiB of 25B literals 2.732k 4.061k i/s - 8.340k times in 3.052737s 2.053682s
|
||||
10KiB of 25B literals 256.552 379.524 i/s - 781.000 times in 3.044220s 2.057840s
|
||||
100KiB of 25B literals 17.804 23.286 i/s - 58.000 times in 3.257733s 2.490779s
|
||||
1KiB of 0B literals 467.714 703.446 i/s - 1.520k times in 3.249846s 2.160791s
|
||||
10KiB of 0B literals 45.376 65.876 i/s - 133.000 times in 2.931045s 2.018955s
|
||||
100KiB of 0B literals 3.072 3.840 i/s - 9.000 times in 2.929458s 2.343586s
|
||||
|
||||
Comparison:
|
||||
1KiB with no literals
|
||||
YJIT: 247755.5 i/s
|
||||
local: 194354.7 i/s - 1.27x slower
|
||||
|
||||
10KiB with no literals
|
||||
YJIT: 58584.6 i/s
|
||||
local: 55733.0 i/s - 1.05x slower
|
||||
|
||||
100KiB with no literals
|
||||
local: 6553.5 i/s
|
||||
YJIT: 6452.6 i/s - 1.02x slower
|
||||
|
||||
1KiB of 25B literals
|
||||
YJIT: 4061.0 i/s
|
||||
local: 2732.0 i/s - 1.49x slower
|
||||
|
||||
10KiB of 25B literals
|
||||
YJIT: 379.5 i/s
|
||||
local: 256.6 i/s - 1.48x slower
|
||||
|
||||
100KiB of 25B literals
|
||||
YJIT: 23.3 i/s
|
||||
local: 17.8 i/s - 1.31x slower
|
||||
|
||||
1KiB of 0B literals
|
||||
YJIT: 703.4 i/s
|
||||
local: 467.7 i/s - 1.50x slower
|
||||
|
||||
10KiB of 0B literals
|
||||
YJIT: 65.9 i/s
|
||||
local: 45.4 i/s - 1.45x slower
|
||||
|
||||
100KiB of 0B literals
|
||||
YJIT: 3.8 i/s
|
||||
local: 3.1 i/s - 1.25x slower
|
||||
```
|
||||
---
|
||||
lib/net/imap/response_reader.rb | 5 ++++-
|
||||
1 file changed, 4 insertions(+), 1 deletion(-)
|
||||
|
||||
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
|
||||
index 71f1980..09d9a79 100644
|
||||
--- a/lib/net/imap/response_reader.rb
|
||||
+++ b/lib/net/imap/response_reader.rb
|
||||
@@ -32,7 +32,10 @@ module Net
|
||||
def empty? = buff.empty?
|
||||
def done? = line_done? && !get_literal_size
|
||||
def line_done? = buff.end_with?(CRLF)
|
||||
- def get_literal_size = buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
||||
+
|
||||
+ def get_literal_size
|
||||
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
||||
+ end
|
||||
|
||||
def read_line
|
||||
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
||||
|
||||
|
||||
From 6f82e28f714ecced2396144bce0155e2cdb0256e Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Tue, 14 Apr 2026 14:40:48 -0400
|
||||
Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=8D=92=20pick=206f82e28f7:=20?=
|
||||
=?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Faster=20ResponseReader:=20parse=20literal?=
|
||||
=?UTF-8?q?=20from=20line?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Unfortunately, neither `#rindex` nor even `#end_with?` appear to be
|
||||
truly `O(1)` for very large strings (100K+). I'm guessing that this is
|
||||
due to memory locality and caching issues. But, by parsing the literal
|
||||
from the latest `line` (rather than the full buffer), we mostly avoid
|
||||
that problem.
|
||||
|
||||
Also, by explicitly parsing literal_size immediately after reading the
|
||||
line, we don't need to parse it again in `#done?`.
|
||||
|
||||
```
|
||||
$ benchmark-driver benchmarks/response_reader.yml --filter KiB
|
||||
Warming up --------------------------------------
|
||||
1KiB with no literals 202.846k i/s - 214.181k times in 1.055878s (4.93μs/i)
|
||||
10KiB with no literals 55.699k i/s - 57.354k times in 1.029717s (17.95μs/i)
|
||||
100KiB with no literals 6.622k i/s - 6.688k times in 1.009943s (151.01μs/i)
|
||||
1KiB of 25B literals 3.428k i/s - 3.751k times in 1.094065s (291.67μs/i)
|
||||
10KiB of 25B literals 342.733 i/s - 350.000 times in 1.021202s (2.92ms/i)
|
||||
100KiB of 25B literals 34.343 i/s - 36.000 times in 1.048234s (29.12ms/i)
|
||||
1KiB of 0B literals 683.800 i/s - 690.000 times in 1.009066s (1.46ms/i)
|
||||
10KiB of 0B literals 69.186 i/s - 70.000 times in 1.011759s (14.45ms/i)
|
||||
100KiB of 0B literals 6.914 i/s - 7.000 times in 1.012449s (144.64ms/i)
|
||||
Calculating -------------------------------------
|
||||
local YJIT
|
||||
1KiB with no literals 193.622k 250.330k i/s - 608.539k times in 3.142929s 2.430944s
|
||||
10KiB with no literals 55.944k 58.881k i/s - 167.096k times in 2.986849s 2.837843s
|
||||
100KiB with no literals 6.550k 6.480k i/s - 19.866k times in 3.033041s 3.065821s
|
||||
1KiB of 25B literals 3.445k 5.520k i/s - 10.285k times in 2.985693s 1.863057s
|
||||
10KiB of 25B literals 338.578 548.670 i/s - 1.028k times in 3.036224s 1.873620s
|
||||
100KiB of 25B literals 33.829 55.860 i/s - 103.000 times in 3.044728s 1.843900s
|
||||
1KiB of 0B literals 626.970 1.103k i/s - 2.051k times in 3.271287s 1.860275s
|
||||
10KiB of 0B literals 66.065 108.347 i/s - 207.000 times in 3.133301s 1.910523s
|
||||
100KiB of 0B literals 6.720 8.159 i/s - 20.000 times in 2.976273s 2.451265s
|
||||
|
||||
Comparison:
|
||||
1KiB with no literals
|
||||
YJIT: 250330.3 i/s
|
||||
local: 193621.6 i/s - 1.29x slower
|
||||
|
||||
10KiB with no literals
|
||||
YJIT: 58881.3 i/s
|
||||
local: 55943.9 i/s - 1.05x slower
|
||||
|
||||
100KiB with no literals
|
||||
local: 6549.9 i/s
|
||||
YJIT: 6479.8 i/s - 1.01x slower
|
||||
|
||||
1KiB of 25B literals
|
||||
YJIT: 5520.5 i/s
|
||||
local: 3444.8 i/s - 1.60x slower
|
||||
|
||||
10KiB of 25B literals
|
||||
YJIT: 548.7 i/s
|
||||
local: 338.6 i/s - 1.62x slower
|
||||
|
||||
100KiB of 25B literals
|
||||
YJIT: 55.9 i/s
|
||||
local: 33.8 i/s - 1.65x slower
|
||||
|
||||
1KiB of 0B literals
|
||||
YJIT: 1102.5 i/s
|
||||
local: 627.0 i/s - 1.76x slower
|
||||
|
||||
10KiB of 0B literals
|
||||
YJIT: 108.3 i/s
|
||||
local: 66.1 i/s - 1.64x slower
|
||||
|
||||
100KiB of 0B literals
|
||||
YJIT: 8.2 i/s
|
||||
local: 6.7 i/s - 1.21x slower
|
||||
```
|
||||
---
|
||||
lib/net/imap/response_reader.rb | 13 ++++++++-----
|
||||
1 file changed, 8 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/lib/net/imap/response_reader.rb b/lib/net/imap/response_reader.rb
|
||||
index 09d9a79..dd19d98 100644
|
||||
--- a/lib/net/imap/response_reader.rb
|
||||
+++ b/lib/net/imap/response_reader.rb
|
||||
@@ -8,6 +8,7 @@ module Net
|
||||
|
||||
def initialize(client, sock)
|
||||
@client, @sock = client, sock
|
||||
+ @buff = @literal_size = nil
|
||||
end
|
||||
|
||||
def read_response_buffer
|
||||
@@ -15,13 +16,13 @@ module Net
|
||||
catch :eof do
|
||||
while true
|
||||
read_line
|
||||
- break unless (@literal_size = get_literal_size)
|
||||
+ break unless literal_size
|
||||
read_literal
|
||||
end
|
||||
end
|
||||
buff
|
||||
ensure
|
||||
- @buff = nil
|
||||
+ @buff = @literal_size = nil
|
||||
end
|
||||
|
||||
private
|
||||
@@ -30,16 +31,18 @@ module Net
|
||||
|
||||
def bytes_read = buff.bytesize
|
||||
def empty? = buff.empty?
|
||||
- def done? = line_done? && !get_literal_size
|
||||
+ def done? = line_done? && !literal_size
|
||||
def line_done? = buff.end_with?(CRLF)
|
||||
|
||||
- def get_literal_size
|
||||
+ def get_literal_size(buff)
|
||||
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
||||
end
|
||||
|
||||
def read_line
|
||||
- buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
||||
+ line = (@sock.gets(CRLF, read_limit) or throw :eof)
|
||||
+ buff << line
|
||||
max_response_remaining! unless line_done?
|
||||
+ @literal_size = get_literal_size(line)
|
||||
end
|
||||
|
||||
def read_literal
|
||||
|
||||
@ -0,0 +1,125 @@
|
||||
From 62eea6ffe1e390060065169474f97edbc42bd2b2 Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Fri, 27 Mar 2026 17:31:11 -0400
|
||||
Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=92=F0=9F=A5=85=20Ensure=20STARTTL?=
|
||||
=?UTF-8?q?S=20tagged=20response=20was=20handled?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Taking a "belt-and-suspenders" approach to a STARTTLS stripping attack:
|
||||
|
||||
This handles `STARTTLS` as a special-case: if the `STARTTLS` handler
|
||||
did not run, for _whatever_ reason, an exception _must_ be raised and
|
||||
the connection dropped.
|
||||
|
||||
_No_ command should ever receive a tagged `OK` prior to completely
|
||||
sending the command. But `STARTTLS` is security-sensitive enough to
|
||||
warrant this special-case handler.
|
||||
---
|
||||
lib/net/imap.rb | 9 +++++++++
|
||||
1 file changed, 9 insertions(+)
|
||||
|
||||
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
|
||||
index 00f1588..e2cbc5a 100644
|
||||
--- a/lib/net/imap.rb
|
||||
+++ b/lib/net/imap.rb
|
||||
@@ -1390,9 +1390,11 @@ module Net
|
||||
#
|
||||
def starttls(**options)
|
||||
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
|
||||
+ handled = false
|
||||
error = nil
|
||||
ok = send_command("STARTTLS") do |resp|
|
||||
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
|
||||
+ handled = true
|
||||
clear_cached_capabilities
|
||||
clear_responses
|
||||
start_tls_session
|
||||
@@ -1404,6 +1406,13 @@ module Net
|
||||
disconnect
|
||||
raise error
|
||||
end
|
||||
+ unless handled
|
||||
+ disconnect
|
||||
+ raise InvalidResponseError,
|
||||
+ "STARTTLS handler was bypassed, although server responded %p" % [
|
||||
+ ok.raw_data.chomp
|
||||
+ ]
|
||||
+ end
|
||||
ok
|
||||
end
|
||||
|
||||
|
||||
|
||||
From 24d5c773d1bb76ca1cd0a26b2218195011c16969 Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Fri, 27 Mar 2026 18:00:09 -0400
|
||||
Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=94=92=F0=9F=A5=85=20Handle=20tagged?=
|
||||
=?UTF-8?q?=20"OK"=20to=20incomplete=20command?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Taking a "belt-and-suspenders" approach:
|
||||
|
||||
This is a potential problem for any command which registers a response
|
||||
handler: a malicious server can easily guess what the next tag will be,
|
||||
and send an `OK` response _before_ the client the response handler is
|
||||
attached.
|
||||
|
||||
`STARTTLS` is an extreme example of this issue: if the `STARTTLS`
|
||||
handler does not run, then `#starttls` will not start the TLS session,
|
||||
and the connection is not secured, _but no error is raised._
|
||||
|
||||
We should _also_ attach the response handler before sending the `CRLF`,
|
||||
but that is neither necessary (the response handler will added before
|
||||
the `synchronize` mutex is unlocked) nor sufficient (the fake `OK` can
|
||||
be sent _much_ earlier).
|
||||
|
||||
On the other hand, it _is_ okay for the server to send an error tagged
|
||||
response (`NO` or `BAD`), before sending the command has completed.
|
||||
---
|
||||
lib/net/imap.rb | 15 +++++++++++++++
|
||||
1 file changed, 15 insertions(+)
|
||||
|
||||
diff --git a/lib/net/imap.rb b/lib/net/imap.rb
|
||||
index e2cbc5a..6af1ca5 100644
|
||||
--- a/lib/net/imap.rb
|
||||
+++ b/lib/net/imap.rb
|
||||
@@ -3086,6 +3086,7 @@ module Net
|
||||
|
||||
synchronize do
|
||||
tag = Thread.current[:net_imap_tag] = generate_tag
|
||||
+ guard_against_tagged_response_skipping_handler!(tag, "IDLE")
|
||||
put_string("#{tag} IDLE#{CRLF}")
|
||||
|
||||
begin
|
||||
@@ -3550,6 +3551,7 @@ module Net
|
||||
put_string(" ")
|
||||
send_data(i, tag)
|
||||
end
|
||||
+ guard_against_tagged_response_skipping_handler!(tag, cmd)
|
||||
put_string(CRLF)
|
||||
if cmd == "LOGOUT"
|
||||
@logout_command_tag = tag
|
||||
@@ -3565,6 +3567,19 @@ module Net
|
||||
end
|
||||
end
|
||||
end
|
||||
+ rescue InvalidResponseError
|
||||
+ disconnect
|
||||
+ raise
|
||||
+ end
|
||||
+
|
||||
+ def guard_against_tagged_response_skipping_handler!(tag, cmd)
|
||||
+ return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
|
||||
+ raise InvalidResponseError, format(
|
||||
+ "Received tagged 'OK' to incomplete %s command (tag=%s). " \
|
||||
+ "This could indicate a malicious server, a man-in-the-middle, or " \
|
||||
+ "client-side command injection. Disconnecting.",
|
||||
+ cmd, tag
|
||||
+ )
|
||||
end
|
||||
|
||||
def generate_tag
|
||||
@ -0,0 +1,98 @@
|
||||
From 9db3e9d Mon Sep 17 00:00:00 2001
|
||||
From: nick evans <nick@rubinick.dev>
|
||||
Date: Thu, 19 Feb 2026 15:06:08 -0500
|
||||
Subject: [PATCH] =?UTF-8?q?=F0=9F=A5=85=20Strictly=20validate=20symbol=20(?=
|
||||
=?UTF-8?q?\flag)=20arguments?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Flags should not allow `atom-specials`.
|
||||
|
||||
Previously, no validation was done on symbol data. Sending atom or flag
|
||||
args which contain atom specials could lead to various errors.
|
||||
|
||||
Although this could theoretically include injection attacks, this is not
|
||||
considered to be a critical vulnerability in `net-imap`, for the
|
||||
following reason: Valid "system flag" inputs are restricted to an
|
||||
enumerated set of RFC-defined flag types. User-defined "keyword" flags
|
||||
are sent as atoms, not flags, which use string inputs (strings which
|
||||
can't be sent as an atom will be quoted or sent as a literal). `\Seen`
|
||||
as a flag (symbol argument) is semantically different from `Seen` as a
|
||||
keyword (string argument). So there is no scenario where it is
|
||||
appropriate to call `#to_sym` on unvetted user input. Any code which
|
||||
calls `#to_sym` indiscriminately on user-input is already buggy.
|
||||
|
||||
Nevertheless, users should reasonably be able to rely on `net-imap` to
|
||||
do very basic input validation on its basic input types.
|
||||
---
|
||||
lib/net/imap/command_data.rb | 33 +++++++++++++++++++++++++++------
|
||||
1 file changed, 27 insertions(+), 6 deletions(-)
|
||||
|
||||
diff --git a/lib/net/imap/command_data.rb b/lib/net/imap/command_data.rb
|
||||
index 02b9a61..2671d5d 100644
|
||||
--- a/lib/net/imap/command_data.rb
|
||||
+++ b/lib/net/imap/command_data.rb
|
||||
@@ -25,6 +25,7 @@ module Net
|
||||
end
|
||||
when Time, Date, DateTime
|
||||
when Symbol
|
||||
+ Flag.validate(data)
|
||||
else
|
||||
data.validate
|
||||
end
|
||||
@@ -45,7 +46,7 @@ module Net
|
||||
when Date
|
||||
send_date_data(data)
|
||||
when Symbol
|
||||
- send_symbol_data(data)
|
||||
+ Flag[data].send_data(self, tag)
|
||||
else
|
||||
data.send_data(self, tag)
|
||||
end
|
||||
@@ -115,11 +116,13 @@ module Net
|
||||
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
||||
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
||||
|
||||
- def send_symbol_data(symbol)
|
||||
- put_string("\\" + symbol.to_s)
|
||||
- end
|
||||
-
|
||||
CommandData = Data.define(:data) do # :nodoc:
|
||||
+ def self.validate(...)
|
||||
+ data = new(...)
|
||||
+ data.validate
|
||||
+ data
|
||||
+ end
|
||||
+
|
||||
def send_data(imap, tag)
|
||||
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
||||
end
|
||||
@@ -135,8 +138,26 @@ module Net
|
||||
end
|
||||
|
||||
class Atom < CommandData # :nodoc:
|
||||
+ def initialize(**)
|
||||
+ super
|
||||
+ validate
|
||||
+ end
|
||||
+
|
||||
+ def validate
|
||||
+ data.to_s.ascii_only? \
|
||||
+ or raise DataFormatError, "#{self.class} must be ASCII only"
|
||||
+ data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
||||
+ and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
||||
+ end
|
||||
+
|
||||
def send_data(imap, tag)
|
||||
- imap.__send__(:put_string, data)
|
||||
+ imap.__send__(:put_string, data.to_s)
|
||||
+ end
|
||||
+ end
|
||||
+
|
||||
+ class Flag < Atom # :nodoc:
|
||||
+ def send_data(imap, tag)
|
||||
+ imap.__send__(:put_string, "\\#{data}")
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user