mruby-bigint: pre-reduce base in mpz_powm_montgomery to satisfy REDC

REDC's input T must satisfy T < R*N; mpz_powm_montgomery() computed
T = base * R^2 without first reducing base modulo n. When base >= n,
T exceeds R*N and REDC silently truncates upper limbs, producing a
wrong base_mont and ultimately a wrong result.

Use mpz_mmod (general division path) to pre-reduce base mod n before
the multiplication by R^2.

Visible effect: (2**160).pow(2, (2**40)+1) returned 0 instead of 1.
A separate pre-existing issue in mpz_mod's Barrett path (broken
precondition check) means that path could return a wrong reduction
for large operands; mmod sidesteps that path entirely.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Yukihiro "Matz" Matsumoto
2026-05-21 17:07:51 +09:00
parent 6ac2c3dc56
commit 206b9f7477
2 changed files with 25 additions and 3 deletions
+11 -3
View File
@@ -5250,12 +5250,19 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result,
mpz_init(ctx, &one_mont);
mpz_montgomery_reduce(ctx, &one_mont, &R2, n, rho);
/* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2) */
mpz_t base_mont, temp;
/* Convert base to Montgomery form: base_mont = base * R mod n = REDC(base * R^2).
* REDC requires its input T to satisfy T < R*N. If `base` is not already
* reduced (e.g. base >= n), `base * R^2` can exceed R*N and REDC produces
* a wrong result. Pre-reduce base modulo n via mpz_mmod (the general
* division path) -- both operands are non-negative here so this is
* semantically equivalent to mpz_mod. */
mpz_t base_mont, base_reduced, temp;
mpz_init(ctx, &base_mont);
mpz_init(ctx, &base_reduced);
mpz_init_temp(ctx, &temp, n->sz * 4);
mpz_mul(ctx, &temp, (mpz_t*)base, &R2);
mpz_mmod(ctx, &base_reduced, (mpz_t*)base, (mpz_t*)n);
mpz_mul(ctx, &temp, &base_reduced, &R2);
mpz_montgomery_reduce(ctx, &base_mont, &temp, n, rho);
/* Initialize accumulator to 1 in Montgomery form */
@@ -5287,6 +5294,7 @@ mpz_powm_montgomery(mpz_ctx_t *ctx, mpz_t *result,
mpz_clear(ctx, &R2);
mpz_clear(ctx, &one_mont);
mpz_clear(ctx, &base_mont);
mpz_clear(ctx, &base_reduced);
mpz_clear(ctx, &temp);
mpz_clear(ctx, &acc);
pool_restore(ctx, pool_state);
+14
View File
@@ -150,6 +150,20 @@ assert 'Bigint pow' do
# assert_equal(-1041439304, n.pow(n, -1234567890))
end
assert 'Bigint Integer#pow(e, m) - Montgomery path' do
# Regression: mpz_powm_montgomery() failed to pre-reduce base mod n,
# producing wrong results when base >= n. Also trim() must restore
# the canonical sn=0 when sz becomes 0, otherwise an inconsistent
# zero bignum (sn!=0, sz=0) propagates through the squaring loop.
m = (2**40) + 1
assert_equal 1, (2**160).pow(2, m)
assert_equal 1, (2**320).pow(2, m)
assert_equal 8, ((2**160) + 1).pow(3, m)
m2 = (2**100) + 3
assert_equal (3**500) % m2, (3**500).pow(1, m2)
assert_equal ((5**300) ** 7) % m2, (5**300).pow(7, m2)
end
assert 'Bigint abs' do
n = 1<<65
assert_equal 36893488147419103232, n.abs