Skip to content

Commit afcee0e

Browse files
committed
crypto: fix TLSWrap use-after-free on pending write
EncOut() passes pointers from the enc_out_ BIO internal buffer to the underlying stream's uv_write(). write_size_ is non-zero while that write is in flight. Calling ssl_.reset() frees the SSL context and its BIOs, turning those pointers into dangling references. When libuv completes the write it accesses freed memory (SIGSEGV). Use ssl_.release() instead of ssl_.reset() when write_size_ != 0 so the BIO data stays alive for the in-flight uv_write. This is a bounded leak (one SSL context per socket destroyed with in-flight writes) that prevents a segfault. Also move RemoveStreamListener() before SSL cleanup so the underlying stream cannot call back into the TLSWrap after its SSL state is gone. Refs: #62393 Made-with: Cursor
1 parent 0f68423 commit afcee0e

2 files changed

Lines changed: 71 additions & 6 deletions

File tree

src/crypto/crypto_tls.cc

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,15 +1317,24 @@ void TLSWrap::Destroy() {
13171317
// And destroy
13181318
InvokeQueued(UV_ECANCELED, "Canceled because of SSL destruction");
13191319

1320-
env()->external_memory_accounter()->Decrease(env()->isolate(), kExternalSize);
1321-
ssl_.reset();
1322-
1323-
enc_in_ = nullptr;
1324-
enc_out_ = nullptr;
1325-
1320+
// Detach from the underlying stream before releasing the SSL context.
13261321
if (underlying_stream() != nullptr)
13271322
underlying_stream()->RemoveStreamListener(this);
13281323

1324+
// EncOut() passes pointers into the enc_out_ BIO internal buffer to the
1325+
// underlying stream via uv_write(). write_size_ is non-zero while that
1326+
// write is in flight. Freeing the SSL context would turn those pointers
1327+
// into dangling references (use-after-free when libuv completes the
1328+
// write). Release ownership without freeing so the BIO data stays alive.
1329+
if (write_size_ != 0) {
1330+
ssl_.release();
1331+
} else {
1332+
ssl_.reset();
1333+
}
1334+
1335+
env()->external_memory_accounter()->Decrease(env()->isolate(), kExternalSize);
1336+
enc_in_ = nullptr;
1337+
enc_out_ = nullptr;
13291338
sc_.reset();
13301339
}
13311340

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
3+
// Regression test for TLSWrap use-after-free when destroySSL() is called
4+
// while an underlying stream write is still in flight.
5+
//
6+
// EncOut() passes pointers into the enc_out_ BIO's internal buffer to the
7+
// underlying stream via uv_write(). If the SSL context is freed before that
8+
// write completes, libuv accesses freed memory (SIGSEGV).
9+
//
10+
// The GC weak callback path triggers the same Destroy() code when a
11+
// TLSSocket with pending writes is collected without an explicit destroy().
12+
13+
const common = require('../common');
14+
if (!common.hasCrypto)
15+
common.skip('missing crypto');
16+
17+
const fixtures = require('../common/fixtures');
18+
const tls = require('node:tls');
19+
20+
const server = tls.createServer({
21+
key: fixtures.readKey('agent1-key.pem'),
22+
cert: fixtures.readKey('agent1-cert.pem'),
23+
}, (socket) => {
24+
// Do not read — TCP backpressure keeps client writes pending in the
25+
// TLSWrap's underlying stream so write_size_ stays non-zero.
26+
socket.pause();
27+
});
28+
29+
server.listen(0, common.mustCall(() => {
30+
const { port } = server.address();
31+
let remaining = 10;
32+
33+
for (let i = 0; i < 10; i++) {
34+
const socket = tls.connect(
35+
{ port, host: '127.0.0.1', rejectUnauthorized: false },
36+
common.mustCall(() => {
37+
// Queue a write large enough to stay pending (server never reads).
38+
socket.write(Buffer.alloc(1024 * 1024));
39+
40+
// Simulate the GC weak callback path: free the SSL context while
41+
// the underlying stream write is still in flight.
42+
if (socket._handle && socket._handle.destroySSL) {
43+
socket._handle.destroySSL();
44+
}
45+
46+
setTimeout(() => {
47+
socket.destroy();
48+
if (--remaining === 0) {
49+
server.close();
50+
}
51+
}, 50);
52+
}),
53+
);
54+
socket.on('error', () => {});
55+
}
56+
}));

0 commit comments

Comments
 (0)