-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
_stream_wrap: prevent use after free in TLS #1910
Changes from all commits
c2aea93
8d46c7b
b349032
b27ff94
94c12fe
6cdda31
8375f78
29b3713
95bc05a
aff1b5f
b6444ee
3d3709b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,23 @@ | ||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const util = require('util'); | ||
const Socket = require('net').Socket; | ||
const JSStream = process.binding('js_stream').JSStream; | ||
const uv = process.binding('uv'); | ||
const debug = util.debuglog('stream_wrap'); | ||
|
||
function StreamWrap(stream) { | ||
var handle = new JSStream(); | ||
const handle = new JSStream(); | ||
|
||
this.stream = stream; | ||
|
||
var self = this; | ||
this._queue = null; | ||
|
||
const self = this; | ||
handle.close = function(cb) { | ||
cb(); | ||
debug('close'); | ||
self.doClose(cb); | ||
}; | ||
handle.isAlive = function() { | ||
return self.isAlive(); | ||
|
@@ -27,21 +32,29 @@ function StreamWrap(stream) { | |
return self.readStop(); | ||
}; | ||
handle.onshutdown = function(req) { | ||
return self.shutdown(req); | ||
return self.doShutdown(req); | ||
}; | ||
handle.onwrite = function(req, bufs) { | ||
return self.write(req, bufs); | ||
return self.doWrite(req, bufs); | ||
}; | ||
|
||
this.stream.pause(); | ||
this.stream.on('error', function(err) { | ||
self.emit('error', err); | ||
}); | ||
this.stream.on('data', function(chunk) { | ||
self._handle.readBuffer(chunk); | ||
setImmediate(function() { | ||
debug('data', chunk.length); | ||
if (self._handle) | ||
self._handle.readBuffer(chunk); | ||
}); | ||
}); | ||
this.stream.once('end', function() { | ||
self._handle.emitEOF(); | ||
}); | ||
this.stream.on('error', function(err) { | ||
self.emit('error', err); | ||
setImmediate(function() { | ||
debug('end'); | ||
if (self._handle) | ||
self._handle.emitEOF(); | ||
}); | ||
}); | ||
|
||
Socket.call(this, { | ||
|
@@ -55,11 +68,11 @@ module.exports = StreamWrap; | |
StreamWrap.StreamWrap = StreamWrap; | ||
|
||
StreamWrap.prototype.isAlive = function isAlive() { | ||
return this.readable && this.writable; | ||
return true; | ||
}; | ||
|
||
StreamWrap.prototype.isClosing = function isClosing() { | ||
return !this.isAlive(); | ||
return !this.readable || !this.writable; | ||
}; | ||
|
||
StreamWrap.prototype.readStart = function readStart() { | ||
|
@@ -72,21 +85,31 @@ StreamWrap.prototype.readStop = function readStop() { | |
return 0; | ||
}; | ||
|
||
StreamWrap.prototype.shutdown = function shutdown(req) { | ||
var self = this; | ||
StreamWrap.prototype.doShutdown = function doShutdown(req) { | ||
const self = this; | ||
const handle = this._handle; | ||
const item = this._enqueue('shutdown', req); | ||
|
||
this.stream.end(function() { | ||
// Ensure that write was dispatched | ||
setImmediate(function() { | ||
self._handle.finishShutdown(req, 0); | ||
if (!self._dequeue(item)) | ||
return; | ||
|
||
handle.finishShutdown(req, 0); | ||
}); | ||
}); | ||
return 0; | ||
}; | ||
|
||
StreamWrap.prototype.write = function write(req, bufs) { | ||
StreamWrap.prototype.doWrite = function doWrite(req, bufs) { | ||
const self = this; | ||
const handle = self._handle; | ||
|
||
var pending = bufs.length; | ||
var self = this; | ||
|
||
// Queue the request to be able to cancel it | ||
const item = self._enqueue('write', req); | ||
|
||
self.stream.cork(); | ||
bufs.forEach(function(buf) { | ||
|
@@ -103,6 +126,10 @@ StreamWrap.prototype.write = function write(req, bufs) { | |
|
||
// Ensure that write was dispatched | ||
setImmediate(function() { | ||
// Do not invoke callback twice | ||
if (!self._dequeue(item)) | ||
return; | ||
|
||
var errCode = 0; | ||
if (err) { | ||
if (err.code && uv['UV_' + err.code]) | ||
|
@@ -111,10 +138,81 @@ StreamWrap.prototype.write = function write(req, bufs) { | |
errCode = uv.UV_EPIPE; | ||
} | ||
|
||
self._handle.doAfterWrite(req); | ||
self._handle.finishWrite(req, errCode); | ||
handle.doAfterWrite(req); | ||
handle.finishWrite(req, errCode); | ||
}); | ||
} | ||
|
||
return 0; | ||
}; | ||
|
||
function QueueItem(type, req) { | ||
this.type = type; | ||
this.req = req; | ||
this.prev = this; | ||
this.next = this; | ||
} | ||
|
||
StreamWrap.prototype._enqueue = function enqueue(type, req) { | ||
const item = new QueueItem(type, req); | ||
if (this._queue === null) { | ||
this._queue = item; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be changed to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! Thanks for the suggestion! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I had the exact same suggestion in mind when I reviewed it last week. +1 |
||
return item; | ||
} | ||
|
||
item.next = this._queue.next; | ||
item.prev = this._queue; | ||
item.next.prev = item; | ||
item.prev.next = item; | ||
|
||
return item; | ||
}; | ||
|
||
StreamWrap.prototype._dequeue = function dequeue(item) { | ||
var next = item.next; | ||
var prev = item.prev; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is when I wish we allowed macros. A simple |
||
|
||
if (next === null && prev === null) | ||
return false; | ||
|
||
item.next = null; | ||
item.prev = null; | ||
|
||
if (next === item) { | ||
prev = null; | ||
next = null; | ||
} else { | ||
prev.next = next; | ||
next.prev = prev; | ||
} | ||
|
||
if (this._queue === item) | ||
this._queue = next; | ||
|
||
return true; | ||
}; | ||
|
||
StreamWrap.prototype.doClose = function doClose(cb) { | ||
const self = this; | ||
const handle = self._handle; | ||
|
||
setImmediate(function() { | ||
while (self._queue !== null) { | ||
const item = self._queue; | ||
const req = item.req; | ||
self._dequeue(item); | ||
|
||
const errCode = uv.UV_ECANCELED; | ||
if (item.type === 'write') { | ||
handle.doAfterWrite(req); | ||
handle.finishWrite(req, errCode); | ||
} else if (item.type === 'shutdown') { | ||
handle.finishShutdown(req, errCode); | ||
} | ||
} | ||
|
||
// Should be already set by net.js | ||
assert(self._handle === null); | ||
cb(); | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
'use strict'; | ||
const common = require('../common'); | ||
const assert = require('assert'); | ||
|
||
const StreamWrap = require('_stream_wrap'); | ||
const Duplex = require('stream').Duplex; | ||
const ShutdownWrap = process.binding('stream_wrap').ShutdownWrap; | ||
|
||
var done = false; | ||
|
||
function testShutdown(callback) { | ||
var stream = new Duplex({ | ||
read: function() { | ||
}, | ||
write: function() { | ||
} | ||
}); | ||
|
||
var wrap = new StreamWrap(stream); | ||
|
||
var req = new ShutdownWrap(); | ||
req.oncomplete = function(code) { | ||
assert(code < 0); | ||
callback(); | ||
}; | ||
req.handle = wrap._handle; | ||
|
||
// Close the handle to simulate | ||
wrap.destroy(); | ||
req.handle.shutdown(req); | ||
} | ||
|
||
testShutdown(function() { | ||
done = true; | ||
}); | ||
|
||
process.on('exit', function() { | ||
assert(done); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: throw in a comment on why the
setImmediate()
is necessary. future proofing for new devs. :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, let's drop it out, and revert it back only in case of any troubles. I no longer think that it might be reasonable. (@EricTheOne: I hope you don't mind)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@indutny I don't understand the code enough to comment on the code level, but I'll retest the final version and let you know if anything new comes up.
btw just to further my understanding, why would setImmediate be resolving possible troubles there?