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

napi_get_buffer_info(): change of behavior in v11.11.0 #26514

Closed
jorangreef opened this issue Mar 8, 2019 · 9 comments
Closed

napi_get_buffer_info(): change of behavior in v11.11.0 #26514

jorangreef opened this issue Mar 8, 2019 · 9 comments
Labels
addons Issues and PRs related to native addons. buffer Issues and PRs related to the buffer subsystem.

Comments

@jorangreef
Copy link
Contributor

  • Version: v11.11.0
  • Platform: all platforms
  • Subsystem: napi

A subtle change of behavior in napi_get_buffer_info() was introduced in v11.11.0.

For v11.10.0 and earlier, the following napi addon's assertions passed, even when the argv buffer value being retrieved was 0 bytes in length.

Since v11.11.0, however, the assert(*buffer != NULL); assertion below fails when the argv buffer value is 0 bytes in length.

This may not be a deal-breaker, and perhaps we need to update our assertions, but I first wanted to get to the bottom of why exactly this is happening, and why this changed in v11.11.0.

// converts a napi_value buffer argument to an unsigned char* buffer:
static int arg_buf(
  napi_env env,
  napi_value value,
  unsigned char** buffer,
  int* length,
  const char* error
) {
  assert(*buffer == NULL);
  assert(*length == 0);
  bool is_buffer;
  OK(napi_is_buffer(env, value, &is_buffer));
  if (!is_buffer) {
    napi_throw_error(env, NULL, error);
    return 0;
  }
  size_t size = 0;
  assert(napi_get_buffer_info(env, value, (void**) buffer, &size) == napi_ok);
  assert(*buffer != NULL);
  if (size > INT_MAX) {
    napi_throw_error(env, NULL, E_BUFFER_LENGTH);
    return 0;
  }
  *length = (int) size;
  assert(*length >= 0);
  return 1;
}

@addaleax, I know you did some recent work in #26301, could that have anything to do with this?

@addaleax addaleax added buffer Issues and PRs related to the buffer subsystem. addons Issues and PRs related to native addons. labels Mar 8, 2019
@addaleax
Copy link
Member

addaleax commented Mar 8, 2019

I know you did some recent work in #26301, could that have anything to do with this?

Yeah, that’s definitely possible.

This may not be a deal-breaker, and perhaps we need to update our assertions

I don’t think we can make guarantees about the contents of *buffer if size == 0 – you cannot dereference the pointer anyway, so it may hold any value.

but I first wanted to get to the bottom of why exactly this is happening, and why this changed in v11.11.0.

For that a full reproduction would be great, and in particular it’s good to know how you created the Buffer instance?

@jorangreef
Copy link
Contributor Author

Thanks @addaleax

I don’t think we can make guarantees about the contents of *buffer if size == 0 – you cannot dereference the pointer anyway, so it may hold any value.

Sure, I don't mind updating the assertions. I would just like to understand the reason for the change. The assertions were helpful to ensure that certain logic had run, especially where some buffer arguments were required and others were optional. For example, in our async work complete handler, before deleting napi_value references, we would assert that the corresponding pointers were in fact defined.

For that a full reproduction would be great, and in particular it’s good to know how you created the Buffer instance?

Sure, here's a gist which reproduces exactly on v11.10.0 (passes) and v11.11.0 (fails):

https://gist.github.com/jorangreef/19956b854e7d5e245c8ecb9eb655f4cd

process.version=v11.10.0
testing Buffer.alloc(0)...
buffer_length=0
testing cipher.update(Buffer.alloc(0))...
buffer_length=0
process.version=v11.11.0
testing Buffer.alloc(0)...
buffer_length=0
testing cipher.update(Buffer.alloc(0))...
Assertion failed: (*buffer != NULL), function arg_buf, file ../binding.c, line 21.
Abort trap: 6

Here, if the addon prints buffer_length=0 it means that the buffer != NULL assertion passed. You can see in these results, that for v11.11.0, the assertion passes for buffers allocated using Buffer.alloc() but not for buffers allocated by cipher.update().

Really appreciate your help with this.

@addaleax
Copy link
Member

I think this is a result of the added Resize() call in 84e02b1#diff-801e3948990f4965a8ea4aca4a423864R4002 – before this, we returned a Buffer with the pointer that was originally allocated, but a size that may have been smaller than the original allocation, thus leaving the remaining memory around as unusable overhead. The added Resize() call may change the base pointer, and in the case of a length of 0, will return nullptr.

@jorangreef
Copy link
Contributor Author

Thanks @addaleax

@jorangreef
Copy link
Contributor Author

@addaleax,

Just out of interest, but perhaps this change of behavior will cause some ecosystem breakage?

For example, I updated our native addon's assertions to assert(buffer != NULL || size == 0).

And I would have thought this would have been enough.

However, our fuzz tests picked up strange behavior when instantiating an hmac using OpenSSL. Technically, an hmac can be instantiated with an empty key, however 84e02b1#diff-801e3948990f4965a8ea4aca4a423864R4002 now means that it's possible for NULL to be passed to OpenSSL, which sees this and fails the initialization.

@jorangreef jorangreef reopened this Mar 18, 2019
@jorangreef
Copy link
Contributor Author

It looks like its turtles all the way down. 🐢

@jorangreef
Copy link
Contributor Author

Instead of Resize() returning nullptr, it should probably rather statically allocate a guard page to return when length is 0, so that the return value looks like a valid pointer for programs that check (even if they never dereference).

addaleax added a commit to addaleax/node that referenced this issue Mar 18, 2019
This fixes issues in which APIs that accept pointers created this way
treat `nullptr` and a zero-length buffer differently.
We already do something similar for our `Malloc()` implementation.

Fixes: nodejs#26514
@addaleax
Copy link
Member

@jorangreef Okay, I’ve opened #26731 to address this.

@jorangreef
Copy link
Contributor Author

Thanks for fixing this quickly.

targos pushed a commit to targos/node that referenced this issue Mar 27, 2019
This fixes issues in which APIs that accept pointers created this way
treat `nullptr` and a zero-length buffer differently.
We already do something similar for our `Malloc()` implementation.

PR-URL: nodejs#26731
Fixes: nodejs#26514
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Richard Lau <riclau@uk.ibm.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
targos pushed a commit that referenced this issue Mar 27, 2019
This fixes issues in which APIs that accept pointers created this way
treat `nullptr` and a zero-length buffer differently.
We already do something similar for our `Malloc()` implementation.

PR-URL: #26731
Fixes: #26514
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Richard Lau <riclau@uk.ibm.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
addons Issues and PRs related to native addons. buffer Issues and PRs related to the buffer subsystem.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants