Files
node/test/cctest/test_linked_binding.cc
Shelley Vohr 25e6c156a6 src: let embedders exempt linked bindings from the addon permission
process._linkedBinding() is subject to the permission model's addon
scope since the check was added to GetLinkedBinding(). For an embedder
that implements part of its runtime as linked bindings this means its
own bootstrap cannot reach them under --permission unless the user
also passes --allow-addons, which allows loading addons from the file
system as well.

Add EnvironmentFlags::kNoAddonPermissionForLinkedBindings. When set,
GetLinkedBinding() skips the addon permission check for that
Environment and the worker threads it creates; process.dlopen() stays
gated and the default behavior is unchanged.

Refs: https://github.com/nodejs/node/pull/65432
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66067
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
2026-09-20 12:58:12 +00:00

422 lines
15 KiB
C++

// Include node.h first to define NAPI_VERSION built with the
// binary.
// The node.h should also be included first in embedder's use case.
#include "node.h"
#include "node_api.h"
#include "node_test_fixture.h"
#include <string>
void InitializeBinding(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
void* priv) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
exports
->Set(context,
v8::String::NewFromOneByte(isolate,
reinterpret_cast<const uint8_t*>("key"))
.ToLocalChecked(),
v8::String::NewFromOneByte(
isolate, reinterpret_cast<const uint8_t*>("value"))
.ToLocalChecked())
.FromJust();
}
NODE_MODULE_LINKED(cctest_linkedbinding, InitializeBinding)
class LinkedBindingTest : public EnvironmentTestFixture {};
TEST_F(LinkedBindingTest, SimpleTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script = "process._linkedBinding('cctest_linkedbinding').key";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate_, completion_value);
CHECK_NOT_NULL(*utf8val);
CHECK_EQ(strcmp(*utf8val, "value"), 0);
}
static std::string RunScript(v8::Isolate* isolate, const char* source) {
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Script> script =
v8::Script::Compile(context,
v8::String::NewFromOneByte(
isolate, reinterpret_cast<const uint8_t*>(source))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate, completion_value);
CHECK_NOT_NULL(*utf8val);
return *utf8val;
}
TEST_F(LinkedBindingTest, PermissionModelDeniesLinkedBindingTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
isolate_data_->options()->per_env->permission = true;
Env test_env{handle_scope, argv};
const char* run_script =
"try { process._linkedBinding('cctest_linkedbinding').key; }"
" catch (err) { err.code; }";
CHECK_EQ(RunScript(isolate_, run_script), "ERR_ACCESS_DENIED");
}
TEST_F(LinkedBindingTest, NoAddonPermissionForLinkedBindingsTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
isolate_data_->options()->per_env->permission = true;
Env test_env{
handle_scope,
argv,
static_cast<node::EnvironmentFlags::Flags>(
node::EnvironmentFlags::kDefaultFlags |
node::EnvironmentFlags::kNoAddonPermissionForLinkedBindings)};
const char* linked_script =
"process._linkedBinding('cctest_linkedbinding').key";
CHECK_EQ(RunScript(isolate_, linked_script), "value");
const char* dlopen_script =
"try { process.dlopen({ exports: {} }, 'addon.node'); }"
" catch (err) { err.code; }";
CHECK_EQ(RunScript(isolate_, dlopen_script), "ERR_DLOPEN_DISABLED");
}
void InitializeLocalBinding(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context,
void* priv) {
++*static_cast<int*>(priv);
v8::Isolate* isolate = v8::Isolate::GetCurrent();
exports
->Set(context,
v8::String::NewFromOneByte(isolate,
reinterpret_cast<const uint8_t*>("key"))
.ToLocalChecked(),
v8::String::NewFromOneByte(
isolate, reinterpret_cast<const uint8_t*>("value"))
.ToLocalChecked())
.FromJust();
}
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
int calls = 0;
AddLinkedBinding(*test_env, "local_linked", InitializeLocalBinding, &calls);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script = "process._linkedBinding('local_linked').key";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate_, completion_value);
CHECK_NOT_NULL(*utf8val);
CHECK_EQ(strcmp(*utf8val, "value"), 0);
CHECK_EQ(calls, 1);
}
napi_value InitializeLocalNapiBinding(napi_env env, napi_value exports) {
napi_value key, value;
CHECK_EQ(napi_create_string_utf8(env, "hello", NAPI_AUTO_LENGTH, &key),
napi_ok);
CHECK_EQ(napi_create_string_utf8(env, "world", NAPI_AUTO_LENGTH, &value),
napi_ok);
CHECK_EQ(napi_set_property(env, exports, key, value), napi_ok);
return nullptr;
}
static napi_module local_linked_napi = {
NAPI_MODULE_VERSION,
node::ModuleFlags::kLinked,
nullptr,
InitializeLocalNapiBinding,
"local_linked_napi",
nullptr,
{0},
};
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env, local_linked_napi);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script = "process._linkedBinding('local_linked_napi').hello";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate_, completion_value);
CHECK_NOT_NULL(*utf8val);
CHECK_EQ(strcmp(*utf8val, "world"), 0);
}
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiCallbackTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env,
"local_linked_napi_cb",
InitializeLocalNapiBinding,
NAPI_VERSION);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script =
"process._linkedBinding('local_linked_napi_cb').hello";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate_, completion_value);
CHECK_NOT_NULL(*utf8val);
CHECK_EQ(strcmp(*utf8val, "world"), 0);
}
static int32_t node_api_version = NODE_API_DEFAULT_MODULE_API_VERSION;
napi_value InitializeLocalNapiRefBinding(napi_env env, napi_value exports) {
napi_value key, value;
CHECK_EQ(
napi_create_string_utf8(env, "napi_ref_created", NAPI_AUTO_LENGTH, &key),
napi_ok);
// In experimental Node-API version we can create napi_ref to any value type.
// Here we are trying to create a reference to the key string.
napi_ref ref{};
if (node_api_version == NAPI_VERSION_EXPERIMENTAL) {
CHECK_EQ(napi_create_reference(env, key, 1, &ref), napi_ok);
CHECK_EQ(napi_delete_reference(env, ref), napi_ok);
} else {
CHECK_EQ(napi_create_reference(env, key, 1, &ref), napi_invalid_arg);
}
CHECK_EQ(napi_get_boolean(env, ref != nullptr, &value), napi_ok);
CHECK_EQ(napi_set_property(env, exports, key, value), napi_ok);
return nullptr;
}
// napi_ref in Node-API version 8 cannot accept strings.
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiRefVersion8Test) {
node_api_version = NODE_API_DEFAULT_MODULE_API_VERSION;
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env,
"local_linked_napi_ref_v8",
InitializeLocalNapiRefBinding,
node_api_version);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script =
"process._linkedBinding('local_linked_napi_ref_v8').napi_ref_created";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
CHECK(completion_value->IsBoolean());
CHECK(!completion_value.As<v8::Boolean>()->Value());
}
// Experimental version of napi_ref in Node-API can accept strings.
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiRefExperimentalTest) {
node_api_version = NAPI_VERSION_EXPERIMENTAL;
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env,
"local_linked_napi_ref_experimental",
InitializeLocalNapiRefBinding,
node_api_version);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script = "process._linkedBinding('local_linked_napi_ref_"
"experimental').napi_ref_created";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
CHECK(completion_value->IsBoolean());
CHECK(completion_value.As<v8::Boolean>()->Value());
}
napi_value NapiLinkedWithInstanceData(napi_env env, napi_value exports) {
int* instance_data = new int(0);
CHECK_EQ(napi_set_instance_data(
env,
instance_data,
[](napi_env env, void* data, void* hint) {
++*static_cast<int*>(data);
},
nullptr),
napi_ok);
napi_value key, value;
CHECK_EQ(napi_create_string_utf8(env, "hello", NAPI_AUTO_LENGTH, &key),
napi_ok);
CHECK_EQ(napi_create_bigint_uint64(
env, reinterpret_cast<uint64_t>(instance_data), &value),
napi_ok);
CHECK_EQ(napi_set_property(env, exports, key, value), napi_ok);
return nullptr;
}
static napi_module local_linked_napi_id = {
NAPI_MODULE_VERSION,
node::ModuleFlags::kLinked,
nullptr,
NapiLinkedWithInstanceData,
"local_linked_napi_id",
nullptr,
{0},
};
TEST_F(LinkedBindingTest, LocallyDefinedLinkedBindingNapiInstanceDataTest) {
const v8::HandleScope handle_scope(isolate_);
int* instance_data = nullptr;
{
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env, local_linked_napi_id);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script =
"process._linkedBinding('local_linked_napi_id').hello";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value =
script->Run(context).ToLocalChecked();
CHECK(completion_value->IsBigInt());
instance_data = reinterpret_cast<int*>(
completion_value.As<v8::BigInt>()->Uint64Value());
CHECK_NE(instance_data, nullptr);
CHECK_EQ(*instance_data, 0);
}
CHECK_EQ(*instance_data, 1);
delete instance_data;
}
TEST_F(LinkedBindingTest,
LocallyDefinedLinkedBindingNapiCallbackInstanceDataTest) {
const v8::HandleScope handle_scope(isolate_);
int* instance_data = nullptr;
{
const Argv argv;
Env test_env{handle_scope, argv};
AddLinkedBinding(*test_env,
"local_linked_napi_id_cb",
NapiLinkedWithInstanceData,
NAPI_VERSION);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script =
"process._linkedBinding('local_linked_napi_id_cb').hello";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value =
script->Run(context).ToLocalChecked();
CHECK(completion_value->IsBigInt());
instance_data = reinterpret_cast<int*>(
completion_value.As<v8::BigInt>()->Uint64Value());
CHECK_NE(instance_data, nullptr);
CHECK_EQ(*instance_data, 0);
}
CHECK_EQ(*instance_data, 1);
delete instance_data;
}
TEST_F(LinkedBindingTest, ManyBindingsTest) {
const v8::HandleScope handle_scope(isolate_);
const Argv argv;
Env test_env{handle_scope, argv};
int calls = 0;
AddLinkedBinding(*test_env, "local_linked1", InitializeLocalBinding, &calls);
AddLinkedBinding(*test_env, "local_linked2", InitializeLocalBinding, &calls);
AddLinkedBinding(*test_env, "local_linked3", InitializeLocalBinding, &calls);
AddLinkedBinding(*test_env,
local_linked_napi); // Add a Node-API addon as well.
AddLinkedBinding(*test_env, "local_linked4", InitializeLocalBinding, &calls);
AddLinkedBinding(*test_env, "local_linked5", InitializeLocalBinding, &calls);
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
const char* run_script =
"for (let i = 1; i <= 5; i++)process._linkedBinding(`local_linked${i}`);"
"process._linkedBinding('local_linked_napi').hello";
v8::Local<v8::Script> script =
v8::Script::Compile(
context,
v8::String::NewFromOneByte(
isolate_, reinterpret_cast<const uint8_t*>(run_script))
.ToLocalChecked())
.ToLocalChecked();
v8::Local<v8::Value> completion_value = script->Run(context).ToLocalChecked();
v8::String::Utf8Value utf8val(isolate_, completion_value);
CHECK_NOT_NULL(*utf8val);
CHECK_EQ(strcmp(*utf8val, "world"), 0);
CHECK_EQ(calls, 5);
}