child_process: fix handleless NODE_HANDLE handling

It is possible that `recvmsg()` may return an error on ancillary data
reception when receiving a `NODE_HANDLE` message (for example
`MSG_CTRUNC`). This would end up, if the handle type was `net.Socket`,
on a `message` event with a non null but invalid `sendHandle`. To
improve the situation, send a `NODE_HANDLE_NACK` that'll cause the
sending process to retransmit the message again. In case the same
message is retransmitted 3 times without success, close the handle and
print a warning.

PR-URL: https://github.com/nodejs/node/pull/13235
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Santiago Gimeno 2017-05-25 12:29:05 +02:00
parent 732ae419b4
commit 71ca122def
No known key found for this signature in database
GPG Key ID: F28C3C8DA33C03BE

View File

@ -24,6 +24,8 @@ const errnoException = util._errnoException;
const SocketListSend = SocketList.SocketListSend; const SocketListSend = SocketList.SocketListSend;
const SocketListReceive = SocketList.SocketListReceive; const SocketListReceive = SocketList.SocketListReceive;
const MAX_HANDLE_RETRANSMISSIONS = 3;
// this object contain function to convert TCP objects to native handle objects // this object contain function to convert TCP objects to native handle objects
// and back again. // and back again.
const handleConversion = { const handleConversion = {
@ -89,17 +91,18 @@ const handleConversion = {
return handle; return handle;
}, },
postSend: function(handle, options, target) { postSend: function(message, handle, options, callback, target) {
// Store the handle after successfully sending it, so it can be closed // Store the handle after successfully sending it, so it can be closed
// when the NODE_HANDLE_ACK is received. If the handle could not be sent, // when the NODE_HANDLE_ACK is received. If the handle could not be sent,
// just close it. // just close it.
if (handle && !options.keepOpen) { if (handle && !options.keepOpen) {
if (target) { if (target) {
// There can only be one _pendingHandle as passing handles are // There can only be one _pendingMessage as passing handles are
// processed one at a time: handles are stored in _handleQueue while // processed one at a time: handles are stored in _handleQueue while
// waiting for the NODE_HANDLE_ACK of the current passing handle. // waiting for the NODE_HANDLE_ACK of the current passing handle.
assert(!target._pendingHandle); assert(!target._pendingMessage);
target._pendingHandle = handle; target._pendingMessage =
{ callback, message, handle, options, retransmissions: 0 };
} else { } else {
handle.close(); handle.close();
} }
@ -250,6 +253,11 @@ function getHandleWrapType(stream) {
return false; return false;
} }
function closePendingHandle(target) {
target._pendingMessage.handle.close();
target._pendingMessage = null;
}
ChildProcess.prototype.spawn = function(options) { ChildProcess.prototype.spawn = function(options) {
var ipc; var ipc;
@ -435,7 +443,7 @@ function setupChannel(target, channel) {
}); });
target._handleQueue = null; target._handleQueue = null;
target._pendingHandle = null; target._pendingMessage = null;
const control = new Control(channel); const control = new Control(channel);
@ -491,16 +499,31 @@ function setupChannel(target, channel) {
// handlers will go through this // handlers will go through this
target.on('internalMessage', function(message, handle) { target.on('internalMessage', function(message, handle) {
// Once acknowledged - continue sending handles. // Once acknowledged - continue sending handles.
if (message.cmd === 'NODE_HANDLE_ACK' ||
message.cmd === 'NODE_HANDLE_NACK') {
if (target._pendingMessage) {
if (message.cmd === 'NODE_HANDLE_ACK') { if (message.cmd === 'NODE_HANDLE_ACK') {
if (target._pendingHandle) { closePendingHandle(target);
target._pendingHandle.close(); } else if (target._pendingMessage.retransmissions++ ===
target._pendingHandle = null; MAX_HANDLE_RETRANSMISSIONS) {
closePendingHandle(target);
process.emitWarning('Handle did not reach the receiving process ' +
'correctly', 'SentHandleNotReceivedWarning');
}
} }
assert(Array.isArray(target._handleQueue)); assert(Array.isArray(target._handleQueue));
var queue = target._handleQueue; var queue = target._handleQueue;
target._handleQueue = null; target._handleQueue = null;
if (target._pendingMessage) {
target._send(target._pendingMessage.message,
target._pendingMessage.handle,
target._pendingMessage.options,
target._pendingMessage.callback);
}
for (var i = 0; i < queue.length; i++) { for (var i = 0; i < queue.length; i++) {
var args = queue[i]; var args = queue[i];
target._send(args.message, args.handle, args.options, args.callback); target._send(args.message, args.handle, args.options, args.callback);
@ -515,6 +538,12 @@ function setupChannel(target, channel) {
if (message.cmd !== 'NODE_HANDLE') return; if (message.cmd !== 'NODE_HANDLE') return;
// It is possible that the handle is not received because of some error on
// ancillary data reception such as MSG_CTRUNC. In this case, report the
// sender about it by sending a NODE_HANDLE_NACK message.
if (!handle)
return target._send({ cmd: 'NODE_HANDLE_NACK' }, null, true);
// Acknowledge handle receival. Don't emit error events (for example if // Acknowledge handle receival. Don't emit error events (for example if
// the other side has disconnected) because this call to send() is not // the other side has disconnected) because this call to send() is not
// initiated by the user and it shouldn't be fatal to be unable to ACK // initiated by the user and it shouldn't be fatal to be unable to ACK
@ -625,7 +654,8 @@ function setupChannel(target, channel) {
net._setSimultaneousAccepts(handle); net._setSimultaneousAccepts(handle);
} }
} else if (this._handleQueue && } else if (this._handleQueue &&
!(message && message.cmd === 'NODE_HANDLE_ACK')) { !(message && (message.cmd === 'NODE_HANDLE_ACK' ||
message.cmd === 'NODE_HANDLE_NACK'))) {
// Queue request anyway to avoid out-of-order messages. // Queue request anyway to avoid out-of-order messages.
this._handleQueue.push({ this._handleQueue.push({
callback: callback, callback: callback,
@ -647,7 +677,7 @@ function setupChannel(target, channel) {
if (!this._handleQueue) if (!this._handleQueue)
this._handleQueue = []; this._handleQueue = [];
if (obj && obj.postSend) if (obj && obj.postSend)
obj.postSend(handle, options, target); obj.postSend(message, handle, options, callback, target);
} }
if (req.async) { if (req.async) {
@ -663,7 +693,7 @@ function setupChannel(target, channel) {
} else { } else {
// Cleanup handle on error // Cleanup handle on error
if (obj && obj.postSend) if (obj && obj.postSend)
obj.postSend(handle, options); obj.postSend(message, handle, options, callback);
if (!options.swallowErrors) { if (!options.swallowErrors) {
const ex = errnoException(err, 'write'); const ex = errnoException(err, 'write');
@ -712,10 +742,8 @@ function setupChannel(target, channel) {
// This marks the fact that the channel is actually disconnected. // This marks the fact that the channel is actually disconnected.
this.channel = null; this.channel = null;
if (this._pendingHandle) { if (this._pendingMessage)
this._pendingHandle.close(); closePendingHandle(this);
this._pendingHandle = null;
}
var fired = false; var fired = false;
function finish() { function finish() {