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

buffer: remove noAssert argument #12119

Closed
wants to merge 3 commits into from

Conversation

bnoordhuis
Copy link
Member

noAssert=true is intrinsically unsafe. Always-on checking does
cause a performance regression but some of that can be regained by
open-coding the checks (which this pull request doesn't do but can
be done at a later time if so desired.)

What's more, I don't think performance is ultra-critical for these
methods. If you want to squeeze out every last ounce of oomph, you
don't call out to generic kitchen-sink methods, you open-code loads
and stores directly in your parser or generator. I can see no good
reason for leaving it in and that is why this commit takes it out.

Refs: #8724 (#8724 (comment) is relevant vis-a-vis performance.)

@nodejs-github-bot nodejs-github-bot added buffer Issues and PRs related to the buffer subsystem. c++ Issues and PRs that require attention from people who are familiar with C++. labels Mar 29, 2017
@Trott Trott added the semver-major PRs that contain breaking changes and should be released in the next major version. label Mar 29, 2017
@@ -43,12 +43,16 @@ assert.throws(function() {
Buffer.from(new AB());
}, TypeError);

// write{Double,Float}{LE,BE} with noAssert should not crash, cf. #3766
// https://github.com/nodejs/node/issues/3766
Copy link
Member

Choose a reason for hiding this comment

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

Since this GitHub issue is specifically about behavior when noAssert is true, I think the URL/comment can be removed.

b.writeDoubleLE(11.11, 0, true);
b.writeDoubleBE(11.11, 0, true);
assert.throws(() => b.writeFloatLE(11.11, 0),
/RangeError: out of range index/);
Copy link
Member

Choose a reason for hiding this comment

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

I think these four tests can be removed if you want, as they were a regression test for a crash when noAssert was true. No noAssert means no regression possible. :-D

assert.throws(() => b.writeFloatLE(11.11, 0),
/RangeError: out of range index/);
assert.throws(() => b.writeFloatBE(11.11, 0),
/RangeError: out of range index/);
Copy link
Member

Choose a reason for hiding this comment

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

If you decide to keep these four tests, though, please add a ^ and $ to the start and end respectively of the RegExp.

@mscdex
Copy link
Contributor

mscdex commented Mar 29, 2017

-1 If noAssert gives ~3.5x performance, why remove the option when it's not causing any problems right now and it's being used responsibly? Not having to copy and paste these implementations into each project/module is a win IMHO, especially if optimizations to node's implementation are made over time, third party copies would then become stale and slower on newer node versions.

@bnoordhuis
Copy link
Member Author

The security mailing list gets emails from people who think they found a security flaw or a denial of service when in fact they are passing $some_truthy_value for noAssert. It happens frequently enough that it stands out. I'd say that fact alone suggests it's a poorly designed API that causes real-world issues.

I also don't really buy the performance argument. When you use the built-in functions you do it for convenience. You don't really care if it takes 50 ns or 100 ns because if you did, you would use hand-coded versions.

(That said, see #8724 (comment) - I'm fairly sure the use of new Function() does something strange to the benchmarks that makes the performance gap look worse than it really is.)

Compromise: remove noAssert and add unsafeWriteUInt8() etc. methods? That leaves little to the imagination.

@sam-github
Copy link
Contributor

I think the noAssert flag is very suspicious, and given the simple conditional check, its hard to see why its 3.5x slower.

As a positional arg, noAssert is easy to specify unintentionally, I like the idea of moving the unchecked array access into another API if they can't be deleted outright.

Aside: I really dislike the use of "safe" in APIs, it doesn't give any clue as to meaning. I'd prefer something more descriptive, like "unchecked". Even better would be to more all the APIs into a different module buffer.unchecked.writeUint8(). They wouldn't be as easy to call as methods, but why make writing unchecked code easy?

@sam-github
Copy link
Contributor

It would be interesting to find an actual module that uses noAssert, maybe one of the DB procotols, and benchmark its real-world code with/without the check to see actual performance impact.

@jasnell
Copy link
Member

jasnell commented Mar 29, 2017

Given that this change would cause code that currently works successfully when noAssert is truthy to potentially begin failing, I don't think we can do this without a proper deprecation cycle. I am perfectly fine with eliminating the additional option at some point, but I do not believe we should do so without deprecatiing.

@bnoordhuis
Copy link
Member Author

That takes too long, I don't have the patience to sit that out.

Middle ground: make the OOB checks in src/node_buffer.cc unconditional. That should be uncontroversial because "code that currently works successfully" doesn't apply to code that violates memory safety.

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.

Other arguments aside, the code changes LGTM.

@jasnell
Copy link
Member

jasnell commented Mar 30, 2017

I could live with that approach.

else
memcpy_num = ts_obj_length - offset;
THROW_AND_RETURN_IF_OOB(false);
memcpy_num = ts_obj_length - offset;
}
Copy link
Member

Choose a reason for hiding this comment

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

Uhh, the memcpy_num assignment is dead code since THROW_AND_RETURN_IF_OOB will always execute because of false.

@Trott
Copy link
Member

Trott commented Aug 5, 2017

@bnoordhuis Is this still something you'd like to move forward? Can you rebase and maybe address @TimothyGu's comment?

noAssert is a Boolean trap which is a terrible thing for an API with security implications. I'm all for getting rid of it. The only question is whether the performance implications are significant or not.

@bnoordhuis
Copy link
Member Author

PTAL, significantly watered-down changeset uploaded. :-/

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

Trott
Trott previously requested changes Aug 8, 2017
/* eslint-disable no-restricted-syntax */
for (const noAssert of [true, false]) {
// Out of bounds.
assert.throws(() => Buffer.from(1).readFloatBE(0, noAssert));
Copy link
Member

Choose a reason for hiding this comment

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

If I'm not mistaken, this and the subsequent lines are throwing on Buffer.from() (because it can't accept a number as an argument) and never getting to readFloatBE(). (Maybe create the Buffer external to the assert.throws() to avoid that possibility in future refactoring etc.?)

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch, this was hiding a real bug.

Verify that the receiver is a buffer object before attempting to write
out a floating point value.

Fixes: nodejs#8724
Check bounds and throw an exception when reading floating point values
from a buffer.  Before this commit, the process would fail a CHECK and
terminate when `noAssert=true`.

Fixes: nodejs#8724
@Trott
Copy link
Member

Trott commented Aug 10, 2017

@TimothyGu Does this LGTY now?

@Trott Trott dismissed their stale review August 10, 2017 03:03

comment addressed

@BridgeAR
Copy link
Member

Ping @TimothyGu

@BridgeAR BridgeAR dismissed TimothyGu’s stale review September 20, 2017 00:17

No response. Please take another look as your comment seems to be addressed.

@BridgeAR
Copy link
Member

@nodejs/tsc PTAL. This is semver-major and needs further reviews.

@Trott
Copy link
Member

Trott commented Sep 20, 2017

Does this require some modifications to buffer.md to explain when noAssert is deprecated and ignored?

@Trott
Copy link
Member

Trott commented Sep 20, 2017

(Also: This only affects the C++ checks, but there are checks in JS-land that still respect noAssert?)

@BridgeAR
Copy link
Member


inline uint64_t Add64Clamp(uint64_t x, uint64_t y) {
return x + y >= x ? x + y : static_cast<uint64_t>(-1);
}
Copy link
Member

Choose a reason for hiding this comment

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

This function seems unused.

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 added it for parity (I had plans to use it for something else) but I can remove it.

Copy link
Member

@BridgeAR BridgeAR Sep 26, 2017

Choose a reason for hiding this comment

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

I would prefer not to add functions that have no functionality are unused.

@BridgeAR
Copy link
Member

BridgeAR commented Oct 2, 2017

Ping @bnoordhuis

@BridgeAR
Copy link
Member

Ping @bnoordhuis

This is ready to land as soon as my comment is addressed. This could also be done while landing if you are fine with that.

@Trott
Copy link
Member

Trott commented Nov 22, 2017

This is ready to land as soon as my comment is addressed.

There's a -1 from @mscdex so this might need to be escalated to TSC if that isn't resolved.

This might need a doc update in buffer.md too so that it's documented when noAssert is ignored.

I would like to see this land.

Copy link
Member

@Trott Trott 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 a doc update.

@BridgeAR
Copy link
Member

@Trott thanks for pointing that out. I missed that. And good that you also caught the doc update :-)

@bnoordhuis @mscdex There will be a small performance penalty but if it truly has a 3.5 times impact, something must be wrong with V8. I think it makes sense to a) rerun the benchmark with current master and b) improve the benchmark by removing new Function. I am curious about those results. It would also be good to check the output of --prof with and without noAssert if the numbers are still weird.

@Trott
Copy link
Member

Trott commented Nov 25, 2017

I wonder if the benchmark is faulty. I can understand seeing a performance degradation with noAssert set to false, but is it plausible that we should be seeing a significant performance degradation with noAssert set to true? Here are the relevant results from a run of benchmark/buffers/buffer-read.js:

 buffers/buffer-read.js millions=1 type="DoubleBE" buffer="fast" noAssert="false"    -13.81 %        *** 2.686215e-10
 buffers/buffer-read.js millions=1 type="DoubleBE" buffer="fast" noAssert="true"     -17.24 %        *** 7.214802e-22
 buffers/buffer-read.js millions=1 type="DoubleBE" buffer="slow" noAssert="false"    -13.80 %        *** 4.725621e-14
 buffers/buffer-read.js millions=1 type="DoubleBE" buffer="slow" noAssert="true"     -15.80 %        *** 6.746154e-15
 buffers/buffer-read.js millions=1 type="DoubleLE" buffer="fast" noAssert="false"    -14.59 %        *** 2.411344e-15
 buffers/buffer-read.js millions=1 type="DoubleLE" buffer="fast" noAssert="true"     -18.60 %        *** 8.171223e-16
 buffers/buffer-read.js millions=1 type="DoubleLE" buffer="slow" noAssert="false"    -17.89 %        *** 1.790047e-19
 buffers/buffer-read.js millions=1 type="DoubleLE" buffer="slow" noAssert="true"     -16.12 %        *** 3.988442e-13
 buffers/buffer-read.js millions=1 type="FloatBE" buffer="fast" noAssert="false"     -16.16 %        *** 1.482242e-15
 buffers/buffer-read.js millions=1 type="FloatBE" buffer="fast" noAssert="true"      -15.38 %        *** 2.969582e-23
 buffers/buffer-read.js millions=1 type="FloatBE" buffer="slow" noAssert="false"     -14.22 %        *** 1.242668e-13
 buffers/buffer-read.js millions=1 type="FloatBE" buffer="slow" noAssert="true"      -17.66 %        *** 6.550393e-24
 buffers/buffer-read.js millions=1 type="FloatLE" buffer="fast" noAssert="false"     -12.73 %        *** 2.119630e-08
 buffers/buffer-read.js millions=1 type="FloatLE" buffer="fast" noAssert="true"      -16.95 %        *** 1.742243e-24
 buffers/buffer-read.js millions=1 type="FloatLE" buffer="slow" noAssert="false"     -12.40 %        *** 1.328481e-09
 buffers/buffer-read.js millions=1 type="FloatLE" buffer="slow" noAssert="true"      -18.43 %        *** 2.863973e-24

Pinging @psmarshall on the benchmark.

[Update: I think I see now why noAssert set to true would still get a performance hit although I'm kind of surprised by the size of it. Still wouldn't mind another pair of eyes on the benchmark, I guess.]

@psmarshall
Copy link
Contributor

From experimenting locally it looks like the regression is caused by the call to Environment::GetCurrent(args). When you give that directly to the macro, it is only actually called in the error case, whereas now it is always called. If you use the version of Uint32Value() where you don't pass a Context as before, then the regression disappears. Not sure what the best solution overall is, but that seems to be where the regression comes from.

@BridgeAR
Copy link
Member

BridgeAR commented Dec 6, 2017

Ping @bnoordhuis

@BridgeAR
Copy link
Member

Ping @bnoordhuis again

@bnoordhuis
Copy link
Member Author

I'll probably try a different tack, by moving the functions to JS. See #17775.

@BridgeAR
Copy link
Member

@bnoordhuis even though #17775 got merged, I guess #8724 is still unresolved and it still makes sense to remove the noAssert option, right? Due to the changes, I guess the flag will also not have a significant impact on the performance anymore.

@BridgeAR
Copy link
Member

Ping @bnoordhuis

@BridgeAR
Copy link
Member

Superseded by #18395

@BridgeAR BridgeAR closed this Jan 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
buffer Issues and PRs related to the buffer subsystem. c++ Issues and PRs that require attention from people who are familiar with C++. semver-major PRs that contain breaking changes and should be released in the next major version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet