diff mbox series

[meta-oe,scarthgap] nodejs: Security fix for CVE-2026-48937

Message ID 20261016170256.30485-1-rsangam@mvista.com
State New
Headers show
Series [meta-oe,scarthgap] nodejs: Security fix for CVE-2026-48937 | expand

Commit Message

Rohini Sangam Oct. 16, 2026, 5:02 p.m. UTC
Pick patch from [1] also mentioned at Debian tracker in [2]

[1] https://github.com/nodejs/node/commit/a1a5bb968303229d4a3a7c9e389dc3ece6237b4c
[2] https://security-tracker.debian.org/tracker/CVE-2026-48937

Signed-off-by: Rohini Sangam <rsangam@mvista.com>
---
 .../nodejs/nodejs/CVE-2026-48937.patch        | 349 ++++++++++++++++++
 .../recipes-devtools/nodejs/nodejs_20.20.2.bb |   1 +
 2 files changed, 350 insertions(+)
 create mode 100644 meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-48937.patch
diff mbox series

Patch

diff --git a/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-48937.patch b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-48937.patch
new file mode 100644
index 0000000000..5f9f9bffce
--- /dev/null
+++ b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2026-48937.patch
@@ -0,0 +1,349 @@ 
+From a1a5bb968303229d4a3a7c9e389dc3ece6237b4c Mon Sep 17 00:00:00 2001
+From: Tim Perry <pimterry@gmail.com>
+Date: Wed, 22 Apr 2026 13:36:27 +0200
+Subject: [PATCH] deps: fix integration issues with the latest nghttp2
+
+This is a set of src & tests fixes for nghttp2 due to changes in
+v1.67.0+ which require a selection of changes to how we handle
+low-level protocol errors when using the latest versions of nghttp2,
+changing both some src error handling and updating some tests to match.
+
+Signed-off-by: Tim Perry <pimterry@gmail.com>
+PR-URL: https://github.com/nodejs/node/pull/62891
+Backport-PR-URL: https://github.com/nodejs/node/pull/63195
+Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
+Reviewed-By: Tim Perry <pimterry@gmail.com>
+Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
+Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
+Refs: https://hackerone.com/reports/3658225
+
+CVE: CVE-2026-48937
+Upstream-Status: Backport [https://github.com/nodejs/node/commit/a1a5bb968303229d4a3a7c9e389dc3ece6237b4c]
+
+Signed-off-by: Rohini Sangam <rsangam@mvista.com>
+---
+ src/node_http2.cc                             | 31 +++++++
+ src/node_http2.h                              |  2 +
+ ...t-http2-misbehaving-flow-control-paused.js | 33 ++++---
+ .../test-http2-misbehaving-flow-control.js    | 33 ++++---
+ ...tp2-options-max-headers-exceeds-nghttp2.js | 12 ++-
+ ...http2-session-cleanup-on-nghttp2-goaway.js | 91 +++++++++++++++++++
+ 6 files changed, 176 insertions(+), 26 deletions(-)
+ create mode 100644 test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
+
+diff --git a/src/node_http2.cc b/src/node_http2.cc
+index 4f44789b..5047e5c4 100644
+--- a/src/node_http2.cc
++++ b/src/node_http2.cc
+@@ -1246,6 +1246,24 @@ int Http2Session::OnFrameSent(nghttp2_session* handle,
+                               void* user_data) {
+   Http2Session* session = static_cast<Http2Session*>(user_data);
+   session->statistics_.frame_sent += 1;
++
++  // If nghttp2 has internally terminated the session (e.g. due to a protocol
++  // error like oversized frames, padding errors, or HPACK compression
++  // failures), it calls nghttp2_session_terminate_session() directly which
++  // queues a GOAWAY but does not invoke any application-level callback.
++  // Detect that case here: a GOAWAY was sent but we never initiated it
++  // (no Close(), no session.close(), no session.goaway()).
++  //
++  // We set a flag here, and then throw the error at the end of
++  // SendPendingData, to wait until the GOAWAY is written before the session
++  // is torn down.
++  if (frame->hd.type == NGHTTP2_GOAWAY && !session->is_closing() &&
++      !session->is_destroyed() && !session->IsGracefulCloseInitiated() &&
++      !session->goaway_initiated_) {
++    Debug(session, "nghttp2 session terminated internally");
++    session->internal_goaway_sent_ = true;
++  }
++
+   return 0;
+ }
+ 
+@@ -1976,6 +1994,18 @@ uint8_t Http2Session::SendPendingData() {
+ 
+   MaybeStopReading();
+ 
++  // If nghttp2 has internally torn down the session (detected in OnFrameSent)
++  // during the nghttp2_session_mem_send loop above, at this point we error:
++  if (internal_goaway_sent_) {
++    internal_goaway_sent_ = false;
++    if (!is_closing() && !is_destroyed()) {
++      Isolate* isolate = env()->isolate();
++      HandleScope scope(isolate);
++      Local<Value> arg = Integer::New(isolate, NGHTTP2_ERR_PROTO);
++      MakeCallback(env()->http2session_on_error_function(), 1, &arg);
++    }
++  }
++
+   return 0;
+ }
+ 
+@@ -2901,6 +2931,7 @@ void Http2Session::Goaway(uint32_t code,
+   if (is_destroyed())
+     return;
+ 
++  goaway_initiated_ = true;
+   Http2Scope h2scope(this);
+   // the last proc stream id is the most recently created Http2Stream.
+   if (lastStreamID <= 0)
+diff --git a/src/node_http2.h b/src/node_http2.h
+index a60a7ba0..91046052 100644
+--- a/src/node_http2.h
++++ b/src/node_http2.h
+@@ -968,6 +968,8 @@ class Http2Session : public AsyncWrap,
+ 
+   // Flag to indicate that JavaScript has initiated a graceful closure
+   bool graceful_close_initiated_ = false;
++  bool goaway_initiated_ = false;
++  bool internal_goaway_sent_ = false;
+ };
+ 
+ struct Http2SessionPerformanceEntryTraits {
+diff --git a/test/parallel/test-http2-misbehaving-flow-control-paused.js b/test/parallel/test-http2-misbehaving-flow-control-paused.js
+index e54bb832..b9a0d5cb 100644
+--- a/test/parallel/test-http2-misbehaving-flow-control-paused.js
++++ b/test/parallel/test-http2-misbehaving-flow-control-paused.js
+@@ -47,36 +47,45 @@ const data = Buffer.from([
+ // Bad client! Bad!
+ //
+ // Fortunately, nghttp2 handles this situation for us by keeping track
+-// of the flow control window and responding with a FLOW_CONTROL_ERROR
+-// causing the stream to get shut down...
+-//
+-// At least, that's what is supposed to happen.
++// of the flow control window and sending GOAWAY to end the session.
+ 
+ let client;
+ 
+ const server = h2.createServer({ settings: { initialWindowSize: 36 } });
+-server.on('stream', (stream) => {
++
++server.on('session', common.mustCall((session) => {
++  session.on('error', common.expectsError({
++    code: 'ERR_HTTP2_ERROR',
++    name: 'Error',
++    message: 'Protocol error'
++  }));
++}));
++
++server.on('stream', common.mustCall((stream) => {
+   // Set the high water mark to zero, since otherwise we still accept
+   // reads from the source stream (if we can consume them).
+   stream._readableState.highWaterMark = 0;
+   stream.pause();
+   stream.on('error', common.expectsError({
+-    code: 'ERR_HTTP2_STREAM_ERROR',
++    code: 'ERR_HTTP2_ERROR',
+     name: 'Error',
+-    message: 'Stream closed with error code NGHTTP2_FLOW_CONTROL_ERROR'
++    message: 'Protocol error'
+   }));
+   stream.on('close', common.mustCall(() => {
+     server.close();
+     client.destroy();
+   }));
+   stream.on('end', common.mustNotCall());
+-});
++}));
+ 
+-server.listen(0, () => {
+-  client = net.connect(server.address().port, () => {
++server.listen(0, common.mustCall(() => {
++  client = net.connect(server.address().port, common.mustCall(() => {
+     client.write(preamble);
+     client.write(data);
+     client.write(data);
+     client.write(data);
+-  });
+-});
++
++    // TCP connection is closed by the server
++    client.on('close', common.mustCall());
++  }));
++}));
+diff --git a/test/parallel/test-http2-misbehaving-flow-control.js b/test/parallel/test-http2-misbehaving-flow-control.js
+index 6774be22..fb0f7a1e 100644
+--- a/test/parallel/test-http2-misbehaving-flow-control.js
++++ b/test/parallel/test-http2-misbehaving-flow-control.js
+@@ -58,18 +58,24 @@ const data = Buffer.from([
+ // Bad client! Bad!
+ //
+ // Fortunately, nghttp2 handles this situation for us by keeping track
+-// of the flow control window and responding with a FLOW_CONTROL_ERROR
+-// causing the stream to get shut down...
+-//
+-// At least, that's what is supposed to happen.
++// of the flow control window and sending GOAWAY to end the session.
+ 
+ let client;
+ const server = h2.createServer({ settings: { initialWindowSize: 18 } });
+-server.on('stream', (stream) => {
++
++server.on('session', common.mustCall((session) => {
++  session.on('error', common.expectsError({
++    code: 'ERR_HTTP2_ERROR',
++    name: 'Error',
++    message: 'Protocol error'
++  }));
++}));
++
++server.on('stream', common.mustCall((stream) => {
+   stream.on('error', common.expectsError({
+-    code: 'ERR_HTTP2_STREAM_ERROR',
++    code: 'ERR_HTTP2_ERROR',
+     name: 'Error',
+-    message: 'Stream closed with error code NGHTTP2_FLOW_CONTROL_ERROR'
++    message: 'Protocol error'
+   }));
+   stream.on('close', common.mustCall(() => {
+     server.close(common.mustCall());
+@@ -78,14 +84,17 @@ server.on('stream', (stream) => {
+   stream.resume();
+   stream.respond();
+   stream.end('ok');
+-});
++}));
+ 
+ server.on('close', common.mustCall());
+ 
+-server.listen(0, () => {
+-  client = net.connect(server.address().port, () => {
++server.listen(0, common.mustCall(() => {
++  client = net.connect(server.address().port, common.mustCall(() => {
+     client.write(preamble);
+     client.write(data);
+     client.write(data);
+-  });
+-});
++
++    // TCP connection is closed by the server
++    client.on('close', common.mustCall());
++  }));
++}));
+diff --git a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
+index 7767dbbc..46f560f9 100644
+--- a/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
++++ b/test/parallel/test-http2-options-max-headers-exceeds-nghttp2.js
+@@ -90,11 +90,19 @@ server.listen(0, common.mustCall(() => {
+     0,
+     common.mustCall(() => {
+       const client = h2.connect(`http://localhost:${server.address().port}`);
+-      client.on('error', common.mustNotCall());
++      // The server sends oversized headers that cause a compression error on
++      // the client side, so nghttp2 internally terminates the client session.
++      client.on('error', common.expectsError({
++        code: 'ERR_HTTP2_ERROR',
++        name: 'Error',
++      }));
+ 
+       const req = client.request();
+       req.on('response', common.mustNotCall());
+-      req.on('error', common.mustNotCall());
++      req.on('error', common.expectsError({
++        code: 'ERR_HTTP2_ERROR',
++        name: 'Error',
++      }));
+       req.end();
+     }),
+   );
+diff --git a/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js b/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
+new file mode 100644
+index 00000000..f3576f9f
+--- /dev/null
++++ b/test/parallel/test-http2-session-cleanup-on-nghttp2-goaway.js
+@@ -0,0 +1,91 @@
++'use strict';
++
++const common = require('../common');
++
++if (!common.hasCrypto)
++  common.skip('missing crypto');
++
++const http2 = require('http2');
++const net = require('net');
++
++// When nghttp2 internally sends a GOAWAY frame due to a protocol error, it
++// may call nghttp2_session_terminate_session() directly, bypassing the
++// on_invalid_frame_recv_callback entirely. This test ensures that even
++// in that scenario, we still correctly clean up the session & connection.
++//
++// This test reproduces this with a client who sends a frame header with
++// a length exceeding the default max_frame_size (16384). nghttp2 responds
++// with GOAWAY(FRAME_SIZE_ERROR) without notifying Node through any callback.
++
++const server = http2.createServer();
++
++server.on('session', common.mustCall((session) => {
++  session.on('error', common.expectsError({
++    code: 'ERR_HTTP2_ERROR',
++    name: 'Error',
++    message: 'Protocol error'
++  }));
++
++  session.on('close', common.mustCall(() => server.close()));
++}));
++
++server.listen(0, common.mustCall(() => {
++  const conn = net.connect({
++    port: server.address().port,
++    allowHalfOpen: true,
++  });
++
++  // HTTP/2 client connection preface.
++  conn.write('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
++
++  // Empty SETTINGS frame.
++  const settingsFrame = Buffer.alloc(9);
++  settingsFrame[3] = 0x04; // type: SETTINGS
++  conn.write(settingsFrame);
++
++  let inbuf = Buffer.alloc(0);
++  let state = 'settingsHeader';
++  let settingsFrameLength;
++
++  conn.on('data', (chunk) => {
++    inbuf = Buffer.concat([inbuf, chunk]);
++
++    switch (state) {
++      case 'settingsHeader':
++        if (inbuf.length < 9) return;
++        settingsFrameLength = inbuf.readUIntBE(0, 3);
++        inbuf = inbuf.slice(9);
++        state = 'readingSettings';
++        // Fallthrough
++      case 'readingSettings': {
++        if (inbuf.length < settingsFrameLength) return;
++        inbuf = inbuf.slice(settingsFrameLength);
++        state = 'done';
++
++        // ACK the server SETTINGS.
++        const ack = Buffer.alloc(9);
++        ack[3] = 0x04; // type: SETTINGS
++        ack[4] = 0x01; // flag: ACK
++        conn.write(ack);
++
++        // Send a HEADERS frame header claiming length 16385, which exceeds
++        // the default max_frame_size of 16384. nghttp2 checks the length
++        // before reading any payload, so no body is needed. This triggers
++        // nghttp2_session_terminate_session(FRAME_SIZE_ERROR) directly in
++        // nghttp2_session_mem_recv2 — bypassing on_invalid_frame_recv_callback.
++        const oversized = Buffer.alloc(9);
++        oversized.writeUIntBE(16385, 0, 3); // length: 16385 (one over max)
++        oversized[3] = 0x01;                // type: HEADERS
++        oversized[4] = 0x04;                // flags: END_HEADERS
++        oversized.writeUInt32BE(1, 5);       // stream id: 1
++        conn.write(oversized);
++
++        // No need to write the data - the header alone triggers the check.
++      }
++    }
++  });
++
++  // The server must close the connection after sending GOAWAY:
++  conn.on('end', common.mustCall(() => conn.end()));
++  conn.on('close', common.mustCall());
++}));
+-- 
+2.44.4
+
diff --git a/meta-oe/recipes-devtools/nodejs/nodejs_20.20.2.bb b/meta-oe/recipes-devtools/nodejs/nodejs_20.20.2.bb
index 710ed988d3..f701b0db75 100644
--- a/meta-oe/recipes-devtools/nodejs/nodejs_20.20.2.bb
+++ b/meta-oe/recipes-devtools/nodejs/nodejs_20.20.2.bb
@@ -28,6 +28,7 @@  SRC_URI = "http://nodejs.org/dist/v${PV}/node-v${PV}.tar.xz \
            file://run-ptest \
            file://CVE-2026-48617.patch \
            file://CVE-2026-48619.patch \
+           file://CVE-2026-48937.patch \
            "
 SRC_URI:append:class-target = " \
            file://0001-Using-native-binaries.patch \