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

http_parser: assert on incoming argument's type #12288

Closed
wants to merge 1 commit into from

Conversation

gireeshpunathil
Copy link
Member

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), or vcbuild test (Windows) passes
  • commit message follows commit guidelines
Affected core subsystem(s)

http_parser

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. http_parser Issues and PRs related to the HTTP Parser dependency or the http_parser binding. labels Apr 9, 2017
@gireeshpunathil
Copy link
Member Author

Reference: #12178

String::NewFromUtf8(parser->env()->isolate(),
"Object conversion of argument failed.")));
return;
}
Copy link
Member

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").

Copy link
Member Author

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?

Copy link
Contributor

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").

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.

Can you add a test please. The current changes LGTM though.

@aqrln
Copy link
Contributor

aqrln commented Apr 9, 2017

@gireeshpunathil do you mind adding the Fixes: https://github.com/nodejs/node/issues/12178 field to the commit message? Thanks!

// 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.");
Copy link
Member

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.

Copy link
Member Author

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.

@gireeshpunathil
Copy link
Member Author

@aqrln - thanks, added the Fixes: tag into the commit message.
Regarding the CHECK (expr && msg) semantics: I found it less useful - this is how the output appears:

out/Release/node[2219]: ../src/node_http_parser.cc:476:static void node::Parser::Consume(const FunctionCallbackInfo<v8::Value> &): Assertion `args[0]->IsExternal() && "parser.consume requires a stream argument."' failed.
 1: node::Abort() [./node]
...

@gireeshpunathil
Copy link
Member Author

gireeshpunathil commented Apr 10, 2017

@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.');
  }));
}

@@ -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());

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@aqrln - done

Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

@aqrln - sure, understood.

@gireeshpunathil gireeshpunathil force-pushed the parsertype branch 2 times, most recently from 15793fc to 86e18ec Compare April 10, 2017 14:37
@gireeshpunathil
Copy link
Member Author

@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) {
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 use const here.

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 also use more descriptive names than q and s.

Copy link
Member Author

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) {
Copy link
Contributor

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().

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

});

server.listen(common.PORT, function(s) {
var rr = http.get('http://localhost:' + common.PORT, function(d) {
Copy link
Contributor

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.

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

server.listen(common.PORT, function(s) {
var rr = http.get('http://localhost:' + common.PORT, function(d) {
rr.parser.consume(0);
process.exit(0);
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 close the server to let the process exit naturally.

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, done.

});
});
}
else {
Copy link
Contributor

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.

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

}
else {
const child = spawn(process.argv[0], [process.argv[1], 'child']);
child.on('stdout', function(d) {
Copy link
Contributor

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?

Copy link
Contributor

@aqrln aqrln Apr 10, 2017

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().

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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

Copy link
Member Author

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);
Copy link
Contributor

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.

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

@DemiMarie
Copy link

Why is crashing with SIGABRT okay? I would expect that this should throw a proper JS exception.

@addaleax
Copy link
Member

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 .parser.

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.

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.

One more nit in the test, but LGTM if the CI passes.

res.end('hello');
});

server.listen(0, function(s) {
Copy link
Contributor

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.

Copy link
Member Author

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

});

server.listen(0, common.mustCall(function(s) {
const rr = http.get({ port: server.address().port }, common.mustCall(function(d) {
Copy link
Contributor

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.

Copy link
Member Author

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' });
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

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

@jasnell
Copy link
Member

jasnell commented Apr 12, 2017

what would the semver level be on this? Should we treat this as a major?

@addaleax
Copy link
Member

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.

Copy link
Contributor

@aqrln aqrln left a 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.

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

const spawn = require('child_process').spawn;

if (process.argv[2] === 'child') {
const server = http.createServer(function(req, res) {
Copy link
Contributor

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?

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, 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);
Copy link
Contributor

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

fixed

Copy link
Contributor

@aqrln aqrln left a 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?

Copy link
Member

@jasnell jasnell left a 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.

@gireeshpunathil
Copy link
Member Author

all clear - a CI please!
/cc @nodejs/http
/cc @nodejs/platform-windows

@santigimeno
Copy link
Member

@gibfahn
Copy link
Member

gibfahn commented Jun 4, 2017

cc/ @nodejs/http
cc/ @nodejs/platform-windows

Duplicating cc/ as I don't think it works if you're not a collaborator (which is a bit of a pain).

@gireeshpunathil
Copy link
Member Author

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.

@refack
Copy link
Contributor

refack commented Jun 4, 2017

Duplicating cc/ as I don't think it works if you're not a collaborator (which is a bit of a pain).

fullish GitHub 🤦‍♂️

@refack
Copy link
Contributor

refack commented Jun 4, 2017

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.

I think the limitation it's just for @nodejs/ teams. This and a inability to assign issues to everyone, IMHO very foolish.

@refack refack self-assigned this Jun 4, 2017
Copy link
Contributor

@refack refack left a 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 },
Copy link
Contributor

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.

Copy link
Member Author

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);
Copy link
Contributor

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

Copy link
Member Author

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
@refack
Copy link
Contributor

refack commented Jun 5, 2017

Pre-land CI: https://ci.nodejs.org/job/node-test-commit/10368/
@gireeshpunathil did you address all comments (I couldn't find @sam-github's)?

@sam-github
Copy link
Contributor

I can't find mine either, ignore me please.

@gibfahn
Copy link
Member

gibfahn commented Jun 5, 2017

(I couldn't find @sam-github's)?

#12288 (comment)

refack pushed a commit to refack/node that referenced this pull request Jun 5, 2017
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]>
@refack
Copy link
Contributor

refack commented Jun 5, 2017

landed in efab784

@refack refack closed this Jun 5, 2017
@refack
Copy link
Contributor

refack commented Jun 5, 2017

extra CI to check master's sanity: https://ci.nodejs.org/job/node-test-commit-linuxone/6400/

jasnell pushed a commit that referenced this pull request Jun 7, 2017
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]>
@gibfahn gibfahn mentioned this pull request Jun 15, 2017
3 tasks
@MylesBorins
Copy link
Contributor

Should this land on v6.x?

@MylesBorins
Copy link
Contributor

ping

@addaleax
Copy link
Member

@MylesBorins yes.

MylesBorins pushed a commit that referenced this pull request Sep 19, 2017
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]>
@MylesBorins MylesBorins mentioned this pull request Sep 20, 2017
@refack refack removed their assignment Oct 20, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. http_parser Issues and PRs related to the HTTP Parser dependency or the http_parser binding.
Projects
None yet
Development

Successfully merging this pull request may close these issues.