diff --git a/src/sage/ext_data/valgrind/valgrind-python.supp b/src/sage/ext_data/valgrind/valgrind-python.supp new file mode 100644 index 00000000000..16aa2858484 --- /dev/null +++ b/src/sage/ext_data/valgrind/valgrind-python.supp @@ -0,0 +1,480 @@ +# From the CPython repository with the suppressions for _PyObject_Free +# and _PyObject_Realloc enabled. See the upstream suppression file for +# details: +# +# https://github.com/python/cpython/blob/main/Misc/valgrind-python.supp + +# all tool names: Addrcheck,Memcheck,cachegrind,helgrind,massif +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:address_in_range +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:address_in_range +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 8 (x86_64 aka amd64) + Memcheck:Value8 + fun:address_in_range +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:address_in_range +} + +# +# Leaks (including possible leaks) +# Hmmm, I wonder if this masks some real leaks. I think it does. +# Will need to fix that. +# + +{ + Suppress leaking the GIL after a fork. + Memcheck:Leak + fun:malloc + fun:PyThread_allocate_lock + fun:PyEval_ReInitThreads +} + +{ + Suppress leaking the autoTLSkey. This looks like it shouldn't leak though. + Memcheck:Leak + fun:malloc + fun:PyThread_create_key + fun:_PyGILState_Init + fun:Py_InitializeEx + fun:Py_Main +} + +{ + Hmmm, is this a real leak or like the GIL? + Memcheck:Leak + fun:malloc + fun:PyThread_ReInitTLS +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:realloc + fun:_PyObject_GC_Resize + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:malloc + fun:_PyObject_GC_New + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +{ + Handle PyMalloc confusing valgrind (possibly leaked) + Memcheck:Leak + fun:malloc + fun:_PyObject_GC_NewVar + fun:COMMENT_THIS_LINE_TO_DISABLE_LEAK_WARNING +} + +# +# Non-python specific leaks +# + +{ + Handle pthread issue (possibly leaked) + Memcheck:Leak + fun:calloc + fun:allocate_dtv + fun:_dl_allocate_tls_storage + fun:_dl_allocate_tls +} + +{ + Handle pthread issue (possibly leaked) + Memcheck:Leak + fun:memalign + fun:_dl_allocate_tls_storage + fun:_dl_allocate_tls +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:_PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:_PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Use of uninitialised value of size 8 + Memcheck:Addr8 + fun:_PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Use of uninitialised value of size 8 + Memcheck:Value8 + fun:_PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:_PyObject_Free +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Addr4 + fun:_PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Invalid read of size 4 + Memcheck:Value4 + fun:_PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Use of uninitialised value of size 8 + Memcheck:Addr8 + fun:_PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Use of uninitialised value of size 8 + Memcheck:Value8 + fun:_PyObject_Realloc +} + +{ + ADDRESS_IN_RANGE/Conditional jump or move depends on uninitialised value + Memcheck:Cond + fun:_PyObject_Realloc +} + +### +### All the suppressions below are for errors that occur within libraries +### that Python uses. The problems to not appear to be related to Python's +### use of the libraries. +### + +{ + Generic ubuntu ld problems + Memcheck:Addr8 + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so + obj:/lib/ld-2.4.so +} + +{ + Generic gentoo ld problems + Memcheck:Cond + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so + obj:/lib/ld-2.3.4.so +} + +{ + DBM problems, see test_dbm + Memcheck:Param + write(buf) + fun:write + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_close +} + +{ + DBM problems, see test_dbm + Memcheck:Value8 + fun:memmove + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + DBM problems, see test_dbm + Memcheck:Cond + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + DBM problems, see test_dbm + Memcheck:Cond + fun:memmove + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + obj:/usr/lib/libdb1.so.2 + fun:dbm_store + fun:dbm_ass_sub +} + +{ + GDBM problems, see test_gdbm + Memcheck:Param + write(buf) + fun:write + fun:gdbm_open + +} + +{ + Uninitialised byte(s) false alarm, see bpo-35561 + Memcheck:Param + epoll_ctl(event) + fun:epoll_ctl + fun:pyepoll_internal_ctl +} + +{ + ZLIB problems, see test_gzip + Memcheck:Cond + obj:/lib/libz.so.1.2.3 + obj:/lib/libz.so.1.2.3 + fun:deflate +} + +{ + Avoid problems w/readline doing a putenv and leaking on exit + Memcheck:Leak + fun:malloc + fun:xmalloc + fun:sh_set_lines_and_columns + fun:_rl_get_screen_size + fun:_rl_init_terminal_io + obj:/lib/libreadline.so.4.3 + fun:rl_initialize +} + +# Valgrind emits "Conditional jump or move depends on uninitialised value(s)" +# false alarms on GCC builtin strcmp() function. The GCC code is correct. +# +# Valgrind bug: https://bugs.kde.org/show_bug.cgi?id=264936 +{ + bpo-38118: Valgrind emits false alarm on GCC builtin strcmp() + Memcheck:Cond + fun:PyUnicode_Decode +} + + +### +### These occur from somewhere within the SSL, when running +### test_socket_sll. They are too general to leave on by default. +### +###{ +### somewhere in SSL stuff +### Memcheck:Cond +### fun:memset +###} +###{ +### somewhere in SSL stuff +### Memcheck:Value4 +### fun:memset +###} +### +###{ +### somewhere in SSL stuff +### Memcheck:Cond +### fun:MD5_Update +###} +### +###{ +### somewhere in SSL stuff +### Memcheck:Value4 +### fun:MD5_Update +###} + +# Fedora's package "openssl-1.0.1-0.1.beta2.fc17.x86_64" on x86_64 +# See http://bugs.python.org/issue14171 +{ + openssl 1.0.1 prng 1 + Memcheck:Cond + fun:bcmp + fun:fips_get_entropy + fun:FIPS_drbg_instantiate + fun:RAND_init_fips + fun:OPENSSL_init_library + fun:SSL_library_init + fun:init_hashlib +} + +{ + openssl 1.0.1 prng 2 + Memcheck:Cond + fun:fips_get_entropy + fun:FIPS_drbg_instantiate + fun:RAND_init_fips + fun:OPENSSL_init_library + fun:SSL_library_init + fun:init_hashlib +} + +{ + openssl 1.0.1 prng 3 + Memcheck:Value8 + fun:_x86_64_AES_encrypt_compact + fun:AES_encrypt +} + +# +# All of these problems come from using test_socket_ssl +# +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_bin2bn +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_num_bits_word +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:BN_num_bits_word +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_mod_exp_mont_word +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BN_mod_exp_mont +} + +{ + from test_socket_ssl + Memcheck:Param + write(buf) + fun:write + obj:/usr/lib/libcrypto.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:RSA_verify +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:RSA_verify +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:DES_set_key_unchecked +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:DES_encrypt2 +} + +{ + from test_socket_ssl + Memcheck:Cond + obj:/usr/lib/libssl.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Value4 + obj:/usr/lib/libssl.so.0.9.7 +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:BUF_MEM_grow_clean +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:memcpy + fun:ssl3_read_bytes +} + +{ + from test_socket_ssl + Memcheck:Cond + fun:SHA1_Update +} + +{ + from test_socket_ssl + Memcheck:Value4 + fun:SHA1_Update +} + +{ + test_buffer_non_debug + Memcheck:Addr4 + fun:PyUnicodeUCS2_FSConverter +} + +{ + test_buffer_non_debug + Memcheck:Addr4 + fun:PyUnicode_FSConverter +} + +{ + wcscmp_false_positive + Memcheck:Addr8 + fun:wcscmp + fun:_PyOS_GetOpt + fun:Py_Main + fun:main +} + +# Additional suppressions for the unified decimal tests: +{ + test_decimal + Memcheck:Addr4 + fun:PyUnicodeUCS2_FSConverter +} + +{ + test_decimal2 + Memcheck:Addr4 + fun:PyUnicode_FSConverter +} + diff --git a/src/sage/symbolic/ginac/numeric.cpp b/src/sage/symbolic/ginac/numeric.cpp index b40ed64edb5..8c55861c147 100644 --- a/src/sage/symbolic/ginac/numeric.cpp +++ b/src/sage/symbolic/ginac/numeric.cpp @@ -1576,6 +1576,62 @@ const numeric numeric::div(const numeric &other) const { } } + +// Compute `a^b` as an integer, where a is an integer. Assign to ``res``` if it is integral, or return ``false``. +// The nonnegative real root is taken for even denominators. To be used inside numeric::integer_rational_power, +// to handle the special case of integral ``a``. +bool integer_rational_power_of_mpz( + numeric& res, + const numeric& a, + const numeric& b +) { + if (a.t != MPZ) + throw std::runtime_error("integer_rational_power_of_mpz: bad input"); + mpz_t z; + mpz_init(z); + mpz_set_ui(z, 0); + int sgn = mpz_sgn(a.v._bigint); + if (mpz_cmp_ui(a.v._bigint, 1) == 0 + or mpz_cmp_ui(mpq_numref(b.v._bigrat), 0) == 0) + mpz_set_ui(z, 1); + else if (sgn == 0) { + res = *_num0_p; + mpz_clear(z); + return true; + } + else if (sgn < 0 and mpz_cmp_ui(mpq_denref(b.v._bigrat), 1)) { + mpz_clear(z); + return false; + } else { + if (not mpz_fits_ulong_p(mpq_numref(b.v._bigrat)) + or not mpz_fits_ulong_p(mpq_denref(b.v._bigrat))) { + // too big to take roots/powers + mpz_clear(z); + return false; + } + if (mpz_cmp_ui(mpq_denref(b.v._bigrat), 2) == 0) { + if (mpz_perfect_square_p(a.v._bigint)) { + mpz_sqrt(z, a.v._bigint); + } else { + mpz_clear(z); + return false; + } + } + else { + bool exact = mpz_root(z, a.v._bigint, + mpz_get_ui(mpq_denref(b.v._bigrat))); + if (not exact) { + mpz_clear(z); + return false; + } + } + mpz_pow_ui(z, z, mpz_get_ui(mpq_numref(b.v._bigrat))); + } + res = numeric(z); // transfers ownership, no mpz_clear + return true; +} + + // Compute `a^b` as an integer, if it is integral, or return ``false``. // The nonnegative real root is taken for even denominators. bool numeric::integer_rational_power(numeric& res, @@ -1598,13 +1654,12 @@ bool numeric::integer_rational_power(numeric& res, if (a.v._long < 0 and mpz_cmp_ui(mpq_denref(b.v._bigrat), 1)) return false; - long z; if (not mpz_fits_ulong_p(mpq_numref(b.v._bigrat)) or not mpz_fits_ulong_p(mpq_denref(b.v._bigrat))) // too big to take roots/powers return false; if (b.is_equal(*_num1_2_p)) { - z = std::lround(std::sqrt(a.v._long)); + long z = std::lround(std::sqrt(a.v._long)); if (a.v._long == z*z) { res = numeric(z); return true; @@ -1613,44 +1668,11 @@ bool numeric::integer_rational_power(numeric& res, } return integer_rational_power(res, a.to_bigint(), b); } - if (a.t != MPZ) - throw std::runtime_error("integer_rational_power: bad input"); - int sgn = mpz_sgn(a.v._bigint); - mpz_t z; - mpz_init(z); - mpz_set_ui(z, 0); - if (mpz_cmp_ui(a.v._bigint, 1) == 0 - or mpz_cmp_ui(mpq_numref(b.v._bigrat), 0) == 0) - mpz_set_ui(z, 1); - else if (sgn == 0) { - res = *_num0_p; - return true; - } - else if (sgn < 0 and mpz_cmp_ui(mpq_denref(b.v._bigrat), 1)) - return false; - else { - if (not mpz_fits_ulong_p(mpq_numref(b.v._bigrat)) - or not mpz_fits_ulong_p(mpq_denref(b.v._bigrat))) - // too big to take roots/powers - return false; - if (mpz_cmp_ui(mpq_denref(b.v._bigrat), 2) == 0) { - if (mpz_perfect_square_p(a.v._bigint)) - mpz_sqrt(z, a.v._bigint); - else - return false; - } - else { - bool exact = mpz_root(z, a.v._bigint, - mpz_get_ui(mpq_denref(b.v._bigrat))); - if (not exact) - return false; - } - mpz_pow_ui(z, z, mpz_get_ui(mpq_numref(b.v._bigrat))); - } - res = numeric(z); - return true; + // otherwise: a is integer + return integer_rational_power_of_mpz(res, a, b); } + // for a^b return c,d such that a^b = c*d^b // only for MPZ/MPQ base and MPQ exponent void rational_power_parts(const numeric& a_orig, const numeric& b_orig, diff --git a/src/sage/tests/memcheck/__init__.py b/src/sage/tests/memcheck/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/sage/tests/memcheck/run_tests.py b/src/sage/tests/memcheck/run_tests.py new file mode 100644 index 00000000000..6ff4503a81b --- /dev/null +++ b/src/sage/tests/memcheck/run_tests.py @@ -0,0 +1,24 @@ +import types + + +def run_tests() -> None: + """ + Run all memcheck tests + """ + from sage.tests.memcheck import symbolic_expression + run_tests_in_module(symbolic_expression) + + +def run_tests_in_module(mod: types.ModuleType) -> None: + """ + Run all memcheck tests in the given module + """ + for entry in dir(mod): + if not entry.startswith('test_'): + continue + test_func = getattr(mod, entry) + test_func() + + +if __name__ == '__main__': + run_tests() diff --git a/src/sage/tests/memcheck/run_tests_in_valgrind.py b/src/sage/tests/memcheck/run_tests_in_valgrind.py new file mode 100644 index 00000000000..df5ad0e92b2 --- /dev/null +++ b/src/sage/tests/memcheck/run_tests_in_valgrind.py @@ -0,0 +1,35 @@ +""" +Launch valgrind and run the memory leak tests + + +From the commandline, run + + sage -python -m sage.tests.memcheck.run_tests_in_valgrind + +to launch valgrind and execute the memory leak tests. Requires valgrind +to be installed. Alternatively, run as a unit test: + + sage: from sage.tests.memcheck.run_tests_in_valgrind import run_tests_in_valgrind + sage: run_tests_in_valgrind() # optional - valgrind +""" + +import subprocess + + +def run_tests_in_valgrind() -> None: + """ + Run the sage.tests.memcheck.run_tests module inside valgrind + """ + subprocess.check_call([ + 'valgrind', + '--suppressions=src/sage/ext_data/valgrind/valgrind-python.supp', + '--show-possibly-lost=no', + '--show-reachable=no', + './venv/bin/python', + '-m', + 'sage.tests.memcheck.run_tests' + ]) + + +if __name__ == '__main__': + run_tests_in_valgrind() diff --git a/src/sage/tests/memcheck/symbolic_expression.py b/src/sage/tests/memcheck/symbolic_expression.py new file mode 100644 index 00000000000..52182fbe62d --- /dev/null +++ b/src/sage/tests/memcheck/symbolic_expression.py @@ -0,0 +1,11 @@ +from sage.tests.memcheck.verify_no_leak import verify_no_leak + + +def test_sqrt_sqrt_2() -> None: + from sage.misc.functional import sqrt + T2 = sqrt(2) + + def sqrt_T2() -> None: + sqrt(T2) + + verify_no_leak(sqrt_T2) diff --git a/src/sage/tests/memcheck/verify_no_leak.py b/src/sage/tests/memcheck/verify_no_leak.py new file mode 100644 index 00000000000..89ca90cf89c --- /dev/null +++ b/src/sage/tests/memcheck/verify_no_leak.py @@ -0,0 +1,27 @@ +from typing import Tuple, Sequence, List, Callable, Any +import valgrind + + +def verify_no_leak(callback: Callable[[], Any], + repeat: int = 10000, + fuzzy: int = 10, + ) -> None: + """ + Verify that the callback does not generate new definitely lost blocks + + Raises an assertion if the callback leaks memory + """ + callback() # warm_up + initial_blocks = (0, 0, 0, 0) + valgrind.memcheck_do_leak_check() + initial_blocks = valgrind.memcheck_count_leak_blocks() + for _ in range(repeat): + callback() + valgrind.memcheck_do_leak_check() + leak_blocks = valgrind.memcheck_count_leak_blocks() + leak = leak_blocks[0] - initial_blocks[0] + if leak < repeat - fuzzy: + return # callback did not leak at least once per call + blocks = round(leak / repeat, 2) + message = f'{callback} leaked {blocks} block on average ({repeat} iterations)' + raise AssertionError(message)