Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new BN(null).toString() causes an infinite loop (cashes the browser) #186

Open
adipascu opened this issue Jun 7, 2018 · 3 comments
Open

Comments

@adipascu
Copy link

adipascu commented Jun 7, 2018

new BN(null).toString() causes an infinite loop (cashes the browser)

It looks like

  • new BN(null) creates an empty BN
  • .toString() causes the VM interpreter to go into a loop

I think the solution is to make .toString() crash on empty instances.
A better fix could be to no longer accept new BN(null), remove empty instance feature or make it accessible with a different api (0 param constructor or something else).

@begelundmuller
Copy link

Also bumped into this. Running

> (new BN(null)).imuln(0).toString(10)

Crashes Node with

#
# Fatal error in , line 0
# API fatal error handler returned after process out of memory
#
Illegal instruction: 4

bpierre added a commit to aragon/client that referenced this issue Oct 31, 2018
When calling getBalance(), it was possible to sometimes get another
value than an big integer as a string.

Having `null` as a result, and passing it to the BN.js constructor,
could lead to an infinit loop [1].

To prevent this issue to happen again:

- In the app, `balance` is now always represented by a BN.js instance.
To represent an unknown balance, `new BN(-1)` is now used rather than `null`.

- The result of getbalance() is now filtered to ensure that we are
passing an integer to BN.js. Otherwise, we pass "-1".

[1] indutny/bn.js#186
bpierre added a commit to aragon/client that referenced this issue Nov 1, 2018
When calling getBalance(), it was possible to sometimes get another value than an big integer as a string.

Having `null` as a result, and passing it to the BN.js constructor, could lead to an infinit loop [1].

To prevent this issue to happen again:

- In the app, `balance` is now always represented by a BN.js instance. To represent an unknown balance, `new BN(-1)` is now used rather than `null`.

- The result of getbalance() is now filtered to ensure that we are passing an integer to BN.js. Otherwise, we pass "-1".

[1] indutny/bn.js#186
@ahpaleus
Copy link

ahpaleus commented Feb 9, 2022

Could you please resolve this issue and patch the codebase?

We figured out that it is possible to trigger this bug by passing the following arguments:

new BN(" ").toString()
new BN("-").toString()
new BN(null).toString()
new BN(undefined).toString()

As the bn.js has nearly 37 million weekly downloads and is used by nearly 3k projects, this bug (leading to Denial of Service) can be impactful.

@disconnect3d
Copy link

disconnect3d commented Feb 9, 2022

cc: @indutny

Below is what we diagnosed with @ahpaleus. The crash occurs due to infinite loop which allocates memory on each loop iteration and so it exhausts the NodeJS or web browser's tab memory.

Related sources:

  • BN constructor:

    bn.js/lib/bn.js

    Lines 21 to 41 in db57519

    function BN (number, base, endian) {
    if (BN.isBN(number)) {
    return number;
    }
    this.negative = 0;
    this.words = null;
    this.length = 0;
    // Reduction context
    this.red = null;
    if (number !== null) {
    if (base === 'le' || base === 'be') {
    endian = base;
    base = 10;
    }
    this._init(number || 0, base || 10, endian || 'be');
    }
    }

  • _init function:

    bn.js/lib/bn.js

    Lines 80 to 111 in db57519

    BN.prototype._init = function init (number, base, endian) {
    if (typeof number === 'number') {
    return this._initNumber(number, base, endian);
    }
    if (typeof number === 'object') {
    return this._initArray(number, base, endian);
    }
    if (base === 'hex') {
    base = 16;
    }
    assert(base === (base | 0) && base >= 2 && base <= 36);
    number = number.toString().replace(/\s+/g, '');
    var start = 0;
    if (number[0] === '-') {
    start++;
    this.negative = 1;
    }
    if (start < number.length) {
    if (base === 16) {
    this._parseHex(number, start, endian);
    } else {
    this._parseBase(number, base, start);
    if (endian === 'le') {
    this._initArray(this.toArray(), base, endian);
    }
    }
    }
    };

Cases:

  • When " " is passed, the _init ends up using an empty string due to number = number.toString().replace(/\s+/g, ''); line and so later neither of the if (number[0] === '-') or the if (start < number.length) conditions trigger
  • When "-" is passed, in the _init the if (number[0] === '-') check passes, so we end up having start=1 but number.length=1so that we still do not parse any numbers from string as theif (start < number.length)` does not pass (and well, there are no numbers to parse)
  • When null or undefined are passed, I believe the _init is never called as it is called only when if (number !== null) condition passes

All those cases result in having a BN instance where BN.length is 0 and so in the .toString() we loop infinitely in:

bn.js/lib/bn.js

Lines 514 to 532 in db57519

while (!c.isZero()) {
var r = c.modrn(groupBase).toString(base);
c = c.idivn(groupBase);
if (!c.isZero()) {
out = zeros[groupSize - r.length] + r + out;
} else {
out = r + out;
}
}
if (this.isZero()) {
out = '0' + out;
}
while (out.length % padding !== 0) {
out = '0' + out;
}
if (this.negative !== 0) {
out = '-' + out;
}

This is because the isZero performs the length check of 1:

bn.js/lib/bn.js

Lines 2834 to 2836 in db57519

BN.prototype.isZero = function isZero () {
return this.length === 1 && this.words[0] === 0;
};

Also FWIW when "" (empty string) is passed, the number || 0 expression in the this._init(number || 0, base || 10, endian || 'be'); line makes it so that a number 0 is passed to the _init and so an empty string returns a big num value of 0.

All those could be fixed by additional assertions in the BN constructor. That could be e.g. this.length !== 0. You may also want to cover the case of empty string, but since it works and return 0, maybe its good to not change this behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants