micropython / micropython/micropython

3-arg pow() with negative exponent silently returns 0 instead of computing modular inverse

Open
#19,143 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug py-core
Dominant language
C
Stars
22.1k
Forks
9k
Avg merge
6d 4h
Merged PRs (30d)
16

Description

Port, board and/or hardware

Unix port, Linux

MicroPython version

MicroPython v1.27.0

Reproduction
print(pow(2, -1, 4))

[!NOTE]
The simplest demonstration would be pow(2, -1, None), where CPython produces the expected result 0.5. However, MicroPython issue #19137 causes pow(x, y, None) to raise TypeError before the negative-exponent code path is even reached, so that example cannot be used here. The reproduction below falls back to a 3-arg form where CPython raises ValueError for non-invertibility, which still exercises the same buggy code path in MicroPython.

Expected behaviour

CPython v3.11.15:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: base is not invertible for the given modulus
Observed behaviour

MicroPython v1.27.0:

0
Additional Information

pow(base, exp, mod) with a negative exponent silently returns 0 instead of computing the modular inverse of base modulo mod (or raising ValueError when the inverse does not exist). This is a CPython compatibility issue.

Root Cause

mpz_pow3_inpl short-circuits any negative exponent by setting dest = 0 and returning, so mp_obj_int_pow3 silently yields 0 for every pow(x, neg_exp, mod) call regardless of whether the modular inverse exists.

// py/mpz.c:1358-1362
void mpz_pow3_inpl(mpz_t *dest, const mpz_t *lhs, const mpz_t *rhs, const mpz_t *mod) {
    if (lhs->len == 0 || rhs->neg != 0 || (mod->len == 1 && mod->dig[0] == 1)) {
        mpz_set_from_int(dest, 0);
        return;
    }
    ...

CPython's long_pow negates the exponent and calls long_invmod to compute the modular inverse, raising ValueError("base is not invertible for the given modulus") when gcd(base, |mod|) != 1.

CPython reference: Objects/longobject.c#L4455-L4474

// If exponent is negative, negate it and replace the base with
// its modular inverse; long_invmod raises ValueError when gcd != 1.
if (Py_SIZE(b) < 0) {
    temp = (PyLongObject *)_PyLong_Copy(b);
    if (temp == NULL)
        goto Error;
    Py_DECREF(b);
    b = temp;
    temp = NULL;
    _PyLong_Negate(&b);
    if (b == NULL)
        goto Error;

    temp = long_invmod(a, c);
    if (temp == NULL)
        goto Error;
    Py_DECREF(a);
    a = temp;
    temp = NULL;
}
Fix

Replace the rhs->neg != 0 short-circuit in mpz_pow3_inpl with a modular-inverse path (extended Euclidean), change the return type to bool, and raise ValueError("base is not invertible for the given modulus") from mp_obj_int_pow3 when the helper signals a non-invertible base.

Code of Conduct

Yes, I agree

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing pow(2, -1, 4) and reading mpz_pow3_inpl in py/mpz.c together with mp_obj_int_pow3 in py/objint_mpz.c. Compare the negative-exponent behavior with the linked CPython long_pow reference. Done means modular inverses are computed when possible and the specified ValueError is raised for non-invertible bases.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, python
Domain
embedded-iot
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.