openssl / openssl

TLS/SSL and crypto library

Home Page:https://www.openssl.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

BN_mod_inverse incorrect result when parameters are aliased

guidovranken opened this issue · comments

#include <openssl/bn.h>

#define CF_CHECK_NE(expr, res) if ( (expr) == (res) ) { goto end; }

int main(void)
{
    char* str = NULL;
    BIGNUM* a = BN_new();
    BIGNUM* b = BN_new();
    BN_CTX* ctx = BN_CTX_new();
    CF_CHECK_NE(BN_dec2bn(&a, "5193817943"), 0);
    CF_CHECK_NE(BN_dec2bn(&b, "3259122431"), 0);
    CF_CHECK_NE(BN_mod_inverse(b, a, b, ctx), NULL);
    str = BN_bn2dec(b);
    printf("%s\n", str);
end:
    BN_free(a);
    BN_free(b);
    BN_CTX_free(ctx);
    OPENSSL_free(str);
    return 0;
}

This prints 0 but should print 2609653924. Note that in the call to BN_mod_inverse, the result is the same pointer as the modulus. The documentation explicitly states that "r may be the same BIGNUM as a or n.".

This also affects @davidben and @botovq.

commented

It's a bug. Is it a documentation or code issue is the question I'd like answered -- I'm willing to go either way here.

commented

That this particular code path doesn't work and probably never has is indicative that it's never used by anyone -- hence a documentation only fix is an option. As mentioned, willing to swing either way here.

There are also functions where aliasing is not explicitly allowed and the function will silently produce the incorrect result instead of failing.

#include <openssl/bn.h>

#define CF_CHECK_EQ(expr, res) if ( (expr) != (res) ) { goto end; }
#define CF_CHECK_NE(expr, res) if ( (expr) == (res) ) { goto end; }

int main(void)
{
    char* str = NULL;
    BIGNUM* a = BN_new();
    BIGNUM* b = BN_new();
    BIGNUM* c = BN_new();
    BN_CTX* ctx = BN_CTX_new();
    CF_CHECK_NE(BN_dec2bn(&a, "3"), 0);
    CF_CHECK_NE(BN_dec2bn(&b, "1"), 0);
    CF_CHECK_NE(BN_dec2bn(&c, "11"), 0);
    CF_CHECK_EQ(BN_mod_exp_simple(c, a, b, c, ctx), 1);
    str = BN_bn2dec(c);
    printf("%s\n", str);
end:
    BN_free(a);
    BN_free(b);
    BN_free(c);
    BN_CTX_free(ctx);
    OPENSSL_free(str);
    return 0;
}

Personally I think it's better to return an error to prevent accidental misuse.

Would each of the maintainers like to be informed about each of these?

I don't see why variable reuse would not be useful. Programming languages have arithmetic assignment operators for this reason. But if you want to base your code on implicit assumptions about the semantics of your users be my guest.

Aliasing is definitely a bit of a curse. 😞 It's very useful to act on values in-place, but also makes reasoning about abstraction boundaries very hard. And while aliasing can reduce copies, having to defensively allow aliasing across API boundaries can also increase copies, which defeats the purpose of believing in aliasing in the first place.

The Rust answer would be to just forbid this kind of aliasing altogether (what the borrow checker does) and, if you want operations that need to work in-place, you make separate in-place and non-aliasing functions. (E.g. one function for a = b + c, no aliasing, and one for a = a + b where a is a single &mut input/output parameter.)

But this isn't Rust and many BIGNUM functions expect to be called with aliasing pointers already, so we're kinda stuck at least sometimes allowing aliased BIGNUMs. Agreed with @botovq that aliasing on the modulus is questionably useful. And, the way a lot of these functions work, allowing it often gets you in the "defensively allowing aliasing causes more copies than before" bucket.

Regardless, BN_mod_inverse is pretty easy to fix, so let's do that, and we ought to define and write down aliasing expectations more clearly.

commented

Would each of the maintainers like to be informed about each of these?

If you have such a list, then I think it would be worthwhile. As you note, aliasing is bad -- it should either be fixed, return an error or minimally be documented. This is one thing FORTRAN got right 😁

edit: restrict declarations would be another option I guess.

If you have such a list, then I think it would be worthwhile. As you note, aliasing is bad -- it should either be fixed, return an error or minimally be documented. This is one thing FORTRAN got right grin

The only ones I've found so far are BN_mod_exp_simple and BN_mod_sub_quick

commented

@bernd-edlinger are those commits part of a PR?

Yes, I've added them to #21124 as separate commits.

I agree with @botovq that allowing aliasing the modulus in these operations does not make much sense and we should simply return error.

The only ones I've found so far are BN_mod_exp_simple and BN_mod_sub_quick

I've also observed errors with BN_div_recp if the result is aliased. This is a public function in OpenSSL but private in LibreSSL and BoringSSL.

Can the fix for this be merged please @bernd-edlinger ?

@guidovranken alternative fix in #22421 done as OTC requested - aliasing of modulus with the result should not be supported.

Now we have different results with BN_mod_exp_simple vs. BN_mod_exp_recp when aliasing result with modulus.
BN_mod_exp_recp fails and BN_mod_exp_simple succeeds - that is nonsense.