Fix DoS via crafted IMAP responses in net-imap.
Fixes CVE-2026-42245. Test commits from PR were dropped. The gem does not have tests in the ruby tar. Backported from https://github.com/ruby/net-imap/commit/f46fe38 https://github.com/ruby/net-imap/commit/0560d26 https://github.com/ruby/net-imap/commit/bfdae21 Resolves: RHEL-181681
This commit is contained in:
parent
133b664551
commit
ddc73cf564
16
ruby.spec
16
ruby.spec
@ -173,7 +173,7 @@
|
||||
Summary: An interpreter of object-oriented scripting language
|
||||
Name: ruby
|
||||
Version: %{ruby_version}%{?development_release}
|
||||
Release: 12%{?dist}
|
||||
Release: 13%{?dist}
|
||||
# Licenses, which are likely not included in binary RPMs:
|
||||
# Apache-2.0:
|
||||
# benchmark/gc/redblack.rb
|
||||
@ -299,6 +299,12 @@ Patch16: ruby-3.4.2-openssl-Fix-SHA-1-PSS-tests.patch
|
||||
# https://github.com/ruby/ruby/commit/a53f3d57d1c70f35534c457de2c471d84a55956a
|
||||
# https://github.com/ruby/ruby/commit/2f223e90edf40f5d537760cb26c77b608bddff36
|
||||
Patch17: rubygem-erb-4.0.3.1-Fix-arbitrary-code-execution-via-deserialization-bypass-CVE-2026-41316.patch
|
||||
# Test commits from PR were dropped. The gem does not have tests in the ruby tar.
|
||||
# Backported from
|
||||
# https://github.com/ruby/net-imap/commit/f46fe38
|
||||
# https://github.com/ruby/net-imap/commit/0560d26
|
||||
# https://github.com/ruby/net-imap/commit/bfdae21
|
||||
Patch18: rubygem-net-imap-0.4.24-DoS-via-crafted-IMAP-responses-CVE-2026-42245.patch
|
||||
|
||||
Requires: %{name}-libs%{?_isa} = %{version}-%{release}
|
||||
%{?with_rubypick:Suggests: rubypick}
|
||||
@ -784,6 +790,10 @@ analysis result in RBS format, a standard type description format for Ruby
|
||||
%patch 16 -p1
|
||||
%patch 17 -p1
|
||||
|
||||
pushd .bundle/gems/net-imap-%{net_imap_version}
|
||||
%patch 18 -p1
|
||||
popd
|
||||
|
||||
# Provide an example of usage of the tapset:
|
||||
cp -a %{SOURCE3} .
|
||||
|
||||
@ -1788,6 +1798,10 @@ make -C %{_vpath_builddir} runruby TESTRUN_SCRIPT=" \
|
||||
|
||||
|
||||
%changelog
|
||||
* Thu Jun 11 2026 Jarek Prokop <jprokop@redhat.com> - 3.3.10-13
|
||||
- Fix DoS via crafted IMAP responses in net-imap. (CVE-2026-42245)
|
||||
Resolves: RHEL-181681
|
||||
|
||||
* Tue Apr 28 2026 Jarek Prokop <jprokop@redhat.com> - 3.3.10-12
|
||||
- Fix arbitrary code execution via deserialization bypass in ERB. (CVE-2026-41316)
|
||||
Resolves: RHEL-171246
|
||||
|
||||
@ -0,0 +1,340 @@
|
||||
From cd5ccc6948eac1a0b5d5da625ab19f56df6cfaa5 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 fd7561f..d59d672 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? end
|
||||
def done?; line_done? && !get_literal_size end
|
||||
def line_done?; buff.end_with?(CRLF) end
|
||||
- def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
|
||||
+ def get_literal_size; buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i end
|
||||
|
||||
def read_line
|
||||
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
||||
|
||||
From c6493156d14dbac9e46a0b14e7d01ee5b83c70a9 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 d59d672..024d67a 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? end
|
||||
def done?; line_done? && !get_literal_size end
|
||||
def line_done?; buff.end_with?(CRLF) end
|
||||
- def get_literal_size; buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i end
|
||||
+
|
||||
+ 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 2e9d425aae61801deb18384fbd295cee6a9850a3 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 024d67a..56958eb 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 end
|
||||
def empty?; buff.empty? end
|
||||
- def done?; line_done? && !get_literal_size end
|
||||
+ def done?; line_done? && !literal_size end
|
||||
def line_done?; buff.end_with?(CRLF) end
|
||||
|
||||
- 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
|
||||
Loading…
Reference in New Issue
Block a user