From b7e9a20bac3eeb83c0a4e96e6b95c90e796b3653 Mon Sep 17 00:00:00 2001 From: Martin Wagner Date: Thu, 2 Jul 2026 17:28:24 +0200 Subject: [PATCH 1/6] lib,permission: fix addon permission drop Signed-off-by: Martin PR-URL: https://github.com/nodejs/node/pull/64007 Reviewed-By: Rafael Gonzaga Reviewed-By: Edy Silva --- src/node_binding.cc | 3 ++ test/parallel/test-permission-drop-addons.js | 38 ++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 test/parallel/test-permission-drop-addons.js diff --git a/src/node_binding.cc b/src/node_binding.cc index 044eadd0eafcda..330c7f167105ea 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -5,6 +5,7 @@ #include "node_errors.h" #include "node_external_reference.h" #include "node_url_pattern.h" +#include "permission/permission.h" #include "util.h" #include @@ -450,6 +451,8 @@ void DLOpen(const FunctionCallbackInfo& args) { return THROW_ERR_DLOPEN_DISABLED( env, "Cannot load native addon because loading addons is disabled."); } + THROW_IF_INSUFFICIENT_PERMISSIONS( + env, permission::PermissionScope::kAddon, ""); auto context = env->context(); diff --git a/test/parallel/test-permission-drop-addons.js b/test/parallel/test-permission-drop-addons.js new file mode 100644 index 00000000000000..baf87c3b95b858 --- /dev/null +++ b/test/parallel/test-permission-drop-addons.js @@ -0,0 +1,38 @@ +// Flags: --permission --allow-addons --allow-fs-read=* +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +const assert = require('assert'); + +let bindingPath; +try { + bindingPath = require.resolve( + `../addons/hello-world/build/${common.buildType}/binding`); +} catch (err) { + if (err.code !== 'MODULE_NOT_FOUND') { + throw err; + } + common.skip('addon not found'); +} + +function openAddon() { + process.dlopen({ exports: {} }, bindingPath); +} + +assert.ok(process.permission.has('addon')); +openAddon(); + +process.permission.drop('addon'); +assert.ok(!process.permission.has('addon')); +assert.throws(() => { + openAddon(); +}, common.expectsError({ + code: 'ERR_ACCESS_DENIED', + permission: 'Addon', +})); From 615749b9b5fa2b4c1c5ac672e8651ca44150d3b8 Mon Sep 17 00:00:00 2001 From: David Evans Date: Thu, 2 Jul 2026 16:28:39 +0100 Subject: [PATCH 2/6] http: fix drain event with cork/uncork When using cork() and uncork() with ServerResponse, the drain event was not reliably emitted after uncorking. This occurred because the uncork() method did not check if a drain was pending (kNeedDrain flag) after flushing the chunked buffer. This fix ensures that when uncork() successfully flushes buffered data and a drain was needed, the drain event is emitted immediately. This commit is a copy of PR #60437 (abandoned) with minor linting fixes. Fixes: https://github.com/nodejs/node/issues/60432 Signed-off-by: David Evans PR-URL: https://github.com/nodejs/node/pull/64038 Reviewed-By: Robert Nagy Reviewed-By: Matteo Collina Reviewed-By: Tim Perry --- lib/_http_outgoing.js | 6 ++ .../parallel/test-http-response-drain-cork.js | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 test/parallel/test-http-response-drain-cork.js diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 4498ee72fe48d8..7694fe4b5b3f3e 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -323,6 +323,12 @@ OutgoingMessage.prototype.uncork = function uncork() { this[kChunkedBuffer].length = 0; this[kChunkedLength] = 0; + + // If we had a pending drain and flushed all data, emit the drain event. + if (this[kNeedDrain] && this.writableLength === 0) { + this[kNeedDrain] = false; + this.emit('drain'); + } }; OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { diff --git a/test/parallel/test-http-response-drain-cork.js b/test/parallel/test-http-response-drain-cork.js new file mode 100644 index 00000000000000..18678748b9c2a4 --- /dev/null +++ b/test/parallel/test-http-response-drain-cork.js @@ -0,0 +1,64 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +// Test that drain event is emitted correctly when using cork/uncork +// with ServerResponse and the write buffer is full + +const server = http.createServer(common.mustCall(async (req, res) => { + res.cork(); + + // Write small amount - won't need drain + assert.strictEqual(res.write('1'.repeat(10)), true); + + // Write enough to exceed highWaterMark (set in 'connection' listener) + assert.strictEqual(res.write('2'.repeat(1000)), false); + + // Verify writableNeedDrain is set + assert.strictEqual(res.writableNeedDrain, true); + + // Wait for drain event after uncorking + const drainPromise = new Promise((resolve) => { + res.once('drain', common.mustCall(() => { + // After drain, writableNeedDrain should be false + assert.strictEqual(res.writableNeedDrain, false); + resolve(); + })); + }); + + // Uncork should trigger drain + res.uncork(); + await drainPromise; + + res.end(); +})); + +server.on('connection', common.mustCall((socket) => { + // Set high water mark large enough to cover HTTP overhead + first + // small content batch, but not enough to cover second batch. + socket._writableState.highWaterMark = 1000; +})); + +server.listen(0, common.localhostIPv4, common.mustCall(() => { + http.get({ + host: common.localhostIPv4, + port: server.address().port, + }, common.mustCall((res) => { + let data = ''; + res.setEncoding('utf8'); + + res.on('data', (chunk) => { + data += chunk; + }); + + res.on('end', common.mustCall(() => { + // Verify we got all the data + assert.strictEqual(data.length, 10 + 1000); + assert.strictEqual(data.substring(0, 10), '1'.repeat(10)); + assert.strictEqual(data.substring(10), '2'.repeat(1000)); + + server.close(common.mustCall()); + })); + })); +})); From a6d3e9369f14d048c33d0e69d77c881a44e3394b Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:28:55 -0700 Subject: [PATCH 3/6] vfs: support writeFileSync with virtual fds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route fs.writeFileSync() and fs.appendFileSync() calls with VFS-owned file descriptors through the virtual file handle instead of falling back to the native fs binding. This preserves descriptor semantics for both memory-backed VFS files and RealFSProvider handles. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: openai:gpt-5.5 PR-URL: https://github.com/nodejs/node/pull/64165 Fixes: https://github.com/nodejs/node/issues/64164 Reviewed-By: Matteo Collina Reviewed-By: Gürgün Dayıoğlu Reviewed-By: James M Snell --- lib/internal/vfs/setup.js | 38 ++++++++++++-- test/parallel/test-vfs-fs-writeFileSync.js | 59 ++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/lib/internal/vfs/setup.js b/lib/internal/vfs/setup.js index fe06f578b2f6ca..e2e641ad841561 100644 --- a/lib/internal/vfs/setup.js +++ b/lib/internal/vfs/setup.js @@ -9,6 +9,7 @@ const { } = primordials; const { Buffer } = require('buffer'); +const { isArrayBufferView } = require('internal/util/types'); const { resolve, sep } = require('path'); const { fileURLToPath, URL } = require('internal/url'); const { kEmptyObject } = require('internal/util'); @@ -46,6 +47,31 @@ function noopFd(fd) { return undefined; } +function toWriteBuffer(data, options) { + if (Buffer.isBuffer(data)) return data; + if (isArrayBufferView(data)) { + return Buffer.from(data.buffer, data.byteOffset, data.byteLength); + } + const encoding = typeof options === 'string' ? options : options?.encoding; + return Buffer.from(data, encoding || 'utf8'); +} + +function writeFileSyncFd(fd, data, options) { + const vfd = getVirtualFd(fd); + if (vfd === undefined) return undefined; + + const buffer = toWriteBuffer(data, options); + let offset = 0; + let length = buffer.byteLength; + while (length > 0) { + const written = vfd.entry.writeSync(buffer, offset, length, null); + offset += written; + length -= written; + } + + return true; +} + // Registry of active VFS instances. const activeVFSList = []; @@ -288,10 +314,14 @@ function createVfsHandlers() { // ==================== Sync path-based write ops ==================== - writeFileSync: (path, data, options) => - vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options)), - appendFileSync: (path, data, options) => - vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options)), + writeFileSync(path, data, options) { + if (typeof path === 'number') return writeFileSyncFd(path, data, options); + return vfsOpVoid(path, (vfs, n) => vfs.writeFileSync(n, data, options)); + }, + appendFileSync(path, data, options) { + if (typeof path === 'number') return writeFileSyncFd(path, data, options); + return vfsOpVoid(path, (vfs, n) => vfs.appendFileSync(n, data, options)); + }, mkdirSync: (path, options) => vfsOp(path, (vfs, n) => ({ result: vfs.mkdirSync(n, options) })), rmdirSync: (path) => vfsOpVoid(path, (vfs, n) => vfs.rmdirSync(n)), diff --git a/test/parallel/test-vfs-fs-writeFileSync.js b/test/parallel/test-vfs-fs-writeFileSync.js index 7469127bb1fde3..eeed2a685f08e3 100644 --- a/test/parallel/test-vfs-fs-writeFileSync.js +++ b/test/parallel/test-vfs-fs-writeFileSync.js @@ -26,4 +26,63 @@ assert.strictEqual(fs.readFileSync(target, 'utf8'), 'fresh more'); fs.writeFileSync(target, Buffer.from('binary')); assert.strictEqual(fs.readFileSync(target, 'utf8'), 'binary'); +// writeFileSync via a VFS fd writes through the open descriptor. +{ + const fdTarget = path.join(mountPoint, 'src/fd.txt'); + const fd = fs.openSync(fdTarget, 'w+'); + try { + fs.writeFileSync(fd, 'hello'); + fs.writeFileSync(fd, new Uint8Array(Buffer.from(' world'))); + assert.strictEqual(fs.readFileSync(fdTarget, 'utf8'), 'hello world'); + } finally { + fs.closeSync(fd); + } +} + +// appendFileSync via a VFS fd follows normal fd write semantics. +{ + const fdTarget = path.join(mountPoint, 'src/append-fd.txt'); + fs.writeFileSync(fdTarget, 'start'); + const fd = fs.openSync(fdTarget, 'a'); + try { + fs.appendFileSync(fd, ' end'); + assert.strictEqual(fs.readFileSync(fdTarget, 'utf8'), 'start end'); + } finally { + fs.closeSync(fd); + } +} + myVfs.unmount(); + +// writeFileSync via a RealFSProvider fd remains tied to the open descriptor +// after the backing path is renamed. +{ + const root = path.join('/tmp', 'vfs-real-writeFileSync-' + process.pid); + const realMountPoint = path.join('/tmp', 'vfs-real-writeFileSync-mount-' + process.pid); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(realMountPoint, { recursive: true, force: true }); + fs.mkdirSync(root, { recursive: true }); + fs.mkdirSync(realMountPoint, { recursive: true }); + + const realVfs = vfs + .create(new vfs.RealFSProvider(root), { emitExperimentalWarning: false }) + .mount(realMountPoint); + try { + const mountedFile = path.join(realMountPoint, 'a.txt'); + fs.writeFileSync(path.join(root, 'a.txt'), 'old'); + const fd = fs.openSync(mountedFile, 'r+'); + try { + fs.renameSync(path.join(root, 'a.txt'), path.join(root, 'b.txt')); + fs.writeFileSync(path.join(root, 'a.txt'), 'new'); + fs.writeFileSync(fd, 'updated'); + assert.strictEqual(fs.readFileSync(path.join(root, 'b.txt'), 'utf8'), 'updated'); + assert.strictEqual(fs.readFileSync(path.join(root, 'a.txt'), 'utf8'), 'new'); + } finally { + fs.closeSync(fd); + } + } finally { + realVfs.unmount(); + fs.rmSync(root, { recursive: true, force: true }); + fs.rmSync(realMountPoint, { recursive: true, force: true }); + } +} From 87a6aa674a01a5330ea2e38ffe0f9c949337e6b4 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Jul 2026 17:29:07 +0200 Subject: [PATCH 4/6] benchmark: trim down the argon2 sets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filip Skokan PR-URL: https://github.com/nodejs/node/pull/64218 Reviewed-By: Luigi Pinca Reviewed-By: Vinícius Lourenço Claro Cardoso Reviewed-By: Yagiz Nizipli --- benchmark/crypto/argon2.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/benchmark/crypto/argon2.js b/benchmark/crypto/argon2.js index ce6a824233e636..c11d5837954a05 100644 --- a/benchmark/crypto/argon2.js +++ b/benchmark/crypto/argon2.js @@ -17,10 +17,12 @@ if (!hasOpenSSL(3, 2)) { const bench = common.createBenchmark(main, { mode: ['sync', 'async'], algorithm: ['argon2d', 'argon2i', 'argon2id'], - passes: [1, 3], - parallelism: [2, 4, 8], - memory: [2 ** 11, 2 ** 16, 2 ** 21], - n: [50], + passes: [3], + parallelism: [1], + // Argon2 memory cost dominates runtime. Keep the default suite small enough + // to finish while still covering a low-cost and a memory-heavy derivation. + memory: [2 ** 11, 2 ** 16], + n: [5], }); function measureSync(n, algorithm, message, nonce, options) { From 592219798f9c091cf45427a827e763d964c3c2f8 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Thu, 2 Jul 2026 18:57:42 +0200 Subject: [PATCH 5/6] child_process: fix permission model propagation via NODE_OPTIONS The substring check env[key].indexOf(--permission) !== -1 in copyPermissionModelFlagsToEnv falsely treats unrelated NODE_OPTIONS values like --title=--permission as if the child already has an explicit Permission Model policy. This prevents flag propagation, causing the child to run without process.permission. Signed-off-by: Matteo Collina PR-URL: https://github.com/nodejs/node/pull/63972 Reviewed-By: Rafael Gonzaga Reviewed-By: Paolo Insogna --- lib/child_process.js | 18 +++- ...n-child-process-inherit-flags-substring.js | 96 +++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-permission-child-process-inherit-flags-substring.js diff --git a/lib/child_process.js b/lib/child_process.js index 824af65556e32b..0e3e04af0d6e32 100644 --- a/lib/child_process.js +++ b/lib/child_process.js @@ -547,10 +547,26 @@ function getPermissionModelFlagsToCopy() { return permissionModelFlagsToCopy; } +function hasPermissionFlagInEnv(nodeOptions) { + // Parse NODE_OPTIONS into individual tokens and check if any token + // is an actual --permission or --permission-audit flag. We use exact + // token matching rather than substring matching to avoid false positives + // when unrelated option values contain '--permission' (e.g., + // --title=--permission). + if (!nodeOptions) return false; + const tokens = nodeOptions.split(/\s+/); + return tokens.some((token) => + token === '--permission' || + token.startsWith('--permission=') || + token === '--permission-audit' || + token.startsWith('--permission-audit='), + ); +} + function copyPermissionModelFlagsToEnv(env, key, args) { // Do not override if permission was already passed to file if (args.includes('--permission') || args.includes('--permission-audit') || - (env[key] && env[key].indexOf('--permission') !== -1)) { + hasPermissionFlagInEnv(env[key])) { return; } diff --git a/test/parallel/test-permission-child-process-inherit-flags-substring.js b/test/parallel/test-permission-child-process-inherit-flags-substring.js new file mode 100644 index 00000000000000..f992a1f3dc8f3b --- /dev/null +++ b/test/parallel/test-permission-child-process-inherit-flags-substring.js @@ -0,0 +1,96 @@ +// Flags: --permission --allow-child-process --allow-fs-read=* --allow-worker +// Tests that NODE_OPTIONS values containing '--permission' as a substring +// in unrelated option values (e.g., --title=--permission) do NOT suppress +// Permission Model flag propagation to child processes. +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} +if (process.config.variables.node_without_node_options) { + common.skip('missing NODE_OPTIONS support'); +} + +const assert = require('assert'); +const childProcess = require('child_process'); + +// Verify that the parent has Permission Model enabled +assert.ok(process.permission.has('child')); +assert.strictEqual(process.env.NODE_OPTIONS, undefined); + +// Test cases: NODE_OPTIONS values that contain '--permission' as a substring +// but are NOT actual permission flags. These should NOT suppress propagation. +const testCases = [ + { name: 'title', nodeOptions: '--title=--permission' }, + { name: 'conditions', nodeOptions: '--conditions=--permission' }, + { name: 'trace-event-categories', nodeOptions: '--trace-event-categories=--permission' }, + { name: 'title-audit', nodeOptions: '--title=--permission-audit' }, +]; + +for (const { name, nodeOptions } of testCases) { + // Spawn a child with the problematic NODE_OPTIONS value. + // Without the fix, the substring check causes propagation to be skipped, + // and the child will not have process.permission. + const { status, stdout } = childProcess.spawnSync( + process.execPath, + [ + '-e', + ` + console.log(typeof process.permission); + console.log(process.permission && process.permission.has("child")); + console.log(process.env.NODE_OPTIONS); + `, + ], + { + env: { + ...process.env, + 'NODE_OPTIONS': nodeOptions, + } + } + ); + + assert.strictEqual(status, 0, `child process for ${name} exited with status ${status}`); + + const [permType, hasChild] = stdout.toString().split('\n'); + + // Verify the child has Permission Model enabled (the bug caused it to be absent) + assert.strictEqual(permType, 'object', `child ${name} should have process.permission object`); + + // Verify the child inherited child permission + assert.strictEqual(hasChild, 'true', `child ${name} should have child permission`); +} + +// Also verify that a child with a real --permission flag in NODE_OPTIONS +// still gets its own flags honored (regression test for existing behavior). +{ + const { status, stdout } = childProcess.spawnSync( + process.execPath, + [ + '-e', + ` + console.log(process.permission.has("fs.write")); + console.log(process.permission.has("fs.read")); + console.log(process.permission.has("child")); + `, + ], + { + env: { + ...process.env, + 'NODE_OPTIONS': '--permission --allow-fs-write=*', + } + } + ); + + assert.strictEqual(status, 0); + const [fsWrite, fsRead, child] = stdout.toString().split('\n'); + assert.strictEqual(fsWrite, 'true'); + assert.strictEqual(fsRead, 'false'); + assert.strictEqual(child, 'false'); +} + +{ + assert.strictEqual(process.env.NODE_OPTIONS, undefined); +} From 5d8693e1244af6f4b37a7f81112d15163560f8ea Mon Sep 17 00:00:00 2001 From: Chengzhong Wu Date: Thu, 2 Jul 2026 15:21:02 -0400 Subject: [PATCH 6/6] module: enable import support for addons by default Signed-off-by: Chengzhong Wu PR-URL: https://github.com/nodejs/node/pull/64221 Reviewed-By: Marco Ippolito Reviewed-By: Edy Silva Reviewed-By: Joyee Cheung --- doc/api/cli.md | 6 +++++- src/node_options.h | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 4d95af1b886cd6..71654cf7ad8bfb 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1039,9 +1039,13 @@ It is possible to run code containing inline types unless the added: - v23.6.0 - v22.20.0 +changes: + - version: REPLACEME + pr-url: https://github.com/nodejs/node/pull/64221 + description: This is enabled by default. --> -> Stability: 1.0 - Early development +> Stability: 1.2 - Release candidate Enable experimental import support for `.node` addons. diff --git a/src/node_options.h b/src/node_options.h index 3b3c6f371a03c4..eb5aadc2347e25 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -126,7 +126,7 @@ class EnvironmentOptions : public Options { bool require_module = true; std::string dns_result_order; bool enable_source_maps = false; - bool experimental_addon_modules = EXPERIMENTALS_DEFAULT_VALUE; + bool experimental_addon_modules = true; bool experimental_eventsource = EXPERIMENTALS_DEFAULT_VALUE; bool experimental_ffi = EXPERIMENTALS_DEFAULT_VALUE; bool experimental_websocket = true;