-
Notifications
You must be signed in to change notification settings - Fork 30.2k
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
Conversation
Instead of using |
@@ -0,0 +1,16 @@ | |||
'use strict'; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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.
lib/_stream_readable.js
Outdated
} | ||
|
||
if (~~hwm !== hwm) { | ||
throw new Error('highWaterMark exceeds valid range'); |
There was a problem hiding this comment.
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.
lib/_stream_readable.js
Outdated
this.highWaterMark = (hwm || hwm === 0) ? hwm : defaultHwm; | ||
if (hwm != null) { | ||
if (typeof hwm !== 'number') { | ||
throw new TypeError('highWaterMark must be a number'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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');
@mscdex Should I use this PR to fix the bug using |
@tniessen It might be good to hear from other @nodejs/collaborators about my proposed solution first. |
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. |
@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. |
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. |
Any updates? |
Any negative value is interpreted by the stream machinery as a I think we should not throw in this case, but instead cast the |
lib/_stream_readable.js
Outdated
throw new TypeError('highWaterMark must be a number'); | ||
} | ||
|
||
if (~~hwm !== hwm) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
I think |
Why is that? I would think the only time that particular constant would matter is if a user tried to |
Currently |
I don't understand this comment. What does that have to do with potentially using Anyway, I think we should still allow larger values via |
lib/_stream_readable.js
Outdated
@@ -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) { |
There was a problem hiding this comment.
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) { // ...
?
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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. |
Fixes integer overflows when supplying values exceeding 2 ^ 31 - 1 for highWaterMark.
@mscdex @cjihrig @mcollina @calvinmetcalf I force-pushed a commit which only contains the integer overflow fix. |
There was a problem hiding this 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 }); |
There was a problem hiding this comment.
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 2147483648
s 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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mscdex Done.
There was a problem hiding this 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
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 |
Thanks @mcollina. I will create a separate PR with the semver-major changes after this gets merged. |
Today is the 10th ;-) Anyway, here is CITGM: https://ci.nodejs.org/view/Node.js-citgm/job/citgm-smoker/772/ |
Updated with the correct date, thanks @mscdex. |
I think the citgm failures are recurring, can someone confirm? |
@mcollina I don't see any relations between the reported errors and this PR. Are there known problems with citgm? |
There was a problem hiding this 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?
@lucamaraschi Done. |
lib/_stream_readable.js
Outdated
@@ -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); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Landed as 11918c4 |
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>
Adds checks for the type of "highWaterMark" and restricts it to non-negative values. Refs: nodejs#12593
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>
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. |
Fixes integer overflows when supplying values exceeding
2 ^ 31 - 1
forhighWaterMark
.Fixes: #12553
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
stream