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

No abort on require() with null bytes #8277

Closed
wants to merge 2 commits into from

Conversation

rvagg
Copy link
Member

@rvagg rvagg commented Aug 26, 2016

  1. Handle errno in InternalModuleReadFile() returning Undefined so we don't get an abort() when uv_fs_read() raises an error (currently it's only on \u0000 that I can find an error but perhaps there are others).
  2. Apply the nullBytes() check in readPackage() so we get a proper error rather than a truncated one. This is also consistent with the behaviour of require() on null bytes prior to the introduction of the fast-path reads that now exist in v4.x+.

This comes from 1bbf8d0 which is in v4.x and v6.x. In v0.12 and prior you'd get a normal "null bytes not allowed" error thrown when using a require() involving \u0000.

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. fs Issues and PRs related to the fs subsystem / file system. module Issues and PRs related to the module subsystem. build Issues and PRs related to build files or the CI. labels Aug 26, 2016
@rvagg rvagg force-pushed the null-bytes-require-abort branch from 1d13b57 to 8a011d3 Compare August 26, 2016 08:16
@bnoordhuis
Copy link
Member

The changes to node_file.cc don't look correct to me at all. What error does uv_fs_read() return? If it's UV_EBADF, it means you did something wrong and it should fail.

lib/fs.js Outdated
@@ -6,6 +6,7 @@
const constants = process.binding('constants').fs;
const util = require('util');
const pathModule = require('path');
const internalFs = require('internal/fs');
Copy link
Member

Choose a reason for hiding this comment

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

This is unfortunately, if I have understood the situation correctly, a breaking change à la #6413?

@ChALkeR ChALkeR added the blocked PRs that are blocked by other issues or PRs. label Aug 26, 2016
@ChALkeR
Copy link
Member

ChALkeR commented Aug 26, 2016

Blocked by #6413.

@rvagg
Copy link
Member Author

rvagg commented Aug 26, 2016

@bnoordhuis if uv_fs_read() fails with < 0 then it continues on with path resolution as per Module._findPath() and then if none is found results in a new Error("Cannot find module '" + request + "'") (& err.code = 'MODULE_NOT_FOUND).

This matches the behaviour before 1bbf8d0 in readPackage():

  try {
    var jsonPath = path.resolve(requestPath, 'package.json');
    var json = fs.readFileSync(jsonPath, 'utf8');
  } catch (e) {
    return false;
  }

i.e. this is equivalent to:

  const json = internalModuleReadFile(path._makeLong(jsonPath));

  if (json === undefined) {
    return false;
  }

@rvagg
Copy link
Member Author

rvagg commented Aug 26, 2016

I should also note that this is failing on all machines in CI but is fine on my machine so I suspect there's something wonky going on with git. Will try again too.

I'll also get rid of the internal/ stuff and just duplicate the function with a note about the duplication as this needs to get fixed in v4.x too. @addaleax & @ChALkeR do you think that's an acceptable solution to undo the block?

@ChALkeR
Copy link
Member

ChALkeR commented Aug 26, 2016

@rvagg Imo there is nothing preventing landing #6413 (or it's alternative) for v7.0, as graceful-fs@3 was updated to not trigger the warning, and hence gulp is fine. See #6413 (comment). I don't think that we should introduce more hacks because of that at the moment.

If that won't happen for some reason, then duplicating would be sufficient as a work-around. It should also be fine for v4.x.

@rvagg
Copy link
Member Author

rvagg commented Aug 26, 2016

OK, well given the current status what I might do is adjust this PR to duplicate, then introduce a new PR that undoes the duplication that can land when it's unblocked, that way the original can go through to v4 and the new can update for v7+.

@bnoordhuis
Copy link
Member

if uv_fs_read() fails with < 0

That doesn't answer my question on why it's failing. What error does it return?

@rvagg
Copy link
Member Author

rvagg commented Aug 26, 2016

That doesn't answer my question on why it's failing. What error does it return?

I haven't cared so far because it's irelevant for the sake of restoring previous functionality. There is no abort() in the old code path and the introduction of an abort() in a non-semver-major is clearly unintentional.

Testing it now with \u0000 shows a EISDIR and that's because it starts resolution with ./.node_modules/. I still don't see the relevance of this line of enquiry though, perhaps you can spell it out more clearly why it would matter now?

@rvagg
Copy link
Member Author

rvagg commented Aug 26, 2016

I have no idea why this is failing on CI but working fine for me locally on both OSX and Linux. I'm guessing it's a fixture thing.

https://ci.nodejs.org/job/node-test-commit/4782/ is the latest attempt if anyone else wants to poke at it.

@rvagg rvagg force-pushed the null-bytes-require-abort branch from ed9877d to 39db98c Compare August 27, 2016 03:42
@rvagg
Copy link
Member Author

rvagg commented Aug 27, 2016

Figured out CI, it's cause I was running with a NODE_PATH locally but of course CI is clean. With the checks in place as they are now, the performance regression seems fairly minimal, at least it looks statistically insignificant when using the http://benchmarking.nodejs.org/ benchmarks for before/after this change.

Here's CI: https://ci.nodejs.org/job/node-test-commit/4788/

PTAL

@@ -130,6 +130,9 @@ function assertEncoding(encoding) {
}
}

// This is duplicated in module.js and needs to be moved to internal/fs.js
// once it is OK again to include internal/ resources in fs.js.
// See: https://github.com/nodejs/node/pull/6413
Copy link
Member

Choose a reason for hiding this comment

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

#6413 should land on Monday.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see a follow-up #8292, thanks.

@ChALkeR ChALkeR removed the blocked PRs that are blocked by other issues or PRs. label Aug 27, 2016
@ChALkeR
Copy link
Member

ChALkeR commented Aug 27, 2016

After splitting this in two PRs, this one is no longer blocked on #6413, #8292 is.

@addaleax addaleax removed the build Issues and PRs related to build files or the CI. label Aug 27, 2016
@bnoordhuis
Copy link
Member

Testing it now with \u0000 shows a EISDIR and that's because it starts resolution with ./.node_modules/.

Okay, that means you need to do the null byte check before passing it on to C++ land. Either that or it should be uv_fs_open() that gets (and deals with) the error, not uv_fs_read().

The change as it is would cause divergence across platforms. FreeBSD allows reading from a file descriptor that refers to a directory, other Unices don't.

I still don't see the relevance of this line of enquiry though, perhaps you can spell it out more clearly why it would matter now?

The changes to node_file.cc look like daubing on more code to make the problem go away without really understanding what the problem is in the first place. More or less confirmed by the above.

@rvagg
Copy link
Member Author

rvagg commented Aug 27, 2016

@bnoordhuis I still don't quite get where the problem is, and I entirely disagree with your assessment of "without really understanding what the problem is", so I'll explain that.

Okay, that means you need to do the null byte check before passing it on to C++ land. Either that or it should be uv_fs_open() that gets (and deals with) the error, not uv_fs_read().

It is, that's what this addition is for:

Module._findPath = function(request, paths, isMain) {
+  nullCheck(request);

That will throw in the event of a null byte so InternalModuleReadFile in node_file.cc should not get a null byte now, that's precisely what's being fixed.

There may be additional errors, however, I haven't introduced tests for any but consider this. The previous incarnation before the optimisations were introduced did:

  try {
    var jsonPath = path.resolve(requestPath, 'package.json');
    var json = fs.readFileSync(jsonPath, 'utf8');
  } catch (e) {
    return false;
  }

i.e. catching any error thrown by fs.

The new version effectively did this with uv_fs_open() thanks to:

   if (fd < 0) {
     return;
  }

which ends up being caught at by the replacement of the try/catch:

  const json = internalModuleReadFile(path._makeLong(jsonPath));

  if (json === undefined) {
    return false;
  }

but any errors occuring within uv_fs_read() would result in a plain abort() thanks to the CHECK_GE(numchars, 0); which has now been replaced by:

-    CHECK_GE(numchars, 0);
+    if (numchars < 0) {
+      break;
+    }

followed by:

+  if (numchars < 0) {
+    args.GetReturnValue().Set(Undefined(env->isolate()));
+  } else {

which perhaps could be just a return; or perhaps the other return could be an explicit undefined return, I don't know (they are inconsistent and I'm fine with making them consistent if you have a recommendation).

So, previously, errors encountered by uv_fs_open() would result in an error bubbling up to either continue the resolution algorithm with different files or "Cannot find module" if the resolution has concluded, but errors arising out of uv_fs_read() would result in a plain abort(), regardless of where it was in the resolution algorithm and what type of error it was. Now, errors caused by null bytes should not be encountered because they are handled in JavaScript first and result in the correct error being thrown (the "no null bytes" v0.12 style error) and further errors encountered by both uv_fs_open() and uv_fs_read() are handled in the same way, that is, they cancel this pass in the resolution algorithm which results in continuing on to the next type of file or a "Cannot find module" error.

I believe this is consistent with the previous behaviour which used fs.readFileSync() which uses fs.readSync() which uses uv_fs_read() which does a ThrowUVException() when called synchronously on receiving a <0 result.

So, there are essentially two problems being fixed here, and two commits to fix them. One is to get the abort() out of the way no matter what happens and make the module resolution & read algorithm behave as it previously did. The other is to restore the nullCheck() to the fast reads in module.js so that if null bytes are encountered they result in the same error as if you use fs directly and the same error that occurred before (and also make consistent any platform-inconsistencies as you mentioned).

@bnoordhuis
Copy link
Member

If I understand you correctly, the node_file.cc changes fix the case of trying to read a package.json that is a directory, not a file, but going by your description that's accidental more than intentional?

(InternalModuleReadFile() is only used for reading package.json files and uv_fs_read() can realistically only fail with EIO or EISDIR. The tests in this PR don't exercise that scenario, therefore QED.)

The changes also don't fix the freebsd/non-freebsd divergence but that is ameliorated by the fact that the directory entry is unlikely to be valid JSON. :-)

I've filed #8307 because I think this is a separate issue.

@rvagg
Copy link
Member Author

rvagg commented Sep 4, 2016

There's no accidental fix here and you're still missing the point I think. The two things being fixed here are the null bytes check not being in place and leading to an abort, any \u0000 in require can crash a program. It's a stretch but I could concoct a scenario where this couod be a DoS vector if require depends at all on user input (bad practice of course but still). The second is the fact that all the usual cases of fs read failure lead to an abort in the new require path. The is-directory failure I mentioned is simply one that can come up by using the test cases we have (edit: the test cases introduced here if you remove the null bytes check, i.e. null bytes can result in trying to read a directory in some cases on some platforms but the null bytes check doesn't even let it it get that far), not something I have actually experienced.

I don't understand what the hesitation is here, it seems that there is a suggestion that I somehow don't understand what I'm fixing (an assessment I don't agree with) and therefore it's not good enough. I don't get what couod be controversial about fixing this.

In lieu of making forward progress here I'd prefer to just revert the commit(s) that introduced this, I'll put those in when I have time so that option can be considered too. Having an abort() in the path of reasonable failure cases should not be acceptable. They should be reserved for the unexpected and surprising (to us).

start = 3; // Skip UTF-8 BOM.
}
if (numchars < 0) {
args.GetReturnValue().Set(Undefined(env->isolate()));
Copy link
Member

Choose a reason for hiding this comment

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

Rather than returning undefined.... can we throw here? Yes, I know that's semver-major but it's better than an abort and it's likely more correct than undefined.

Copy link
Member Author

Choose a reason for hiding this comment

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

That would short circuit the require lookup algorithm which may potentially find a working match in a further iteration. We can do it but it's not a small breaking change and IMO would be unnecessarily disruptive for little gain.

Copy link
Member

Choose a reason for hiding this comment

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

See also the comment above the function:

// Used to speed up module loading.  Returns the contents of the file as
// a string or undefined when the file cannot be opened.  The speedup
// comes from not creating Error objects on failure.

Copy link
Member

Choose a reason for hiding this comment

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

+1 works for me.

On Sunday, September 4, 2016, Anna Henningsen notifications@github.com
wrote:

In src/node_file.cc
#8277 (comment):

@@ -566,17 +569,21 @@ static void InternalModuleReadFile(const FunctionCallbackInfo& args) {
CHECK_EQ(0, uv_fs_close(loop, &close_req, fd, nullptr));
uv_fs_req_cleanup(&close_req);

  • size_t start = 0;
  • if (chars.size() >= 3 && 0 == memcmp(&chars[0], "\xEF\xBB\xBF", 3)) {
  • start = 3; // Skip UTF-8 BOM.
  • }
  • if (numchars < 0) {
  • args.GetReturnValue().Set(Undefined(env->isolate()));

See also the comment above the function:

// Used to speed up module loading. Returns the contents of the file as// a string or undefined when the file cannot be opened. The speedup// comes from not creating Error objects on failure.


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/nodejs/node/pull/8277/files/39db98ca5e20b1c28b5058c7b049747324d955bd#r77449453,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAa2eWcj_tAWKN-vBdhw8Czm5djPYWULks5qmpA1gaJpZM4Jt1xz
.

@rvagg rvagg force-pushed the master branch 2 times, most recently from c133999 to 83c7a88 Compare October 18, 2016 17:02
@jasnell
Copy link
Member

jasnell commented Mar 1, 2017

ping @rvagg @bnoordhuis ...

@jasnell jasnell added the stalled Issues and PRs that are stalled. label Mar 1, 2017
@bnoordhuis
Copy link
Member

Needs a rebase first, 3 out of 4 touched files conflict. However, I'm not convinced this is a) the right fix, and b) necessary.

The threat scenario seems to be require(untrustedUnsanitizedInput) which strikes me as highly implausible. Who in their right mind does that?

@fhinkel
Copy link
Member

fhinkel commented Jun 7, 2017

ping @rvagg Are you still working on this or have we found a different solution?

@jasnell
Copy link
Member

jasnell commented Aug 24, 2017

Closing due to lack of forward progress. We can reopen if necessary.

@jasnell jasnell closed this Aug 24, 2017
@rvagg
Copy link
Member Author

rvagg commented Aug 25, 2017

Still needs resolution, just need to find a path forward that satisfies the concerns here, tricky to navigate it seems but really needs attention. If someone else wants to pick this up in the meantime then please do.

@jasnell
Copy link
Member

jasnell commented Aug 25, 2017

I believe @bnoordhuis had been dabbling with this lately. I'll verify

@cjihrig
Copy link
Contributor

cjihrig commented Aug 25, 2017

#14854?

@saper
Copy link

saper commented Aug 26, 2017

@cjihrig #14854 is about require('\u0000ab') and this one is about file that contains NUL bytes in the contents.

@cjihrig
Copy link
Contributor

cjihrig commented Aug 26, 2017

You said something similar in #14905 (comment), but it doesn't seem to be clear to anyone else. Quoting @bnoordhuis: Why are nul bytes in a file relevant?

@saper
Copy link

saper commented Aug 26, 2017

@cjihrig one thing is to make sure to understand those are two different issues. A quick look at the proposed test should make it clear (it unpack JSON file containing nulls). Crashing node just because the input file has some funny characters is not good. But I misread the test, it seems to me it does not test this fix.

@BridgeAR
Copy link
Member

@saper I am not sure what you mean with "unpack JSON file containing null". There is no JSON file in the tests and as I pointed out before (#14905 (comment)) the tests work fine and Node.js is not crashing anymore since #14854 landed.

@saper
Copy link

saper commented Aug 26, 2017

This is really confusing, you are right.

I think that 39db98c is obsoleted by #14854

But de95b66 needs a test and possibly a nicer solution.

@BridgeAR
Copy link
Member

@saper as far as I see it this is not needed at all anymore. If you can come up with a test case that fails I would ask you to post that. Otherwise there is little to do.

abhishekumar-tyagi pushed a commit to abhishekumar-tyagi/node that referenced this pull request May 5, 2024
abhishekumar-tyagi pushed a commit to abhishekumar-tyagi/node that referenced this pull request May 5, 2024
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++. fs Issues and PRs related to the fs subsystem / file system. module Issues and PRs related to the module subsystem. stalled Issues and PRs that are stalled.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants