Title: napi_threadsafe_function is very hard to use safely · Issue #55706 · nodejs/node · GitHub
Open Graph Title: napi_threadsafe_function is very hard to use safely · Issue #55706 · nodejs/node
X Title: napi_threadsafe_function is very hard to use safely · Issue #55706 · nodejs/node
Description: Version v22.10.0 Platform Linux s7 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux Subsystem node-api What steps will reproduce the bug? The handle obtained with napi_create_threadsafe_functio...
Open Graph Description: Version v22.10.0 Platform Linux s7 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux Subsystem node-api What steps will reproduce the bug? The handle obta...
X Description: Version v22.10.0 Platform Linux s7 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux Subsystem node-api What steps will reproduce the bug? The handle obta...
Opengraph URL: https://github.com/nodejs/node/issues/55706
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"napi_threadsafe_function is very hard to use safely","articleBody":"### Version\n\nv22.10.0\n\n### Platform\n\n```text\nLinux s7 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux\n```\n\n\n### Subsystem\n\nnode-api\n\n### What steps will reproduce the bug?\n\nThe handle obtained with `napi_create_threadsafe_function` is supposed to be usable by arbitrary threads.\r\nIn particular, the following operations should be safe to call from arbitrary threads (until that thread calls `napi_release_threadsafe_function` or receives `napi_closing` from `napi_call_threadsafe_function`):\r\n`napi_get_threadsafe_function_context`, `napi_call_threadsafe_function`, `napi_acquire_threadsafe_function`, and `napi_release_threadsafe_function`.\r\n\r\nHowever, this is currently not the case.\r\nIn particular, in the face of Node.js environment shutdown there are data races and use-after-frees that occur when threads call any of these functions during or after the cleanup that happens when the Node.js environment shuts down.\r\n\r\nThere are two main issues I already found:\r\n- During finalization, the queue is accessed without holding its mutex [here](https://github.com/nodejs/node/blob/b38e3124862f7623fd8e8f0a9b738065d9296a74/src/node_api.cc#L300)\r\n - If another thread calls `napi_call_threadsafe_function` concurrently, this leads to a data race on the queue internals [here](https://github.com/nodejs/node/blob/b38e3124862f7623fd8e8f0a9b738065d9296a74/src/node_api.cc#L243)\r\n- At the end of finalization, the whole internal state is just deleted [here](https://github.com/nodejs/node/blob/4f5db8b26d906f1cbe9f6a9ac2028b0f7ad88c91/src/node_api.cc#L303) even if there are still threads holding handles to the tsnf.\r\n - Afterwards each use of any of the above-mentioned functions is a use-after-free bug\r\n\r\nSee https://github.com/mika-fischer/node-bug-napi-tsfn for a detailed reproduction of the issue on Linux using valgrind\r\n\r\nWhile working around this is technically possible (see [src/run/fixed.hpp](https://github.com/mika-fischer/node-bug-napi-tsfn/blob/main/src/run_fixed.hpp)) it involves attaching a finalizer in order to track the finalization state in an external flag, whose lifetime must be managed via a shared_ptr or similar and all access to the TSFN must be protected by another mutex (or read-write-lock). This makes the whole thing very unergonomic to use and I'm pretty sure nobody will jump through these hoops.\r\n\r\nIt should also be relatively easy to fix and not break API or ABI\n\n### How often does it reproduce? Is there a required condition?\n\nAlways\n\n### What is the expected behavior? Why is that the expected behavior?\n\n- Finalization should lock the mutex to make concurrent calls safe.\r\n- Finalization should put the TSFN into a state where all its resources are released, but *only* delete the actual TSFN object if there are no more handles to it. Otherwise the actual deletion should be deferred until one of `napi_release_threadsafe_function` or `napi_call_threadsafe_function` decreases the thread_count to zero.\r\n- In this finalized-but-not-yet deleted state, the operations mentioned above should work as follows:\r\n - `napi_get_threadsafe_function_context` should return the stored context pointer as usual\r\n - `napi_call_threadsafe_function` should return `napi_closing`, decrease the thread_count and if it falls to zero delete the TSFN\r\n - `napi_acquire_threadsafe_function` should return `napi_closing`\r\n - `napi_release_threadsafe_function` should return `napi_ok`, decrease the thread_count and if it falls to zero delete the TSFN\n\n### What do you see instead?\n\ndata races \u0026 use-after-frees leading to crashes\n\n### Additional information\n\n_No response_","author":{"url":"https://github.com/mika-fischer","@type":"Person","name":"mika-fischer"},"datePublished":"2024-11-03T17:22:10.000Z","interactionStatistic":{"@type":"InteractionCounter","interactionType":"https://schema.org/CommentAction","userInteractionCount":6},"url":"https://github.com/55706/node/issues/55706"}
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:a354f038-5469-f203-c8a0-596b4c4ae1e5 |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | AB0E:3555CE:9479A:C79C2:6A4D2E47 |
| html-safe-nonce | 8075f39700a65edcaedc98d3583c15fb73a83912e734aeece92fc44a69f55dae |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBQjBFOjM1NTVDRTo5NDc5QTpDNzlDMjo2QTREMkU0NyIsInZpc2l0b3JfaWQiOiIyMTg0OTUyNzg4NzI2MDQyMzEiLCJyZWdpb25fZWRnZSI6ImlhZCIsInJlZ2lvbl9yZW5kZXIiOiJpYWQifQ== |
| visitor-hmac | d5ee1a532f74c0ac0ea297eaea6dfce97026e485d1c0d987688d802029fae6fc |
| hovercard-subject-tag | issue:2631341857 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/55706/issue_layout |
| twitter:image | https://opengraph.githubassets.com/87a8a55b2648c416d3cb37f5203e133ec79855241d7448a0db3e9bfce70cde51/nodejs/node/issues/55706 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/87a8a55b2648c416d3cb37f5203e133ec79855241d7448a0db3e9bfce70cde51/nodejs/node/issues/55706 |
| og:image:alt | Version v22.10.0 Platform Linux s7 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux Subsystem node-api What steps will reproduce the bug? The handle obta... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | mika-fischer |
| hostname | github.com |
| expected-hostname | github.com |
| None | 92571a8944142227b7e19cd10918b1ddd06e5066c1ad5bc7e4769cf6140a87e6 |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 93f17a978ee60bc4668e1d7b90e6bd2d622261fd |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
Links:
Viewport: width=device-width