Version in base suite: 20.19.2+dfsg-1 Base version: nodejs_20.19.2+dfsg-1 Target version: nodejs_20.19.2+dfsg-1+deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/n/nodejs/nodejs_20.19.2+dfsg-1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/n/nodejs/nodejs_20.19.2+dfsg-1+deb13u1.dsc changelog | 31 patches/sec/10-zlib-fix-pointer-alignment.patch | 59 patches/sec/12-http2-fix-check-for-frame-type-goaway.patch | 52 patches/sec/15-fix-os-getinterface-addresses-leak.patch | 37 patches/sec/17-fix-possible-dereference-of-null-pointer.patch | 31 patches/sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch | 63 + patches/sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch | 98 + patches/sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch | 58 patches/sec/29-fix-order-of-check-not-null.patch | 37 patches/sec/33-tls-route-callback-exceptions-through-error-handlers.patch | 612 ++++++++++ patches/sec/34-lib-add-tlssocket-default-error-handler.patch | 41 patches/sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch | 172 ++ patches/sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch | 307 +++++ patches/sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch | 475 +++++++ patches/sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch | 292 ++++ patches/sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch | 50 patches/series | 15 17 files changed, 2430 insertions(+) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpth2_70yo/nodejs_20.19.2+dfsg-1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpth2_70yo/nodejs_20.19.2+dfsg-1+deb13u1.dsc: no acceptable signature found diff -Nru nodejs-20.19.2+dfsg/debian/changelog nodejs-20.19.2+dfsg/debian/changelog --- nodejs-20.19.2+dfsg/debian/changelog 2025-05-14 21:43:31.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/changelog 2026-03-05 10:05:11.000000000 +0000 @@ -1,3 +1,34 @@ +nodejs (20.19.2+dfsg-1+deb13u1) trixie-security; urgency=medium + + * Upstream security patches: + + CVE-2025-23085: follow-up fix wrong check for NGHTTP2_GOAWAY + + CVE-2026-21637: TLS error handling allows remote attackers to + crash or exhaust resources of a TLS server when `pskCallback` + or `ALPNCallback` are in use. + + CVE-2025-59465: malformed `HTTP/2 HEADERS` frame with oversized + invalid `HPACK` data can cause a crash. + + CVE-2025-55132: permission model allows a file's access and + modification timestamps to be changed via `futimes()` even when + the process has only read permissions. + + CVE-2025-55130: permissions model allows attackers to bypass + `--allow-fs-read` and `--allow-fs-write` restrictions using + crafted relative symlink paths. + + CVE-2025-59466: "Maximum call stack size exceeded" errors become + uncatchable when `async_hooks.createHook()` is enabled. + + CVE-2025-55131: buffer allocation logic can expose uninitialized + memory when allocations are interrupted, when using the `vm` module + with the timeout option. + * Upstream critical fixes (see sec/NN patches) + + zlib: fix pointer alignment (10) + + os: fix GetInterfaceAddresses memory leak (15) + + src: fix possible dereference of null pointers (17, 29) + + v8: fix missing callback in heap utils destroy (19) + + v8: loong64 - avoid memory access under stack pointer (27) + + http2: do not crash on mismatched ping buffer length (28) + + v8: riscv64 - Fix sp handling in MacroAssembler::LeaveFrame (44) + + -- Jérémy Lal Thu, 05 Mar 2026 11:05:11 +0100 + nodejs (20.19.2+dfsg-1) unstable; urgency=medium * New upstream version 20.19.2+dfsg diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/10-zlib-fix-pointer-alignment.patch nodejs-20.19.2+dfsg/debian/patches/sec/10-zlib-fix-pointer-alignment.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/10-zlib-fix-pointer-alignment.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/10-zlib-fix-pointer-alignment.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,59 @@ +From 9dc9604502b6b4cf890ba71e1766867cb1a63bcc Mon Sep 17 00:00:00 2001 +From: jhofstee +Date: Wed, 9 Apr 2025 12:24:13 +0200 +Subject: [PATCH] zlib: fix pointer alignment +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +The function AllocForBrotli prefixes the allocated memory with its +size, and returns a pointer to the region after it. This pointer can +however no longer be suitably aligned. Correct this by allocating +the maximum of the the size of the size_t and the max alignment. + +On Arm 32bits the size_t is 4 bytes long, but the alignment is 8 for +some NEON instructions. When Brotli is compiled with optimizations +enabled newer GCC versions will use the NEON instructions and trigger +a bus error killing node. + +see https://github.com/google/brotli/issues/1159 + +PR-URL: https://github.com/nodejs/node/pull/57727 +Reviewed-By: Shelley Vohr +Reviewed-By: Tobias Nießen +Reviewed-By: Daniel Lemire +Reviewed-By: Gerhard Stöbich +--- + src/node_zlib.cc | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +diff --git a/src/node_zlib.cc b/src/node_zlib.cc +index 66370e4165979c..a537e766722852 100644 +--- a/src/node_zlib.cc ++++ b/src/node_zlib.cc +@@ -493,20 +493,22 @@ class CompressionStream : public AsyncWrap, public ThreadPoolWork { + } + + static void* AllocForBrotli(void* data, size_t size) { +- size += sizeof(size_t); ++ constexpr size_t offset = std::max(sizeof(size_t), alignof(max_align_t)); ++ size += offset; + CompressionStream* ctx = static_cast(data); + char* memory = UncheckedMalloc(size); + if (UNLIKELY(memory == nullptr)) return nullptr; + *reinterpret_cast(memory) = size; + ctx->unreported_allocations_.fetch_add(size, + std::memory_order_relaxed); +- return memory + sizeof(size_t); ++ return memory + offset; + } + + static void FreeForZlib(void* data, void* pointer) { + if (UNLIKELY(pointer == nullptr)) return; + CompressionStream* ctx = static_cast(data); +- char* real_pointer = static_cast(pointer) - sizeof(size_t); ++ constexpr size_t offset = std::max(sizeof(size_t), alignof(max_align_t)); ++ char* real_pointer = static_cast(pointer) - offset; + size_t real_size = *reinterpret_cast(real_pointer); + ctx->unreported_allocations_.fetch_sub(real_size, + std::memory_order_relaxed); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/12-http2-fix-check-for-frame-type-goaway.patch nodejs-20.19.2+dfsg/debian/patches/sec/12-http2-fix-check-for-frame-type-goaway.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/12-http2-fix-check-for-frame-type-goaway.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/12-http2-fix-check-for-frame-type-goaway.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,52 @@ +From ff5cf8a4282cb99354aa1deae70ac99f4e889a8b Mon Sep 17 00:00:00 2001 +From: hanguanqiang +Date: Wed, 9 Apr 2025 19:48:11 +0800 +Subject: [PATCH] http2: fix check for `frame->hd.type` + +Related to CVE-2025-23085 +According to the comment, this should be checking whether +`frame->hd.type` is `NGHTTP2_GOAWAY`, i.e. `0x07` and not `0x03`. + +PR-URL: https://github.com/nodejs/node/pull/57644 +Refs: https://github.com/nodejs/node/commit/1b693fa03a0d36bc1dc9ec8d95060e3e5ceeee7b +Reviewed-By: Matteo Collina +Reviewed-By: Rafael Gonzaga +--- + src/node_http2.cc | 2 +- + test/parallel/test-http2-premature-close.js | 6 +++--- + 2 files changed, 4 insertions(+), 4 deletions(-) + +--- a/src/node_http2.cc ++++ b/src/node_http2.cc +@@ -1195,7 +1195,7 @@ + // closed but the Http2Session will still be up causing a memory leak. + // Therefore, if the GOAWAY frame couldn't be send due to + // ERR_SESSION_CLOSING we should force close from our side. +- if (frame->hd.type != 0x03) { ++ if (frame->hd.type != NGHTTP2_GOAWAY) { + return 0; + } + } +--- a/test/parallel/test-http2-premature-close.js ++++ b/test/parallel/test-http2-premature-close.js +@@ -29,9 +29,9 @@ + // Send a valid HEADERS frame + const headersFrame = Buffer.concat([ + Buffer.from([ +- 0x00, 0x00, 0x0c, // Length: 12 bytes ++ 0x00, 0x00, 0x0e, // Length: 14 bytes + 0x01, // Type: HEADERS +- 0x05, // Flags: END_HEADERS + END_STREAM ++ 0x04, // Flags: END_HEADERS + (streamId >> 24) & 0xFF, // Stream ID: high byte + (streamId >> 16) & 0xFF, + (streamId >> 8) & 0xFF, +@@ -41,7 +41,7 @@ + 0x82, // Indexed Header Field Representation (Predefined ":method: GET") + 0x84, // Indexed Header Field Representation (Predefined ":path: /") + 0x86, // Indexed Header Field Representation (Predefined ":scheme: http") +- 0x44, 0x0a, // Custom ":authority: localhost" ++ 0x41, 0x09, // ":authority: localhost" Length: 9 bytes + 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x68, 0x6f, 0x73, 0x74, + ]), + ]); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/15-fix-os-getinterface-addresses-leak.patch nodejs-20.19.2+dfsg/debian/patches/sec/15-fix-os-getinterface-addresses-leak.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/15-fix-os-getinterface-addresses-leak.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/15-fix-os-getinterface-addresses-leak.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,37 @@ +From 043dcdd628ab478ea0a3b48df1c49c58aa440f58 Mon Sep 17 00:00:00 2001 +From: theanarkh +Date: Sun, 6 Jul 2025 01:33:23 +0800 +Subject: [PATCH] os: fix GetInterfaceAddresses memory lieaky +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +PR-URL: https://github.com/nodejs/node/pull/58940 +Reviewed-By: Juan José Arboleda +Reviewed-By: Chengzhong Wu +--- + src/node_os.cc | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/src/node_os.cc b/src/node_os.cc +index ce2af8d83b7443..ddaf61d5eb6a82 100644 +--- a/src/node_os.cc ++++ b/src/node_os.cc +@@ -192,6 +192,9 @@ static void GetInterfaceAddresses(const FunctionCallbackInfo& args) { + return args.GetReturnValue().SetUndefined(); + } + ++ auto cleanup = ++ OnScopeLeave([&]() { uv_free_interface_addresses(interfaces, count); }); ++ + Local no_scope_id = Integer::New(isolate, -1); + std::vector> result; + result.reserve(count * 7); +@@ -243,7 +246,6 @@ static void GetInterfaceAddresses(const FunctionCallbackInfo& args) { + } + } + +- uv_free_interface_addresses(interfaces, count); + args.GetReturnValue().Set(Array::New(isolate, result.data(), result.size())); + } + diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/17-fix-possible-dereference-of-null-pointer.patch nodejs-20.19.2+dfsg/debian/patches/sec/17-fix-possible-dereference-of-null-pointer.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/17-fix-possible-dereference-of-null-pointer.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/17-fix-possible-dereference-of-null-pointer.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,31 @@ +From 563e61f0121984b6d8aca4b861acfdde12a4ca22 Mon Sep 17 00:00:00 2001 +From: Eusgor <100363036+Eusgor@users.noreply.github.com> +Date: Fri, 30 May 2025 02:01:48 +0600 +Subject: [PATCH] src: fix possible dereference of null pointer + +There is a CHECK_NOT_NULL check before dereferencing node_env on +line 710 in the "if" block, but there is no CHECK_NOT_NULL check before +dereferencing node_env on line 721. Maybe it makes sense to put +CHECK_NOT_NULL right after calling the Environment::GetCurrent function. + +PR-URL: https://github.com/nodejs/node/pull/58459 +Reviewed-By: Anna Henningsen +Reviewed-By: Michael Dawson +Reviewed-By: Chengzhong Wu +--- + src/node_api.cc | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/node_api.cc ++++ b/src/node_api.cc +@@ -693,9 +693,9 @@ + napi_addon_register_func init, + int32_t module_api_version) { + node::Environment* node_env = node::Environment::GetCurrent(context); ++ CHECK_NOT_NULL(node_env); + std::string module_filename = ""; + if (init == nullptr) { +- CHECK_NOT_NULL(node_env); + node_env->ThrowError("Module has no declared entry point."); + return; + } diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch nodejs-20.19.2+dfsg/debian/patches/sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,63 @@ +From 077d5020c43fa6c50be8af0b5753afdf46fe7566 Mon Sep 17 00:00:00 2001 +From: Ruben Bridgewater +Date: Mon, 30 Jun 2025 13:51:40 +0200 +Subject: [PATCH] v8: fix missing callback in heap utils destroy + +This fixes the v8.getHeapSnapshot() calls not properly being +destroyed. Pipeline calls would for example not properly end +without the callback being in place. + +PR-URL: https://github.com/nodejs/node/pull/58846 +Reviewed-By: Ethan Arrowood +Reviewed-By: Luigi Pinca +Reviewed-By: Chengzhong Wu +Reviewed-By: Shelley Vohr +--- + lib/internal/heap_utils.js | 3 ++- + test/sequential/test-heapdump.js | 11 +++++++++++ + 2 files changed, 13 insertions(+), 1 deletion(-) + +diff --git a/lib/internal/heap_utils.js b/lib/internal/heap_utils.js +index c39d811ab793b0..6d72fb1d628540 100644 +--- a/lib/internal/heap_utils.js ++++ b/lib/internal/heap_utils.js +@@ -53,11 +53,12 @@ class HeapSnapshotStream extends Readable { + this[kHandle].readStart(); + } + +- _destroy() { ++ _destroy(err, callback) { + // Release the references on the handle so that + // it can be garbage collected. + this[kHandle][owner_symbol] = undefined; + this[kHandle] = undefined; ++ callback(err); + } + + [kUpdateTimer]() { +diff --git a/test/sequential/test-heapdump.js b/test/sequential/test-heapdump.js +index 1388623e61f939..ee025f57fb8258 100644 +--- a/test/sequential/test-heapdump.js ++++ b/test/sequential/test-heapdump.js +@@ -8,6 +8,7 @@ if (!common.isMainThread) + const { writeHeapSnapshot, getHeapSnapshot } = require('v8'); + const assert = require('assert'); + const fs = require('fs'); ++const { promises: { pipeline }, PassThrough } = require('stream'); + const tmpdir = require('../common/tmpdir'); + + tmpdir.refresh(); +@@ -76,3 +77,13 @@ process.chdir(tmpdir.path); + JSON.parse(data); + })); + } ++ ++{ ++ const passthrough = new PassThrough(); ++ passthrough.on('data', common.mustCallAtLeast(1)); ++ ++ pipeline( ++ getHeapSnapshot(), ++ passthrough, ++ ).then(common.mustCall()); ++} diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch nodejs-20.19.2+dfsg/debian/patches/sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,98 @@ +From acec79989e3d87b220d31cedf69084044443a59c Mon Sep 17 00:00:00 2001 +From: zhoumingtao +Date: Wed, 30 Jul 2025 12:02:51 +0800 +Subject: [PATCH] deps: V8: cherry-pick 6b1b9bca2a8 + +Origin commit message: + + [loong64][codegen] Avoid memory access under stack pointer + + According to LoongArch ABI doc chapter 8.2: + "Procedures must not assume the persistence on-stack data of which + the addresses lie below the stack pointer." + + Change-Id: I92735e052227495ac9884c4290b57eaffbf905e1 + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/6786372 + Auto-Submit: Zhao Jiazhong + Reviewed-by: Matthias Liedtke + Commit-Queue: Matthias Liedtke + Cr-Commit-Position: refs/heads/main@{#101634} + +Backport-PR-URL: https://github.com/nodejs/node/pull/59662 +Refs: https://github.com/v8/v8/commit/6b1b9bca2a8fe9824bcc815a9ce642da904b91c3 +PR-URL: https://github.com/nodejs/node/pull/59283 +Reviewed-By: Marco Ippolito +--- + common.gypi | 2 +- + .../loong64/macro-assembler-loong64.cc | 20 ++++++++++--------- + 2 files changed, 12 insertions(+), 10 deletions(-) + +--- a/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc ++++ b/deps/v8/src/codegen/loong64/macro-assembler-loong64.cc +@@ -1256,21 +1256,24 @@ + } + + void MacroAssembler::MultiPush(RegList regs) { +- int16_t stack_offset = 0; ++ int16_t num_to_push = regs.Count(); ++ int16_t stack_offset = num_to_push * kSystemPointerSize; + ++ Sub_d(sp, sp, Operand(stack_offset)); + for (int16_t i = kNumRegisters - 1; i >= 0; i--) { + if ((regs.bits() & (1 << i)) != 0) { + stack_offset -= kSystemPointerSize; + St_d(ToRegister(i), MemOperand(sp, stack_offset)); + } + } +- addi_d(sp, sp, stack_offset); + } + + void MacroAssembler::MultiPush(RegList regs1, RegList regs2) { + DCHECK((regs1 & regs2).is_empty()); +- int16_t stack_offset = 0; ++ int16_t num_to_push = regs1.Count() + regs2.Count(); ++ int16_t stack_offset = num_to_push * kSystemPointerSize; + ++ Sub_d(sp, sp, Operand(stack_offset)); + for (int16_t i = kNumRegisters - 1; i >= 0; i--) { + if ((regs1.bits() & (1 << i)) != 0) { + stack_offset -= kSystemPointerSize; +@@ -1283,15 +1286,16 @@ + St_d(ToRegister(i), MemOperand(sp, stack_offset)); + } + } +- addi_d(sp, sp, stack_offset); + } + + void MacroAssembler::MultiPush(RegList regs1, RegList regs2, RegList regs3) { + DCHECK((regs1 & regs2).is_empty()); + DCHECK((regs1 & regs3).is_empty()); + DCHECK((regs2 & regs3).is_empty()); +- int16_t stack_offset = 0; ++ int16_t num_to_push = regs1.Count() + regs2.Count() + regs3.Count(); ++ int16_t stack_offset = num_to_push * kSystemPointerSize; + ++ Sub_d(sp, sp, Operand(stack_offset)); + for (int16_t i = kNumRegisters - 1; i >= 0; i--) { + if ((regs1.bits() & (1 << i)) != 0) { + stack_offset -= kSystemPointerSize; +@@ -1310,7 +1314,6 @@ + St_d(ToRegister(i), MemOperand(sp, stack_offset)); + } + } +- addi_d(sp, sp, stack_offset); + } + + void MacroAssembler::MultiPop(RegList regs) { +@@ -3563,9 +3566,8 @@ + + void MacroAssembler::LeaveFrame(StackFrame::Type type) { + ASM_CODE_COMMENT(this); +- addi_d(sp, fp, 2 * kSystemPointerSize); +- Ld_d(ra, MemOperand(fp, 1 * kSystemPointerSize)); +- Ld_d(fp, MemOperand(fp, 0 * kSystemPointerSize)); ++ Move(sp, fp); ++ Pop(ra, fp); + } + + void MacroAssembler::EnterExitFrame(int stack_space, diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch nodejs-20.19.2+dfsg/debian/patches/sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,58 @@ +From 20616f17506051d4851cb0552c627d5e6a6ff6a3 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ren=C3=A9?= +Date: Thu, 9 Oct 2025 00:23:34 +0100 +Subject: [PATCH] http2: do not crash on mismatched ping buffer length + +PR-URL: https://github.com/nodejs/node/pull/60135 +Reviewed-By: Ruben Bridgewater +Reviewed-By: Matteo Collina +Reviewed-By: Tim Perry +Reviewed-By: Rafael Gonzaga +Reviewed-By: Colin Ihrig +Reviewed-By: Luigi Pinca +--- + lib/internal/http2/core.js | 6 +++--- + test/parallel/test-http2-ping.js | 7 ++++--- + 2 files changed, 7 insertions(+), 6 deletions(-) + +--- a/lib/internal/http2/core.js ++++ b/lib/internal/http2/core.js +@@ -1413,9 +1413,9 @@ + } + if (payload) { + validateBuffer(payload, 'payload'); +- } +- if (payload && payload.length !== 8) { +- throw new ERR_HTTP2_PING_LENGTH(); ++ if (payload.byteLength !== 8) { ++ throw new ERR_HTTP2_PING_LENGTH(); ++ } + } + validateFunction(callback, 'callback'); + +--- a/test/parallel/test-http2-ping.js ++++ b/test/parallel/test-http2-ping.js +@@ -64,11 +64,11 @@ + }))); + } + { +- const payload = Buffer.from('abcdefgi'); ++ const payload = new Uint16Array([1, 2, 3, 4]); + assert(client.ping(payload, common.mustCall((err, duration, ret) => { + assert.strictEqual(err, null); + assert.strictEqual(typeof duration, 'number'); +- assert.deepStrictEqual(payload, ret); ++ assert.deepStrictEqual(payload.buffer, ret.buffer); + }))); + } + +@@ -99,7 +99,8 @@ + { + const shortPayload = Buffer.from('abcdefg'); + const longPayload = Buffer.from('abcdefghi'); +- [shortPayload, longPayload].forEach((payloadWithInvalidLength) => ++ const mismatchedPayload = new Uint32Array(8); ++ [shortPayload, longPayload, mismatchedPayload].forEach((payloadWithInvalidLength) => + assert.throws( + () => client.ping(payloadWithInvalidLength), + { diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/29-fix-order-of-check-not-null.patch nodejs-20.19.2+dfsg/debian/patches/sec/29-fix-order-of-check-not-null.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/29-fix-order-of-check-not-null.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/29-fix-order-of-check-not-null.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,37 @@ +From b8d145db2c0ff1177765abb21de9352782ca16e6 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Tobias=20Nie=C3=9Fen?= +Date: Mon, 18 Aug 2025 14:17:47 +0200 +Subject: [PATCH] src: fix order of CHECK_NOT_NULL/dereference +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +`ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS` may dereference +`req_wrap_async`, so `CHECK_NOT_NULL(req_wrap_async)` should be used +before and not after. + +PR-URL: https://github.com/nodejs/node/pull/59487 +Reviewed-By: Luigi Pinca +Reviewed-By: Richard Lau +Reviewed-By: Anna Henningsen +Reviewed-By: Ulises Gascón +--- + src/node_file.cc | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/node_file.cc ++++ b/src/node_file.cc +@@ -1635,12 +1635,12 @@ + + if (argc > 1) { // unlink(path, req) + FSReqBase* req_wrap_async = GetReqWrap(args, 1); ++ CHECK_NOT_NULL(req_wrap_async); + ASYNC_THROW_IF_INSUFFICIENT_PERMISSIONS( + env, + req_wrap_async, + permission::PermissionScope::kFileSystemWrite, + path.ToStringView()); +- CHECK_NOT_NULL(req_wrap_async); + FS_ASYNC_TRACE_BEGIN1( + UV_FS_UNLINK, req_wrap_async, "path", TRACE_STR_COPY(*path)) + AsyncCall(env, req_wrap_async, args, "unlink", UTF8, AfterNoArgs, diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/33-tls-route-callback-exceptions-through-error-handlers.patch nodejs-20.19.2+dfsg/debian/patches/sec/33-tls-route-callback-exceptions-through-error-handlers.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/33-tls-route-callback-exceptions-through-error-handlers.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/33-tls-route-callback-exceptions-through-error-handlers.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,612 @@ +From 85f73e7057e9badf6e7713f7440769375cdb5df5 Mon Sep 17 00:00:00 2001 +From: Matteo Collina +Date: Mon, 22 Dec 2025 18:25:33 +0100 +Subject: [PATCH] tls: route callback exceptions through error handlers + +Wrap pskCallback and ALPNCallback invocations in try-catch blocks +to route exceptions through owner.destroy() instead of letting them +become uncaught exceptions. This prevents remote attackers from +crashing TLS servers or causing resource exhaustion. + +Fixes: https://hackerone.com/reports/3473882 +PR-URL: https://github.com/nodejs-private/node-private/pull/782 +PR-URL: https://github.com/nodejs-private/node-private/pull/796 +Reviewed-By: Matteo Collina +CVE-ID: CVE-2026-21637 +--- + lib/_tls_wrap.js | 157 ++++---- + test/parallel/test-tls-alpn-server-client.js | 32 +- + ...ls-psk-alpn-callback-exception-handling.js | 338 ++++++++++++++++++ + 3 files changed, 446 insertions(+), 81 deletions(-) + create mode 100644 test/parallel/test-tls-psk-alpn-callback-exception-handling.js + +diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js +index 4eb7b7ffa69f4a..c3e48a6cbc81ae 100644 +--- a/lib/_tls_wrap.js ++++ b/lib/_tls_wrap.js +@@ -233,39 +233,44 @@ function callALPNCallback(protocolsBuffer) { + const handle = this; + const socket = handle[owner_symbol]; + +- const servername = handle.getServername(); ++ try { ++ const servername = handle.getServername(); + +- // Collect all the protocols from the given buffer: +- const protocols = []; +- let offset = 0; +- while (offset < protocolsBuffer.length) { +- const protocolLen = protocolsBuffer[offset]; +- offset += 1; ++ // Collect all the protocols from the given buffer: ++ const protocols = []; ++ let offset = 0; ++ while (offset < protocolsBuffer.length) { ++ const protocolLen = protocolsBuffer[offset]; ++ offset += 1; + +- const protocol = protocolsBuffer.slice(offset, offset + protocolLen); +- offset += protocolLen; ++ const protocol = protocolsBuffer.slice(offset, offset + protocolLen); ++ offset += protocolLen; + +- protocols.push(protocol.toString('ascii')); +- } ++ protocols.push(protocol.toString('ascii')); ++ } + +- const selectedProtocol = socket[kALPNCallback]({ +- servername, +- protocols, +- }); ++ const selectedProtocol = socket[kALPNCallback]({ ++ servername, ++ protocols, ++ }); + +- // Undefined -> all proposed protocols rejected +- if (selectedProtocol === undefined) return undefined; ++ // Undefined -> all proposed protocols rejected ++ if (selectedProtocol === undefined) return undefined; + +- const protocolIndex = protocols.indexOf(selectedProtocol); +- if (protocolIndex === -1) { +- throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols); +- } +- let protocolOffset = 0; +- for (let i = 0; i < protocolIndex; i++) { +- protocolOffset += 1 + protocols[i].length; +- } ++ const protocolIndex = protocols.indexOf(selectedProtocol); ++ if (protocolIndex === -1) { ++ throw new ERR_TLS_ALPN_CALLBACK_INVALID_RESULT(selectedProtocol, protocols); ++ } ++ let protocolOffset = 0; ++ for (let i = 0; i < protocolIndex; i++) { ++ protocolOffset += 1 + protocols[i].length; ++ } + +- return protocolOffset; ++ return protocolOffset; ++ } catch (err) { ++ socket.destroy(err); ++ return undefined; ++ } + } + + function requestOCSP(socket, info) { +@@ -372,63 +377,75 @@ function onnewsession(sessionId, session) { + + function onPskServerCallback(identity, maxPskLen) { + const owner = this[owner_symbol]; +- const ret = owner[kPskCallback](owner, identity); +- if (ret == null) +- return undefined; + +- let psk; +- if (isArrayBufferView(ret)) { +- psk = ret; +- } else { +- if (typeof ret !== 'object') { +- throw new ERR_INVALID_ARG_TYPE( +- 'ret', +- ['Object', 'Buffer', 'TypedArray', 'DataView'], +- ret, ++ try { ++ const ret = owner[kPskCallback](owner, identity); ++ if (ret == null) ++ return undefined; ++ ++ let psk; ++ if (isArrayBufferView(ret)) { ++ psk = ret; ++ } else { ++ if (typeof ret !== 'object') { ++ throw new ERR_INVALID_ARG_TYPE( ++ 'ret', ++ ['Object', 'Buffer', 'TypedArray', 'DataView'], ++ ret, ++ ); ++ } ++ psk = ret.psk; ++ validateBuffer(psk, 'psk'); ++ } ++ ++ if (psk.length > maxPskLen) { ++ throw new ERR_INVALID_ARG_VALUE( ++ 'psk', ++ psk, ++ `Pre-shared key exceeds ${maxPskLen} bytes`, + ); + } +- psk = ret.psk; +- validateBuffer(psk, 'psk'); +- } + +- if (psk.length > maxPskLen) { +- throw new ERR_INVALID_ARG_VALUE( +- 'psk', +- psk, +- `Pre-shared key exceeds ${maxPskLen} bytes`, +- ); ++ return psk; ++ } catch (err) { ++ owner.destroy(err); ++ return undefined; + } +- +- return psk; + } + + function onPskClientCallback(hint, maxPskLen, maxIdentityLen) { + const owner = this[owner_symbol]; +- const ret = owner[kPskCallback](hint); +- if (ret == null) +- return undefined; + +- validateObject(ret, 'ret'); ++ try { ++ const ret = owner[kPskCallback](hint); ++ if (ret == null) ++ return undefined; ++ ++ validateObject(ret, 'ret'); ++ ++ validateBuffer(ret.psk, 'psk'); ++ if (ret.psk.length > maxPskLen) { ++ throw new ERR_INVALID_ARG_VALUE( ++ 'psk', ++ ret.psk, ++ `Pre-shared key exceeds ${maxPskLen} bytes`, ++ ); ++ } + +- validateBuffer(ret.psk, 'psk'); +- if (ret.psk.length > maxPskLen) { +- throw new ERR_INVALID_ARG_VALUE( +- 'psk', +- ret.psk, +- `Pre-shared key exceeds ${maxPskLen} bytes`, +- ); +- } ++ validateString(ret.identity, 'identity'); ++ if (Buffer.byteLength(ret.identity) > maxIdentityLen) { ++ throw new ERR_INVALID_ARG_VALUE( ++ 'identity', ++ ret.identity, ++ `PSK identity exceeds ${maxIdentityLen} bytes`, ++ ); ++ } + +- validateString(ret.identity, 'identity'); +- if (Buffer.byteLength(ret.identity) > maxIdentityLen) { +- throw new ERR_INVALID_ARG_VALUE( +- 'identity', +- ret.identity, +- `PSK identity exceeds ${maxIdentityLen} bytes`, +- ); ++ return { psk: ret.psk, identity: ret.identity }; ++ } catch (err) { ++ owner.destroy(err); ++ return undefined; + } +- +- return { psk: ret.psk, identity: ret.identity }; + } + + function onkeylog(line) { +diff --git a/test/parallel/test-tls-alpn-server-client.js b/test/parallel/test-tls-alpn-server-client.js +index 8f1a4b8e439aab..1ef32ca5938e20 100644 +--- a/test/parallel/test-tls-alpn-server-client.js ++++ b/test/parallel/test-tls-alpn-server-client.js +@@ -252,25 +252,35 @@ function TestALPNCallback() { + function TestBadALPNCallback() { + // Server always returns a fixed invalid value: + const serverOptions = { ++ key: loadPEM('agent2-key'), ++ cert: loadPEM('agent2-cert'), + ALPNCallback: common.mustCall(() => 'http/5') + }; + +- const clientsOptions = [{ +- ALPNProtocols: ['http/1', 'h2'], +- }]; ++ const server = tls.createServer(serverOptions); + +- process.once('uncaughtException', common.mustCall((error) => { ++ // Error should be emitted via tlsClientError, not as uncaughtException ++ server.on('tlsClientError', common.mustCall((error, socket) => { + assert.strictEqual(error.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT'); ++ socket.destroy(); + })); + +- runTest(clientsOptions, serverOptions, function(results) { +- // Callback returns 'http/5' => doesn't match client ALPN => error & reset +- assert.strictEqual(results[0].server, undefined); +- const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL']; +- assert.ok(allowedErrors.includes(results[0].client.error.code), `'${results[0].client.error.code}' was not one of ${allowedErrors}.`); ++ server.listen(0, serverIP, common.mustCall(() => { ++ const client = tls.connect({ ++ port: server.address().port, ++ host: serverIP, ++ rejectUnauthorized: false, ++ ALPNProtocols: ['http/1', 'h2'], ++ }, common.mustNotCall()); + +- TestALPNOptionsCallback(); +- }); ++ client.on('error', common.mustCall((err) => { ++ // Client gets reset when server handles error via tlsClientError ++ const allowedErrors = ['ECONNRESET', 'ERR_SSL_TLSV1_ALERT_NO_APPLICATION_PROTOCOL']; ++ assert.ok(allowedErrors.includes(err.code), `'${err.code}' was not one of ${allowedErrors}.`); ++ server.close(); ++ TestALPNOptionsCallback(); ++ })); ++ })); + } + + function TestALPNOptionsCallback() { +diff --git a/test/parallel/test-tls-psk-alpn-callback-exception-handling.js b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +new file mode 100644 +index 00000000000000..153853a3a9a4f0 +--- /dev/null ++++ b/test/parallel/test-tls-psk-alpn-callback-exception-handling.js +@@ -0,0 +1,338 @@ ++'use strict'; ++ ++// This test verifies that exceptions in pskCallback and ALPNCallback are ++// properly routed through tlsClientError instead of becoming uncaught ++// exceptions. This is a regression test for a vulnerability where callback ++// validation errors would bypass all standard TLS error handlers. ++// ++// The vulnerability allows remote attackers to crash TLS servers or cause ++// resource exhaustion (file descriptor leaks) when pskCallback or ALPNCallback ++// throw exceptions during validation. ++ ++const common = require('../common'); ++ ++if (!common.hasCrypto) ++ common.skip('missing crypto'); ++ ++const assert = require('assert'); ++const { describe, it } = require('node:test'); ++const tls = require('tls'); ++const fixtures = require('../common/fixtures'); ++ ++const CIPHERS = 'PSK+HIGH'; ++const TEST_TIMEOUT = 5000; ++ ++// Helper to create a promise that rejects on uncaughtException or timeout ++function createTestPromise() { ++ let resolve, reject; ++ const promise = new Promise((res, rej) => { ++ resolve = res; ++ reject = rej; ++ }); ++ let settled = false; ++ ++ const cleanup = () => { ++ if (!settled) { ++ settled = true; ++ process.removeListener('uncaughtException', onUncaught); ++ clearTimeout(timeout); ++ } ++ }; ++ ++ const onUncaught = (err) => { ++ cleanup(); ++ reject(new Error( ++ `Uncaught exception instead of tlsClientError: ${err.code || err.message}` ++ )); ++ }; ++ ++ const timeout = setTimeout(() => { ++ cleanup(); ++ reject(new Error('Test timed out - tlsClientError was not emitted')); ++ }, TEST_TIMEOUT); ++ ++ process.on('uncaughtException', onUncaught); ++ ++ return { ++ resolve: (value) => { ++ cleanup(); ++ resolve(value); ++ }, ++ reject: (err) => { ++ cleanup(); ++ reject(err); ++ }, ++ promise, ++ }; ++} ++ ++describe('TLS callback exception handling', () => { ++ ++ // Test 1: PSK server callback returning invalid type should emit tlsClientError ++ it('pskCallback returning invalid type emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ ciphers: CIPHERS, ++ pskCallback: () => { ++ // Return invalid type (string instead of object/Buffer) ++ return 'invalid-should-be-object-or-buffer'; ++ }, ++ pskIdentityHint: 'test-hint', ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ ciphers: CIPHERS, ++ checkServerIdentity: () => {}, ++ pskCallback: () => ({ ++ psk: Buffer.alloc(32), ++ identity: 'test-identity', ++ }), ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); ++ ++ // Test 2: PSK server callback throwing should emit tlsClientError ++ it('pskCallback throwing emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ ciphers: CIPHERS, ++ pskCallback: () => { ++ throw new Error('Intentional callback error'); ++ }, ++ pskIdentityHint: 'test-hint', ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.message, 'Intentional callback error'); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ ciphers: CIPHERS, ++ checkServerIdentity: () => {}, ++ pskCallback: () => ({ ++ psk: Buffer.alloc(32), ++ identity: 'test-identity', ++ }), ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); ++ ++ // Test 3: ALPN callback returning non-matching protocol should emit tlsClientError ++ it('ALPNCallback returning invalid result emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ key: fixtures.readKey('agent2-key.pem'), ++ cert: fixtures.readKey('agent2-cert.pem'), ++ ALPNCallback: () => { ++ // Return a protocol not in the client's list ++ return 'invalid-protocol-not-in-list'; ++ }, ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.code, 'ERR_TLS_ALPN_CALLBACK_INVALID_RESULT'); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ rejectUnauthorized: false, ++ ALPNProtocols: ['http/1.1', 'h2'], ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); ++ ++ // Test 4: ALPN callback throwing should emit tlsClientError ++ it('ALPNCallback throwing emits tlsClientError', async (t) => { ++ const server = tls.createServer({ ++ key: fixtures.readKey('agent2-key.pem'), ++ cert: fixtures.readKey('agent2-cert.pem'), ++ ALPNCallback: () => { ++ throw new Error('Intentional ALPN callback error'); ++ }, ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('tlsClientError', common.mustCall((err, socket) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.message, 'Intentional ALPN callback error'); ++ socket.destroy(); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ rejectUnauthorized: false, ++ ALPNProtocols: ['http/1.1'], ++ }); ++ ++ client.on('error', () => {}); ++ ++ await promise; ++ }); ++ ++ // Test 5: PSK client callback returning invalid type should emit error event ++ it('client pskCallback returning invalid type emits error', async (t) => { ++ const PSK = Buffer.alloc(32); ++ ++ const server = tls.createServer({ ++ ciphers: CIPHERS, ++ pskCallback: () => PSK, ++ pskIdentityHint: 'test-hint', ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ ciphers: CIPHERS, ++ checkServerIdentity: () => {}, ++ pskCallback: () => { ++ // Return invalid type - should cause validation error ++ return 'invalid-should-be-object'; ++ }, ++ }); ++ ++ client.on('error', common.mustCall((err) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE'); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ await promise; ++ }); ++ ++ // Test 6: PSK client callback throwing should emit error event ++ it('client pskCallback throwing emits error', async (t) => { ++ const PSK = Buffer.alloc(32); ++ ++ const server = tls.createServer({ ++ ciphers: CIPHERS, ++ pskCallback: () => PSK, ++ pskIdentityHint: 'test-hint', ++ }); ++ ++ t.after(() => server.close()); ++ ++ const { promise, resolve, reject } = createTestPromise(); ++ ++ server.on('secureConnection', common.mustNotCall(() => { ++ reject(new Error('secureConnection should not fire')); ++ })); ++ ++ await new Promise((res) => server.listen(0, res)); ++ ++ const client = tls.connect({ ++ port: server.address().port, ++ host: '127.0.0.1', ++ ciphers: CIPHERS, ++ checkServerIdentity: () => {}, ++ pskCallback: () => { ++ throw new Error('Intentional client PSK callback error'); ++ }, ++ }); ++ ++ client.on('error', common.mustCall((err) => { ++ try { ++ assert.ok(err instanceof Error); ++ assert.strictEqual(err.message, 'Intentional client PSK callback error'); ++ resolve(); ++ } catch (e) { ++ reject(e); ++ } ++ })); ++ ++ await promise; ++ }); ++}); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/34-lib-add-tlssocket-default-error-handler.patch nodejs-20.19.2+dfsg/debian/patches/sec/34-lib-add-tlssocket-default-error-handler.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/34-lib-add-tlssocket-default-error-handler.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/34-lib-add-tlssocket-default-error-handler.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,41 @@ +From 1febc48d5b0bcf9070085589d9cc586d74053f9c Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Fri, 31 Oct 2025 16:27:48 -0300 +Subject: [PATCH] lib: add TLSSocket default error handler + +This prevents the server from crashing due to an unhandled rejection +when a TLSSocket connection is abruptly destroyed during initialization +and the user has not attached an error handler to the socket. +e.g: + +```js +const server = http2.createSecureServer({ ... }) +server.on('secureConnection', socket => { + socket.on('error', err => { + console.log(err) + }) +}) +``` + +PR-URL: https://github.com/nodejs-private/node-private/pull/797 +Fixes: https://github.com/nodejs/node/issues/44751 +Refs: https://hackerone.com/bugs?subject=nodejs&report_id=3262404 +Reviewed-By: Matteo Collina +Reviewed-By: Anna Henningsen +CVE-ID: CVE-2025-59465 +--- + lib/_tls_wrap.js | 1 + + 1 file changed, 1 insertion(+) + +diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js +index c3e48a6cbc81ae..d9c7e32174d558 100644 +--- a/lib/_tls_wrap.js ++++ b/lib/_tls_wrap.js +@@ -1268,6 +1268,7 @@ function tlsConnectionListener(rawSocket) { + socket[kErrorEmitted] = false; + socket.on('close', onSocketClose); + socket.on('_tlsError', onSocketTLSError); ++ socket.on('error', onSocketTLSError); + } + + // AUTHENTICATION MODES diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch nodejs-20.19.2+dfsg/debian/patches/sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,172 @@ +From 14fbbb510c6d62b4510e3f48ee801807d9a5fbab Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Tue, 21 Oct 2025 18:25:31 -0300 +Subject: [PATCH] lib: disable futimes when permission model is enabled + +Refs: https://hackerone.com/reports/3390084 +PR-URL: https://github.com/nodejs-private/node-private/pull/748 +Reviewed-By: Matteo Collina +Reviewed-By: Anna Henningsen +CVE-ID: CVE-2025-55132 +PR-URL: https://github.com/nodejs-private/node-private/pull/802 +Reviewed-By: Rafael Gonzaga +CVE-ID: CVE-2025-55132 +--- + lib/fs.js | 24 ++++++++++ + test/fixtures/permission/fs-write.js | 45 +++++++++++++++++++ + test/parallel/test-permission-fs-supported.js | 17 ++++++- + 3 files changed, 85 insertions(+), 1 deletion(-) + +diff --git a/lib/fs.js b/lib/fs.js +index 05be1f18410037..0ee3ec59069189 100644 +--- a/lib/fs.js ++++ b/lib/fs.js +@@ -1275,6 +1275,11 @@ function rmSync(path, options) { + function fdatasync(fd, callback) { + const req = new FSReqCallback(); + req.oncomplete = makeCallback(callback); ++ ++ if (permission.isEnabled()) { ++ callback(new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.')); ++ return; ++ } + binding.fdatasync(fd, req); + } + +@@ -1286,6 +1291,9 @@ function fdatasync(fd, callback) { + * @returns {void} + */ + function fdatasyncSync(fd) { ++ if (permission.isEnabled()) { ++ throw new ERR_ACCESS_DENIED('fdatasync API is disabled when Permission Model is enabled.'); ++ } + binding.fdatasync(fd); + } + +@@ -1299,6 +1307,10 @@ function fdatasyncSync(fd) { + function fsync(fd, callback) { + const req = new FSReqCallback(); + req.oncomplete = makeCallback(callback); ++ if (permission.isEnabled()) { ++ callback(new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.')); ++ return; ++ } + binding.fsync(fd, req); + } + +@@ -1309,6 +1321,9 @@ function fsync(fd, callback) { + * @returns {void} + */ + function fsyncSync(fd) { ++ if (permission.isEnabled()) { ++ throw new ERR_ACCESS_DENIED('fsync API is disabled when Permission Model is enabled.'); ++ } + binding.fsync(fd); + } + +@@ -2165,6 +2180,11 @@ function futimes(fd, atime, mtime, callback) { + mtime = toUnixTimestamp(mtime, 'mtime'); + callback = makeCallback(callback); + ++ if (permission.isEnabled()) { ++ callback(new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.')); ++ return; ++ } ++ + const req = new FSReqCallback(); + req.oncomplete = callback; + binding.futimes(fd, atime, mtime, req); +@@ -2180,6 +2200,10 @@ function futimes(fd, atime, mtime, callback) { + * @returns {void} + */ + function futimesSync(fd, atime, mtime) { ++ if (permission.isEnabled()) { ++ throw new ERR_ACCESS_DENIED('futimes API is disabled when Permission Model is enabled.'); ++ } ++ + binding.futimes( + fd, + toUnixTimestamp(atime, 'atime'), +diff --git a/test/fixtures/permission/fs-write.js b/test/fixtures/permission/fs-write.js +index 5dd3b07ed9a0cf..5461a21aa234f2 100644 +--- a/test/fixtures/permission/fs-write.js ++++ b/test/fixtures/permission/fs-write.js +@@ -553,4 +553,49 @@ const relativeProtectedFolder = process.env.RELATIVEBLOCKEDFOLDER; + }, { + code: 'ERR_ACCESS_DENIED', + }); ++} ++ ++// fs.utimes with read-only fd ++{ ++ assert.throws(() => { ++ // blocked file is allowed to read ++ const fd = fs.openSync(blockedFile, 'r'); ++ const date = new Date(); ++ date.setFullYear(2100,0,1); ++ ++ fs.futimes(fd, date, date, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ })); ++ fs.futimesSync(fd, date, date); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ }); ++} ++ ++// fs.fdatasync with read-only fd ++{ ++ assert.throws(() => { ++ // blocked file is allowed to read ++ const fd = fs.openSync(blockedFile, 'r'); ++ fs.fdatasync(fd, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ })); ++ fs.fdatasyncSync(fd); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ }); ++} ++ ++// fs.fsync with read-only fd ++{ ++ assert.throws(() => { ++ // blocked file is allowed to read ++ const fd = fs.openSync(blockedFile, 'r'); ++ fs.fsync(fd, common.expectsError({ ++ code: 'ERR_ACCESS_DENIED', ++ })); ++ fs.fsyncSync(fd); ++ }, { ++ code: 'ERR_ACCESS_DENIED', ++ }); + } +\ No newline at end of file +diff --git a/test/parallel/test-permission-fs-supported.js b/test/parallel/test-permission-fs-supported.js +index 1062117798b800..805365f28b3bc1 100644 +--- a/test/parallel/test-permission-fs-supported.js ++++ b/test/parallel/test-permission-fs-supported.js +@@ -77,7 +77,22 @@ const ignoreList = [ + 'unwatchFile', + ...syncAndAsyncAPI('lstat'), + ...syncAndAsyncAPI('realpath'), +- // fd required methods ++ // File descriptor–based metadata operations ++ // ++ // The kernel does not allow opening a file descriptor for an inode ++ // with write access if the inode itself is read-only. However, it still ++ // permits modifying the inode’s metadata (e.g., permission bits, ownership, ++ // timestamps) because you own the file. These changes can be made either ++ // by referring to the file by name (e.g., chmod) or through any existing ++ // file descriptor that identifies the same inode (e.g., fchmod). ++ // ++ // If the kernel required write access to change metadata, it would be ++ // impossible to modify the permissions of a file once it was made read-only. ++ // For that reason, syscalls such as fchmod, fchown, and futimes bypass ++ // the file descriptor’s access mode. Even a read-only ('r') descriptor ++ // can still update metadata. To prevent unintended modifications, ++ // these APIs are therefore blocked by default when permission model is ++ // enabled. + ...syncAndAsyncAPI('close'), + ...syncAndAsyncAPI('fchown'), + ...syncAndAsyncAPI('fchmod'), diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch nodejs-20.19.2+dfsg/debian/patches/sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,307 @@ +From 494f62dc230b407461f4a2de893a7c6a44273d08 Mon Sep 17 00:00:00 2001 +From: RafaelGSS +Date: Mon, 10 Nov 2025 19:27:51 -0300 +Subject: [PATCH] lib,permission: require full read and write to symlink APIs + +Refs: https://hackerone.com/reports/3417819 +PR-URL: https://github.com/nodejs-private/node-private/pull/760 +Reviewed-By: Matteo Collina +CVE-ID: CVE-2025-55130 +Signed-off-by: RafaelGSS +--- + lib/fs.js | 34 ++++++------------- + lib/internal/fs/promises.js | 20 +++-------- + .../permission/fs-symlink-target-write.js | 18 ++-------- + test/fixtures/permission/fs-symlink.js | 18 ++++++++-- + .../test-permission-fs-symlink-relative.js | 10 +++--- + test/parallel/test-permission-fs-symlink.js | 14 ++++++++ + 6 files changed, 52 insertions(+), 62 deletions(-) + +diff --git a/lib/fs.js b/lib/fs.js +index 0ee3ec59069189..655f14c303a7ce 100644 +--- a/lib/fs.js ++++ b/lib/fs.js +@@ -59,7 +59,6 @@ const { + } = constants; + + const pathModule = require('path'); +-const { isAbsolute } = pathModule; + const { isArrayBufferView } = require('internal/util/types'); + + const binding = internalBinding('fs'); +@@ -1745,18 +1744,12 @@ function symlink(target, path, type_, callback_) { + const type = (typeof type_ === 'string' ? type_ : null); + const callback = makeCallback(arguments[arguments.length - 1]); + +- if (permission.isEnabled()) { +- // The permission model's security guarantees fall apart in the presence of +- // relative symbolic links. Thus, we have to prevent their creation. +- if (BufferIsBuffer(target)) { +- if (!isAbsolute(BufferToString(target))) { +- callback(new ERR_ACCESS_DENIED('relative symbolic link target')); +- return; +- } +- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { +- callback(new ERR_ACCESS_DENIED('relative symbolic link target')); +- return; +- } ++ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass ++ // the permission model security guarantees. Thus, this API is disabled unless fs.read ++ // and fs.write permission has been given. ++ if (permission.isEnabled() && !permission.has('fs')) { ++ callback(new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.')); ++ return; + } + + target = getValidatedPath(target, 'target'); +@@ -1816,16 +1809,11 @@ function symlinkSync(target, path, type) { + } + } + +- if (permission.isEnabled()) { +- // The permission model's security guarantees fall apart in the presence of +- // relative symbolic links. Thus, we have to prevent their creation. +- if (BufferIsBuffer(target)) { +- if (!isAbsolute(BufferToString(target))) { +- throw new ERR_ACCESS_DENIED('relative symbolic link target'); +- } +- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { +- throw new ERR_ACCESS_DENIED('relative symbolic link target'); +- } ++ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass ++ // the permission model security guarantees. Thus, this API is disabled unless fs.read ++ // and fs.write permission has been given. ++ if (permission.isEnabled() && !permission.has('fs')) { ++ throw new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.'); + } + + target = getValidatedPath(target, 'target'); +diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js +index 459b9ad1318901..d96584bcab265b 100644 +--- a/lib/internal/fs/promises.js ++++ b/lib/internal/fs/promises.js +@@ -17,7 +17,6 @@ const { + Symbol, + Uint8Array, + FunctionPrototypeBind, +- uncurryThis, + } = primordials; + + const { fs: constants } = internalBinding('constants'); +@@ -31,8 +30,6 @@ const { + + const binding = internalBinding('fs'); + const { Buffer } = require('buffer'); +-const { isBuffer: BufferIsBuffer } = Buffer; +-const BufferToString = uncurryThis(Buffer.prototype.toString); + + const { + codes: { +@@ -88,8 +85,6 @@ const { + kValidateObjectAllowNullable, + } = require('internal/validators'); + const pathModule = require('path'); +-const { isAbsolute } = pathModule; +-const { toPathIfFileURL } = require('internal/url'); + const { + kEmptyObject, + lazyDOMException, +@@ -987,16 +982,11 @@ async function symlink(target, path, type_) { + } + } + +- if (permission.isEnabled()) { +- // The permission model's security guarantees fall apart in the presence of +- // relative symbolic links. Thus, we have to prevent their creation. +- if (BufferIsBuffer(target)) { +- if (!isAbsolute(BufferToString(target))) { +- throw new ERR_ACCESS_DENIED('relative symbolic link target'); +- } +- } else if (typeof target !== 'string' || !isAbsolute(toPathIfFileURL(target))) { +- throw new ERR_ACCESS_DENIED('relative symbolic link target'); +- } ++ // Due to the nature of Node.js runtime, symlinks has different edge cases that can bypass ++ // the permission model security guarantees. Thus, this API is disabled unless fs.read ++ // and fs.write permission has been given. ++ if (permission.isEnabled() && !permission.has('fs')) { ++ throw new ERR_ACCESS_DENIED('fs.symlink API requires full fs.read and fs.write permissions.'); + } + + target = getValidatedPath(target, 'target'); +diff --git a/test/fixtures/permission/fs-symlink-target-write.js b/test/fixtures/permission/fs-symlink-target-write.js +index c17d674d59ee97..6e07bfa838e2f5 100644 +--- a/test/fixtures/permission/fs-symlink-target-write.js ++++ b/test/fixtures/permission/fs-symlink-target-write.js +@@ -26,8 +26,7 @@ const writeOnlyFolder = process.env.WRITEONLYFOLDER; + fs.symlinkSync(path.join(readOnlyFolder, 'file'), path.join(readWriteFolder, 'link-to-read-only'), 'file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemWrite', +- resource: path.toNamespacedPath(path.join(readOnlyFolder, 'file')), ++ message: 'fs.symlink API requires full fs.read and fs.write permissions.', + })); + assert.throws(() => { + fs.linkSync(path.join(readOnlyFolder, 'file'), path.join(readWriteFolder, 'link-to-read-only')); +@@ -37,18 +36,6 @@ const writeOnlyFolder = process.env.WRITEONLYFOLDER; + resource: path.toNamespacedPath(path.join(readOnlyFolder, 'file')), + })); + +- // App will be able to symlink to a writeOnlyFolder +- fs.symlink(path.join(readWriteFolder, 'file'), path.join(writeOnlyFolder, 'link-to-read-write'), 'file', (err) => { +- assert.ifError(err); +- // App will won't be able to read the symlink +- fs.readFile(path.join(writeOnlyFolder, 'link-to-read-write'), common.expectsError({ +- code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemRead', +- })); +- +- // App will be able to write to the symlink +- fs.writeFile(path.join(writeOnlyFolder, 'link-to-read-write'), 'some content', common.mustSucceed()); +- }); + fs.link(path.join(readWriteFolder, 'file'), path.join(writeOnlyFolder, 'link-to-read-write2'), (err) => { + assert.ifError(err); + // App will won't be able to read the link +@@ -66,8 +53,7 @@ const writeOnlyFolder = process.env.WRITEONLYFOLDER; + fs.symlinkSync(path.join(readWriteFolder, 'file'), path.join(readOnlyFolder, 'link-to-read-only'), 'file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemWrite', +- resource: path.toNamespacedPath(path.join(readOnlyFolder, 'link-to-read-only')), ++ message: 'fs.symlink API requires full fs.read and fs.write permissions.', + })); + assert.throws(() => { + fs.linkSync(path.join(readWriteFolder, 'file'), path.join(readOnlyFolder, 'link-to-read-only')); +diff --git a/test/fixtures/permission/fs-symlink.js b/test/fixtures/permission/fs-symlink.js +index 4cf3b45f0ebcfb..ba60f7811bdde5 100644 +--- a/test/fixtures/permission/fs-symlink.js ++++ b/test/fixtures/permission/fs-symlink.js +@@ -54,7 +54,6 @@ const symlinkFromBlockedFile = process.env.EXISTINGSYMLINK; + fs.readFileSync(blockedFile); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemRead', + })); + assert.throws(() => { + fs.appendFileSync(blockedFile, 'data'); +@@ -68,7 +67,6 @@ const symlinkFromBlockedFile = process.env.EXISTINGSYMLINK; + fs.symlinkSync(regularFile, blockedFolder + '/asdf', 'file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemWrite', + })); + assert.throws(() => { + fs.linkSync(regularFile, blockedFolder + '/asdf'); +@@ -82,7 +80,6 @@ const symlinkFromBlockedFile = process.env.EXISTINGSYMLINK; + fs.symlinkSync(blockedFile, path.join(__dirname, '/asdf'), 'file'); + }, common.expectsError({ + code: 'ERR_ACCESS_DENIED', +- permission: 'FileSystemRead', + })); + assert.throws(() => { + fs.linkSync(blockedFile, path.join(__dirname, '/asdf')); +@@ -90,4 +87,19 @@ const symlinkFromBlockedFile = process.env.EXISTINGSYMLINK; + code: 'ERR_ACCESS_DENIED', + permission: 'FileSystemRead', + })); ++} ++ ++// fs.symlink API is blocked by default ++{ ++ assert.throws(() => { ++ fs.symlinkSync(regularFile, regularFile); ++ }, common.expectsError({ ++ message: 'fs.symlink API requires full fs.read and fs.write permissions.', ++ code: 'ERR_ACCESS_DENIED', ++ })); ++ ++ fs.symlink(regularFile, regularFile, common.expectsError({ ++ message: 'fs.symlink API requires full fs.read and fs.write permissions.', ++ code: 'ERR_ACCESS_DENIED', ++ })); + } +\ No newline at end of file +diff --git a/test/parallel/test-permission-fs-symlink-relative.js b/test/parallel/test-permission-fs-symlink-relative.js +index 4cc7d920593c23..9080f16c663130 100644 +--- a/test/parallel/test-permission-fs-symlink-relative.js ++++ b/test/parallel/test-permission-fs-symlink-relative.js +@@ -1,4 +1,4 @@ +-// Flags: --experimental-permission --allow-fs-read=* --allow-fs-write=* ++// Flags: --experimental-permission --allow-fs-read=* + 'use strict'; + + const common = require('../common'); +@@ -10,7 +10,7 @@ const { symlinkSync, symlink, promises: { symlink: symlinkAsync } } = require('f + + const error = { + code: 'ERR_ACCESS_DENIED', +- message: /relative symbolic link target/, ++ message: /symlink API requires full fs\.read and fs\.write permissions/, + }; + + for (const targetString of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', 'ntfs:alternate']) { +@@ -27,14 +27,14 @@ for (const targetString of ['a', './b/c', '../d', 'e/../f', 'C:drive-relative', + } + } + +-// Absolute should not throw ++// Absolute should throw too + for (const targetString of [path.resolve('.')]) { + for (const target of [targetString, Buffer.from(targetString)]) { + for (const path of [__filename]) { + symlink(target, path, common.mustCall((err) => { + assert(err); +- assert.strictEqual(err.code, 'EEXIST'); +- assert.match(err.message, /file already exists/); ++ assert.strictEqual(err.code, error.code); ++ assert.match(err.message, error.message); + })); + } + } +diff --git a/test/parallel/test-permission-fs-symlink.js b/test/parallel/test-permission-fs-symlink.js +index c7d753c267c1e7..268a8ecb9aad43 100644 +--- a/test/parallel/test-permission-fs-symlink.js ++++ b/test/parallel/test-permission-fs-symlink.js +@@ -21,15 +21,26 @@ const commonPathWildcard = path.join(__filename, '../../common*'); + const blockedFile = fixtures.path('permission', 'deny', 'protected-file.md'); + const blockedFolder = tmpdir.resolve('subdirectory'); + const symlinkFromBlockedFile = tmpdir.resolve('example-symlink.md'); ++const allowedFolder = tmpdir.resolve('allowed-folder'); ++const traversalSymlink = path.join(allowedFolder, 'deep1', 'deep2', 'deep3', 'gotcha'); + + { + tmpdir.refresh(); + fs.mkdirSync(blockedFolder); ++ // Create deep directory structure for path traversal test ++ fs.mkdirSync(allowedFolder); ++ fs.writeFileSync(path.resolve(allowedFolder, '../protected-file.md'), 'protected'); ++ fs.mkdirSync(path.join(allowedFolder, 'deep1')); ++ fs.mkdirSync(path.join(allowedFolder, 'deep1', 'deep2')); ++ fs.mkdirSync(path.join(allowedFolder, 'deep1', 'deep2', 'deep3')); + } + + { + // Symlink previously created ++ // fs.symlink API is allowed when full-read and full-write access + fs.symlinkSync(blockedFile, symlinkFromBlockedFile); ++ // Create symlink for path traversal test - symlink points to parent directory ++ fs.symlinkSync(allowedFolder, traversalSymlink); + } + + { +@@ -38,6 +49,7 @@ const symlinkFromBlockedFile = tmpdir.resolve('example-symlink.md'); + [ + '--experimental-permission', + `--allow-fs-read=${file}`, `--allow-fs-read=${commonPathWildcard}`, `--allow-fs-read=${symlinkFromBlockedFile}`, ++ `--allow-fs-read=${allowedFolder}`, + `--allow-fs-write=${symlinkFromBlockedFile}`, + file, + ], +@@ -47,6 +59,8 @@ const symlinkFromBlockedFile = tmpdir.resolve('example-symlink.md'); + BLOCKEDFOLDER: blockedFolder, + BLOCKEDFILE: blockedFile, + EXISTINGSYMLINK: symlinkFromBlockedFile, ++ TRAVERSALSYMLINK: traversalSymlink, ++ ALLOWEDFOLDER: allowedFolder, + }, + } + ); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch nodejs-20.19.2+dfsg/debian/patches/sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,475 @@ +From d7a5c587c02ebe18f9fe4de986bac55d80c2868f Mon Sep 17 00:00:00 2001 +From: Matteo Collina +Date: Tue, 9 Dec 2025 23:50:18 +0100 +Subject: [PATCH] src: rethrow stack overflow exceptions in async_hooks + +When a stack overflow exception occurs during async_hooks callbacks +(which use TryCatchScope::kFatal), detect the specific "Maximum call +stack size exceeded" RangeError and re-throw it instead of immediately +calling FatalException. This allows user code to catch the exception +with try-catch blocks instead of requiring uncaughtException handlers. + +The implementation adds IsStackOverflowError() helper to detect stack +overflow RangeErrors and re-throws them in TryCatchScope destructor +instead of calling FatalException. + +This fixes the issue where async_hooks would cause stack overflow +exceptions to exit with code 7 (kExceptionInFatalExceptionHandler) +instead of being catchable. + +Fixes: https://github.com/nodejs/node/issues/37989 +Ref: https://hackerone.com/reports/3456295 +PR-URL: https://github.com/nodejs-private/node-private/pull/773 +Refs: https://hackerone.com/reports/3456295 +Reviewed-By: Robert Nagy +Reviewed-By: Paolo Insogna +Reviewed-By: Marco Ippolito +Reviewed-By: Rafael Gonzaga +Reviewed-By: Anna Henningsen +CVE-ID: CVE-2025-59466 +--- + src/async_wrap.cc | 9 ++- + src/debug_utils.cc | 3 +- + src/node_errors.cc | 71 ++++++++++++++-- + src/node_errors.h | 2 +- + src/node_report.cc | 3 +- + ...async-hooks-stack-overflow-nested-async.js | 80 +++++++++++++++++++ + ...st-async-hooks-stack-overflow-try-catch.js | 47 +++++++++++ + .../test-async-hooks-stack-overflow.js | 47 +++++++++++ + ...andler-stack-overflow-on-stack-overflow.js | 29 +++++++ + ...caught-exception-handler-stack-overflow.js | 29 +++++++ + 10 files changed, 306 insertions(+), 14 deletions(-) + create mode 100644 test/parallel/test-async-hooks-stack-overflow-nested-async.js + create mode 100644 test/parallel/test-async-hooks-stack-overflow-try-catch.js + create mode 100644 test/parallel/test-async-hooks-stack-overflow.js + create mode 100644 test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js + create mode 100644 test/parallel/test-uncaught-exception-handler-stack-overflow.js + +--- a/src/async_wrap.cc ++++ b/src/async_wrap.cc +@@ -67,7 +67,8 @@ + void AsyncWrap::DestroyAsyncIdsCallback(Environment* env) { + Local fn = env->async_hooks_destroy_function(); + +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + + do { + std::vector destroy_async_id_list; +@@ -96,7 +97,8 @@ + + HandleScope handle_scope(env->isolate()); + Local async_id_value = Number::New(env->isolate(), async_id); +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + USE(fn->Call(env->context(), Undefined(env->isolate()), 1, &async_id_value)); + } + +@@ -668,7 +670,8 @@ + object, + }; + +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + USE(init_fn->Call(env->context(), object, arraysize(argv), argv)); + } + +--- a/src/debug_utils.cc ++++ b/src/debug_utils.cc +@@ -333,7 +333,8 @@ + } + + Local stack; +- if (!GetCurrentStackTrace(isolate).ToLocal(&stack)) { ++ if (!GetCurrentStackTrace(isolate).ToLocal(&stack) || ++ stack->GetFrameCount() == 0) { + return; + } + +--- a/src/node_errors.cc ++++ b/src/node_errors.cc +@@ -193,7 +193,7 @@ + } + + static std::atomic is_in_oom{false}; +-static std::atomic is_retrieving_js_stacktrace{false}; ++static thread_local std::atomic is_retrieving_js_stacktrace{false}; + MaybeLocal GetCurrentStackTrace(Isolate* isolate, int frame_count) { + if (isolate == nullptr) { + return MaybeLocal(); +@@ -221,9 +221,6 @@ + StackTrace::CurrentStackTrace(isolate, frame_count, options); + + is_retrieving_js_stacktrace.store(false); +- if (stack->GetFrameCount() == 0) { +- return MaybeLocal(); +- } + + return scope.Escape(stack); + } +@@ -298,7 +295,8 @@ + + void PrintCurrentStackTrace(Isolate* isolate, StackTracePrefix prefix) { + Local stack; +- if (GetCurrentStackTrace(isolate).ToLocal(&stack)) { ++ if (GetCurrentStackTrace(isolate).ToLocal(&stack) && ++ stack->GetFrameCount() > 0) { + PrintStackTrace(isolate, stack, prefix); + } + } +@@ -669,13 +667,52 @@ + }; + } + ++// Check if an exception is a stack overflow error (RangeError with ++// "Maximum call stack size exceeded" message). This is used to handle ++// stack overflow specially in TryCatchScope - instead of immediately ++// exiting, we can use the red zone to re-throw to user code. ++static bool IsStackOverflowError(Isolate* isolate, Local exception) { ++ if (!exception->IsNativeError()) return false; ++ ++ Local err_obj = exception.As(); ++ Local constructor_name = err_obj->GetConstructorName(); ++ ++ // Must be a RangeError ++ Utf8Value name(isolate, constructor_name); ++ if (name.ToStringView() != "RangeError") return false; ++ ++ // Check for the specific stack overflow message ++ Local context = isolate->GetCurrentContext(); ++ Local message_val; ++ if (!err_obj->Get(context, String::NewFromUtf8Literal(isolate, "message")) ++ .ToLocal(&message_val)) { ++ return false; ++ } ++ ++ if (!message_val->IsString()) return false; ++ ++ Utf8Value message(isolate, message_val.As()); ++ return message.ToStringView() == "Maximum call stack size exceeded"; ++} ++ + namespace errors { + + TryCatchScope::~TryCatchScope() { +- if (HasCaught() && !HasTerminated() && mode_ == CatchMode::kFatal) { ++ if (HasCaught() && !HasTerminated() && mode_ != CatchMode::kNormal) { + HandleScope scope(env_->isolate()); + Local exception = Exception(); + Local message = Message(); ++ ++ // Special handling for stack overflow errors in async_hooks: instead of ++ // immediately exiting, re-throw the exception. This allows the exception ++ // to propagate to user code's try-catch blocks. ++ if (mode_ == CatchMode::kFatalRethrowStackOverflow && ++ IsStackOverflowError(env_->isolate(), exception)) { ++ ReThrow(); ++ Reset(); ++ return; ++ } ++ + EnhanceFatalException enhance = CanContinue() ? + EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance; + if (message.IsEmpty()) +@@ -1230,8 +1267,26 @@ + if (env->can_call_into_js()) { + // We do not expect the global uncaught exception itself to throw any more + // exceptions. If it does, exit the current Node.js instance. +- errors::TryCatchScope try_catch(env, +- errors::TryCatchScope::CatchMode::kFatal); ++ // Special case: if the original error was a stack overflow and calling ++ // _fatalException causes another stack overflow, rethrow it to allow ++ // user code's try-catch blocks to potentially catch it. ++ auto is_stack_overflow = [&] { ++ return IsStackOverflowError(env->isolate(), error); ++ }; ++ // Without a JS stack, rethrowing may or may not do anything. ++ // TODO(addaleax): In V8, expose a way to check whether there is a JS stack ++ // or TryCatch that would capture the rethrown exception. ++ auto has_js_stack = [&] { ++ HandleScope handle_scope(env->isolate()); ++ Local stack; ++ return GetCurrentStackTrace(env->isolate(), 1).ToLocal(&stack) && ++ stack->GetFrameCount() > 0; ++ }; ++ errors::TryCatchScope::CatchMode mode = ++ is_stack_overflow() && has_js_stack() ++ ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow ++ : errors::TryCatchScope::CatchMode::kFatal; ++ errors::TryCatchScope try_catch(env, mode); + // Explicitly disable verbose exception reporting - + // if process._fatalException() throws an error, we don't want it to + // trigger the per-isolate message listener which will call this +--- a/src/node_errors.h ++++ b/src/node_errors.h +@@ -265,7 +265,7 @@ + + class TryCatchScope : public v8::TryCatch { + public: +- enum class CatchMode { kNormal, kFatal }; ++ enum class CatchMode { kNormal, kFatal, kFatalRethrowStackOverflow }; + + explicit TryCatchScope(Environment* env, CatchMode mode = CatchMode::kNormal) + : v8::TryCatch(env->isolate()), env_(env), mode_(mode) {} +--- a/src/node_report.cc ++++ b/src/node_report.cc +@@ -465,7 +465,8 @@ + const char* trigger) { + HandleScope scope(isolate); + Local stack; +- if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack)) { ++ if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack) || ++ stack->GetFrameCount() == 0) { + PrintEmptyJavaScriptStack(writer); + return; + } +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow-nested-async.js +@@ -0,0 +1,80 @@ ++'use strict'; ++ ++// This test verifies that stack overflow during deeply nested async operations ++// with async_hooks enabled can be caught by try-catch. This simulates real-world ++// scenarios like processing deeply nested JSON structures where each level ++// creates async operations (e.g., database calls, API requests). ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ // Enable async_hooks with all callbacks (simulates APM tools) ++ createHook({ ++ init() {}, ++ before() {}, ++ after() {}, ++ destroy() {}, ++ promiseResolve() {}, ++ }).enable(); ++ ++ // Simulate an async operation (like a database call or API request) ++ async function fetchThing(id) { ++ return { id, data: `data-${id}` }; ++ } ++ ++ // Recursively process deeply nested data structure ++ // This will cause stack overflow when the nesting is deep enough ++ function processData(data, depth = 0) { ++ if (Array.isArray(data)) { ++ for (const item of data) { ++ // Create a promise to trigger async_hooks init callback ++ fetchThing(depth); ++ processData(item, depth + 1); ++ } ++ } ++ } ++ ++ // Create deeply nested array structure iteratively (to avoid stack overflow ++ // during creation) ++ function createNestedArray(depth) { ++ let result = 'leaf'; ++ for (let i = 0; i < depth; i++) { ++ result = [result]; ++ } ++ return result; ++ } ++ ++ // Create a very deep nesting that will cause stack overflow during processing ++ const deeplyNested = createNestedArray(50000); ++ ++ try { ++ processData(deeplyNested); ++ // Should not complete successfully - the nesting is too deep ++ console.log('UNEXPECTED: Processing completed without error'); ++ process.exit(1); ++ } catch (err) { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ console.log('SUCCESS: try-catch caught the stack overflow in nested async'); ++ process.exit(0); ++ } ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit successfully (try-catch worked) ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++ // Verify the error was handled by try-catch ++ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); ++} +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow-try-catch.js +@@ -0,0 +1,47 @@ ++'use strict'; ++ ++// This test verifies that when a stack overflow occurs with async_hooks ++// enabled, the exception can be caught by try-catch blocks in user code. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ createHook({ init() {} }).enable(); ++ ++ function recursive(depth = 0) { ++ // Create a promise to trigger async_hooks init callback ++ new Promise(() => {}); ++ return recursive(depth + 1); ++ } ++ ++ try { ++ recursive(); ++ // Should not reach here ++ process.exit(1); ++ } catch (err) { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ console.log('SUCCESS: try-catch caught the stack overflow'); ++ process.exit(0); ++ } ++ ++ // Should not reach here ++ process.exit(2); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0 (try-catch worked), got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); ++} +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow.js +@@ -0,0 +1,47 @@ ++'use strict'; ++ ++// This test verifies that when a stack overflow occurs with async_hooks ++// enabled, the uncaughtException handler is still called instead of the ++// process crashing with exit code 7. ++ ++const common = require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ let handlerCalled = false; ++ ++ function recursive() { ++ // Create a promise to trigger async_hooks init callback ++ new Promise(() => {}); ++ return recursive(); ++ } ++ ++ createHook({ init() {} }).enable(); ++ ++ process.on('uncaughtException', common.mustCall((err) => { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ // Ensure handler is only called once ++ assert.strictEqual(handlerCalled, false); ++ handlerCalled = true; ++ })); ++ ++ setImmediate(recursive); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with code 0 (handler was called and handled the exception) ++ // Previously would exit with code 7 (kExceptionInFatalExceptionHandler) ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} +--- /dev/null ++++ b/test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js +@@ -0,0 +1,29 @@ ++'use strict'; ++ ++// This test verifies that when the uncaughtException handler itself causes ++// a stack overflow, the process exits with a non-zero exit code. ++// This is important to ensure we don't silently swallow errors. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ function f() { f(); } ++ process.on('uncaughtException', f); ++ f(); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with non-zero exit code since the uncaughtException handler ++ // itself caused a stack overflow. ++ assert.notStrictEqual(result.status, 0, ++ `Expected non-zero exit code, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} +--- /dev/null ++++ b/test/parallel/test-uncaught-exception-handler-stack-overflow.js +@@ -0,0 +1,29 @@ ++'use strict'; ++ ++// This test verifies that when the uncaughtException handler itself causes ++// a stack overflow, the process exits with a non-zero exit code. ++// This is important to ensure we don't silently swallow errors. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ function f() { f(); } ++ process.on('uncaughtException', f); ++ throw new Error('X'); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with non-zero exit code since the uncaughtException handler ++ // itself caused a stack overflow. ++ assert.notStrictEqual(result.status, 0, ++ `Expected non-zero exit code, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch nodejs-20.19.2+dfsg/debian/patches/sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,292 @@ +From 51f4de4b4a52b5b0eb2c63ecbb4126577e05f636 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?=D0=A1=D0=BA=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D0=B4=D0=B0=20?= + =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20=D0=90=D0=BD=D0=B4=D1=80?= + =?UTF-8?q?=D0=B5=D0=B5=D0=B2=D0=B8=D1=87?= +Date: Fri, 7 Nov 2025 11:50:57 -0300 +Subject: [PATCH] src,lib: refactor unsafe buffer creation to remove zero-fill + toggle + +This removes the zero-fill toggle mechanism that allowed JavaScript +to control ArrayBuffer initialization via shared memory. Instead, +unsafe buffer creation now uses a dedicated C++ API. + +Refs: https://hackerone.com/reports/3405778 +Co-Authored-By: Rafael Gonzaga +Co-Authored-By: Joyee Cheung +Signed-off-by: RafaelGSS +PR-URL: https://github.com/nodejs-private/node-private/pull/759 +Backport-PR-URL: https://github.com/nodejs-private/node-private/pull/799 +CVE-ID: CVE-2025-55131 +--- + deps/v8/include/v8-array-buffer.h | 7 +++ + deps/v8/src/api/api.cc | 17 ++++++ + lib/internal/buffer.js | 23 ++------ + lib/internal/process/pre_execution.js | 2 - + src/api/environment.cc | 3 +- + src/node_buffer.cc | 84 ++++++++++++++++----------- + 6 files changed, 82 insertions(+), 54 deletions(-) + +diff --git a/deps/v8/include/v8-array-buffer.h b/deps/v8/include/v8-array-buffer.h +index 804fc42c4b56dd..e03ed1a6fc7fbb 100644 +--- a/deps/v8/include/v8-array-buffer.h ++++ b/deps/v8/include/v8-array-buffer.h +@@ -244,6 +244,13 @@ class V8_EXPORT ArrayBuffer : public Object { + */ + static std::unique_ptr NewBackingStore(Isolate* isolate, + size_t byte_length); ++ /** ++ * Returns a new standalone BackingStore with uninitialized memory and ++ * return nullptr on failure. ++ * This variant is for not breaking ABI on Node.js LTS. DO NOT USE. ++ */ ++ static std::unique_ptr NewBackingStoreForNodeLTS( ++ Isolate* isolate, size_t byte_length); + /** + * Returns a new standalone BackingStore that takes over the ownership of + * the given buffer. The destructor of the BackingStore invokes the given +diff --git a/deps/v8/src/api/api.cc b/deps/v8/src/api/api.cc +index a06394e6c1cd09..da0c960f99ff93 100644 +--- a/deps/v8/src/api/api.cc ++++ b/deps/v8/src/api/api.cc +@@ -8743,6 +8743,23 @@ std::unique_ptr v8::ArrayBuffer::NewBackingStore( + static_cast(backing_store.release())); + } + ++std::unique_ptr v8::ArrayBuffer::NewBackingStoreForNodeLTS( ++ Isolate* v8_isolate, size_t byte_length) { ++ i::Isolate* i_isolate = reinterpret_cast(v8_isolate); ++ API_RCS_SCOPE(i_isolate, ArrayBuffer, NewBackingStore); ++ CHECK_LE(byte_length, i::JSArrayBuffer::kMaxByteLength); ++ ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate); ++ std::unique_ptr backing_store = ++ i::BackingStore::Allocate(i_isolate, byte_length, ++ i::SharedFlag::kNotShared, ++ i::InitializedFlag::kUninitialized); ++ if (!backing_store) { ++ return nullptr; ++ } ++ return std::unique_ptr( ++ static_cast(backing_store.release())); ++} ++ + std::unique_ptr v8::ArrayBuffer::NewBackingStore( + void* data, size_t byte_length, v8::BackingStore::DeleterCallback deleter, + void* deleter_data) { +diff --git a/lib/internal/buffer.js b/lib/internal/buffer.js +index fbe9de249348b3..23df382f14ddf0 100644 +--- a/lib/internal/buffer.js ++++ b/lib/internal/buffer.js +@@ -30,7 +30,7 @@ const { + hexWrite, + ucs2Write, + utf8Write, +- getZeroFillToggle, ++ createUnsafeArrayBuffer, + } = internalBinding('buffer'); + + const { +@@ -1053,26 +1053,14 @@ function markAsUntransferable(obj) { + obj[untransferable_object_private_symbol] = true; + } + +-// A toggle used to access the zero fill setting of the array buffer allocator +-// in C++. +-// |zeroFill| can be undefined when running inside an isolate where we +-// do not own the ArrayBuffer allocator. Zero fill is always on in that case. +-let zeroFill = getZeroFillToggle(); + function createUnsafeBuffer(size) { +- zeroFill[0] = 0; +- try { ++ if (size <= 64) { ++ // Allocated in heap, doesn't call backing store anyway ++ // This is the same that the old impl did implicitly, but explicit now + return new FastBuffer(size); +- } finally { +- zeroFill[0] = 1; + } +-} + +-// The connection between the JS land zero fill toggle and the +-// C++ one in the NodeArrayBufferAllocator gets lost if the toggle +-// is deserialized from the snapshot, because V8 owns the underlying +-// memory of this toggle. This resets the connection. +-function reconnectZeroFillToggle() { +- zeroFill = getZeroFillToggle(); ++ return new FastBuffer(createUnsafeArrayBuffer(size)); + } + + module.exports = { +@@ -1082,5 +1070,4 @@ module.exports = { + createUnsafeBuffer, + readUInt16BE, + readUInt32BE, +- reconnectZeroFillToggle, + }; +diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js +index 0bbabb80c26a12..96bbfb4c35982d 100644 +--- a/lib/internal/process/pre_execution.js ++++ b/lib/internal/process/pre_execution.js +@@ -24,7 +24,6 @@ const { + refreshOptions, + getEmbedderOptions, + } = require('internal/options'); +-const { reconnectZeroFillToggle } = require('internal/buffer'); + const { + exposeInterface, + exposeLazyInterfaces, +@@ -98,7 +97,6 @@ function prepareExecution(options) { + const { expandArgv1, initializeModules, isMainThread } = options; + + refreshRuntimeOptions(); +- reconnectZeroFillToggle(); + + // Patch the process object and get the resolved main entry point. + const mainEntry = patchProcessObject(expandArgv1); +diff --git a/src/api/environment.cc b/src/api/environment.cc +index 1f732e6bb49de8..cb3d3be27e043c 100644 +--- a/src/api/environment.cc ++++ b/src/api/environment.cc +@@ -107,8 +107,9 @@ void* NodeArrayBufferAllocator::Allocate(size_t size) { + ret = allocator_->Allocate(size); + else + ret = allocator_->AllocateUninitialized(size); +- if (LIKELY(ret != nullptr)) ++ if (ret != nullptr) [[likely]] { + total_mem_usage_.fetch_add(size, std::memory_order_relaxed); ++ } + return ret; + } + +diff --git a/src/node_buffer.cc b/src/node_buffer.cc +index d438b2bbdd89ac..803233ef413a8e 100644 +--- a/src/node_buffer.cc ++++ b/src/node_buffer.cc +@@ -75,7 +75,6 @@ using v8::Object; + using v8::SharedArrayBuffer; + using v8::String; + using v8::Uint32; +-using v8::Uint32Array; + using v8::Uint8Array; + using v8::Value; + +@@ -1177,35 +1176,6 @@ void SetBufferPrototype(const FunctionCallbackInfo& args) { + realm->set_buffer_prototype_object(proto); + } + +-void GetZeroFillToggle(const FunctionCallbackInfo& args) { +- Environment* env = Environment::GetCurrent(args); +- NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator(); +- Local ab; +- // It can be a nullptr when running inside an isolate where we +- // do not own the ArrayBuffer allocator. +- if (allocator == nullptr) { +- // Create a dummy Uint32Array - the JS land can only toggle the C++ land +- // setting when the allocator uses our toggle. With this the toggle in JS +- // land results in no-ops. +- ab = ArrayBuffer::New(env->isolate(), sizeof(uint32_t)); +- } else { +- uint32_t* zero_fill_field = allocator->zero_fill_field(); +- std::unique_ptr backing = +- ArrayBuffer::NewBackingStore(zero_fill_field, +- sizeof(*zero_fill_field), +- [](void*, size_t, void*) {}, +- nullptr); +- ab = ArrayBuffer::New(env->isolate(), std::move(backing)); +- } +- +- ab->SetPrivate( +- env->context(), +- env->untransferable_object_private_symbol(), +- True(env->isolate())).Check(); +- +- args.GetReturnValue().Set(Uint32Array::New(ab, 0, 1)); +-} +- + void DetachArrayBuffer(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (args[0]->IsArrayBuffer()) { +@@ -1397,6 +1367,54 @@ void CopyArrayBuffer(const FunctionCallbackInfo& args) { + memcpy(dest, src, bytes_to_copy); + } + ++// Converts a number parameter to size_t suitable for ArrayBuffer sizes ++// Could be larger than uint32_t ++// See v8::internal::TryNumberToSize and v8::internal::NumberToSize ++inline size_t CheckNumberToSize(Local number) { ++ CHECK(number->IsNumber()); ++ double value = number.As()->Value(); ++ // See v8::internal::TryNumberToSize on this (and on < comparison) ++ double maxSize = static_cast(std::numeric_limits::max()); ++ CHECK(value >= 0 && value < maxSize); ++ size_t size = static_cast(value); ++#ifdef V8_ENABLE_SANDBOX ++ CHECK_LE(size, kMaxSafeBufferSizeForSandbox); ++#endif ++ return size; ++} ++ ++void CreateUnsafeArrayBuffer(const FunctionCallbackInfo& args) { ++ Environment* env = Environment::GetCurrent(args); ++ if (args.Length() != 1) { ++ env->ThrowRangeError("Invalid array buffer length"); ++ return; ++ } ++ ++ size_t size = CheckNumberToSize(args[0]); ++ ++ Isolate* isolate = env->isolate(); ++ ++ Local buf; ++ ++ NodeArrayBufferAllocator* allocator = env->isolate_data()->node_allocator(); ++ // 0-length, or zero-fill flag is set, or building snapshot ++ if (size == 0 || per_process::cli_options->zero_fill_all_buffers || ++ allocator == nullptr) { ++ buf = ArrayBuffer::New(isolate, size); ++ } else { ++ std::unique_ptr store = ++ ArrayBuffer::NewBackingStoreForNodeLTS(isolate, size); ++ if (!store) { ++ // This slightly differs from the old behavior, ++ // as in v8 that's a RangeError, and this is an Error with code ++ return env->ThrowRangeError("Array buffer allocation failed"); ++ } ++ buf = ArrayBuffer::New(isolate, std::move(store)); ++ } ++ ++ args.GetReturnValue().Set(buf); ++} ++ + void Initialize(Local target, + Local unused, + Local context, +@@ -1428,6 +1446,8 @@ void Initialize(Local target, + + SetMethod(context, target, "detachArrayBuffer", DetachArrayBuffer); + SetMethod(context, target, "copyArrayBuffer", CopyArrayBuffer); ++ SetMethodNoSideEffect( ++ context, target, "createUnsafeArrayBuffer", CreateUnsafeArrayBuffer); + + SetMethod(context, target, "swap16", Swap16); + SetMethod(context, target, "swap32", Swap32); +@@ -1464,8 +1484,6 @@ void Initialize(Local target, + SetMethod(context, target, "hexWrite", StringWrite); + SetMethod(context, target, "ucs2Write", StringWrite); + SetMethod(context, target, "utf8Write", StringWrite); +- +- SetMethod(context, target, "getZeroFillToggle", GetZeroFillToggle); + } + + } // anonymous namespace +@@ -1508,10 +1526,10 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { + registry->Register(StringWrite); + registry->Register(StringWrite); + registry->Register(StringWrite); +- registry->Register(GetZeroFillToggle); + + registry->Register(DetachArrayBuffer); + registry->Register(CopyArrayBuffer); ++ registry->Register(CreateUnsafeArrayBuffer); + + registry->Register(Atob); + registry->Register(Btoa); diff -Nru nodejs-20.19.2+dfsg/debian/patches/sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch nodejs-20.19.2+dfsg/debian/patches/sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch --- nodejs-20.19.2+dfsg/debian/patches/sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch 1970-01-01 00:00:00.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch 2026-03-05 10:05:11.000000000 +0000 @@ -0,0 +1,50 @@ +From 5c053264f1ed9e869a001e30f2d26950ef39513d Mon Sep 17 00:00:00 2001 +From: Vivian Wang +Date: Tue, 3 Feb 2026 16:46:02 +0800 +Subject: [PATCH] deps: V8: backport 6a0a25abaed3 + +Original commit message: + + [riscv] Fix sp handling in MacroAssembler::LeaveFrame + + Keep sp <= fp to ensure that data right above fp doesn't get clobbered + by an inopportune signal and its handler. + + Such clobbering can happen in e.g. Node.js when JIT-compiled code is + interrupted by a SIGCHLD handler. + + Bug: None + Change-Id: Ief0836032ada7942e89f081f7605f61632c4d414 + Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/7540554 + Reviewed-by: Ji Qiu + Commit-Queue: Yahan Lu (LuYahan) + Reviewed-by: Rezvan Mahdavi Hezaveh + Cr-Commit-Position: refs/heads/main@{#105069} + +Refs: https://github.com/v8/v8/commit/6a0a25abaed397f83eb0d92e4b33a5e18204f8bc +Co-authored-by: kxxt +PR-URL: https://github.com/nodejs/node/pull/61687 +Reviewed-By: Marco Ippolito +Reviewed-By: Richard Lau +--- + common.gypi | 2 +- + deps/v8/AUTHORS | 1 + + deps/v8/src/codegen/riscv/macro-assembler-riscv.cc | 3 ++- + 3 files changed, 4 insertions(+), 2 deletions(-) + +diff --git a/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc b/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc +index b39661c7715c5f..7f09d052920507 100644 +--- a/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc ++++ b/deps/v8/src/codegen/riscv/macro-assembler-riscv.cc +@@ -5530,9 +5530,10 @@ void MacroAssembler::EnterFrame(StackFrame::Type type) { + + void MacroAssembler::LeaveFrame(StackFrame::Type type) { + ASM_CODE_COMMENT(this); +- addi(sp, fp, 2 * kSystemPointerSize); ++ Move(sp, fp); + LoadWord(ra, MemOperand(fp, 1 * kSystemPointerSize)); + LoadWord(fp, MemOperand(fp, 0 * kSystemPointerSize)); ++ AddWord(sp, sp, 2 * kSystemPointerSize); + } + + void MacroAssembler::EnterExitFrame(int stack_space, diff -Nru nodejs-20.19.2+dfsg/debian/patches/series nodejs-20.19.2+dfsg/debian/patches/series --- nodejs-20.19.2+dfsg/debian/patches/series 2025-05-14 21:41:56.000000000 +0000 +++ nodejs-20.19.2+dfsg/debian/patches/series 2026-03-05 10:05:11.000000000 +0000 @@ -17,3 +17,18 @@ build/ada.patch deps/v8-no-static-zlib.patch build/no-deps.patch +sec/10-zlib-fix-pointer-alignment.patch +sec/12-http2-fix-check-for-frame-type-goaway.patch +sec/15-fix-os-getinterface-addresses-leak.patch +sec/17-fix-possible-dereference-of-null-pointer.patch +sec/19-v8-fix-missing-callback-in-heap-utils-destroy.patch +sec/27-v8-loong64-avoid-memory-access-under-stack-pointer.patch +sec/28-http2-do-not-crash-on-mismatched-ping-buffer-length.patch +sec/29-fix-order-of-check-not-null.patch +sec/33-tls-route-callback-exceptions-through-error-handlers.patch +sec/34-lib-add-tlssocket-default-error-handler.patch +sec/35-lib-disable-futimes-when-permission-model-is-enabled.patch +sec/36-lib-permission-require-full-read-and-write-to-symlink-apis.patch +sec/37-rethrow-stack-overflow-exceptions-in-async-hooks.patch +sec/38-refactor-unsafe-buffer-creation-to-remove-zero-fill-toggle.patch +sec/44-v8-riscv-fix-sp-handling-in-macroassembler-leave-frame.patch