-
Notifications
You must be signed in to change notification settings - Fork 29.9k
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
http_parser: assert on incoming argument's type #12288
Conversation
Reference: #12178 |
src/node_http_parser.cc
Outdated
String::NewFromUtf8(parser->env()->isolate(), | ||
"Object conversion of argument failed."))); | ||
return; | ||
} |
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 not a simple CHECK(args[0]->IsExternal())
? If you want to throw an exception, don't reinvent the wheel and use parser->env()->ThrowError("error message")
.
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.
out/Release/node[8983]: ../src/node_http_parser.cc:486:static void node::Parser::Consume(const FunctionCallbackInfo<v8::Value> &): Assertion `args[0]->IsExternal()' failed.
...
With the CHECK macro, the failure is a simple assertion as above (with callstack) and the message is not very intuitive and hence I went with a throw.
Regarding throw: made the changes are suggested.
Hope this is fine by you?
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.
@gireeshpunathil FWIW, you can use a message with a CHECK assertion (though one may qualify it as a little hack). A common pattern used in some parts of the codebase is CHECK(expr && "message")
.
1e0305a
to
b403883
Compare
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 add a test please. The current changes LGTM though.
@gireeshpunathil do you mind adding the |
src/node_http_parser.cc
Outdated
// Argument coming from JS land with unchecked type. | ||
// Make sure it is a valid external object. | ||
if (!args[0]->IsExternal()) { | ||
parser->env()->ThrowError("Object conversion of argument failed."); |
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 comment is superfluous and the error message is misleading: no conversion is attempted, it's just a type check.
I'd simply turn it into a CHECK and even then it's not all that useful; a few lines below we cast the wrapped value to a StreamBase without ever checking if it's actually a StreamBase instance.
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.
@bnoordhuis - done, followed your suggestion.
b403883
to
54cdc1c
Compare
@aqrln - thanks, added the Fixes: tag into the commit message.
|
@cjihrig - thanks. I have this test program (sort of a regression test) devised - based on the Exception throwing logic, but @bnoordhuis 's latest suggestion (use of CHECK) would mean that we would assert in C++ land on failure, and managing / validating that in a JS test case seems hacky. Any suggestions? #cat test/parallel/test-http-parser-consume.js
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const spawn = require('child_process').spawn;
if (process.argv[2] === 'child') {
var server = http.createServer(function(q, s) {
s.end('hello');
});
server.listen(common.PORT, function(s) {
var rr = http.get('http://localhost:' + common.PORT, function(d) {
try {
rr.parser.consume(0);
}
catch(e) {
e.toString().includes('parser.consume requires a stream argument.');
process.exit(0);
}
});
});
}
else {
const child = spawn(process.argv[0], [process.argv[1], 'child']);
child.on('stdout', function(d) {
console.log(d.toString());
});
child.on('stderr', function(d) {
console.log(d.toString());
});
child.on('exit', common.mustCall(function(code, signal) {
assert(signal === null && code === 0, 'Parser::Consume should not crash the process.');
}));
} |
src/node_http_parser.cc
Outdated
@@ -472,6 +472,9 @@ class Parser : public AsyncWrap { | |||
static void Consume(const FunctionCallbackInfo<Value>& args) { | |||
Parser* parser; | |||
ASSIGN_OR_RETURN_UNWRAP(&parser, args.Holder()); | |||
|
|||
CHECK(args[0]->IsExternal()); | |||
|
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.
Nit: I'd remove this empty line.
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.
@aqrln - 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.
I was only talking about removing the empty line I left the comment at, not both of them. The idea was to make everything related to extracting stream
from args
one visual block, if my comment was too short to be clear :)
You are not obliged to do that though, and I'm fine with both of your variants too, that's just my personal preference you may or may not agree with.
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.
@aqrln - sure, understood.
15793fc
to
86e18ec
Compare
@cjihrig - Added a test case to capture this. As it stands out, the evolved change in the parser code means that the process now ABRTs as opposed to a SEGV in response to bad input, and the test is made to validate this fact only. |
const spawn = require('child_process').spawn; | ||
|
||
if (process.argv[2] === 'child') { | ||
var server = http.createServer(function(q, s) { |
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 use const
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.
Can you also use more descriptive names than q
and s
.
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.
@cjihrig - done, followed your suggestions.
s.end('hello'); | ||
}); | ||
|
||
server.listen(common.PORT, function(s) { |
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.
Instead of using common.PORT
, you can just request a port from the operating system. Also, please wrap the callback in common.mustCall()
.
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
}); | ||
|
||
server.listen(common.PORT, function(s) { | ||
var rr = http.get('http://localhost:' + common.PORT, function(d) { |
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 the first parameter just be { port: server.address().port }
. Also, please use const
.
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
server.listen(common.PORT, function(s) { | ||
var rr = http.get('http://localhost:' + common.PORT, function(d) { | ||
rr.parser.consume(0); | ||
process.exit(0); |
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 close the server to let the process exit naturally.
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.
yes, done.
}); | ||
}); | ||
} | ||
else { |
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.
Style nit: else
should go on the previous line.
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
} | ||
else { | ||
const child = spawn(process.argv[0], [process.argv[1], 'child']); | ||
child.on('stdout', function(d) { |
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.
These can be removed right?
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.
And even if you do need these for some reason, I think a better way to do that is
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
EDIT: or, even better than that, pass { stdio: 'inherit' }
to spawn()
.
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 don't think child.on('stdout', ...)
is even a thing.
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.
Yeah, I don't recall those events too.
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.
inheriting stdio is the right way
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.
Sorry, don't know how I picked up 'stdout' as an event. Removed them and inherited from parent.
Debugging issues and isolating them between test issues and node issues have been effortful, so please allow me to retain the child output, if it is fine by you?
}); | ||
child.on('exit', common.mustCall(function(code, signal) { | ||
assert(signal === 'SIGABRT', | ||
'parser.consume should not crash, but ended with ' + signal); |
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.
Please move this over one more space.
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
Why is crashing with SIGABRT okay? I would expect that this should throw a proper JS exception. |
I think the reasoning of @bnoordhuis (and maybe others) here is that this kind of crash can only be produced by messing with or accessing what Node.js would consider its internals, here by using I talked about this again with @bengl a few days ago, and while we agreed that we would like to see a more un-crashable Node, we also agreed that Ben has a good and valid point here. |
86e18ec
to
698cae1
Compare
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.
One more nit in the test, but LGTM if the CI passes.
res.end('hello'); | ||
}); | ||
|
||
server.listen(0, function(s) { |
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 needs common.mustCall()
on the callback.
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.
made this change, thanks @cjihrig
698cae1
to
321369e
Compare
}); | ||
|
||
server.listen(0, common.mustCall(function(s) { | ||
const rr = http.get({ port: server.address().port }, common.mustCall(function(d) { |
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 line is longer than 80 characters.
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.
sliced it, thanks.
})); | ||
})); | ||
} else { | ||
const child = spawn(process.argv[0], [process.argv[1], 'child'], { stdio: 'inherit' }); |
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.
Ditto.
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
321369e
to
3ab6774
Compare
what would the semver level be on this? Should we treat this as a major? |
I’d say it’s semver-patch at most (semver-doesntreallymatteranyway if we’re being honest). It turns one kind of hard crash into another, and practically speaking it’s not a bug that gets triggered unless you are actively looking into how to crash 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.
LGTM. Now that the PR performs a CHECK()
instead of throwing an exception like it did initially, I'm not sure that it fixes #12178 in a way one would expect it to, so, arguably, it's worth changing "Fixes:" to "Refs:". That said, I'm +1 for this change.
const spawn = require('child_process').spawn; | ||
|
||
if (process.argv[2] === 'child') { | ||
const server = http.createServer(function(req, res) { |
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.
Nit: why not make these callbacks arrow functions?
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, all the callbacks are arrow'ed.
server.listen(0, common.mustCall(function(s) { | ||
const rr = http.get({ port: server.address().port }, | ||
common.mustCall(function(d) { | ||
rr.parser.consume(0); |
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.
Not sure about the indentation 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.
fixed
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.
node-test-linter
on CI doesn't seem to be happy. Can you please fix the linter errors?
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'm good with semver-patch. LGTM with the linting issue fixed.
all clear - a CI please! |
cc/ @nodejs/http Duplicating cc/ as I don't think it works if you're not a collaborator (which is a bit of a pain). |
o! I never knew - thanks @gibfahn for that. I would have (and had) spent a lot of cc/ stuff thinking that someone will get a mail. |
fullish GitHub 🤦♂️ |
I think the limitation it's just for |
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.
2 nits
})); | ||
|
||
server.listen(0, common.mustCall((s) => { | ||
const rr = http.get({ port: server.address().port }, |
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 know people have asked for a 1e6 changes... but, would you reindent with both arguments in a new line (nececitating less whitespace):
server.listen(0, common.mustCall((s) => {
const rr = http.get(
{ port: server.address().port },
common.mustCall((d) => {
rr.parser.consume(0);
server.close();
}));
})
);
I'm fine if you don't want to.
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.
@refack - thanks, done
server.listen(0, common.mustCall((s) => { | ||
const rr = http.get({ port: server.address().port }, | ||
common.mustCall((d) => { | ||
rr.parser.consume(0); |
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.
Nit: Add comment that this line should abort the process
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.
added a comment to explain the code
Unchecked argument conversion in Parser::Consume crashes node in an slightly undesirable manner - 'unreachable code' in parser. Make sure we validate the incoming type at the earliest point. Refs: nodejs#12178
6300f18
to
e340d28
Compare
Pre-land CI: https://ci.nodejs.org/job/node-test-commit/10368/ |
I can't find mine either, ignore me please. |
|
Unchecked argument conversion in Parser::Consume crashes node in an slightly undesirable manner - 'unreachable code' in parser. Make sure we validate the incoming type at the earliest point. PR-URL: nodejs#12288 Fixes: nodejs#12178 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Alexey Orlenko <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Santiago Gimeno <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
landed in efab784 |
extra CI to check |
Unchecked argument conversion in Parser::Consume crashes node in an slightly undesirable manner - 'unreachable code' in parser. Make sure we validate the incoming type at the earliest point. PR-URL: #12288 Fixes: #12178 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Alexey Orlenko <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Santiago Gimeno <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
Should this land on v6.x? |
ping |
@MylesBorins yes. |
Unchecked argument conversion in Parser::Consume crashes node in an slightly undesirable manner - 'unreachable code' in parser. Make sure we validate the incoming type at the earliest point. PR-URL: #12288 Fixes: #12178 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: Anna Henningsen <[email protected]> Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Alexey Orlenko <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Santiago Gimeno <[email protected]> Reviewed-By: Tobias Nießen <[email protected]> Reviewed-By: Refael Ackermann <[email protected]>
Unchecked argument conversion in Parser::Consume crashes node
in an slightly undesirable manner - 'unreachable code' in parser.
Make sure we validate the incoming type at the earliest point.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
http_parser