Fix CVE-2026-59886: denial of service in Real type __float__()
Backport upstream commit 90d5e17 to fix CVE-2026-59886, a denial
of service vulnerability in the Real type's __float__() method.
The flaw allowed astronomically large integer materialization
via pow(). The patch rewrites __float__() to use math.ldexp()
for base-2 values, adds overflow protection for base-10 values,
fixes integer division in __normalizeBase10(), and handles zero
mantissa early. Comprehensive tests are added for BER, CER, DER
decoders and the Real type unit tests.
CVE: CVE-2026-59886
Upstream patches:
- 90d5e17239.patch
Resolves: RHEL-217908
This commit was backported by Ymir, a Red Hat Enterprise Linux software maintenance AI agent.
Assisted-by: Ymir
This commit is contained in:
parent
5a10953bf5
commit
b8c0657d9b
247
python-pyasn1-0.3.7-CVE-2026-59886.patch
Normal file
247
python-pyasn1-0.3.7-CVE-2026-59886.patch
Normal file
@ -0,0 +1,247 @@
|
||||
From e8f259647c04cf9a41130c8387cef2690d8ec19e Mon Sep 17 00:00:00 2001
|
||||
From: Simon Pichugin <simon.pichugin@gmail.com>
|
||||
Date: Wed, 8 Jul 2026 17:32:09 -0700
|
||||
Subject: [PATCH] Merge commit from fork
|
||||
|
||||
---
|
||||
pyasn1/type/univ.py | 21 ++++++++++---
|
||||
tests/codec/ber/test_decoder.py | 54 +++++++++++++++++++++++++++------
|
||||
tests/codec/cer/test_decoder.py | 11 +++++++
|
||||
tests/codec/der/test_decoder.py | 20 ++++++++++++
|
||||
tests/type/test_univ.py | 40 ++++++++++++++++++++++++
|
||||
5 files changed, 131 insertions(+), 15 deletions(-)
|
||||
|
||||
diff --git a/pyasn1/type/univ.py b/pyasn1/type/univ.py
|
||||
index a90c648..5d6c23a 100644
|
||||
--- a/pyasn1/type/univ.py
|
||||
+++ b/pyasn1/type/univ.py
|
||||
@@ -1444,7 +1444,7 @@ class Real(base.AbstractSimpleAsn1Item):
|
||||
def __normalizeBase10(value):
|
||||
m, b, e = value
|
||||
while m and m % 10 == 0:
|
||||
- m /= 10
|
||||
+ m //= 10
|
||||
e += 1
|
||||
return m, b, e
|
||||
|
||||
@@ -1595,10 +1595,21 @@ class Real(base.AbstractSimpleAsn1Item):
|
||||
def __float__(self):
|
||||
if self._value in self._inf:
|
||||
return self._value
|
||||
- else:
|
||||
- return float(
|
||||
- self._value[0] * pow(self._value[1], self._value[2])
|
||||
- )
|
||||
+
|
||||
+ mantissa, base, exponent = self._value
|
||||
+
|
||||
+ if not mantissa:
|
||||
+ return 0.0
|
||||
+
|
||||
+ if base == 2:
|
||||
+ return math.ldexp(float(mantissa), exponent)
|
||||
+
|
||||
+ # base is 10 (prettyIn() rejects everything else); refuse to
|
||||
+ # materialize astronomically large integers via pow()
|
||||
+ if exponent > sys.float_info.max_10_exp:
|
||||
+ raise OverflowError('Real value too large to convert to float')
|
||||
+
|
||||
+ return float(mantissa * pow(base, exponent))
|
||||
|
||||
def __abs__(self):
|
||||
return self.clone(abs(float(self)))
|
||||
diff --git a/tests/codec/ber/test_decoder.py b/tests/codec/ber/test_decoder.py
|
||||
index ba7dfec..9c98864 100644
|
||||
--- a/tests/codec/ber/test_decoder.py
|
||||
+++ b/tests/codec/ber/test_decoder.py
|
||||
@@ -13,7 +13,7 @@ except ImportError:
|
||||
from tests.base import BaseTestCase
|
||||
|
||||
from pyasn1.type import tag, namedtype, univ, char
|
||||
-from pyasn1.codec.ber import decoder, eoo
|
||||
+from pyasn1.codec.ber import decoder, encoder, eoo
|
||||
from pyasn1.compat.octets import ints2octs, str2octs, null
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
@@ -501,17 +501,51 @@ class RealDecoderTestCase(BaseTestCase):
|
||||
ints2octs((9, 4, 161, 255, 1, 3))
|
||||
) == (univ.Real((3, 2, -1020)), null)
|
||||
|
||||
-# TODO: this requires Real type comparison fix
|
||||
+ def testBin6(self): # large exponent, base = 16
|
||||
+ value, rest = decoder.decode(
|
||||
+ ints2octs((9, 5, 162, 0, 255, 255, 1))
|
||||
+ )
|
||||
+
|
||||
+ assert tuple(value) == (1, 2, 262140)
|
||||
+ assert rest == null
|
||||
+
|
||||
+ def testBin7(self): # large exponent in 4-octet form, base = 16
|
||||
+ value, rest = decoder.decode(
|
||||
+ ints2octs((9, 7, 227, 4, 1, 35, 69, 103, 1))
|
||||
+ )
|
||||
|
||||
-# def testBin6(self):
|
||||
-# assert decoder.decode(
|
||||
-# ints2octs((9, 5, 162, 0, 255, 255, 1))
|
||||
-# ) == (univ.Real((1, 2, 262140)), null)
|
||||
+ assert tuple(value) == (-1, 2, 76354972)
|
||||
+ assert rest == null
|
||||
+
|
||||
+ def testLargeBinaryRoundTrip(self):
|
||||
+ substrate = encoder.encode(univ.Real((-1, 2, 76354972)))
|
||||
+ value, rest = decoder.decode(substrate)
|
||||
+
|
||||
+ assert tuple(value) == (-1, 2, 76354972)
|
||||
+ assert rest == null
|
||||
+
|
||||
+ def testLongFormBinaryRealExponentLength(self):
|
||||
+ value, rest = decoder.decode(
|
||||
+ ints2octs((9, 6, 0x83, 3, 0x0f, 0x42, 0x40, 1))
|
||||
+ )
|
||||
|
||||
-# def testBin7(self):
|
||||
-# assert decoder.decode(
|
||||
-# ints2octs((9, 7, 227, 4, 1, 35, 69, 103, 1))
|
||||
-# ) == (univ.Real((-1, 2, 76354972)), null)
|
||||
+ assert tuple(value) == (1, 2, 1000000)
|
||||
+ assert rest == null
|
||||
+
|
||||
+ def testLargeBinaryPrettyPrintOverflow(self):
|
||||
+ value, rest = decoder.decode(
|
||||
+ b'\t\t\xeb\x060662.666\xd0B\x00\x00\x00\x00\x00\x00\x00'
|
||||
+ )
|
||||
+
|
||||
+ assert value.prettyPrint() == '<overflow>'
|
||||
+ assert rest == b'6\xd0B\x00\x00\x00\x00\x00\x00\x00'
|
||||
+
|
||||
+ try:
|
||||
+ float(value)
|
||||
+ except OverflowError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ assert 0, '__float__() tolerated overflow'
|
||||
|
||||
def testPlusInf(self):
|
||||
assert decoder.decode(
|
||||
diff --git a/tests/codec/cer/test_decoder.py b/tests/codec/cer/test_decoder.py
|
||||
index 828c17f..c4a91db 100644
|
||||
--- a/tests/codec/cer/test_decoder.py
|
||||
+++ b/tests/codec/cer/test_decoder.py
|
||||
@@ -14,6 +14,7 @@ except ImportError:
|
||||
from tests.base import BaseTestCase
|
||||
|
||||
from pyasn1.codec.cer import decoder
|
||||
+from pyasn1.codec.cer import encoder
|
||||
from pyasn1.compat.octets import ints2octs, str2octs, null
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
@@ -65,6 +66,16 @@ class OctetStringDecoderTestCase(BaseTestCase):
|
||||
# TODO: test failures on short chunked and long unchunked substrate samples
|
||||
|
||||
|
||||
+class RealDecoderTestCase(BaseTestCase):
|
||||
+ def testLargeBinaryRoundTrip(self):
|
||||
+ substrate = encoder.encode(univ.Real((-1, 2, 76354972)))
|
||||
+ value, rest = decoder.decode(substrate)
|
||||
+
|
||||
+ assert tuple(value) == (-1, 2, 76354972)
|
||||
+ assert rest == null
|
||||
+
|
||||
+
|
||||
+
|
||||
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
||||
|
||||
if __name__ == '__main__':
|
||||
diff --git a/tests/codec/der/test_decoder.py b/tests/codec/der/test_decoder.py
|
||||
index 6f292fd..08101a4 100644
|
||||
--- a/tests/codec/der/test_decoder.py
|
||||
+++ b/tests/codec/der/test_decoder.py
|
||||
@@ -14,6 +14,7 @@ except ImportError:
|
||||
from tests.base import BaseTestCase
|
||||
|
||||
from pyasn1.codec.der import decoder
|
||||
+from pyasn1.codec.der import encoder
|
||||
from pyasn1.compat.octets import ints2octs, null
|
||||
from pyasn1.error import PyAsn1Error
|
||||
|
||||
@@ -72,6 +73,25 @@ class OctetStringDecoderTestCase(BaseTestCase):
|
||||
assert 0, 'chunked encoding tolerated'
|
||||
|
||||
|
||||
+class RealDecoderTestCase(BaseTestCase):
|
||||
+ def testCanonicalLargeBinaryReal(self):
|
||||
+ substrate = encoder.encode(univ.Real((1, 2, 1000000)))
|
||||
+ assert substrate == ints2octs((9, 5, 0x82, 0x0f, 0x42, 0x40, 1))
|
||||
+
|
||||
+ value, rest = decoder.decode(substrate)
|
||||
+
|
||||
+ assert tuple(value) == (1, 2, 1000000)
|
||||
+ assert rest == null
|
||||
+
|
||||
+ def testLargeBinaryRoundTrip(self):
|
||||
+ substrate = encoder.encode(univ.Real((-1, 2, 76354972)))
|
||||
+ value, rest = decoder.decode(substrate)
|
||||
+
|
||||
+ assert tuple(value) == (-1, 2, 76354972)
|
||||
+ assert rest == null
|
||||
+
|
||||
+
|
||||
+
|
||||
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
|
||||
|
||||
if __name__ == '__main__':
|
||||
diff --git a/tests/type/test_univ.py b/tests/type/test_univ.py
|
||||
index f5124df..3453d11 100644
|
||||
--- a/tests/type/test_univ.py
|
||||
+++ b/tests/type/test_univ.py
|
||||
@@ -632,9 +632,49 @@ class RealTestCase(BaseTestCase):
|
||||
def testFloat(self):
|
||||
assert float(univ.Real(4.0)) == 4.0, '__float__() fails'
|
||||
|
||||
+ def testFloatBase10Precision(self):
|
||||
+ assert float(univ.Real((3, 10, 23))) == 3e23, '__float__() lost base-10 behavior'
|
||||
+
|
||||
+ def testFloatOverflow(self):
|
||||
+ try:
|
||||
+ float(univ.Real((1, 2, 1000000)))
|
||||
+ except OverflowError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ assert 0, '__float__() tolerated overflow'
|
||||
+
|
||||
+ assert univ.Real((1, 2, 1000000)).prettyPrint() == '<overflow>'
|
||||
+
|
||||
+ def testFloatUnderflow(self):
|
||||
+ assert float(univ.Real((1, 2, -1000000))) == 0.0, '__float__() failed underflow'
|
||||
+
|
||||
+ def testFloatZeroMantissa(self):
|
||||
+ assert float(univ.Real((0, 10, 1000000000))) == 0.0, '__float__() failed zero mantissa'
|
||||
+ assert float(univ.Real((0, 2, 1000000000))) == 0.0, '__float__() failed zero mantissa'
|
||||
+
|
||||
+ def testFloatBase10Overflow(self):
|
||||
+ try:
|
||||
+ float(univ.Real((1, 10, sys.float_info.max_10_exp + 1)))
|
||||
+ except OverflowError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ assert 0, '__float__() tolerated base-10 overflow'
|
||||
+
|
||||
+ def testFloatBase10NormalizedOverflow(self):
|
||||
+ try:
|
||||
+ float(univ.Real((10, 10, sys.float_info.max_10_exp)))
|
||||
+ except OverflowError:
|
||||
+ pass
|
||||
+ else:
|
||||
+ assert 0, '__float__() tolerated normalized base-10 overflow'
|
||||
+
|
||||
def testPrettyIn(self):
|
||||
assert univ.Real((3, 10, 0)) == 3, 'prettyIn() fails'
|
||||
|
||||
+ def testPrettyInBigBase10Mantissa(self):
|
||||
+ assert tuple(univ.Real((10 ** 400, 10, 0))) == (1, 10, 400), \
|
||||
+ 'prettyIn() big mantissa normalization fails'
|
||||
+
|
||||
# infinite float values
|
||||
def testStrInf(self):
|
||||
assert str(univ.Real('inf')) == 'inf', 'str() fails'
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
Name: python-pyasn1
|
||||
Version: 0.3.7
|
||||
Release: 6%{?dist}.1
|
||||
Release: 6%{?dist}.2
|
||||
Summary: ASN.1 tools for Python
|
||||
License: BSD
|
||||
Group: System Environment/Libraries
|
||||
@ -13,6 +13,8 @@ URL: http://pyasn1.sourceforge.net/
|
||||
BuildArch: noarch
|
||||
|
||||
Patch1: 0001-Backport-commit-be353d7.patch
|
||||
# https://github.com/pyasn1/pyasn1/commit/90d5e1723913d756c23709bb90feb6f9e8bf585f
|
||||
Patch2: python-pyasn1-0.3.7-CVE-2026-59886.patch
|
||||
|
||||
|
||||
%description
|
||||
@ -89,6 +91,11 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} %{__python3} setup.py test
|
||||
%doc doc/build/html/*
|
||||
|
||||
%changelog
|
||||
* Wed Jul 29 2026 RHEL Packaging Agent <redhat-ymir-agent@redhat.com> - 0.3.7-6.2
|
||||
- Fix denial of service in Real type __float__() method
|
||||
(CVE-2026-59886)
|
||||
- Resolves: RHEL-217908
|
||||
|
||||
* Wed Feb 18 2026 Masahiro Matsuya <mmatsuya@redhat.com> - 0.3.7-6.el8_10.1
|
||||
- Resolves: RHEL-148145
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user