Skip to content

Commit 0884c70

Browse files
santigimenoevanlucas
authored andcommitted
child_process: workaround fd passing issue on OS X
There's an issue on some `OS X` versions when passing fd's between processes. When the handle associated to a specific file descriptor is closed by the sender process before it's received in the destination, the handle is indeed closed while it should remain opened. In order to fix this behaviour, don't close the handle until the `NODE_HANDLE_ACK` is received by the sender. Added `test-child-process-pass-fd` that is basically `test-cluster-net-send` but creating lots of workers, so the issue reproduces on `OS X` consistently. Fixes: #7512 PR-URL: #7572 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 4deb054 commit 0884c70

File tree

2 files changed

+86
-8
lines changed

2 files changed

+86
-8
lines changed

lib/internal/child_process.js

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,21 @@ const handleConversion = {
9494
return handle;
9595
},
9696

97-
postSend: function(handle, options) {
98-
// Close the Socket handle after sending it
99-
if (handle && !options.keepOpen)
100-
handle.close();
97+
postSend: function(handle, options, target) {
98+
// Store the handle after successfully sending it, so it can be closed
99+
// when the NODE_HANDLE_ACK is received. If the handle could not be sent,
100+
// just close it.
101+
if (handle && !options.keepOpen) {
102+
if (target) {
103+
// There can only be one _pendingHandle as passing handles are
104+
// processed one at a time: handles are stored in _handleQueue while
105+
// waiting for the NODE_HANDLE_ACK of the current passing handle.
106+
assert(!target._pendingHandle);
107+
target._pendingHandle = handle;
108+
} else {
109+
handle.close();
110+
}
111+
}
101112
},
102113

103114
got: function(message, handle, emit) {
@@ -400,6 +411,7 @@ ChildProcess.prototype.unref = function() {
400411
function setupChannel(target, channel) {
401412
target._channel = channel;
402413
target._handleQueue = null;
414+
target._pendingHandle = null;
403415

404416
const control = new class extends EventEmitter {
405417
constructor() {
@@ -465,6 +477,11 @@ function setupChannel(target, channel) {
465477
target.on('internalMessage', function(message, handle) {
466478
// Once acknowledged - continue sending handles.
467479
if (message.cmd === 'NODE_HANDLE_ACK') {
480+
if (target._pendingHandle) {
481+
target._pendingHandle.close();
482+
target._pendingHandle = null;
483+
}
484+
468485
assert(Array.isArray(target._handleQueue));
469486
var queue = target._handleQueue;
470487
target._handleQueue = null;
@@ -610,13 +627,16 @@ function setupChannel(target, channel) {
610627
var err = channel.writeUtf8String(req, string, handle);
611628

612629
if (err === 0) {
613-
if (handle && !this._handleQueue)
614-
this._handleQueue = [];
630+
if (handle) {
631+
if (!this._handleQueue)
632+
this._handleQueue = [];
633+
if (obj && obj.postSend)
634+
obj.postSend(handle, options, target);
635+
}
636+
615637
req.oncomplete = function() {
616638
if (this.async === true)
617639
control.unref();
618-
if (obj && obj.postSend)
619-
obj.postSend(handle, options);
620640
if (typeof callback === 'function')
621641
callback(null);
622642
};
@@ -677,6 +697,11 @@ function setupChannel(target, channel) {
677697
// This marks the fact that the channel is actually disconnected.
678698
this._channel = null;
679699

700+
if (this._pendingHandle) {
701+
this._pendingHandle.close();
702+
this._pendingHandle = null;
703+
}
704+
680705
var fired = false;
681706
function finish() {
682707
if (fired) return;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const fork = require('child_process').fork;
5+
const net = require('net');
6+
7+
if ((process.config.variables.arm_version === '6') ||
8+
(process.config.variables.arm_version === '7')) {
9+
common.skip('Too slow for armv6 and armv7 bots');
10+
return;
11+
}
12+
13+
const N = 80;
14+
15+
if (process.argv[2] !== 'child') {
16+
for (let i = 0; i < N; ++i) {
17+
const worker = fork(__filename, ['child', common.PORT + i]);
18+
worker.once('message', common.mustCall((msg, handle) => {
19+
assert.strictEqual(msg, 'handle');
20+
assert.ok(handle);
21+
worker.send('got');
22+
23+
let recvData = '';
24+
handle.on('data', common.mustCall((data) => {
25+
recvData += data;
26+
}));
27+
28+
handle.on('end', () => {
29+
assert.strictEqual(recvData, 'hello');
30+
worker.kill();
31+
});
32+
}));
33+
}
34+
} else {
35+
let socket;
36+
const port = process.argv[3];
37+
let cbcalls = 0;
38+
function socketConnected() {
39+
if (++cbcalls === 2)
40+
process.send('handle', socket);
41+
}
42+
43+
const server = net.createServer((c) => {
44+
process.once('message', function(msg) {
45+
assert.strictEqual(msg, 'got');
46+
c.end('hello');
47+
});
48+
socketConnected();
49+
});
50+
server.listen(port, common.localhostIPv4, () => {
51+
socket = net.connect(port, common.localhostIPv4, socketConnected);
52+
});
53+
}

0 commit comments

Comments
 (0)