Commit 25e6c156a6a for nodejs
commit 25e6c156a6a9cbb34c3a993ccc3d4b97562c3233
Author: Shelley Vohr <shelley.vohr@gmail.com>
Date: Sun Sep 20 14:58:12 2026 +0200
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>
diff --git a/src/env-inl.h b/src/env-inl.h
index a194751e8b3..1423767e398 100644
--- a/src/env-inl.h
+++ b/src/env-inl.h
@@ -697,6 +697,10 @@ inline bool Environment::no_browser_globals() const {
#endif
}
+inline bool Environment::no_addon_permission_for_linked_bindings() const {
+ return flags_ & EnvironmentFlags::kNoAddonPermissionForLinkedBindings;
+}
+
void Environment::set_source_maps_enabled(bool on) {
source_maps_enabled_ = on;
}
diff --git a/src/env.h b/src/env.h
index cb701b197df..751e3acec59 100644
--- a/src/env.h
+++ b/src/env.h
@@ -916,6 +916,7 @@ class Environment final : public MemoryRetainer {
inline bool no_global_search_paths() const;
inline bool should_start_debug_signal_handler() const;
inline bool no_browser_globals() const;
+ inline bool no_addon_permission_for_linked_bindings() const;
inline uint64_t thread_id() const;
inline std::string_view thread_name() const;
inline worker::Worker* worker_context() const;
diff --git a/src/node.h b/src/node.h
index ab5a542270c..ca81c4818b4 100644
--- a/src/node.h
+++ b/src/node.h
@@ -651,7 +651,12 @@ enum Flags : uint64_t {
// Controls whether the InspectorAgent created for this Environment waits for
// Inspector frontend events during the Environment creation. It's used to
// call node::Stop(env) on a Worker thread that is waiting for the events.
- kNoWaitForInspectorFrontend = 1 << 11
+ kNoWaitForInspectorFrontend = 1 << 11,
+ // Set this flag to exempt process._linkedBinding() from the permission
+ // model's addon scope (--allow-addons): linked bindings are compiled into
+ // the executable by the embedder, unlike addons loaded from the file system
+ // through process.dlopen(), which stays gated. Inherited by worker threads.
+ kNoAddonPermissionForLinkedBindings = 1 << 12
};
} // namespace EnvironmentFlags
diff --git a/src/node_binding.cc b/src/node_binding.cc
index b93e4200f29..568325e8496 100644
--- a/src/node_binding.cc
+++ b/src/node_binding.cc
@@ -1024,8 +1024,10 @@ void GetLinkedBinding(const FunctionCallbackInfo<Value>& args) {
node::Utf8Value module_name_v(env->isolate(), module_name);
const char* name = *module_name_v;
- THROW_IF_INSUFFICIENT_PERMISSIONS(
- env, permission::PermissionScope::kAddon, module_name_v.ToStringView());
+ if (!env->no_addon_permission_for_linked_bindings()) {
+ THROW_IF_INSUFFICIENT_PERMISSIONS(
+ env, permission::PermissionScope::kAddon, module_name_v.ToStringView());
+ }
node_module* mod = nullptr;
diff --git a/src/node_worker.cc b/src/node_worker.cc
index 62f1d703c9f..a90495409d1 100644
--- a/src/node_worker.cc
+++ b/src/node_worker.cc
@@ -748,6 +748,10 @@ void Worker::New(const FunctionCallbackInfo<Value>& args) {
worker->environment_flags_ |= EnvironmentFlags::kNoGlobalSearchPaths;
if (env->no_browser_globals())
worker->environment_flags_ |= EnvironmentFlags::kNoBrowserGlobals;
+ if (env->no_addon_permission_for_linked_bindings()) {
+ worker->environment_flags_ |=
+ EnvironmentFlags::kNoAddonPermissionForLinkedBindings;
+ }
}
void Worker::StartThread(const FunctionCallbackInfo<Value>& args) {
diff --git a/test/cctest/test_linked_binding.cc b/test/cctest/test_linked_binding.cc
index be097abd90b..d36b5874e74 100644
--- a/test/cctest/test_linked_binding.cc
+++ b/test/cctest/test_linked_binding.cc
@@ -5,6 +5,8 @@
#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,
@@ -46,6 +48,52 @@ TEST_F(LinkedBindingTest, SimpleTest) {
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,