From 3e11ab49484c19cc2c84eb05df141cc1d7da05de Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 11 Feb 2026 05:31:11 +0300 Subject: [PATCH 01/11] Support ints in benchmarks --- bench/collatz.py | 2 ++ bench/mul.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/bench/collatz.py b/bench/collatz.py index 19826554..3b08a4a3 100644 --- a/bench/collatz.py +++ b/bench/collatz.py @@ -8,6 +8,8 @@ from gmpy2 import mpz elif os.getenv("T") == "flint.fmpz": from flint import fmpz as mpz +elif os.getenv("T") == "int": + mpz = int else: from gmp import mpz diff --git a/bench/mul.py b/bench/mul.py index 65ff8973..09cc5758 100644 --- a/bench/mul.py +++ b/bench/mul.py @@ -9,6 +9,8 @@ from gmpy2 import mpz elif os.getenv("T") == "flint.fmpz": from flint import fmpz as mpz +elif os.getenv("T") == "int": + mpz = int else: from gmp import mpz From a4bc8b592f9d56713720ea0e9338d6af595da343 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 11 Feb 2026 10:39:53 +0300 Subject: [PATCH 02/11] Simplify MPZ_from_str() --- gmp.c | 56 +------------------------------------------------------- 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/gmp.c b/gmp.c index b3f0b01a..9a329604 100644 --- a/gmp.c +++ b/gmp.c @@ -140,61 +140,12 @@ MPZ_from_str(PyObject *obj, int base) if (!str) { return NULL; /* LCOV_EXCL_LINE */ } - if (base < 0) { - goto bad_base; - } MPZ_Object *res = MPZ_new(); if (!res) { return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_LINE */ } - while (isspace(*str)) { - str++; - } - - bool cast_negative = (str[0] == '-'); - - str += cast_negative; - if (str[0] == '+') { - str++; - } - if (str[0] == '0') { - if (base == 0) { - if (tolower(str[1]) == 'b') { - base = 2; - } - else if (tolower(str[1]) == 'o') { - base = 8; - } - else if (tolower(str[1]) == 'x') { - base = 16; - } - else if (!isspace(str[1]) && str[1] != '\0') { - goto err; - } - } - if ((tolower(str[1]) == 'b' && base == 2) - || (tolower(str[1]) == 'o' && base == 8) - || (tolower(str[1]) == 'x' && base == 16)) - { - str += 2; - if (str[0] == '_') { - str++; - } - } - else { - goto skip_negation; - } - } - else { -skip_negation: - str -= cast_negative; - cast_negative = false; - } - if (base == 0) { - base = 10; - } zz_err ret = zz_set_str(str, base, &res->z); @@ -206,22 +157,17 @@ MPZ_from_str(PyObject *obj, int base) } else if (ret == ZZ_VAL) { Py_DECREF(res); - if (2 <= base && base <= 36) { -err: + if (!base || (2 <= base && base <= 36)) { PyErr_Format(PyExc_ValueError, "invalid literal for mpz() with base %d: %.200R", base, obj); } else { -bad_base: PyErr_SetString(PyExc_ValueError, "mpz base must be >= 2 and <= 36, or 0"); } return NULL; } - if (cast_negative) { - (void)zz_neg(&res->z, &res->z); - } return res; } From fba10a78ad37987c23e2ecdca395781d38bf682a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 13 Feb 2026 13:29:16 +0300 Subject: [PATCH 03/11] Use libzz v0.9.0a2 --- .readthedocs.yaml | 2 +- README.rst | 2 +- scripts/cibw_before_all.sh | 2 +- tests/test_mpz.py | 9 ++++----- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 160f9270..f1520fed 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,7 @@ build: pre_create_environment: - | PREFIX=$(pwd)/.local - ZZ_VERSION=0.8.0 + ZZ_VERSION=0.9.0a2 ZZ_DIR=zz-${ZZ_VERSION} GITHUB_URL=https://github.com/diofant/zz/releases/download/ ZZ_URL=${GITHUB_URL}v${ZZ_VERSION}/${ZZ_DIR}.tar.gz diff --git a/README.rst b/README.rst index aa437551..2be0e62f 100644 --- a/README.rst +++ b/README.rst @@ -2,7 +2,7 @@ Python-GMP ========== Python extension module, providing bindings to the GNU GMP via the `ZZ library -`_ (version 0.8.0 or later required). This +`_ (version 0.9.0 or later required). This module shouldn't crash the interpreter. The gmp can be used as a `gmpy2`_/`python-flint`_ replacement to provide diff --git a/scripts/cibw_before_all.sh b/scripts/cibw_before_all.sh index 4978b953..effe2f67 100644 --- a/scripts/cibw_before_all.sh +++ b/scripts/cibw_before_all.sh @@ -53,7 +53,7 @@ make --silent all install cd .. -ZZ_VERSION=0.8.0 +ZZ_VERSION=0.9.0a2 ZZ_DIR=zz-${ZZ_VERSION} ZZ_URL=https://github.com/diofant/zz/releases/download/v${ZZ_VERSION}/${ZZ_DIR}.tar.gz diff --git a/tests/test_mpz.py b/tests/test_mpz.py index bac29f57..96f142db 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -725,17 +725,16 @@ def test_power_errors(): pow(mpz(1<<64), (1<<31) - 1) with pytest.raises(MemoryError): pow(mpz(1<<64), (1<<31)) + with pytest.raises(OverflowError): + mpz(1) << ((1 << 63) - 1) else: with pytest.raises(MemoryError): pow(mpz(1<<64), (1<<63) - 1) if platform.system() != "Darwin": with pytest.raises(MemoryError): pow(mpz(1<<64), (1<<63)) - with pytest.raises(OverflowError): - mpz(1) << ((1 << 64) - 1) - if platform.system() != "Darwin": - with pytest.raises(MemoryError): - mpz(1) << ((1 << 63) - 1) + with pytest.raises(OverflowError): + mpz(1) << ((1 << 64) - 1) @given(bigints(), integers(max_value=12345)) From 66ddf910113595958fc098f4a27e200ddbc73ade Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 14 Feb 2026 05:03:30 +0300 Subject: [PATCH 04/11] Use zz_get() in hash() --- gmp.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gmp.c b/gmp.c index 9a329604..69dcf638 100644 --- a/gmp.c +++ b/gmp.c @@ -818,8 +818,10 @@ hash(PyObject *self) assert((int64_t)INT64_MAX > pyhash_modulus); (void)zz_div(&u->z, (int64_t)pyhash_modulus, NULL, &w); - Py_hash_t r = w.size ? (Py_hash_t)w.digits[0] : 0; + Py_hash_t r; + assert(sizeof(Py_hash_t) == 8); + (void)zz_get(&w, (int64_t *)&r); if (zz_isneg(&u->z) && r) { r = -(pyhash_modulus - r); } From ca418c5c83c666c0c8b19d5d7dc39aee8539dd4b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 14 Feb 2026 10:36:09 +0300 Subject: [PATCH 05/11] Relax default timeout more (for PyPy) --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index bd334309..838918b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ def pytest_configure(config): from hypothesis import settings default = settings.get_profile("default") - settings.register_profile("default", settings(default, deadline=1600)) + settings.register_profile("default", settings(default, deadline=3000)) ci = settings.get_profile("ci") if platform.python_implementation() != "GraalVM": ci = settings(ci, max_examples=10000) From f0645b30887737ea0b97ec30a18988c4f7ccd2fd Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Sat, 14 Feb 2026 05:57:40 +0300 Subject: [PATCH 06/11] Handle integer overflows in power() --- gmp.c | 28 ++++++++++++++++++++-------- tests/test_mpz.py | 8 ++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/gmp.c b/gmp.c index 69dcf638..aef553e8 100644 --- a/gmp.c +++ b/gmp.c @@ -1495,17 +1495,29 @@ power(PyObject *self, PyObject *other, PyObject *module) Py_DECREF(vf); return resf; } - res = MPZ_new(); int64_t exp; - if (!res || zz_get(&v->z, &exp) - || zz_pow(&u->z, (zz_digit_t)exp, &res->z)) - { - /* LCOV_EXCL_START */ - Py_CLEAR(res); - PyErr_SetNone(PyExc_MemoryError); - /* LCOV_EXCL_STOP */ + if (zz_get(&v->z, &exp)) { + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + } + else { + res = MPZ_new(); + if (res) { + zz_err ret = zz_pow(&u->z, (uint64_t)exp, &res->z); + + if (ret) { + Py_CLEAR(res); + if (ret == ZZ_MEM) { + PyErr_SetNone(PyExc_MemoryError); /* LCOV_EXCL_LINE */ + } + else { + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + } + } + } } Py_DECREF(u); Py_DECREF(v); diff --git a/tests/test_mpz.py b/tests/test_mpz.py index 96f142db..881480ac 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -721,17 +721,17 @@ def test_power_errors(): if platform.python_implementation() == "GraalVM": return # issue oracle/graalpython#551 if BITCNT_MAX < (1<<64) - 1: - with pytest.raises(MemoryError): + with pytest.raises(OverflowError): pow(mpz(1<<64), (1<<31) - 1) - with pytest.raises(MemoryError): + with pytest.raises(OverflowError): pow(mpz(1<<64), (1<<31)) with pytest.raises(OverflowError): mpz(1) << ((1 << 63) - 1) else: - with pytest.raises(MemoryError): + with pytest.raises(OverflowError): pow(mpz(1<<64), (1<<63) - 1) if platform.system() != "Darwin": - with pytest.raises(MemoryError): + with pytest.raises(OverflowError): pow(mpz(1<<64), (1<<63)) with pytest.raises(OverflowError): mpz(1) << ((1 << 64) - 1) From 7b59b256bc93dc508946568f3e0390f9f508eb53 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 16 Feb 2026 11:48:19 +0300 Subject: [PATCH 07/11] Handle integer overflows in __round__() --- gmp.c | 67 ++++++++++++++++++++++++++--------------------- tests/test_mpz.py | 6 +++++ 2 files changed, 43 insertions(+), 30 deletions(-) diff --git a/gmp.c b/gmp.c index aef553e8..5f1a852b 100644 --- a/gmp.c +++ b/gmp.c @@ -1831,58 +1831,67 @@ __round__(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } MPZ_Object *ndigits = NULL; - zz_t exp, ten, p; - CHECK_OP_INT(ndigits, args[0]); - - if (zz_isneg(&ndigits->z)) { - if (zz_init(&exp) || zz_pos(&ndigits->z, &exp)) { - /* LCOV_EXCL_START */ - zz_clear(&exp); - Py_DECREF(ndigits); - return PyErr_NoMemory(); - /* LCOV_EXCL_STOP */ - } + if (MPZ_Check(args[0])) { + ndigits = (MPZ_Object *)plus(args[0]); } else { + ndigits = MPZ_from_int(args[0]); + } + if (!ndigits) { + return NULL; + } + if (!zz_isneg(&ndigits->z)) { Py_DECREF(ndigits); goto noop; } - Py_DECREF(ndigits); - if (zz_init(&ten) || zz_set(10, &ten) || exp.size != 1) { + + zz_t divisor; + int64_t exp; + + (void)zz_neg(&ndigits->z, &ndigits->z); + if (zz_init(&divisor) || zz_set(10, &divisor)) { /* LCOV_EXCL_START */ - zz_clear(&exp); - zz_clear(&ten); + zz_clear(&divisor); return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } - if (zz_init(&p) || zz_pow(&ten, exp.digits[0], &p)) { - /* LCOV_EXCL_START */ - zz_clear(&exp); - zz_clear(&ten); - zz_clear(&p); - return PyErr_NoMemory(); - /* LCOV_EXCL_STOP */ + if (zz_get(&ndigits->z, &exp)) { + zz_clear(&divisor); + goto overflow; } - zz_clear(&exp); - zz_clear(&ten); + + zz_err ret = zz_pow(&divisor, (uint64_t)exp, &divisor); + + if (ret) { + zz_clear(&divisor); + Py_DECREF(ndigits); + if (ret == ZZ_MEM) { + return PyErr_NoMemory(); /* LCOV_EXCL_LINE */ + } +overflow: + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + } + Py_DECREF(ndigits); MPZ_Object *res = MPZ_new(); if (!res) { /* LCOV_EXCL_START */ - zz_clear(&p); + zz_clear(&divisor); return NULL; /* LCOV_EXCL_STOP */ } - if (zz_divnear(&u->z, &p, NULL, &res->z)) { + if (zz_divnear(&u->z, &divisor, NULL, &res->z)) { /* LCOV_EXCL_START */ - zz_clear(&p); + zz_clear(&divisor); Py_DECREF(res); return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } - zz_clear(&p); + zz_clear(&divisor); if (zz_sub(&u->z, &res->z, &res->z)) { /* LCOV_EXCL_START */ Py_DECREF(res); @@ -1890,8 +1899,6 @@ __round__(PyObject *self, PyObject *const *args, Py_ssize_t nargs) /* LCOV_EXCL_STOP */ } return (PyObject *)res; -end: - return NULL; } static PyObject * diff --git a/tests/test_mpz.py b/tests/test_mpz.py index 881480ac..fff435a1 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -980,6 +980,12 @@ def test_round_interface(): x.__round__(1, 2) with pytest.raises(TypeError): x.__round__(1j) + with pytest.raises(OverflowError): + x.__round__((-1<<64) + 1) + with pytest.raises(OverflowError): + x.__round__((-1<<62)) + with pytest.raises(OverflowError): + x.__round__(-1<<127) @pytest.mark.skipif(platform.python_implementation() == "PyPy", From ba18f1b0d5a63111ea0a903da8e189a91cb68ec3 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 16 Feb 2026 11:48:35 +0300 Subject: [PATCH 08/11] Handle integer overflows in *mpmath*() functions --- gmp.c | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/gmp.c b/gmp.c index 5f1a852b..844beb12 100644 --- a/gmp.c +++ b/gmp.c @@ -2474,14 +2474,12 @@ zz_mpmath_normalize(zz_bitcnt_t prec, zz_rnd rnd, bool *negative, } zz_t tmp; + zz_err ret = ZZ_OK; - if (zz_init(&tmp) || shift > INT64_MAX - || zz_set((int64_t)shift, &tmp) - || zz_add(exp, &tmp, exp)) - { + if (zz_init(&tmp) || (ret = zz_add(exp, shift, exp))) { /* LCOV_EXCL_START */ zz_clear(&tmp); - return ZZ_MEM; + return ret; /* LCOV_EXCL_STOP */ } zz_clear(&tmp); @@ -2498,14 +2496,12 @@ zz_mpmath_normalize(zz_bitcnt_t prec, zz_rnd rnd, bool *negative, } zz_t tmp; + zz_err ret = ZZ_OK; - if (zz_init(&tmp) || zbits > INT64_MAX - || zz_set((int64_t)zbits, &tmp) - || zz_add(exp, &tmp, exp)) - { + if (zz_init(&tmp) || (ret = zz_add(exp, zbits, exp))) { /* LCOV_EXCL_START */ zz_clear(&tmp); - return ZZ_MEM; + return ret; /* LCOV_EXCL_STOP */ } zz_clear(&tmp); @@ -2545,13 +2541,19 @@ gmp__mpmath_normalize(PyObject *self, PyObject *const *args, Py_ssize_t nargs) MPZ_Object *man = (MPZ_Object *)plus(args[1]); MPZ_Object *exp = MPZ_from_int(args[2]); + zz_err ret = ZZ_OK; - if (!exp || !man || zz_mpmath_normalize(prec, rnd, &negative, - &man->z, &exp->z, &bc)) + if (!exp || !man || (ret = zz_mpmath_normalize(prec, rnd, &negative, + &man->z, &exp->z, &bc))) { /* LCOV_EXCL_START */ Py_XDECREF((PyObject *)man); Py_XDECREF((PyObject *)exp); + if (ret == ZZ_BUF) { + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + } return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } @@ -2628,13 +2630,19 @@ gmp__mpmath_create(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } MPZ_Object *exp = MPZ_from_int(args[1]); + zz_err ret = ZZ_OK; - if (!exp || zz_mpmath_normalize(prec, rnd, &negative, - &man->z, &exp->z, &bc)) + if (!exp || (ret = zz_mpmath_normalize(prec, rnd, &negative, + &man->z, &exp->z, &bc))) { /* LCOV_EXCL_START */ Py_DECREF(man); Py_XDECREF((PyObject *)exp); + if (ret == ZZ_BUF) { + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + } return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } From 178f607a98d8444301c69054521ef6866b32f029 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 16 Feb 2026 11:48:50 +0300 Subject: [PATCH 09/11] Handle more integer overflows --- gmp.c | 149 ++++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 41 deletions(-) diff --git a/gmp.c b/gmp.c index 844beb12..708c0d80 100644 --- a/gmp.c +++ b/gmp.c @@ -56,7 +56,7 @@ int OPT_PREFIX = 0x2; PyObject * MPZ_to_str(MPZ_Object *u, int base, int options) { - size_t len; + size_t len = 0; bool negative = zz_isneg(&u->z); if (zz_sizeinbase(&u->z, base, &len)) { @@ -144,7 +144,7 @@ MPZ_from_str(PyObject *obj, int base) MPZ_Object *res = MPZ_new(); if (!res) { - return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_LINE */ + return NULL; /* LCOV_EXCL_LINE */ } zz_err ret = zz_set_str(str, base, &res->z); @@ -155,6 +155,13 @@ MPZ_from_str(PyObject *obj, int base) return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } + else if (ret == ZZ_BUF) { + /* LCOV_EXCL_START */ + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + /* LCOV_EXCL_STOP */ + } else if (ret == ZZ_VAL) { Py_DECREF(res); if (!base || (2 <= base && base <= 36)) { @@ -185,10 +192,22 @@ MPZ_from_int(PyObject *obj) } if (long_export.digits) { res = MPZ_new(); - if (!res || zz_import((size_t)long_export.ndigits, - long_export.digits, *int_layout, &res->z)) - { - return (MPZ_Object *)PyErr_NoMemory(); /* LCOV_EXCL_LINE */ + if (!res) { + return NULL; /* LCOV_EXCL_LINE */ + } + + zz_err ret = zz_import((size_t)long_export.ndigits, + long_export.digits, *int_layout, &res->z); + + if (ret) { + /* LCOV_EXCL_START */ + if (ret == ZZ_MEM) { + return (MPZ_Object *)PyErr_NoMemory(); + } + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + /* LCOV_EXCL_STOP */ } if (long_export.negative) { (void)zz_neg(&res->z, &res->z); @@ -262,7 +281,7 @@ MPZ_to_int(MPZ_Object *u) if (zz_get_str(&u->z, 16, buf)) { /* LCOV_EXCL_START */ free(buf); - return NULL; + return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } @@ -329,7 +348,7 @@ zz_get_bytes(const zz_t *u, size_t length, bool is_signed, size_t gap = length - (nbits + bits_per_digit/8 - 1)/(bits_per_digit/8); - zz_export(u, bytes_layout, length - gap, *buffer + gap); + (void)zz_export(u, bytes_layout, length - gap, *buffer + gap); memset(*buffer, is_negative ? 0xFF : 0, gap); zz_clear(&tmp); return ZZ_OK; @@ -383,13 +402,18 @@ zz_set_bytes(const unsigned char *buffer, size_t length, bool is_signed, if (!length) { return zz_set(0, u); } - if (zz_import(length, buffer, bytes_layout, u)) { - return ZZ_MEM; /* LCOV_EXCL_LINE */ + + zz_err ret = zz_import(length, buffer, bytes_layout, u); + + if (ret) { + return ret; /* LCOV_EXCL_LINE */ } (void)zz_abs(u, u); if (is_signed && zz_bitlen(u) == 8*(size_t)length) { zz_t tmp; + /* we assume, that bytes were created by zz_get_bytes(), + else there could be overflow in zz_mul_2exp() */ if (zz_init(&tmp) || zz_set(1, &tmp) || zz_mul_2exp(&tmp, 8*length, &tmp) || zz_sub(&tmp, u, u) || zz_neg(u, u)) @@ -436,19 +460,33 @@ MPZ_from_bytes(PyObject *obj, int is_little, int is_signed) MPZ_Object *res = MPZ_new(); - if (!res || zz_set_bytes(buffer, (size_t)length, is_signed, &res->z)) { + if (!res) { /* LCOV_EXCL_START */ Py_DECREF(bytes); if (is_little) { free(buffer); } - Py_XDECREF((PyObject *)res); - return (MPZ_Object *)PyErr_NoMemory(); + return NULL; /* LCOV_EXCL_STOP */ } + + zz_err ret = zz_set_bytes(buffer, (size_t)length, is_signed, &res->z); + + Py_DECREF(bytes); if (is_little) { free(buffer); } + if (ret) { + /* LCOV_EXCL_START */ + Py_DECREF((PyObject *)res); + if (ret == ZZ_MEM) { + return (MPZ_Object *)PyErr_NoMemory(); + } + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + /* LCOV_EXCL_STOP */ + } return res; } @@ -831,23 +869,36 @@ hash(PyObject *self) return u->hash_cache = r; } -#define UNOP(suff, func) \ - static PyObject * \ - func(PyObject *self) \ - { \ - MPZ_Object *u = (MPZ_Object *)self; \ - MPZ_Object *res = MPZ_new(); \ - \ - if (res && zz_##suff(&u->z, &res->z)) { \ - PyErr_NoMemory(); /* LCOV_EXCL_LINE */ \ - } \ - return (PyObject *)res; \ +#define UNOP(suff, func) \ + static PyObject * \ + func(PyObject *self) \ + { \ + MPZ_Object *u = (MPZ_Object *)self; \ + MPZ_Object *res = MPZ_new(); \ + \ + if (!res) { \ + return NULL; \ + } \ + \ + zz_err ret = zz_##suff(&u->z, &res->z); \ + \ + if (ret) { \ + Py_CLEAR(res); \ + if (ret == ZZ_BUF) { \ + PyErr_SetString(PyExc_OverflowError, \ + "too many digits in integer"); \ + } \ + else { \ + PyErr_NoMemory(); \ + } \ + } \ + return (PyObject *)res; \ } UNOP(pos, plus) UNOP(neg, nb_negative) UNOP(abs, nb_absolute) -UNOP(invert, nb_invert) +UNOP(invert, nb_invert) /* may overflow */ PyObject * to_int(PyObject *self) @@ -920,17 +971,19 @@ to_bool(PyObject *self) } \ ret = zz_##suff(&u->z, &v->z, &res->z); \ done: \ - if (ret == ZZ_OK) { \ - goto end; \ - } \ - if (ret == ZZ_VAL) { \ - Py_CLEAR(res); \ - PyErr_SetString(PyExc_ZeroDivisionError, \ - "division by zero"); \ - } \ - else { \ + if (ret) { \ Py_CLEAR(res); \ - PyErr_NoMemory(); \ + if (ret == ZZ_VAL) { \ + PyErr_SetString(PyExc_ZeroDivisionError, \ + "division by zero"); \ + } \ + else if (ret == ZZ_BUF) { \ + PyErr_SetString(PyExc_OverflowError, \ + "too many digits in integer"); \ + } \ + else { \ + PyErr_NoMemory(); \ + } \ } \ end: \ Py_XDECREF((PyObject *)u); \ @@ -980,6 +1033,7 @@ BINOP(mul, PyNumber_Multiply) #define zz_quo_(u, v, w) zz_div((u), (v), (w), NULL) #define zz_rem_(u, v, w) zz_div((u), (v), NULL, (w)) +/* can't overflow */ BINOP(quo_, PyNumber_FloorDivide) BINOP(rem_, PyNumber_Remainder) @@ -1097,6 +1151,8 @@ zz_divnear(const zz_t *u, const zz_t *v, zz_t *q, zz_t *r) if (cmp == ZZ_EQ && !zz_isodd(v) && !zz_iszero(q) && zz_isodd(q)) { cmp = unexpect; } + /* No overflow is possible, since q can't have maximal value + below (that means v==1) and r have same sign as v (with |r| < |v|) */ if (cmp == unexpect && (zz_add(q, 1, q) || zz_sub(r, v, r))) { goto err; /* LCOV_EXCL_LINE */ } @@ -1179,6 +1235,8 @@ zz_truediv(const zz_t *u, const zz_t *v, double *res) } } shift += DBL_MANT_DIG; + /* No overflow is possible in shifts, since shift value + much smaller here than zz_get_bitcnt_max() */ if (shift > 0 && zz_mul_2exp(&a, (uint64_t)shift, &a)) { goto tmp_clear; /* LCOV_EXCL_LINE */ } @@ -1311,7 +1369,7 @@ nb_truediv(PyObject *self, PyObject *other) res = MPZ_new(); \ zz_err ret = ZZ_OK; \ \ - if (!res || (ret = zz_##suff(&u->z, &v->z, &res->z))) { \ + if (res && (ret = zz_##suff(&u->z, &v->z, &res->z))) { \ /* LCOV_EXCL_START */ \ Py_CLEAR(res); \ if (ret == ZZ_VAL) { \ @@ -1918,7 +1976,7 @@ __sizeof__(PyObject *self, PyObject *Py_UNUSED(ignored)) MPZ_Object *u = (MPZ_Object *)self; return PyLong_FromSize_t(sizeof(MPZ_Object) - + (unsigned int)(u->z).alloc*sizeof(zz_digit_t)); + + (size_t)(u->z).alloc*sizeof(zz_digit_t)); } static PyObject * @@ -2153,10 +2211,18 @@ gmp_lcm(PyObject *Py_UNUSED(module), PyObject *const *args, Py_ssize_t nargs) Py_DECREF(arg); continue; } - if (zz_lcm(&res->z, &arg->z, &res->z)) { + + zz_err ret = zz_lcm(&res->z, &arg->z, &res->z); + + if (ret) { /* LCOV_EXCL_START */ Py_DECREF(res); Py_DECREF(arg); + if (ret == ZZ_BUF) { + PyErr_SetString(PyExc_OverflowError, + "too many digits in integer"); + return NULL; + } return PyErr_NoMemory(); /* LCOV_EXCL_STOP */ } @@ -2188,7 +2254,7 @@ gmp_isqrt(PyObject *Py_UNUSED(module), PyObject *arg) PyErr_SetString(PyExc_ValueError, "isqrt() argument must be nonnegative"); } - if (ret == ZZ_MEM) { + else { PyErr_NoMemory(); /* LCOV_EXCL_LINE */ } end: @@ -2217,11 +2283,11 @@ gmp_isqrt_rem(PyObject *Py_UNUSED(module), PyObject *arg) if (ret == ZZ_OK) { tup = PyTuple_Pack(2, root, rem); } - if (ret == ZZ_VAL) { + else if (ret == ZZ_VAL) { PyErr_SetString(PyExc_ValueError, "isqrt() argument must be nonnegative"); } - if (ret == ZZ_MEM) { + else { PyErr_NoMemory(); /* LCOV_EXCL_LINE */ } end: @@ -2512,6 +2578,7 @@ zz_mpmath_normalize(zz_bitcnt_t prec, zz_rnd rnd, bool *negative, } return ZZ_OK; } + static PyObject * gmp__mpmath_normalize(PyObject *self, PyObject *const *args, Py_ssize_t nargs) { From 55075e6c56d7aab5689eb30e5fb784b0f60bc42d Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 17 Feb 2026 06:38:37 +0300 Subject: [PATCH 10/11] Use libzz v0.9.0a3 --- .readthedocs.yaml | 2 +- scripts/cibw_before_all.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f1520fed..289e1887 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,7 @@ build: pre_create_environment: - | PREFIX=$(pwd)/.local - ZZ_VERSION=0.9.0a2 + ZZ_VERSION=0.9.0a3 ZZ_DIR=zz-${ZZ_VERSION} GITHUB_URL=https://github.com/diofant/zz/releases/download/ ZZ_URL=${GITHUB_URL}v${ZZ_VERSION}/${ZZ_DIR}.tar.gz diff --git a/scripts/cibw_before_all.sh b/scripts/cibw_before_all.sh index effe2f67..fbcc22a1 100644 --- a/scripts/cibw_before_all.sh +++ b/scripts/cibw_before_all.sh @@ -53,7 +53,7 @@ make --silent all install cd .. -ZZ_VERSION=0.9.0a2 +ZZ_VERSION=0.9.0a3 ZZ_DIR=zz-${ZZ_VERSION} ZZ_URL=https://github.com/diofant/zz/releases/download/v${ZZ_VERSION}/${ZZ_DIR}.tar.gz From 30a54ae4e1ee4d572b89f96819af1eecbd9f3cff Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 17 Feb 2026 08:05:17 +0300 Subject: [PATCH 11/11] Use zz_set/get() for unsigned types --- gmp.c | 64 +++++++++++++++++++++++++++++------------------ tests/test_mpz.py | 2 ++ 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/gmp.c b/gmp.c index 708c0d80..b06e7987 100644 --- a/gmp.c +++ b/gmp.c @@ -1470,7 +1470,7 @@ zz_and_i64(const zz_t *u, int64_t v, zz_t *w) return zz_set(0, w); } assert(!zz_isneg(u) && v > 0); - return zz_set((int64_t)(u->digits[0] & (zz_digit_t)v), w); + return zz_set(u->digits[0] & (zz_digit_t)v, w); } #define zz_i64_and(x, y, r) zz_and_i64((y), (x), (r)) @@ -1485,12 +1485,12 @@ zz_lshift(const zz_t *u, const zz_t *v, zz_t *w) return ZZ_VAL; } - int64_t shift; + uint64_t shift; if (zz_get(v, &shift)) { return ZZ_BUF; } - return zz_mul_2exp(u, (zz_bitcnt_t)shift, w); + return zz_mul_2exp(u, shift, w); } static inline zz_err @@ -1500,12 +1500,12 @@ zz_rshift(const zz_t *u, const zz_t *v, zz_t *w) return ZZ_VAL; } - int64_t shift; + uint64_t shift; if (zz_get(v, &shift)) { return zz_set(zz_isneg(u) ? -1 : 0, w); } - return zz_quo_2exp(u, (zz_bitcnt_t)shift, w); + return zz_quo_2exp(u, shift, w); } BINOP_INT(lshift) @@ -1554,7 +1554,7 @@ power(PyObject *self, PyObject *other, PyObject *module) return resf; } - int64_t exp; + uint64_t exp; if (zz_get(&v->z, &exp)) { PyErr_SetString(PyExc_OverflowError, @@ -1563,7 +1563,7 @@ power(PyObject *self, PyObject *other, PyObject *module) else { res = MPZ_new(); if (res) { - zz_err ret = zz_pow(&u->z, (uint64_t)exp, &res->z); + zz_err ret = zz_pow(&u->z, exp, &res->z); if (ret) { Py_CLEAR(res); @@ -1905,7 +1905,7 @@ __round__(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } zz_t divisor; - int64_t exp; + uint64_t exp; (void)zz_neg(&ndigits->z, &ndigits->z); if (zz_init(&divisor) || zz_set(10, &divisor)) { @@ -1919,7 +1919,7 @@ __round__(PyObject *self, PyObject *const *args, Py_ssize_t nargs) goto overflow; } - zz_err ret = zz_pow(&divisor, (uint64_t)exp, &divisor); + zz_err ret = zz_pow(&divisor, exp, &divisor); if (ret) { zz_clear(&divisor); @@ -2311,17 +2311,24 @@ gmp_fac(PyObject *Py_UNUSED(module), PyObject *arg) goto err; } - int64_t n; + uint64_t n; - if (zz_get(&x->z, &n) || n > LONG_MAX) { + if (zz_get(&x->z, &n)) { +overflow: PyErr_Format(PyExc_OverflowError, "fac() argument should not exceed %ld", - LONG_MAX); + ULONG_MAX); goto err; } Py_XDECREF((PyObject *)x); - if (zz_fac((zz_digit_t)n, &res->z)) { + + zz_err ret = zz_fac((zz_digit_t)n, &res->z); + + if (ret) { /* LCOV_EXCL_START */ + if (ret == ZZ_BUF) { + goto overflow; + } PyErr_NoMemory(); goto err; /* LCOV_EXCL_STOP */ @@ -2354,11 +2361,10 @@ gmp_comb(PyObject *self, PyObject *const *args, Py_ssize_t nargs) goto err; } - int64_t n, k; + uint64_t n, k; - if ((zz_get(&x->z, &n) || n > ULONG_MAX) - || (zz_get(&y->z, &k) || k > ULONG_MAX)) - { + if (zz_get(&x->z, &n) || zz_get(&y->z, &k)) { +overflow: PyErr_Format(PyExc_OverflowError, "comb() arguments should not exceed %ld", ULONG_MAX); @@ -2366,8 +2372,14 @@ gmp_comb(PyObject *self, PyObject *const *args, Py_ssize_t nargs) } Py_XDECREF((PyObject *)x); Py_XDECREF((PyObject *)y); - if (zz_bin((zz_digit_t)n, (zz_digit_t)k, &res->z)) { + + zz_err ret = zz_bin(n, k, &res->z); + + if (ret) { /* LCOV_EXCL_START */ + if (ret == ZZ_BUF) { + goto overflow; + } PyErr_NoMemory(); goto err; /* LCOV_EXCL_STOP */ @@ -2403,11 +2415,10 @@ gmp_perm(PyObject *self, PyObject *const *args, Py_ssize_t nargs) goto err; } - int64_t n, k; + uint64_t n, k; - if ((zz_get(&x->z, &n) || n > ULONG_MAX) - || (zz_get(&y->z, &k) || k > ULONG_MAX)) - { + if (zz_get(&x->z, &n) || zz_get(&y->z, &k)) { +overflow: PyErr_Format(PyExc_OverflowError, "perm() arguments should not exceed %ld", ULONG_MAX); @@ -2427,12 +2438,17 @@ gmp_perm(PyObject *self, PyObject *const *args, Py_ssize_t nargs) goto err; /* LCOV_EXCL_STOP */ } - if (zz_fac((zz_digit_t)n, &res->z) - || zz_fac((zz_digit_t)(n-k), &den->z) + + zz_err ret = zz_fac((zz_digit_t)n, &res->z); + + if (ret || zz_fac((zz_digit_t)(n-k), &den->z) || zz_div(&res->z, &den->z, &res->z, NULL)) { /* LCOV_EXCL_START */ Py_DECREF(den); + if (ret == ZZ_BUF) { + goto overflow; + } PyErr_NoMemory(); goto err; /* LCOV_EXCL_STOP */ diff --git a/tests/test_mpz.py b/tests/test_mpz.py index fff435a1..72b4cb72 100644 --- a/tests/test_mpz.py +++ b/tests/test_mpz.py @@ -720,6 +720,8 @@ def test_power_errors(): pow(object(), mpz(321)) if platform.python_implementation() == "GraalVM": return # issue oracle/graalpython#551 + with pytest.raises(OverflowError): + pow(mpz(2), mpz(1<<64)) if BITCNT_MAX < (1<<64) - 1: with pytest.raises(OverflowError): pow(mpz(1<<64), (1<<31) - 1)