Skip to content

Commit d2ea166

Browse files
committed
http: fix use-after-free when freeParser is called during llhttp_execute
When pipelined requests arrive in one TCP segment, llhttp_execute() parses them all in a single call. If a synchronous 'close' event handler invokes freeParser() mid-execution, cleanParser() nulls out parser state while llhttp_execute() is still on the stack, crashing on the next callback. Add an is_being_freed_ flag that freeParser() sets via parser.markFreed() before cleaning state. Proxy::Raw checks the flag before every callback and returns HPE_USER to abort execution early if set.
1 parent afc30b7 commit d2ea166

File tree

3 files changed

+37
-0
lines changed

3 files changed

+37
-0
lines changed

lib/_http_common.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ function freeParser(parser, req, socket) {
189189
if (parser) {
190190
if (parser._consumed)
191191
parser.unconsume();
192+
parser.markFreed();
192193
cleanParser(parser);
193194
parser.remove();
194195
if (parsers.free(parser) === false) {

src/node_http_parser.cc

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,12 @@ class Parser : public AsyncWrap, public StreamListener {
623623
parser->EmitDestroy();
624624
}
625625

626+
static void MarkFreed(const FunctionCallbackInfo<Value>& args) {
627+
Parser* parser;
628+
ASSIGN_OR_RETURN_UNWRAP(&parser, args.This());
629+
parser->is_being_freed_ = true;
630+
}
631+
626632
// TODO(@anonrig): Add V8 Fast API
627633
static void Remove(const FunctionCallbackInfo<Value>& args) {
628634
Parser* parser;
@@ -1012,6 +1018,7 @@ class Parser : public AsyncWrap, public StreamListener {
10121018
num_values_ = 0;
10131019
have_flushed_ = false;
10141020
got_exception_ = false;
1021+
is_being_freed_ = false;
10151022
headers_completed_ = false;
10161023
max_http_header_size_ = max_http_header_size;
10171024
}
@@ -1056,6 +1063,7 @@ class Parser : public AsyncWrap, public StreamListener {
10561063
size_t num_values_;
10571064
bool have_flushed_;
10581065
bool got_exception_;
1066+
bool is_being_freed_ = false;
10591067
size_t current_buffer_len_;
10601068
const char* current_buffer_data_;
10611069
bool headers_completed_ = false;
@@ -1075,6 +1083,9 @@ class Parser : public AsyncWrap, public StreamListener {
10751083
struct Proxy<int (Parser::*)(Args...), Member> {
10761084
static int Raw(llhttp_t* p, Args ... args) {
10771085
Parser* parser = ContainerOf(&Parser::parser_, p);
1086+
if (parser->is_being_freed_) {
1087+
return 0;
1088+
}
10781089
int rv = (parser->*Member)(std::forward<Args>(args)...);
10791090
if (rv == 0) {
10801091
rv = parser->MaybePause();
@@ -1332,6 +1343,7 @@ void CreatePerIsolateProperties(IsolateData* isolate_data,
13321343
t->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
13331344
SetProtoMethod(isolate, t, "close", Parser::Close);
13341345
SetProtoMethod(isolate, t, "free", Parser::Free);
1346+
SetProtoMethod(isolate, t, "markFreed", Parser::MarkFreed);
13351347
SetProtoMethod(isolate, t, "remove", Parser::Remove);
13361348
SetProtoMethod(isolate, t, "execute", Parser::Execute);
13371349
SetProtoMethod(isolate, t, "finish", Parser::Finish);
@@ -1401,6 +1413,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
14011413
registry->Register(Parser::New);
14021414
registry->Register(Parser::Close);
14031415
registry->Register(Parser::Free);
1416+
registry->Register(Parser::MarkFreed);
14041417
registry->Register(Parser::Remove);
14051418
registry->Register(Parser::Execute);
14061419
registry->Register(Parser::Finish);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
const { createServer } = require('http');
5+
const { connect } = require('net');
6+
7+
// Regression test: ensure llhttp_execute() is aborted when freeParser() is
8+
// called synchronously during parsing of pipelined requests.
9+
const server = createServer(common.mustCall((req, res) => {
10+
req.socket.emit('close');
11+
res.end();
12+
}, 1));
13+
14+
server.unref();
15+
16+
server.listen(0, common.mustCall(() => {
17+
// Two pipelined requests in one write, processed by a single llhttp_execute().
18+
const client = connect(server.address().port);
19+
client.end(
20+
'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n' +
21+
'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n',
22+
);
23+
}));

0 commit comments

Comments
 (0)