Skip to content

Commit 3a80d9c

Browse files
indutnymscdex
authored andcommittedJul 9, 2015
_stream_wrap: prevent use after free in TLS
Queued write requests should be invoked on handle close, otherwise the "consumer" might be already destroyed when the write callbacks of the "consumed" handle will be invoked. Same applies to the shutdown requests. Make sure to "move" away socket from server to not break the `connections` counter in `net.js`. Otherwise it might not call `close` callback, or call it too early. Fix: nodejs#1696 PR-URL: nodejs#1910 Reviewed-By: Trevor Norris <[email protected]>
1 parent bb2180a commit 3a80d9c

7 files changed

+239
-33
lines changed
 

‎lib/_stream_wrap.js

+115-19
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
'use strict';
22

3+
const assert = require('assert');
34
const util = require('util');
45
const Socket = require('net').Socket;
56
const JSStream = process.binding('js_stream').JSStream;
67
const uv = process.binding('uv');
8+
const debug = util.debuglog('stream_wrap');
79

810
function StreamWrap(stream) {
9-
var handle = new JSStream();
11+
const handle = new JSStream();
1012

1113
this.stream = stream;
1214

13-
var self = this;
15+
this._list = null;
16+
17+
const self = this;
1418
handle.close = function(cb) {
15-
cb();
19+
debug('close');
20+
self.doClose(cb);
1621
};
1722
handle.isAlive = function() {
1823
return self.isAlive();
@@ -27,21 +32,25 @@ function StreamWrap(stream) {
2732
return self.readStop();
2833
};
2934
handle.onshutdown = function(req) {
30-
return self.shutdown(req);
35+
return self.doShutdown(req);
3136
};
3237
handle.onwrite = function(req, bufs) {
33-
return self.write(req, bufs);
38+
return self.doWrite(req, bufs);
3439
};
3540

3641
this.stream.pause();
42+
this.stream.on('error', function(err) {
43+
self.emit('error', err);
44+
});
3745
this.stream.on('data', function(chunk) {
38-
self._handle.readBuffer(chunk);
46+
debug('data', chunk.length);
47+
if (self._handle)
48+
self._handle.readBuffer(chunk);
3949
});
4050
this.stream.once('end', function() {
41-
self._handle.emitEOF();
42-
});
43-
this.stream.on('error', function(err) {
44-
self.emit('error', err);
51+
debug('end');
52+
if (self._handle)
53+
self._handle.emitEOF();
4554
});
4655

4756
Socket.call(this, {
@@ -55,11 +64,11 @@ module.exports = StreamWrap;
5564
StreamWrap.StreamWrap = StreamWrap;
5665

5766
StreamWrap.prototype.isAlive = function isAlive() {
58-
return this.readable && this.writable;
67+
return true;
5968
};
6069

6170
StreamWrap.prototype.isClosing = function isClosing() {
62-
return !this.isAlive();
71+
return !this.readable || !this.writable;
6372
};
6473

6574
StreamWrap.prototype.readStart = function readStart() {
@@ -72,21 +81,31 @@ StreamWrap.prototype.readStop = function readStop() {
7281
return 0;
7382
};
7483

75-
StreamWrap.prototype.shutdown = function shutdown(req) {
76-
var self = this;
84+
StreamWrap.prototype.doShutdown = function doShutdown(req) {
85+
const self = this;
86+
const handle = this._handle;
87+
const item = this._enqueue('shutdown', req);
7788

7889
this.stream.end(function() {
7990
// Ensure that write was dispatched
8091
setImmediate(function() {
81-
self._handle.finishShutdown(req, 0);
92+
if (!self._dequeue(item))
93+
return;
94+
95+
handle.finishShutdown(req, 0);
8296
});
8397
});
8498
return 0;
8599
};
86100

87-
StreamWrap.prototype.write = function write(req, bufs) {
101+
StreamWrap.prototype.doWrite = function doWrite(req, bufs) {
102+
const self = this;
103+
const handle = self._handle;
104+
88105
var pending = bufs.length;
89-
var self = this;
106+
107+
// Queue the request to be able to cancel it
108+
const item = self._enqueue('write', req);
90109

91110
self.stream.cork();
92111
bufs.forEach(function(buf) {
@@ -103,6 +122,10 @@ StreamWrap.prototype.write = function write(req, bufs) {
103122

104123
// Ensure that write was dispatched
105124
setImmediate(function() {
125+
// Do not invoke callback twice
126+
if (!self._dequeue(item))
127+
return;
128+
106129
var errCode = 0;
107130
if (err) {
108131
if (err.code && uv['UV_' + err.code])
@@ -111,10 +134,83 @@ StreamWrap.prototype.write = function write(req, bufs) {
111134
errCode = uv.UV_EPIPE;
112135
}
113136

114-
self._handle.doAfterWrite(req);
115-
self._handle.finishWrite(req, errCode);
137+
handle.doAfterWrite(req);
138+
handle.finishWrite(req, errCode);
116139
});
117140
}
118141

119142
return 0;
120143
};
144+
145+
function QueueItem(type, req) {
146+
this.type = type;
147+
this.req = req;
148+
this.prev = this;
149+
this.next = this;
150+
}
151+
152+
StreamWrap.prototype._enqueue = function enqueue(type, req) {
153+
const item = new QueueItem(type, req);
154+
if (this._list === null) {
155+
this._list = item;
156+
return item;
157+
}
158+
159+
item.next = this._list.next;
160+
item.prev = this._list;
161+
item.next.prev = item;
162+
item.prev.next = item;
163+
164+
return item;
165+
};
166+
167+
StreamWrap.prototype._dequeue = function dequeue(item) {
168+
assert(item instanceof QueueItem);
169+
170+
var next = item.next;
171+
var prev = item.prev;
172+
173+
if (next === null && prev === null)
174+
return false;
175+
176+
item.next = null;
177+
item.prev = null;
178+
179+
if (next === item) {
180+
prev = null;
181+
next = null;
182+
} else {
183+
prev.next = next;
184+
next.prev = prev;
185+
}
186+
187+
if (this._list === item)
188+
this._list = next;
189+
190+
return true;
191+
};
192+
193+
StreamWrap.prototype.doClose = function doClose(cb) {
194+
const self = this;
195+
const handle = self._handle;
196+
197+
setImmediate(function() {
198+
while (self._list !== null) {
199+
const item = self._list;
200+
const req = item.req;
201+
self._dequeue(item);
202+
203+
const errCode = uv.UV_ECANCELED;
204+
if (item.type === 'write') {
205+
handle.doAfterWrite(req);
206+
handle.finishWrite(req, errCode);
207+
} else if (item.type === 'shutdown') {
208+
handle.finishShutdown(req, errCode);
209+
}
210+
}
211+
212+
// Should be already set by net.js
213+
assert(self._handle === null);
214+
cb();
215+
});
216+
};

‎lib/_tls_wrap.js

+28-4
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ function TLSSocket(socket, options) {
254254
this.encrypted = true;
255255

256256
net.Socket.call(this, {
257-
handle: this._wrapHandle(wrap && wrap._handle),
257+
handle: this._wrapHandle(wrap),
258258
allowHalfOpen: socket && socket.allowHalfOpen,
259259
readable: false,
260260
writable: false
@@ -279,7 +279,7 @@ util.inherits(TLSSocket, net.Socket);
279279
exports.TLSSocket = TLSSocket;
280280

281281
var proxiedMethods = [
282-
'close', 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6',
282+
'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6',
283283
'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive',
284284
'setSimultaneousAccepts', 'setBlocking',
285285

@@ -295,8 +295,20 @@ proxiedMethods.forEach(function(name) {
295295
};
296296
});
297297

298-
TLSSocket.prototype._wrapHandle = function(handle) {
298+
tls_wrap.TLSWrap.prototype.close = function closeProxy(cb) {
299+
if (this._parentWrap && this._parentWrap._handle === this._parent) {
300+
setImmediate(cb);
301+
return this._parentWrap.destroy();
302+
}
303+
return this._parent.close(cb);
304+
};
305+
306+
TLSSocket.prototype._wrapHandle = function(wrap) {
299307
var res;
308+
var handle;
309+
310+
if (wrap)
311+
handle = wrap._handle;
300312

301313
var options = this._tlsOptions;
302314
if (!handle) {
@@ -310,6 +322,7 @@ TLSSocket.prototype._wrapHandle = function(handle) {
310322
tls.createSecureContext();
311323
res = tls_wrap.wrap(handle, context.context, options.isServer);
312324
res._parent = handle;
325+
res._parentWrap = wrap;
313326
res._secureContext = context;
314327
res.reading = handle.reading;
315328
Object.defineProperty(handle, 'reading', {
@@ -355,7 +368,13 @@ TLSSocket.prototype._init = function(socket, wrap) {
355368
// represent real writeQueueSize during regular writes.
356369
ssl.writeQueueSize = 1;
357370

358-
this.server = options.server || null;
371+
this.server = options.server;
372+
373+
// Move the server to TLSSocket, otherwise both `socket.destroy()` and
374+
// `TLSSocket.destroy()` will decrement number of connections of the TLS
375+
// server, leading to misfiring `server.close()` callback
376+
if (socket && socket.server === this.server)
377+
socket.server = null;
359378

360379
// For clients, we will always have either a given ca list or be using
361380
// default one
@@ -418,6 +437,7 @@ TLSSocket.prototype._init = function(socket, wrap) {
418437
// set `.onsniselect` callback.
419438
if (process.features.tls_sni &&
420439
options.isServer &&
440+
options.SNICallback &&
421441
options.server &&
422442
(options.SNICallback !== SNICallback ||
423443
options.server._contexts.length)) {
@@ -554,6 +574,10 @@ TLSSocket.prototype._start = function() {
554574
return;
555575
}
556576

577+
// Socket was destroyed before the connection was established
578+
if (!this._handle)
579+
return;
580+
557581
debug('start');
558582
if (this._tlsOptions.requestOCSP)
559583
this._handle.requestOCSP();

‎src/js_stream.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ template <class Wrap>
168168
void JSStream::Finish(const FunctionCallbackInfo<Value>& args) {
169169
Wrap* w = Unwrap<Wrap>(args[0].As<Object>());
170170

171-
w->Done(args[0]->Int32Value());
171+
w->Done(args[1]->Int32Value());
172172
}
173173

174174

‎src/tls_wrap.cc

+4-3
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,10 @@ void TLSWrap::EncOutCb(WriteWrap* req_wrap, int status) {
320320
TLSWrap* wrap = req_wrap->wrap()->Cast<TLSWrap>();
321321
req_wrap->Dispose();
322322

323+
// We should not be getting here after `DestroySSL`, because all queued writes
324+
// must be invoked with UV_ECANCELED
325+
CHECK_NE(wrap->ssl_, nullptr);
326+
323327
// Handle error
324328
if (status) {
325329
// Ignore errors after shutdown
@@ -331,9 +335,6 @@ void TLSWrap::EncOutCb(WriteWrap* req_wrap, int status) {
331335
return;
332336
}
333337

334-
if (wrap->ssl_ == nullptr)
335-
return;
336-
337338
// Commit
338339
NodeBIO::FromBIO(wrap->enc_out_)->Read(nullptr, wrap->write_size_);
339340

‎test/parallel/test-stream-wrap.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
5+
const StreamWrap = require('_stream_wrap');
6+
const Duplex = require('stream').Duplex;
7+
const ShutdownWrap = process.binding('stream_wrap').ShutdownWrap;
8+
9+
var done = false;
10+
11+
function testShutdown(callback) {
12+
var stream = new Duplex({
13+
read: function() {
14+
},
15+
write: function() {
16+
}
17+
});
18+
19+
var wrap = new StreamWrap(stream);
20+
21+
var req = new ShutdownWrap();
22+
req.oncomplete = function(code) {
23+
assert(code < 0);
24+
callback();
25+
};
26+
req.handle = wrap._handle;
27+
28+
// Close the handle to simulate
29+
wrap.destroy();
30+
req.handle.shutdown(req);
31+
}
32+
33+
testShutdown(function() {
34+
done = true;
35+
});
36+
37+
process.on('exit', function() {
38+
assert(done);
39+
});

‎test/parallel/test-tls-connect-given-socket.js

+23-6
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,33 @@ var server = tls.createServer(options, function(socket) {
4343
});
4444
assert(client.readable);
4545
assert(client.writable);
46+
47+
return client;
4648
}
4749

48-
// Already connected socket
49-
var connected = net.connect(common.PORT, function() {
50-
establish(connected);
50+
// Immediate death socket
51+
var immediateDeath = net.connect(common.PORT);
52+
establish(immediateDeath).destroy();
53+
54+
// Outliving
55+
var outlivingTCP = net.connect(common.PORT);
56+
outlivingTCP.on('connect', function() {
57+
outlivingTLS.destroy();
58+
next();
5159
});
60+
var outlivingTLS = establish(outlivingTCP);
61+
62+
function next() {
63+
// Already connected socket
64+
var connected = net.connect(common.PORT, function() {
65+
establish(connected);
66+
});
67+
68+
// Connecting socket
69+
var connecting = net.connect(common.PORT);
70+
establish(connecting);
5271

53-
// Connecting socket
54-
var connecting = net.connect(common.PORT);
55-
establish(connecting);
72+
}
5673
});
5774

5875
process.on('exit', function() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
var assert = require('assert');
3+
var common = require('../common');
4+
5+
if (!common.hasCrypto) {
6+
console.log('1..0 # Skipped: missing crypto');
7+
process.exit();
8+
}
9+
var tls = require('tls');
10+
var stream = require('stream');
11+
12+
var delay = new stream.Duplex({
13+
read: function read() {
14+
},
15+
write: function write(data, enc, cb) {
16+
console.log('pending');
17+
setTimeout(function() {
18+
console.log('done');
19+
cb();
20+
}, 200);
21+
}
22+
});
23+
24+
var secure = tls.connect({
25+
socket: delay
26+
});
27+
setImmediate(function() {
28+
secure.destroy();
29+
});

0 commit comments

Comments
 (0)
Please sign in to comment.