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

stream: fix highWaterMark integer overflow #12593

Closed
wants to merge 4 commits into from

Conversation

tniessen
Copy link
Member

@tniessen tniessen commented Apr 22, 2017

Fixes integer overflows when supplying values exceeding 2 ^ 31 - 1 for highWaterMark.

Fixes: #12553

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines
Affected core subsystem(s)

stream

@nodejs-github-bot nodejs-github-bot added the stream Issues and PRs related to the stream subsystem. label Apr 22, 2017
@vsemozhetbyt
Copy link
Contributor

@mscdex
Copy link
Contributor

mscdex commented Apr 22, 2017

Instead of using ~~ to cast to integer, why not use something like Math.trunc() that will still convert to integer but allows larger numbers (>32 bits)? That should be a much simpler change.

@@ -0,0 +1,16 @@
'use strict';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add this to an appropriate existing test or give the file a better name. regress-GH-*.js names aren't really helpful for people working on the tests in the future.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could not find an existing test file matching this issue, so pick a new name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the convention would be then test-streams-highwatermark.js or similar


assert.throws(() => {
stream.Readable({
highWaterMark: 'foo'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you test a wider range of "bad" inputs.

const stream = require('stream');

assert.throws(() => {
stream.Readable({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be tests for Writable too.

}

if (~~hwm !== hwm) {
throw new Error('highWaterMark exceeds valid range');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a RangeError. It might be helpful to say what the valid range actually is.

this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm;
if (hwm != null) {
if (typeof hwm !== 'number') {
throw new TypeError('highWaterMark must be a number');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're standardizing on putting the offending data's name in double quotes. So "highWaterMark" in this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This raises a good question... at some point we will need these errors to work with internals/errors but we need to make sure we are coordinating with the readable-streams folks.

@@ -0,0 +1,16 @@
'use strict';
const common = require('../common');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter: 'common' is assigned a value but never used. This can be just:

require('../common');

@tniessen
Copy link
Member Author

@mscdex These changes are based on the discussion below #12553 which suggested to disallow exceeding values. If it is consensus that larger values than 2GB should be supported, I will adapt the PR accordingly.

@mscdex
Copy link
Contributor

mscdex commented Apr 23, 2017

@tniessen I could see throwing on negative values, but I think most people would expect larger values to just work (as #12553 shows). Separating out the two changes would allow just the negative value check to be semver-major while the Math.trunc() fix could be backported as a bug fix (IMHO).

@tniessen
Copy link
Member Author

tniessen commented Apr 23, 2017

@mscdex Should I use this PR to fix the bug using Math.trunc() and another to check against negative values (and invalid types)?

@mscdex
Copy link
Contributor

mscdex commented Apr 23, 2017

@tniessen It might be good to hear from other @nodejs/collaborators about my proposed solution first.

@mcollina
Copy link
Member

I'm on vacation untill the end of this week.

This should be reviewed by @nodejs/streams. If someone else from the wg has time, we need to check this deeply as we do not want to break the ecosystem.

Generically, I'm -1 on making a semver-major out of this, so we should stick to throwing on negative values.

@mscdex
Copy link
Contributor

mscdex commented Apr 23, 2017

@mcollina Throwing where we previously didn't would be semver-major I think. At least I believe that's been the general trend in the past.

@mcollina
Copy link
Member

I think if highWaterMark is < 0, none of the stream machinery would work. I am without a laptop, so I need to check this. I would classify this as a bug fix, as the original code would fail silently.

I'm -1 on pumping semver-major for this right now, because of the maintenance burden it will take on readable-stream.

@tniessen
Copy link
Member Author

tniessen commented May 3, 2017

Any updates?

@mcollina mcollina added the semver-major PRs that contain breaking changes and should be released in the next major version. label May 3, 2017
@mcollina
Copy link
Member

mcollina commented May 3, 2017

Any negative value is interpreted by the stream machinery as a 0, which makes this bug tricky: a user is putting in a value that is extremely high with the intent of "buffer forever", and instead it gets a stream that is always flowing.

I think we should not throw in this case, but instead cast the highWaterMark to 2147483647 if there is an overflow. Also, I would normalize any negative value to 0. Those should not be semver-major changes.

throw new TypeError('highWaterMark must be a number');
}

if (~~hwm !== hwm) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the way we do things other places in node?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is how the old implementation converted hvm to a 32-bit integer. This is open for discussion, see this comment by @mscdex.

@calvinmetcalf
Copy link
Contributor

I think we should not throw in this case, but instead cast the highWaterMark to 2147483647 if there is an overflow. Also, I would normalize any negative value to 0. Those should not be semver-major changes.

I think buffer.kMaxLength is the actual upper bounds for how large hwm could be

@mscdex
Copy link
Contributor

mscdex commented May 3, 2017

I think buffer.kMaxLength is the actual upper bounds for how large hwm could be

Why is that? I would think the only time that particular constant would matter is if a user tried to .read(n) a very large number of bytes that exceeded that constant and there was enough buffered data to actually cover that kind of length. Otherwise such a single, large Buffer shouldn't ever be created.

@mcollina
Copy link
Member

mcollina commented May 4, 2017

I would think the only time that particular constant would matter is if a user tried to .read(n) a very large number of bytes that exceeded that constant and there was enough buffered data to actually cover that kind of length. Otherwise such a single, large Buffer shouldn't ever be created.

Currently hightWaterMark  is going from 2147483647 to -2147483647.

@mscdex
Copy link
Contributor

mscdex commented May 4, 2017

Currently hightWaterMark  is going from 2147483647 to -2147483647.

I don't understand this comment. What does that have to do with potentially using buffer.kMaxLength as the upper bound? Right now it's just coincidence that buffer.kMaxLength and ~~ produce the same max value.

Anyway, I think we should still allow larger values via Math.trunc().

@@ -68,11 +68,19 @@ function ReadableState(options, stream) {
// the point at which it stops calling _read() to fill the buffer
// Note: 0 is a valid value, means "don't call _read preemptively ever"
var hwm = options.highWaterMark;
var defaultHwm = this.objectMode ? 16 : 16 * 1024;
this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm;
if (hwm != null) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the loose null check? why not do if (hwm) { // ... ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess because hwm can be 0 (zero)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a zero watermark doesn't make sense, anything less than 1 is nothing getting processed.

also why delete defaultHwm?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a 0 is allowed for comparability reasons

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a 0 is allowed for comparability reasons

please embellish, what/when does that comparison happen?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not want to change the behavior of accepting 0. I just deleted defaultHwm because I think it is pretty obvious what 16 * 1024 is in the new code, though I can use a variable as well.

@tniessen
Copy link
Member Author

tniessen commented May 4, 2017

Without further discussions of the code, are there any objections to @mscdex suggestion? Otherwise, I am going to split this into two PRs as suggested.

@mcollina
Copy link
Member

mcollina commented May 5, 2017

@tniessen go ahead and implement @mscdex suggestion.

@tniessen tniessen changed the title stream: add highWaterMark validity checks stream: fix highWaterMark integer overflow May 10, 2017
Fixes integer overflows when supplying values exceeding 2 ^ 31 - 1 for
highWaterMark.
@tniessen
Copy link
Member Author

@mscdex @cjihrig @mcollina @calvinmetcalf I force-pushed a commit which only contains the integer overflow fix.

Copy link
Contributor

@cjihrig cjihrig left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one request.

const assert = require('assert');
const stream = require('stream');

const readable = stream.Readable({ highWaterMark: 2147483648 });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move all of the 2147483648s into a variable, and add a comment on why that specific value was chosen. That should prevent anyone from changing it to a small value in the future.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better value to test is Number.MAX_SAFE_INTEGER.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mscdex Done.

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, dropping this to semver-minor

@mcollina mcollina added semver-minor PRs that contain new features and should be released in the next minor version. and removed semver-major PRs that contain breaking changes and should be released in the next major version. labels May 10, 2017
@mcollina
Copy link
Member

mcollina commented May 10, 2017

I'll be landing this tomorrow (11th of May), if no one objects.

CI: https://ci.nodejs.org/job/node-test-pull-request/7991/

edit: 11th of May

@tniessen
Copy link
Member Author

tniessen commented May 10, 2017

Thanks @mcollina. I will create a separate PR with the semver-major changes after this gets merged.

@mscdex
Copy link
Contributor

mscdex commented May 10, 2017

I'll be landing this tomorrow (10th of May), if no one objects.

Today is the 10th ;-)

Anyway, here is CITGM: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/772/

@mcollina
Copy link
Member

Updated with the correct date, thanks @mscdex.

@mcollina
Copy link
Member

I think the citgm failures are recurring, can someone confirm?

@tniessen
Copy link
Member Author

@mcollina I don't see any relations between the reported errors and this PR. Are there known problems with citgm?

Copy link
Contributor

@lucamaraschi lucamaraschi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
@tniessen can you please add the general description of the test as indicated in the general guidelines?

@tniessen
Copy link
Member Author

@lucamaraschi Done.

@tniessen
Copy link
Member Author

@mscdex @mcollina Thoughts about citgm? Was this tested on master? I built 57a08e2 (which was the last commit on master before I branched) and tried npm test on node-ftp (which failed on citgm). I was able to reproduce the error without my PR.

@@ -72,7 +72,7 @@ function ReadableState(options, stream) {
this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm;

// cast to ints.
this.highWaterMark = ~~this.highWaterMark;
this.highWaterMark = Math.trunc(this.highWaterMark);
Copy link
Contributor

@mscdex mscdex May 14, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we may want to use Math.floor() instead, it seems to be significantly faster (~6x at least on V8 5.8).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tniessen can you update that?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -72,7 +72,7 @@ function ReadableState(options, stream) {
this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm;

// cast to ints.
this.highWaterMark = ~~this.highWaterMark;
this.highWaterMark = Math.floor(this.highWaterMark);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit: This changes the result when passed a negative floating point number.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thefourtheye what do you suggest? Can you make an example?
This was just changed at @mscdex suggestion from Math.trunc.

All negative values are interpreted as 0 by the machinery, so nothing should change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcollina Oh thats okay. This is just a nit. I am just making an observation here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'll be even less of an issue once we start throwing on invalid values.

Copy link
Member Author

@tniessen tniessen May 15, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, but not an actual problem. According to @mcollina:

Any negative value is interpreted by the stream machinery as a 0

Therefore, it does not matter whether the value is Math.floor(-233.5) = -234 or ~~(-233.5) = -233, not even in the special case of Math.floor(-0.5) = -1 whilst ~~(-0.5) = 0.

We are planning to throw on negative values anyway.

Edit: Sorry, the last three messages were invisible to me until now, I did not know this had already been discussed with @mcollina and @mscdex.

@mcollina
Copy link
Member

Landed as 11918c4

@mcollina mcollina closed this May 16, 2017
mcollina pushed a commit that referenced this pull request May 16, 2017
Fixes integer overflows when supplying values exceeding MAX_SAFE_INTEGER
for highWaterMark.

PR-URL: #12593
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luca Maraschi <luca.maraschi@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
tniessen added a commit to tniessen/node that referenced this pull request May 16, 2017
Adds checks for the type of "highWaterMark" and restricts it to
non-negative values.

Refs: nodejs#12593
anchnk pushed a commit to anchnk/node that referenced this pull request May 19, 2017
Fixes integer overflows when supplying values exceeding MAX_SAFE_INTEGER
for highWaterMark.

PR-URL: nodejs#12593
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Luca Maraschi <luca.maraschi@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
@jasnell jasnell mentioned this pull request May 28, 2017
@gibfahn
Copy link
Member

gibfahn commented Jan 15, 2018

Release team opted not to backport to v6.x, as it seems there is a chance it might break things, and we haven't seen any requests for it. If there are reasons to backport let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
semver-minor PRs that contain new features and should be released in the next minor version. stream Issues and PRs related to the stream subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Int32 overflow when creating a buffer