From b48745651465badd2939fabbd5327405820106c8 Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Fri, 13 Feb 2026 18:11:39 -0300 Subject: [PATCH 1/6] src: add C++ support for diagnostics channels Add a C++ API for diagnostics channels that allows native code to check for subscribers and publish messages without unnecessary JS boundary crossings. Uses a shared AliasedUint32Array buffer between C++ and JS to track subscriber counts per channel, enabling a fast inline check (HasSubscribers) that reads the buffer directly. --- lib/diagnostics_channel.js | 19 +- node.gyp | 2 + src/base_object_types.h | 1 + src/node_binding.cc | 1 + src/node_binding.h | 1 + src/node_diagnostics_channel.cc | 176 ++++++++++++++++ src/node_diagnostics_channel.h | 90 +++++++++ src/node_external_reference.h | 1 + src/node_snapshotable.cc | 1 + test/cctest/test_diagnostics_channel.cc | 255 ++++++++++++++++++++++++ 10 files changed, 546 insertions(+), 1 deletion(-) create mode 100644 src/node_diagnostics_channel.cc create mode 100644 src/node_diagnostics_channel.h create mode 100644 test/cctest/test_diagnostics_channel.cc diff --git a/lib/diagnostics_channel.js b/lib/diagnostics_channel.js index 3deb301e7f3cd2..4d983390883b82 100644 --- a/lib/diagnostics_channel.js +++ b/lib/diagnostics_channel.js @@ -31,6 +31,9 @@ const { const { triggerUncaughtException } = internalBinding('errors'); +const dc_binding = internalBinding('diagnostics_channel'); +const { subscribers: subscriberCounts } = dc_binding; + const { WeakReference } = require('internal/util'); // Can't delete when weakref count reaches 0 as it could increment again. @@ -108,6 +111,7 @@ class ActiveChannel { this._subscribers = ArrayPrototypeSlice(this._subscribers); ArrayPrototypePush(this._subscribers, subscription); channels.incRef(this.name); + if (this._index !== undefined) subscriberCounts[this._index]++; } unsubscribe(subscription) { @@ -120,6 +124,7 @@ class ActiveChannel { ArrayPrototypePushApply(this._subscribers, after); channels.decRef(this.name); + if (this._index !== undefined) subscriberCounts[this._index]--; maybeMarkInactive(this); return true; @@ -127,7 +132,10 @@ class ActiveChannel { bindStore(store, transform) { const replacing = this._stores.has(store); - if (!replacing) channels.incRef(this.name); + if (!replacing) { + channels.incRef(this.name); + if (this._index !== undefined) subscriberCounts[this._index]++; + } this._stores.set(store, transform); } @@ -139,6 +147,7 @@ class ActiveChannel { this._stores.delete(store); channels.decRef(this.name); + if (this._index !== undefined) subscriberCounts[this._index]--; maybeMarkInactive(this); return true; @@ -183,6 +192,9 @@ class Channel { this._subscribers = undefined; this._stores = undefined; this.name = name; + if (typeof name === 'string') { + this._index = dc_binding.getOrCreateChannelIndex(name); + } channels.set(name, this); } @@ -434,6 +446,11 @@ function tracingChannel(nameOrChannels) { return new TracingChannel(nameOrChannels); } +dc_binding.setPublishCallback((name, message) => { + const ch = channels.get(name); + if (ch) ch.publish(message); +}); + module.exports = { channel, hasSubscribers, diff --git a/node.gyp b/node.gyp index 566377625bc712..b11b8e154c49d7 100644 --- a/node.gyp +++ b/node.gyp @@ -133,6 +133,7 @@ 'src/node_main_instance.cc', 'src/node_messaging.cc', 'src/node_metadata.cc', + 'src/node_diagnostics_channel.cc', 'src/node_modules.cc', 'src/node_options.cc', 'src/node_os.cc', @@ -270,6 +271,7 @@ 'src/node_messaging.h', 'src/node_metadata.h', 'src/node_mutex.h', + 'src/node_diagnostics_channel.h', 'src/node_modules.h', 'src/node_object_wrap.h', 'src/node_options.h', diff --git a/src/base_object_types.h b/src/base_object_types.h index 9cfe6a77f71708..cd1a06e41a3071 100644 --- a/src/base_object_types.h +++ b/src/base_object_types.h @@ -10,6 +10,7 @@ namespace node { // what the class passes to SET_BINDING_ID(), the second argument should match // the C++ class name. #define SERIALIZABLE_BINDING_TYPES(V) \ + V(diagnostics_channel_binding_data, diagnostics_channel::BindingData) \ V(encoding_binding_data, encoding_binding::BindingData) \ V(fs_binding_data, fs::BindingData) \ V(mksnapshot_binding_data, mksnapshot::BindingData) \ diff --git a/src/node_binding.cc b/src/node_binding.cc index 740706e917b7d2..b76ecc8cab47df 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -48,6 +48,7 @@ V(constants) \ V(contextify) \ V(credentials) \ + V(diagnostics_channel) \ V(encoding_binding) \ V(errors) \ V(fs) \ diff --git a/src/node_binding.h b/src/node_binding.h index dbb4c137afc776..83f8cee7c9f7b0 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -49,6 +49,7 @@ static_assert(static_cast(NM_F_LINKED) == V(blob) \ V(builtins) \ V(contextify) \ + V(diagnostics_channel) \ V(encoding_binding) \ V(fs) \ V(fs_dir) \ diff --git a/src/node_diagnostics_channel.cc b/src/node_diagnostics_channel.cc new file mode 100644 index 00000000000000..3b8ecce773646e --- /dev/null +++ b/src/node_diagnostics_channel.cc @@ -0,0 +1,176 @@ +#include "node_diagnostics_channel.h" + +#include "env-inl.h" +#include "node_external_reference.h" +#include "util-inl.h" +#include "v8.h" + +#include + +namespace node { +namespace diagnostics_channel { + +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::Object; +using v8::ObjectTemplate; +using v8::SnapshotCreator; +using v8::String; +using v8::Value; + +BindingData::BindingData(Realm* realm, + Local wrap, + InternalFieldInfo* info) + : SnapshotableObject(realm, wrap, type_int), + subscribers_(realm->isolate(), + kMaxChannels, + MAYBE_FIELD_PTR(info, subscribers)) { + if (info == nullptr) { + wrap->Set(realm->context(), + FIXED_ONE_BYTE_STRING(realm->isolate(), "subscribers"), + subscribers_.GetJSArray()) + .Check(); + } else { + subscribers_.Deserialize(realm->context()); + } + subscribers_.MakeWeak(); +} + +void BindingData::MemoryInfo(MemoryTracker* tracker) const { + tracker->TrackField("subscribers", subscribers_); +} + +uint32_t BindingData::GetOrCreateChannelIndex(const std::string& name) { + auto it = channel_indices_.find(name); + if (it != channel_indices_.end()) { + return it->second; + } + CHECK_LT(next_channel_index_, kMaxChannels); + uint32_t index = next_channel_index_++; + channel_indices_.emplace(name, index); + return index; +} + +void BindingData::GetOrCreateChannelIndex( + const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + BindingData* binding = realm->GetBindingData(); + CHECK_NOT_NULL(binding); + + CHECK(args[0]->IsString()); + Utf8Value name(realm->isolate(), args[0]); + + uint32_t index = binding->GetOrCreateChannelIndex(*name); + args.GetReturnValue().Set(index); +} + +void BindingData::SetPublishCallback( + const FunctionCallbackInfo& args) { + Realm* realm = Realm::GetCurrent(args); + BindingData* binding = realm->GetBindingData(); + CHECK_NOT_NULL(binding); + + CHECK(args[0]->IsFunction()); + binding->publish_callback_.Reset(realm->isolate(), + args[0].As()); +} + +bool BindingData::PrepareForSerialization(Local context, + SnapshotCreator* creator) { + DCHECK_NULL(internal_field_info_); + internal_field_info_ = InternalFieldInfoBase::New(type()); + internal_field_info_->subscribers = + subscribers_.Serialize(context, creator); + publish_callback_.Reset(); + return true; +} + +InternalFieldInfoBase* BindingData::Serialize(int index) { + DCHECK_IS_SNAPSHOT_SLOT(index); + InternalFieldInfo* info = internal_field_info_; + internal_field_info_ = nullptr; + return info; +} + +void BindingData::Deserialize(Local context, + Local holder, + int index, + InternalFieldInfoBase* info) { + DCHECK_IS_SNAPSHOT_SLOT(index); + HandleScope scope(Isolate::GetCurrent()); + Realm* realm = Realm::GetCurrent(context); + BindingData* binding = realm->AddBindingData( + holder, static_cast(info)); + CHECK_NOT_NULL(binding); +} + +void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, + Local target) { + Isolate* isolate = isolate_data->isolate(); + SetMethod( + isolate, target, "getOrCreateChannelIndex", GetOrCreateChannelIndex); + SetMethod(isolate, target, "setPublishCallback", SetPublishCallback); +} + +void BindingData::CreatePerContextProperties(Local target, + Local unused, + Local context, + void* priv) { + Realm* realm = Realm::GetCurrent(context); + BindingData* const binding = realm->AddBindingData(target); + if (binding == nullptr) return; +} + +void BindingData::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(GetOrCreateChannelIndex); + registry->Register(SetPublishCallback); +} + +Channel::Channel(BindingData* binding_data, uint32_t index, const char* name) + : binding_data_(binding_data), index_(index), name_(name) {} + +Channel Channel::Get(Environment* env, const char* name) { + Realm* realm = env->principal_realm(); + BindingData* binding = realm->GetBindingData(); + if (binding == nullptr) { + return Channel(nullptr, 0, name); + } + uint32_t index = binding->GetOrCreateChannelIndex(std::string(name)); + return Channel(binding, index, name); +} + +void Channel::Publish(Environment* env, Local message) const { + if (!HasSubscribers()) return; + + Isolate* isolate = env->isolate(); + HandleScope handle_scope(isolate); + Local context = env->context(); + Context::Scope context_scope(context); + + if (binding_data_->publish_callback_.IsEmpty()) return; + + Local callback = + binding_data_->publish_callback_.Get(isolate); + Local channel_name = + String::NewFromUtf8(isolate, name_).ToLocalChecked(); + + Local argv[] = {channel_name, message}; + USE(callback->Call(context, v8::Undefined(isolate), 2, argv)); +} + +} // namespace diagnostics_channel +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + diagnostics_channel, + node::diagnostics_channel::BindingData::CreatePerContextProperties) +NODE_BINDING_PER_ISOLATE_INIT( + diagnostics_channel, + node::diagnostics_channel::BindingData::CreatePerIsolateProperties) +NODE_BINDING_EXTERNAL_REFERENCE( + diagnostics_channel, + node::diagnostics_channel::BindingData::RegisterExternalReferences) diff --git a/src/node_diagnostics_channel.h b/src/node_diagnostics_channel.h new file mode 100644 index 00000000000000..53964ea92eea12 --- /dev/null +++ b/src/node_diagnostics_channel.h @@ -0,0 +1,90 @@ +#ifndef SRC_NODE_DIAGNOSTICS_CHANNEL_H_ +#define SRC_NODE_DIAGNOSTICS_CHANNEL_H_ + +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include +#include +#include +#include "aliased_buffer.h" +#include "node_snapshotable.h" + +namespace node { +class ExternalReferenceRegistry; + +namespace diagnostics_channel { + +class Channel; + +class BindingData : public SnapshotableObject { + public: + static constexpr size_t kMaxChannels = 1024; + + struct InternalFieldInfo : public node::InternalFieldInfoBase { + AliasedBufferIndex subscribers; + }; + + BindingData(Realm* realm, + v8::Local wrap, + InternalFieldInfo* info = nullptr); + + SERIALIZABLE_OBJECT_METHODS() + SET_BINDING_ID(diagnostics_channel_binding_data) + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_SELF_SIZE(BindingData) + SET_MEMORY_INFO_NAME(BindingData) + + AliasedUint32Array subscribers_; + + uint32_t next_channel_index_ = 0; + std::unordered_map channel_indices_; + + uint32_t GetOrCreateChannelIndex(const std::string& name); + + v8::Global publish_callback_; + + static void GetOrCreateChannelIndex( + const v8::FunctionCallbackInfo& args); + static void SetPublishCallback( + const v8::FunctionCallbackInfo& args); + + static void CreatePerIsolateProperties( + IsolateData* isolate_data, + v8::Local target); + static void CreatePerContextProperties( + v8::Local target, + v8::Local unused, + v8::Local context, + void* priv); + static void RegisterExternalReferences( + ExternalReferenceRegistry* registry); + + private: + InternalFieldInfo* internal_field_info_ = nullptr; +}; + +class Channel { + public: + static Channel Get(Environment* env, const char* name); + + inline bool HasSubscribers() const { + return binding_data_ != nullptr && binding_data_->subscribers_[index_] > 0; + } + + void Publish(Environment* env, v8::Local message) const; + + private: + Channel(BindingData* binding_data, uint32_t index, const char* name); + + BindingData* binding_data_; + uint32_t index_; + const char* name_; +}; + +} // namespace diagnostics_channel +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_DIAGNOSTICS_CHANNEL_H_ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index 835471d4f70846..5ce9ec0a8c207b 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -71,6 +71,7 @@ class ExternalReferenceRegistry { V(config) \ V(contextify) \ V(credentials) \ + V(diagnostics_channel) \ V(encoding_binding) \ V(env_var) \ V(errors) \ diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 5f39029563eecb..0c7a4cd35bae8f 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -14,6 +14,7 @@ #include "node_blob.h" #include "node_builtins.h" #include "node_contextify.h" +#include "node_diagnostics_channel.h" #include "node_errors.h" #include "node_external_reference.h" #include "node_file.h" diff --git a/test/cctest/test_diagnostics_channel.cc b/test/cctest/test_diagnostics_channel.cc new file mode 100644 index 00000000000000..f4592fad9318a3 --- /dev/null +++ b/test/cctest/test_diagnostics_channel.cc @@ -0,0 +1,255 @@ +#include "node_diagnostics_channel.h" + +#include "gtest/gtest.h" +#include "node_test_fixture.h" + +using node::diagnostics_channel::Channel; + +class DiagnosticsChannelTest : public EnvironmentTestFixture {}; + +static v8::Local RunJS(v8::Isolate* isolate, const char* code) { + v8::Local context = isolate->GetCurrentContext(); + v8::Local script = + v8::Script::Compile( + context, + v8::String::NewFromUtf8(isolate, code).ToLocalChecked()) + .ToLocalChecked(); + return script->Run(context).ToLocalChecked(); +} + +// Channel::HasSubscribers() returns false when there are no subscribers. +TEST_F(DiagnosticsChannelTest, HasSubscribersReturnsFalseWithoutSubscribers) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + // Load the environment to initialize bindings. + node::LoadEnvironment( + *env, + "require('diagnostics_channel');"); + + Channel ch = Channel::Get(*env, "test:cctest:no-subscribers"); + EXPECT_FALSE(ch.HasSubscribers()); +} + +// Channel::HasSubscribers() returns true after JS subscribes. +TEST_F(DiagnosticsChannelTest, HasSubscribersReturnsTrueAfterSubscribe) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + node::LoadEnvironment( + *env, + "const dc = require('diagnostics_channel');" + "dc.subscribe('test:cctest:with-sub', () => {});"); + + Channel ch = Channel::Get(*env, "test:cctest:with-sub"); + EXPECT_TRUE(ch.HasSubscribers()); +} + +// Channel::Get() with the same name returns consistent subscriber state. +TEST_F(DiagnosticsChannelTest, GetReturnsSameChannelState) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + node::LoadEnvironment( + *env, + "const dc = require('diagnostics_channel');" + "dc.subscribe('test:cctest:same-channel', () => {});"); + + Channel ch1 = Channel::Get(*env, "test:cctest:same-channel"); + Channel ch2 = Channel::Get(*env, "test:cctest:same-channel"); + EXPECT_TRUE(ch1.HasSubscribers()); + EXPECT_TRUE(ch2.HasSubscribers()); +} + +// Channel::Publish() delivers messages to JS subscribers. +TEST_F(DiagnosticsChannelTest, PublishDeliversToJSSubscribers) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + node::LoadEnvironment( + *env, + "const dc = require('diagnostics_channel');" + "const assert = require('assert');" + "dc.subscribe('test:cctest:publish', (message, name) => {" + " assert.strictEqual(name, 'test:cctest:publish');" + " assert.strictEqual(message.value, 42);" + " globalThis.__publishReceived = true;" + "});"); + + v8::Local context = (*env)->context(); + + Channel ch = Channel::Get(*env, "test:cctest:publish"); + ASSERT_TRUE(ch.HasSubscribers()); + + v8::Local msg = v8::Object::New(isolate_); + msg->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "value"), + v8::Integer::New(isolate_, 42)).Check(); + + ch.Publish(*env, msg); + + v8::Local received = + context->Global() + ->Get(context, + v8::String::NewFromUtf8Literal(isolate_, "__publishReceived")) + .ToLocalChecked(); + EXPECT_TRUE(received->IsTrue()); +} + +// C++ creates a channel first, then JS subscribes to the same name. +// Verifies C++ Channel reflects the JS subscriber via the shared buffer. +TEST_F(DiagnosticsChannelTest, CppChannelVisibleFromJS) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + // Expose dc on globalThis so RunJS (v8::Script) can access it. + node::LoadEnvironment( + *env, + "globalThis.__dc = require('diagnostics_channel');"); + + Channel ch = Channel::Get(*env, "test:cctest:cpp-first"); + EXPECT_FALSE(ch.HasSubscribers()); + + // JS subscribes to the same channel name via globalThis.__dc. + RunJS(isolate_, + "globalThis.__dc.subscribe('test:cctest:cpp-first', () => {});"); + + EXPECT_TRUE(ch.HasSubscribers()); + + RunJS(isolate_, + "globalThis.__cppFirstMsg = null;" + "globalThis.__dc.subscribe('test:cctest:cpp-first', (msg) => {" + " globalThis.__cppFirstMsg = msg;" + "});"); + + v8::Local context = (*env)->context(); + v8::Local msg = v8::Object::New(isolate_); + msg->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "from"), + v8::String::NewFromUtf8Literal(isolate_, "cpp")).Check(); + + ch.Publish(*env, msg); + + v8::Local received = + context->Global() + ->Get(context, + v8::String::NewFromUtf8Literal(isolate_, "__cppFirstMsg")) + .ToLocalChecked(); + ASSERT_TRUE(received->IsObject()); + v8::Local from_val = + received.As() + ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "from")) + .ToLocalChecked(); + v8::String::Utf8Value from_str(isolate_, from_val); + EXPECT_STREQ(*from_str, "cpp"); +} + +// JS creates a channel and subscribes, then C++ gets the same channel, +// verifies it shares state, and publishes messages that JS receives. +TEST_F(DiagnosticsChannelTest, JSChannelVisibleFromCpp) { + const v8::HandleScope handle_scope(isolate_); + Argv argv; + Env env{handle_scope, argv}; + + SetProcessExitHandler(*env, [&](node::Environment* env_, int exit_code) { + EXPECT_EQ(exit_code, 0); + node::Stop(*env); + }); + + node::LoadEnvironment( + *env, + "const dc = require('diagnostics_channel');" + "globalThis.__dc = dc;" + "globalThis.__jsFirstMessages = [];" + "dc.subscribe('test:cctest:js-first', (msg) => {" + " globalThis.__jsFirstMessages.push(msg);" + "});"); + + v8::Local context = (*env)->context(); + + Channel ch = Channel::Get(*env, "test:cctest:js-first"); + ASSERT_TRUE(ch.HasSubscribers()); + + // Publish from C++ — JS subscriber should receive it. + v8::Local msg1 = v8::Object::New(isolate_); + msg1->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "seq"), + v8::Integer::New(isolate_, 1)).Check(); + ch.Publish(*env, msg1); + + v8::Local msg2 = v8::Object::New(isolate_); + msg2->Set(context, + v8::String::NewFromUtf8Literal(isolate_, "seq"), + v8::Integer::New(isolate_, 2)).Check(); + ch.Publish(*env, msg2); + + v8::Local msgs_val = + context->Global() + ->Get(context, + v8::String::NewFromUtf8Literal(isolate_, + "__jsFirstMessages")) + .ToLocalChecked(); + ASSERT_TRUE(msgs_val->IsArray()); + v8::Local msgs = msgs_val.As(); + EXPECT_EQ(msgs->Length(), 2u); + + // Check first message: { seq: 1 } + v8::Local m1 = msgs->Get(context, 0).ToLocalChecked(); + ASSERT_TRUE(m1->IsObject()); + v8::Local seq1 = + m1.As() + ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "seq")) + .ToLocalChecked(); + EXPECT_EQ(seq1->Int32Value(context).FromJust(), 1); + + // Check second message: { seq: 2 } + v8::Local m2 = msgs->Get(context, 1).ToLocalChecked(); + ASSERT_TRUE(m2->IsObject()); + v8::Local seq2 = + m2.As() + ->Get(context, v8::String::NewFromUtf8Literal(isolate_, "seq")) + .ToLocalChecked(); + EXPECT_EQ(seq2->Int32Value(context).FromJust(), 2); + + RunJS(isolate_, + "globalThis.__jsHasSubs =" + " globalThis.__dc.hasSubscribers('test:cctest:js-first');"); + + v8::Local js_has_subs = + context->Global() + ->Get(context, + v8::String::NewFromUtf8Literal(isolate_, "__jsHasSubs")) + .ToLocalChecked(); + EXPECT_TRUE(js_has_subs->IsTrue()); + EXPECT_TRUE(ch.HasSubscribers()); +} From 7b468bbfdaff81d99c15b852e23dd26f3559f1ae Mon Sep 17 00:00:00 2001 From: RafaelGSS Date: Tue, 17 Feb 2026 14:40:47 -0300 Subject: [PATCH 2/6] src,permission: add --permission-audit Add --permission-audit flag that enables the permission model in warning-only mode. Instead of throwing ERR_ACCESS_DENIED, it emits a message via diagnostics channel and allows the operation to continue. Publish permission check results to per-scope diagnostics channels (e.g., node:permission-model:fs) so users can observe permission decisions at runtime via diagnostics_channel. Refs: https://github.com/nodejs/node/issues/59935 --- doc/api/cli.md | 11 +++ doc/node.1 | 7 ++ lib/internal/process/pre_execution.js | 18 ++++- src/env.cc | 5 +- src/node_options.cc | 5 ++ src/node_options.h | 1 + src/permission/permission.cc | 73 ++++++++++++++++++- src/permission/permission.h | 17 ++--- test/parallel/test-bootstrap-modules.js | 1 + .../test-permission-diagnostics-channel.js | 29 ++++++++ 10 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 test/parallel/test-permission-diagnostics-channel.js diff --git a/doc/api/cli.md b/doc/api/cli.md index 651e06f6910ebe..3387fb0981e30b 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -2174,6 +2174,16 @@ following permissions are restricted: * WASI - manageable through [`--allow-wasi`][] flag * Addons - manageable through [`--allow-addons`][] flag +### `--permission-audit` + + + +Enable audit only for the permission model. When enabled, permission checks +are performed but access is not denied. Instead, a warning is emitted for +each permission violation via diagnostics channel. + ### `--preserve-symlinks`