Commit 5d6dedb3a69 for nodejs

commit 5d6dedb3a69aaa8778626117aab5cd3df384d560
Author: Matteo Collina <hello@matteocollina.com>
Date:   Fri Oct 30 08:32:57 2026 +0100

    http: never execute llhttp re-entrantly

    llhttp is not reentrant: it keeps its position in the input in the
    parser state and only updates it when `llhttp_execute()` returns.
    Instead of supporting nested calls, make sure that llhttp never runs
    inside itself.

    - `socketOnData` on the server and the client puts data that arrives
      while the parser is executing back into the socket, and resumes the
      socket after the current call.
    - `Parser::Execute()` does not run llhttp while it is already running.
      A nested call with data fails the outer call with
      `HPE_REENTRANT_EXECUTE`. A nested `finish()`, which comes from
      tearing down the connection in a callback, is skipped.

    This replaces the buffer bounds check on data callbacks.

    Refs: https://hackerone.com/reports/4150886
    Signed-off-by: Matteo Collina <hello@matteocollina.com>
    PR-URL: https://github.com/nodejs-private/node-private/pull/1021
    Reviewed-By: Paolo Insogna <paolo@cowtech.it>
    Reviewed-By: Tim Perry <pimterry@gmail.com>
    CVE-ID: CVE-2026-123864

diff --git a/lib/internal/http/client.js b/lib/internal/http/client.js
index 1952e7b69af..ecc87255311 100644
--- a/lib/internal/http/client.js
+++ b/lib/internal/http/client.js
@@ -124,6 +124,8 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {

 const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
 const kError = Symbol('kError');
+const kParserExecuting = Symbol('kParserExecuting');
+const kParserDeferred = Symbol('kParserDeferred');
 const kPath = Symbol('kPath');
 const kAuthority = Symbol('kAuthority');
 const kProxyRewrittenToAbsolute = Symbol('kProxyRewrittenToAbsolute');
@@ -837,9 +839,31 @@ function socketOnData(d) {
   const req = this._httpMessage;
   const parser = this.parser;

+  // The llhttp parser is not reentrant. JS that runs in a parser callback can
+  // make the socket emit 'data' synchronously, for example by pushing into a
+  // Duplex. Put the data back and parse it after the current call.
+  if (parser?.[kParserExecuting]) {
+    socket.pause();
+    socket.unshift(d);
+    parser[kParserDeferred] = true;
+    return;
+  }
+
   assert(parser && parser.socket === socket);

-  const ret = parser.execute(d);
+  parser[kParserExecuting] = true;
+  let ret;
+  try {
+    ret = parser.execute(d);
+  } finally {
+    parser[kParserExecuting] = false;
+  }
+  if (parser[kParserDeferred]) {
+    parser[kParserDeferred] = false;
+    // Resume after the code below has handled the result, unless the socket
+    // was handed over or destroyed in the meantime.
+    process.nextTick(resumeDeferredSocket, socket);
+  }
   if (ret instanceof Error) {
     prepareError(ret, parser, d);
     debug('parse error', ret);
@@ -901,6 +925,13 @@ function socketOnData(d) {
   }
 }

+function resumeDeferredSocket(socket) {
+  if (!socket.destroyed && socket.readableFlowing === false &&
+      socket.listenerCount('data', socketOnData) > 0) {
+    socket.resume();
+  }
+}
+
 function statusIsInformational(status) {
   // 100 (Continue)    RFC7231 Section 6.2.1
   // 102 (Processing)  RFC2518
diff --git a/lib/internal/http/server.js b/lib/internal/http/server.js
index 406eede9300..9e13aeba63a 100644
--- a/lib/internal/http/server.js
+++ b/lib/internal/http/server.js
@@ -121,6 +121,8 @@ const kUpgradeStream = Symbol('UpgradeStream');

 const kOptimizeEmptyRequests = Symbol('OptimizeEmptyRequestsOption');
 const kConnectionState = Symbol('ConnectionState');
+const kParserExecuting = Symbol('kParserExecuting');
+const kParserDeferred = Symbol('kParserDeferred');
 const kResponseOptions = Symbol('ResponseOptions');

 const statusLineCache = [];
@@ -977,11 +979,34 @@ function socketOnEnd(server, socket, parser, state) {
 }

 function socketOnData(server, socket, parser, state, d) {
+  // The llhttp parser is not reentrant. JS that runs in a parser callback can
+  // make the socket emit 'data' synchronously, for example by pushing into a
+  // Duplex. Put the data back and parse it after the current call.
+  if (parser[kParserExecuting]) {
+    socket.pause();
+    socket.unshift(d);
+    parser[kParserDeferred] = true;
+    return;
+  }
   assert(!socket._paused);
   debug('SERVER socketOnData %d', d.length);

-  const ret = parser.execute(d);
+  parser[kParserExecuting] = true;
+  let ret;
+  try {
+    ret = parser.execute(d);
+  } finally {
+    parser[kParserExecuting] = false;
+  }
+  const deferred = parser[kParserDeferred];
+  parser[kParserDeferred] = false;
+
   onParserExecuteCommon(server, socket, parser, state, ret, d);
+
+  if (deferred && socket.parser === parser && !socket._paused &&
+      !socket.destroyed) {
+    socket.resume();
+  }
 }

 function onRequestTimeout(socket) {
diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc
index 043f3811de3..07698328622 100644
--- a/src/node_http_parser.cc
+++ b/src/node_http_parser.cc
@@ -903,9 +903,20 @@ class Parser : public AsyncWrap, public StreamListener {
   Local<Value> Execute(const char* data, size_t len) {
     EscapableHandleScope scope(env()->isolate());

-    // A callback can run JS that executes the parser again, for example by
-    // pushing data into the socket synchronously. Restore the caller's buffer
-    // afterwards so that its own callbacks are checked against it.
+    // llhttp is not reentrant: it keeps its position in the input in
+    // parser_ and only updates it when llhttp_execute() returns. A callback
+    // can run JS that gets here again, for example by pushing data into the
+    // socket synchronously. lib/ defers that data, so a nested call with data
+    // is a bug: do not run llhttp on top of itself, fail the outer call
+    // instead. A nested finish() comes from tearing down the connection in a
+    // callback, and the parser is freed right after it, so just skip it.
+    if (executing_) {
+      if (data != nullptr) reentered_ = true;
+      return scope.Escape(Integer::New(env()->isolate(), 0));
+    }
+
+    // The callback that reports the result of a consumed read can also get
+    // here, outside of llhttp. Restore the caller's buffer afterwards.
     const char* const outer_data = current_buffer_data_;
     const size_t outer_len = current_buffer_len_;
     current_buffer_len_ = len;
@@ -914,12 +925,25 @@ class Parser : public AsyncWrap, public StreamListener {

     llhttp_errno_t err;

+    executing_ = true;
     if (data == nullptr) {
       err = llhttp_finish(&parser_);
     } else {
       err = llhttp_execute(&parser_, data, len);
       Save();
     }
+    executing_ = false;
+
+    if (reentered_) {
+      reentered_ = false;
+      if (err == HPE_OK || err == HPE_PAUSED || err == HPE_PAUSED_UPGRADE) {
+        err = HPE_USER;
+        parser_.error = err;
+        parser_.error_pos = data + len;
+        llhttp_set_error_reason(
+            &parser_, "HPE_REENTRANT_EXECUTE:Parser was executed re-entrantly");
+      }
+    }

     // Deliver buffered body spans while `data` is still alive.
     FlushBody();
@@ -1166,6 +1190,8 @@ class Parser : public AsyncWrap, public StreamListener {
   size_t header_pairs_ = 0;
   size_t max_header_pairs_ = 0;
   bool pending_pause_ = false;
+  bool executing_ = false;
+  bool reentered_ = false;
   bool received_data_ = false;
   uint64_t header_nread_ = 0;
   uint64_t chunk_extensions_nread_ = 0;
@@ -1187,11 +1213,6 @@ class Parser : public AsyncWrap, public StreamListener {
       if (parser->is_being_freed_) {
         return 0;
       }
-      if (!parser->IsInCurrentBuffer(args...)) {
-        llhttp_set_error_reason(
-            p, "HPE_REENTRANT_EXECUTE:Parser re-entered in the middle of data");
-        return HPE_USER;
-      }
       int rv = (parser->*Member)(std::forward<Args>(args)...);
       if (rv == 0) {
         rv = parser->MaybePause();
@@ -1200,20 +1221,6 @@ class Parser : public AsyncWrap, public StreamListener {
     }
   };

-  // A nested Execute() that stops in the middle of a span (e.g. a chunk of the
-  // body) leaves llhttp pointing into the nested call's buffer, which can be
-  // freed by the time the outer call reports the span. Data callbacks must
-  // only ever see the buffer that is being executed.
-  bool IsInCurrentBuffer() const {
-    return true;
-  }
-  bool IsInCurrentBuffer(const char* at, size_t length) const {
-    const uintptr_t begin = reinterpret_cast<uintptr_t>(current_buffer_data_);
-    const uintptr_t pos = reinterpret_cast<uintptr_t>(at);
-    return pos >= begin && pos - begin <= current_buffer_len_ &&
-           length <= current_buffer_len_ - (pos - begin);
-  }
-
   typedef int (Parser::*Call)();
   typedef int (Parser::*DataCall)(const char* at, size_t length);

diff --git a/test/parallel/test-http-parser-reentrant-data.js b/test/parallel/test-http-parser-reentrant-data.js
new file mode 100644
index 00000000000..7daa5625d82
--- /dev/null
+++ b/test/parallel/test-http-parser-reentrant-data.js
@@ -0,0 +1,81 @@
+// Flags: --expose-internals
+'use strict';
+
+// Data that a socket emits synchronously from inside a parser callback, for
+// example by pushing into a Duplex, must be parsed after the current call
+// to the parser returns, not from inside it.
+
+const common = require('../common');
+const assert = require('assert');
+const http = require('http');
+const { Duplex } = require('stream');
+const { internalBinding } = require('internal/test/binding');
+const { HTTPParser } = internalBinding('http_parser');
+
+const execute = HTTPParser.prototype.execute;
+let depth = 0;
+HTTPParser.prototype.execute = common.mustCallAtLeast(function(...args) {
+  assert.strictEqual(depth, 0);
+  depth++;
+  try {
+    return execute.apply(this, args);
+  } finally {
+    depth--;
+  }
+}, 4);
+
+{
+  // Server: the request handler of the first request pushes the second one.
+  const urls = [];
+  let output = '';
+  const connection = new Duplex({
+    read() {},
+    write(chunk, encoding, callback) {
+      output += chunk;
+      callback();
+    },
+  });
+  const server = http.createServer(common.mustCall((req, res) => {
+    urls.push(req.url);
+    if (req.url === '/a')
+      connection.push('GET /b HTTP/1.1\r\nHost: x\r\n\r\n');
+    res.end('ok');
+  }, 2));
+  server.emit('connection', connection);
+  connection.push('GET /a HTTP/1.1\r\nHost: x\r\n\r\n');
+  setImmediate(common.mustCall(() => {
+    assert.deepStrictEqual(urls, ['/a', '/b']);
+    assert.strictEqual(output.split('HTTP/1.1 200 OK\r\n').length, 3);
+  }));
+}
+
+{
+  // Client: the 'information' handler pushes the final response.
+  let socket;
+  const req = http.request('http://localhost', {
+    createConnection() {
+      socket = new Duplex({
+        read() {},
+        write(chunk, encoding, callback) {
+          if (chunk.toString().includes('\r\n\r\n'))
+            this.push('HTTP/1.1 100 Continue\r\n\r\n');
+          callback();
+        },
+      });
+      return socket;
+    },
+  });
+  req.on('information', common.mustCall(({ statusCode }) => {
+    assert.strictEqual(statusCode, 100);
+    socket.push('HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok');
+    socket.push(null);
+  }));
+  req.on('response', common.mustCall((res) => {
+    assert.strictEqual(res.statusCode, 200);
+    res.setEncoding('utf8');
+    let body = '';
+    res.on('data', (chunk) => body += chunk);
+    res.on('end', common.mustCall(() => assert.strictEqual(body, 'ok')));
+  }));
+  req.end();
+}