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,n-api: release external buffers from BackingStore callback #33321

Closed

Conversation

addaleax
Copy link
Member

@addaleax addaleax commented May 9, 2020

(The first commit is #33320, split out due to different backportability, hence the blocked label.)

Release Buffer and ArrayBuffer instances that were created through
our addon APIs and have finalizers attached to them only after V8 has
called the deleter callback passed to the BackingStore, instead of
relying on our own GC callback(s).

This fixes the following race condition:

  1. Addon code allocates pointer P via malloc.
  2. P is passed into napi_create_external_buffer with a finalization
    callback which calls free(P). P is inserted into V8’s global array
    buffer table for tracking.
  3. The finalization callback is executed on GC. P is freed and returned
    to the allocator. P is not yet removed from V8’s global array
    buffer table. (!)
  4. Addon code attempts to allocate memory once again. The allocator
    returns P, as it is now available.
  5. P is passed into napi_create_external_buffer. P still has not been
    removed from the v8 global array buffer table.
  6. The world ends with Check failed: result.second.

Since our API contract is to call the finalizer on the JS thread on
which the ArrayBuffer was created, but V8 may call the BackingStore
deleter callback on another thread, fixing this requires posting
a task back to the JS thread.

Refs: #32463 (comment)
Fixes: #32463

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • commit message follows commit guidelines

In some situations, it can be useful to use threadsafe callbacks
on an `Environment` to perform cleanup operations that should run
even when the process would otherwise be ending.
Release `Buffer` and `ArrayBuffer` instances that were created through
our addon APIs and have finalizers attached to them only after V8 has
called the deleter callback passed to the `BackingStore`, instead of
relying on our own GC callback(s).

This fixes the following race condition:

1. Addon code allocates pointer P via `malloc`.
2. P is passed into `napi_create_external_buffer` with a finalization
   callback which calls `free(P)`. P is inserted into V8’s global array
   buffer table for tracking.
3. The finalization callback is executed on GC. P is freed and returned
   to the allocator. P is not yet removed from V8’s global array
   buffer table. (!)
4. Addon code attempts to allocate memory once again. The allocator
   returns P, as it is now available.
5. P is passed into `napi_create_external_buffer`. P still has not been
   removed from the v8 global array buffer table.
6. The world ends with `Check failed: result.second`.

Since our API contract is to call the finalizer on the JS thread on
which the `ArrayBuffer` was created, but V8 may call the `BackingStore`
deleter callback on another thread, fixing this requires posting
a task back to the JS thread.

Refs: nodejs#32463 (comment)
Fixes: nodejs#32463
@addaleax addaleax added buffer Issues and PRs related to the buffer subsystem. blocked PRs that are blocked by other issues or PRs. addons Issues and PRs related to native addons. node-api Issues and PRs related to the Node-API. dont-land-on-v10.x labels May 9, 2020
@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. labels May 9, 2020
@nodejs-github-bot
Copy link
Collaborator

@addaleax
Copy link
Member Author

Updated the test because timing for setImmediate() vs uv_async_t is different on Windows vs POSIX (I think we've run into this before).

@nodejs-github-bot
Copy link
Collaborator

nodejs-github-bot commented May 10, 2020

@davedoesdev
Copy link
Contributor

This fixes my use case (mmap/munmap), thanks.

@addaleax
Copy link
Member Author

Landed in c1ee70e

addaleax added a commit that referenced this pull request May 16, 2020
Release `Buffer` and `ArrayBuffer` instances that were created through
our addon APIs and have finalizers attached to them only after V8 has
called the deleter callback passed to the `BackingStore`, instead of
relying on our own GC callback(s).

This fixes the following race condition:

1. Addon code allocates pointer P via `malloc`.
2. P is passed into `napi_create_external_buffer` with a finalization
   callback which calls `free(P)`. P is inserted into V8’s global array
   buffer table for tracking.
3. The finalization callback is executed on GC. P is freed and returned
   to the allocator. P is not yet removed from V8’s global array
   buffer table. (!)
4. Addon code attempts to allocate memory once again. The allocator
   returns P, as it is now available.
5. P is passed into `napi_create_external_buffer`. P still has not been
   removed from the v8 global array buffer table.
6. The world ends with `Check failed: result.second`.

Since our API contract is to call the finalizer on the JS thread on
which the `ArrayBuffer` was created, but V8 may call the `BackingStore`
deleter callback on another thread, fixing this requires posting
a task back to the JS thread.

Refs: #32463 (comment)
Fixes: #32463

PR-URL: #33321
Reviewed-By: James M Snell <jasnell@gmail.com>
@addaleax addaleax closed this May 16, 2020
@addaleax addaleax deleted the buffer-callback-after-backingstore-free branch May 16, 2020 10:17
codebytere pushed a commit that referenced this pull request May 16, 2020
Release `Buffer` and `ArrayBuffer` instances that were created through
our addon APIs and have finalizers attached to them only after V8 has
called the deleter callback passed to the `BackingStore`, instead of
relying on our own GC callback(s).

This fixes the following race condition:

1. Addon code allocates pointer P via `malloc`.
2. P is passed into `napi_create_external_buffer` with a finalization
   callback which calls `free(P)`. P is inserted into V8’s global array
   buffer table for tracking.
3. The finalization callback is executed on GC. P is freed and returned
   to the allocator. P is not yet removed from V8’s global array
   buffer table. (!)
4. Addon code attempts to allocate memory once again. The allocator
   returns P, as it is now available.
5. P is passed into `napi_create_external_buffer`. P still has not been
   removed from the v8 global array buffer table.
6. The world ends with `Check failed: result.second`.

Since our API contract is to call the finalizer on the JS thread on
which the `ArrayBuffer` was created, but V8 may call the `BackingStore`
deleter callback on another thread, fixing this requires posting
a task back to the JS thread.

Refs: #32463 (comment)
Fixes: #32463

PR-URL: #33321
Reviewed-By: James M Snell <jasnell@gmail.com>
addaleax added a commit to node-ffi-napi/ref-napi that referenced this pull request May 17, 2020
@codebytere codebytere mentioned this pull request May 18, 2020
@mafintosh
Copy link
Member

mafintosh commented May 19, 2020

@addaleax I keep getting Check failed: result.second still with this commit, in our fuse-native module when making external buffers from n-api (node 12 is fine).

Any ideas? Get the crash wheather I using finalisers or not.

(Update: Misread, I'm not getting the Check failed on create_external_buffer anymore but on get_buffer_info)

@addaleax
Copy link
Member Author

addaleax commented May 19, 2020

@mafintosh I have a hard time following the fuse-native code, but as far as I can tell, it does things that seem fishy here.

If you use napi_create_external_buffer() a second time on the same pointer without waiting for the buffer created from the first time to be released, you’ll get a crash, that behavior is not changed by this PR, and it seems to me like that’s something that happens in the fuse-native code. I don’t like the changed behavior too much either, but that’s kind of what has been handed down from V8 to us.

@mafintosh
Copy link
Member

@addaleax its a bit of macros on macros I know 😅. That’s a good pointer! I’ll investigate that. The actual crash is coming from the get buffer info call know. Do you know on the top of your head, if that would crash if i made two external buffers from the same pointer?

@addaleax
Copy link
Member Author

@mafintosh I don’t, no, sorry – I don’t quite know why it would fail at that point

@mafintosh
Copy link
Member

Ok, I’ll try to poke around it a bit and avoid the dual external buffer on the same pointer and see if it works

@lovell
Copy link
Contributor

lovell commented May 20, 2020

@addaleax Danke!

@srguiwiz
Copy link

@addaleax I hope the reproducible failure I have filed with package ext2fs issue 76 can help find an even better correction of the underlying problem.

@addaleax
Copy link
Member Author

@srguiwiz The problem can’t really be corrected in Node.js (without non-trivial overhead), so this is something to be addressed in the addons themselves, unfortunately.

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. c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. node-api Issues and PRs related to the Node-API.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Check failed: result.second
7 participants