From 39063b3c00c7e319ae9f7f5dc944efc41d39873e Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 5 Jun 2019 23:29:18 +0200 Subject: [PATCH] n-api: defer Buffer finalizer with SetImmediate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We have a test that verifies that JS execution from the Buffer finalizer is accepted, and that errors thrown are passed down synchronously. However, since the finalizer executes during GC, this is behaviour is fundamentally invalid and, for good reasons, disallowed by the JS engine. This leaves us with the options of either finding a way to allow JS execution from the callback, or explicitly forbidding it on the N-API side as well. This commit implements the former option, since it is the more backwards-compatible one, in the sense that the current situation sometimes appears to work as well and we should not break that behaviour if we don’t have to, but rather try to actually make it work reliably. Since GC timing is largely unobservable anyway, this commit moves the callback into a `SetImmediate()`, as we do elsewhere in the code, and a second pass callback is not an easily implemented option, as the API is supposed to wrap around Node’s `Buffer` API. In this case, exceptions are handled like other uncaught exceptions. Two tests have to be adjusted to account for the timing difference. This is unfortunate, but unavoidable if we want to conform to the JS engine API contract and keep all tests. Fixes: https://github.com/nodejs/node/issues/26754 PR-URL: https://github.com/nodejs/node/pull/28082 Reviewed-By: Michael Dawson Reviewed-By: Gabriel Schulhof Reviewed-By: James M Snell --- src/node_api.cc | 29 +++++++++++++++--------- test/node-api/test_buffer/test.js | 33 +++++++++++++++++----------- test/node-api/test_exception/test.js | 5 ++++- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/node_api.cc b/src/node_api.cc index 91e6a14cdc1570..c4d86342868e7f 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -32,21 +32,30 @@ namespace v8impl { namespace { -class BufferFinalizer: private Finalizer { +class BufferFinalizer : private Finalizer { public: // node::Buffer::FreeCallback static void FinalizeBufferCallback(char* data, void* hint) { BufferFinalizer* finalizer = static_cast(hint); - if (finalizer->_finalize_callback != nullptr) { - NapiCallIntoModuleThrow(finalizer->_env, [&]() { - finalizer->_finalize_callback( - finalizer->_env, - data, - finalizer->_finalize_hint); - }); - } + finalizer->_finalize_data = data; + static_cast(finalizer->_env)->node_env() + ->SetImmediate([](node::Environment* env, void* hint) { + BufferFinalizer* finalizer = static_cast(hint); + + if (finalizer->_finalize_callback != nullptr) { + v8::HandleScope handle_scope(finalizer->_env->isolate); + v8::Context::Scope context_scope(finalizer->_env->context()); + + NapiCallIntoModuleThrow(finalizer->_env, [&]() { + finalizer->_finalize_callback( + finalizer->_env, + finalizer->_finalize_data, + finalizer->_finalize_hint); + }); + } - Delete(finalizer); + Delete(finalizer); + }, hint); } }; diff --git a/test/node-api/test_buffer/test.js b/test/node-api/test_buffer/test.js index 740b0474a79c60..6b6c2089afa76e 100644 --- a/test/node-api/test_buffer/test.js +++ b/test/node-api/test_buffer/test.js @@ -4,18 +4,25 @@ const common = require('../../common'); const binding = require(`./build/${common.buildType}/test_buffer`); const assert = require('assert'); +const setImmediatePromise = require('util').promisify(setImmediate); -assert.strictEqual(binding.newBuffer().toString(), binding.theText); -assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText); -console.log('gc1'); -global.gc(); -assert.strictEqual(binding.getDeleterCallCount(), 1); -assert.strictEqual(binding.copyBuffer().toString(), binding.theText); +(async function() { + assert.strictEqual(binding.newBuffer().toString(), binding.theText); + assert.strictEqual(binding.newExternalBuffer().toString(), binding.theText); + console.log('gc1'); + global.gc(); + assert.strictEqual(binding.getDeleterCallCount(), 0); + await setImmediatePromise(); + assert.strictEqual(binding.getDeleterCallCount(), 1); + assert.strictEqual(binding.copyBuffer().toString(), binding.theText); -let buffer = binding.staticBuffer(); -assert.strictEqual(binding.bufferHasInstance(buffer), true); -assert.strictEqual(binding.bufferInfo(buffer), true); -buffer = null; -global.gc(); -console.log('gc2'); -assert.strictEqual(binding.getDeleterCallCount(), 2); + let buffer = binding.staticBuffer(); + assert.strictEqual(binding.bufferHasInstance(buffer), true); + assert.strictEqual(binding.bufferInfo(buffer), true); + buffer = null; + global.gc(); + assert.strictEqual(binding.getDeleterCallCount(), 1); + await setImmediatePromise(); + console.log('gc2'); + assert.strictEqual(binding.getDeleterCallCount(), 2); +})().then(common.mustCall()); diff --git a/test/node-api/test_exception/test.js b/test/node-api/test_exception/test.js index d5d675ab7e2066..1373d8c06fb747 100644 --- a/test/node-api/test_exception/test.js +++ b/test/node-api/test_exception/test.js @@ -9,7 +9,10 @@ const test_exception = require(`./build/${common.buildType}/test_exception`); function testFinalize(binding) { let x = test_exception[binding](); x = null; - assert.throws(() => { global.gc(); }, /Error during Finalize/); + global.gc(); + process.on('uncaughtException', (err) => { + assert.strictEqual(err.message, 'Error during Finalize'); + }); // To assuage the linter's concerns. (function() {})(x);