Skip to content

Commit d7ba31b

Browse files
committed
Replace covert pipe with self-pipe SIGCHLD handler
For background, see ninja-build#2444 (comment). In short, when running subprocesses that share the terminal, ninja intentionally leaves a pipe open before exec() so that it can use EOF from that pipe to detect when the subprocess has exited. That mechanism is problematic: If the subprocess ends up spawning background processes (e.g. sccache), those would also inherit the pipe by default. In that case, ninja may not detect process termination until all background processes have quitted. This patch makes it so that, instead of propagating the pipe file descriptor to the subprocess without its knowledge, ninja keeps both ends of the pipe to itself, and uses a SIGCHLD handler to close the write end of the pipe when the subprocess has truly exited. During testing I found Subprocess::Finish() lacked EINTR retrying, which made ninja crash prematurely. This patch also fixes that. Fixes ninja-build#2444
1 parent 048a68d commit d7ba31b

File tree

2 files changed

+130
-7
lines changed

2 files changed

+130
-7
lines changed

src/subprocess-posix.cc

+129-7
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ extern char** environ;
3636

3737
using namespace std;
3838

39+
static void CloseFileWhenPidExits(pid_t pid, int fd);
40+
3941
Subprocess::Subprocess(bool use_console) : fd_(-1), pid_(-1),
4042
use_console_(use_console) {
4143
}
@@ -103,12 +105,13 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
103105
err = posix_spawn_file_actions_adddup2(&action, output_pipe[1], 2);
104106
if (err != 0)
105107
Fatal("posix_spawn_file_actions_adddup2: %s", strerror(err));
106-
err = posix_spawn_file_actions_addclose(&action, output_pipe[1]);
107-
if (err != 0)
108-
Fatal("posix_spawn_file_actions_addclose: %s", strerror(err));
109108
// In the console case, output_pipe is still inherited by the child and
110109
// closed when the subprocess finishes, which then notifies ninja.
111110
}
111+
err = posix_spawn_file_actions_addclose(&action, output_pipe[1]);
112+
if (err != 0)
113+
Fatal("posix_spawn_file_actions_addclose: %s", strerror(err));
114+
112115
#ifdef POSIX_SPAWN_USEVFORK
113116
flags |= POSIX_SPAWN_USEVFORK;
114117
#endif
@@ -117,20 +120,42 @@ bool Subprocess::Start(SubprocessSet* set, const string& command) {
117120
if (err != 0)
118121
Fatal("posix_spawnattr_setflags: %s", strerror(err));
119122

123+
if (use_console_) {
124+
// Block SIGCHLD to prevent a race where the new subprocess exits before we
125+
// register its PID.
126+
sigset_t signals;
127+
sigemptyset(&signals);
128+
sigaddset(&signals, SIGCHLD);
129+
if (sigprocmask(SIG_BLOCK, &signals, NULL) < 0)
130+
Fatal("sigprocmask: %s", strerror(errno));
131+
}
132+
120133
const char* spawned_args[] = { "/bin/sh", "-c", command.c_str(), NULL };
121134
err = posix_spawn(&pid_, "/bin/sh", &action, &attr,
122135
const_cast<char**>(spawned_args), environ);
123136
if (err != 0)
124137
Fatal("posix_spawn: %s", strerror(err));
125138

139+
if (use_console_) {
140+
// Shared console case: We keep the write-side of the pipe just so that we
141+
// can close it on SIGCHLD reception.
142+
CloseFileWhenPidExits(pid_, output_pipe[1]);
143+
sigset_t signals;
144+
sigemptyset(&signals);
145+
sigaddset(&signals, SIGCHLD);
146+
if (sigprocmask(SIG_UNBLOCK, &signals, NULL) < 0)
147+
Fatal("sigprocmask: %s", strerror(errno));
148+
} else {
149+
// Not shared console: Only the subprocess needs the write-end of the pipe.
150+
close(output_pipe[1]);
151+
}
152+
126153
err = posix_spawnattr_destroy(&attr);
127154
if (err != 0)
128155
Fatal("posix_spawnattr_destroy: %s", strerror(err));
129156
err = posix_spawn_file_actions_destroy(&action);
130157
if (err != 0)
131158
Fatal("posix_spawn_file_actions_destroy: %s", strerror(err));
132-
133-
close(output_pipe[1]);
134159
return true;
135160
}
136161

@@ -150,8 +175,10 @@ void Subprocess::OnPipeReady() {
150175
ExitStatus Subprocess::Finish() {
151176
assert(pid_ != -1);
152177
int status;
153-
if (waitpid(pid_, &status, 0) < 0)
154-
Fatal("waitpid(%d): %s", pid_, strerror(errno));
178+
while (waitpid(pid_, &status, 0) < 0) {
179+
if (errno != EINTR)
180+
Fatal("waitpid(%d): %s", pid_, strerror(errno));
181+
}
155182
pid_ = -1;
156183

157184
#ifdef _AIX
@@ -205,6 +232,93 @@ void SubprocessSet::HandlePendingInterruption() {
205232
interrupted_ = SIGHUP;
206233
}
207234

235+
// SIGCHLD handling:
236+
237+
struct PidFdEntry;
238+
typedef unsigned int IxEntry;
239+
// PidFdList is used to store the PID and file descriptor pairs of the
240+
// subprocesses being run by ninja that share a terminal.
241+
// On SIGCHLD the PID of the signal is matched with one of the entries and the
242+
// associated file descriptor closed. Then the entry is freed for reuse.
243+
// Normally we would use something like std::unordered_map<>, but that involves
244+
// memory allocations which are not reentrant.
245+
struct PidFdList {
246+
PidFdEntry* entries; // Entry with index 0 is reserved as NULL and not used.
247+
size_t capacity; // Number of PidFdEntry allocated for pid_fds.
248+
IxEntry ix_head_used;
249+
IxEntry ix_head_free;
250+
};
251+
252+
// A PidFdEntry may be used or free.
253+
struct PidFdEntry {
254+
pid_t pid; // -1 if this is a free entry
255+
int fd;
256+
// ix_next of a used entry points to another used entry, ix_next of free entry
257+
// points to another free entry.
258+
IxEntry ix_next;
259+
};
260+
261+
static PidFdList PID_FD_LIST = { nullptr, 0, 0, 1 };
262+
263+
static void initializePidFdEntries(PidFdEntry* entries, IxEntry ix_start, size_t capacity) {
264+
for (unsigned int i = ix_start; i < capacity; i++) {
265+
entries[i].pid = -1;
266+
entries[i].fd = -1;
267+
entries[i].ix_next = i + 1;
268+
}
269+
entries[capacity - 1].ix_next = 0; // end of the free list.
270+
}
271+
272+
// Must only be called with SIGCHLD blocked.
273+
static void CloseFileWhenPidExits(pid_t pid, int fd) {
274+
PidFdList* list = &PID_FD_LIST;
275+
if (!list->entries) {
276+
// First use, allocate the list.
277+
list->capacity = 16;
278+
list->entries = new PidFdEntry[list->capacity];
279+
initializePidFdEntries(list->entries, 1, list->capacity);
280+
} else if (!list->ix_head_free) {
281+
// Expand the list to get more free entries.
282+
size_t old_capacity = list->capacity;
283+
list->capacity *= 2;
284+
list->entries = static_cast<PidFdEntry*>(realloc(list->entries,
285+
sizeof(PidFdEntry) * list->capacity));
286+
initializePidFdEntries(list->entries, old_capacity, list->capacity);
287+
list->ix_head_free = old_capacity;
288+
}
289+
// Replace the head free entry and make it the new used head.
290+
IxEntry ix_entry = list->ix_head_free;
291+
PidFdEntry* entry = &list->entries[ix_entry];
292+
IxEntry ix_next_free = entry->ix_next;
293+
entry->pid = pid;
294+
entry->fd = fd;
295+
entry->ix_next = list->ix_head_used;
296+
list->ix_head_used = ix_entry;
297+
list->ix_head_free = ix_next_free;
298+
}
299+
300+
static void SigChldHandler(int signo, siginfo_t* info, void* context) {
301+
int pid = info->si_pid;
302+
303+
PidFdList* list = &PID_FD_LIST;
304+
if (!list->entries)
305+
return;
306+
for (IxEntry* ix = &list->ix_head_used; *ix; ix = &list->entries[*ix].ix_next) {
307+
PidFdEntry* entry = &list->entries[*ix];
308+
if (entry->pid == pid) {
309+
// Found a match. Close the pipe and remove the entry.
310+
close(entry->fd);
311+
IxEntry ix_next_used = entry->ix_next;
312+
entry->fd = -1;
313+
entry->pid = -1;
314+
entry->ix_next = list->ix_head_free;
315+
list->ix_head_free = *ix;
316+
*ix = ix_next_used;
317+
break;
318+
}
319+
}
320+
}
321+
208322
SubprocessSet::SubprocessSet() {
209323
sigset_t set;
210324
sigemptyset(&set);
@@ -223,6 +337,12 @@ SubprocessSet::SubprocessSet() {
223337
Fatal("sigaction: %s", strerror(errno));
224338
if (sigaction(SIGHUP, &act, &old_hup_act_) < 0)
225339
Fatal("sigaction: %s", strerror(errno));
340+
341+
memset(&act, 0, sizeof(act));
342+
act.sa_flags = SA_SIGINFO | SA_NOCLDSTOP;
343+
act.sa_sigaction = SigChldHandler;
344+
if (sigaction(SIGCHLD, &act, &old_chld_act_) < 0)
345+
Fatal("sigaction: %s", strerror(errno));
226346
}
227347

228348
SubprocessSet::~SubprocessSet() {
@@ -234,6 +354,8 @@ SubprocessSet::~SubprocessSet() {
234354
Fatal("sigaction: %s", strerror(errno));
235355
if (sigaction(SIGHUP, &old_hup_act_, 0) < 0)
236356
Fatal("sigaction: %s", strerror(errno));
357+
if (sigaction(SIGCHLD, &old_chld_act_, 0) < 0)
358+
Fatal("sigaction: %s", strerror(errno));
237359
if (sigprocmask(SIG_SETMASK, &old_mask_, 0) < 0)
238360
Fatal("sigprocmask: %s", strerror(errno));
239361
}

src/subprocess.h

+1
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ struct SubprocessSet {
106106
struct sigaction old_int_act_;
107107
struct sigaction old_term_act_;
108108
struct sigaction old_hup_act_;
109+
struct sigaction old_chld_act_;
109110
sigset_t old_mask_;
110111
#endif
111112
};

0 commit comments

Comments
 (0)