Overview
On macOS, when Process.start is called with a command that doesn't exist, the exec failure cleanup path generates a SIGPIPE signal that kills the process.
I believe this is because ExitCodeHandler::ExitCodeHandlerEntry writes to a pipe whose read end has already been closed by the error handling in ProcessStarter::Start()
The bug is easily reproducible in Flutter macOS apps (issue ticket with simple test code) but the root cause is in the Dart VM's runtime/bin/process_macos.cc. Neither flutter nor Dart SIG_IGN sigpipe.
I have tried but not been able to actually trigger the bug in Dart directly, but I've traced it with a Mk1 eyeball.
Reproduction
This flutter ticket has repro code for it, but do note that its a flutter app, I've not been able to reproduce it in native Dart (see below why I still think its a dart issue)
Code sample
import 'dart:io';
import 'package:flutter/material.dart';
void main() {
debugPrint('=== About to call Process.start("no-such-command") ===');
Process.start('no-such-command', []).then((process) {
debugPrint('=== Process started (unexpected): pid=${process.pid} ===');
}).catchError((error) {
debugPrint('=== Process.start error: $error ===');
});
runApp(const SigpipeTestApp());
}
class SigpipeTestApp extends StatelessWidget {
const SigpipeTestApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SIGPIPE Test',
home: Scaffold(
appBar: AppBar(title: const Text('SIGPIPE Test')),
body: const Center(
child: Text('App is running. If SIGPIPE hits, this will exit.'),
),
),
);
}
}
Root cause
In runtime/bin/process_macos.cc, when exec fails:
ProcessStarter::Start() error path closes the read end of the exit code pipe.
ExitCodeHandler::ExitCodeHandlerEntry reaps the child via wait(), then calls FDUtils::WriteToBlocking to write the exit code to the write end of the same pipe.
- Writing to a pipe with no reader delivers
SIGPIPE to the process.
The code already handles the resulting EPIPE errno correctly (there's even a comment about it), but it doesn't prevent the SIGPIPE signal from being delivered alongside it.
Proposed solution
There's a couple of ideas I've had but the cleanest one is probably to mask sigpipe in the thread.
diff --git a/runtime/bin/process_macos.cc b/runtime/bin/process_macos.cc
index 8aebf479c70..40a2213b291 100644
--- a/runtime/bin/process_macos.cc
+++ b/runtime/bin/process_macos.cc
@@ -209,8 +209,17 @@ class ExitCodeHandler {
intptr_t exit_code_fd = ProcessInfoList::LookupProcessExitFd(pid);
if (exit_code_fd != 0) {
int message[2] = {exit_code, negative};
- ssize_t result =
- FDUtils::WriteToBlocking(exit_code_fd, &message, sizeof(message));
+ // Block SIGPIPE on this thread. If the read end of the exit
+ // code pipe has already been closed (exec failed and the
+ // parent discarded the pipe), this write will harmlessly
+ // fail with EPIPE instead of killing the process.
+ sigset_t block_sigpipe, old_mask;
+ sigemptyset(&block_sigpipe);
+ sigaddset(&block_sigpipe, SIGPIPE);
+ pthread_sigmask(SIG_BLOCK, &block_sigpipe, &old_mask);
+ ssize_t result = FDUtils::WriteToBlocking(exit_code_fd, &message,
+ sizeof(message));
+ pthread_sigmask(SIG_SETMASK, &old_mask, nullptr);
// If the process has been closed, the read end of the exit
// pipe has been closed. It is therefore not a problem that
// write fails with a broken pipe error. Other errors should
I'm not tied to any particular solution, this is just a nice tidy one. I'm sure there are other ways to fix it.
Workaround
You can workaround this issue by call signal(SIGPIPE, SIG_IGN) early in the app in swift code (e.g., in the macOS AppDelegate.applicationWillFinishLaunching) prevents the crash.
Justification
Despite this not being able to be reproduced in Dart directly, and needing Flutter (so far), I believe this is a bug with Dart VM because the VM code is writing to a pipe it knows may have no reader. The comments even say so:
// If the process has been closed, the read end of the exit
// pipe has been closed. It is therefore not a problem that
// write fails with a broken pipe error. Other errors should
// not happen.
The issue is that the code handles the EPIPE but ignores that it will generate a SIGPIPE
Overview
On macOS, when
Process.startis called with a command that doesn't exist, the exec failure cleanup path generates a SIGPIPE signal that kills the process.I believe this is because
ExitCodeHandler::ExitCodeHandlerEntrywrites to a pipe whose read end has already been closed by the error handling inProcessStarter::Start()The bug is easily reproducible in Flutter macOS apps (issue ticket with simple test code) but the root cause is in the Dart VM's runtime/bin/process_macos.cc. Neither flutter nor Dart SIG_IGN sigpipe.
I have tried but not been able to actually trigger the bug in Dart directly, but I've traced it with a Mk1 eyeball.
Reproduction
This flutter ticket has repro code for it, but do note that its a flutter app, I've not been able to reproduce it in native Dart (see below why I still think its a dart issue)
Code sample
Root cause
In
runtime/bin/process_macos.cc, when exec fails:ProcessStarter::Start()error path closes the read end of the exit code pipe.ExitCodeHandler::ExitCodeHandlerEntryreaps the child via wait(), then callsFDUtils::WriteToBlockingto write the exit code to the write end of the same pipe.SIGPIPEto the process.The code already handles the resulting EPIPE errno correctly (there's even a comment about it), but it doesn't prevent the SIGPIPE signal from being delivered alongside it.
Proposed solution
There's a couple of ideas I've had but the cleanest one is probably to mask sigpipe in the thread.
I'm not tied to any particular solution, this is just a nice tidy one. I'm sure there are other ways to fix it.
Workaround
You can workaround this issue by call signal(SIGPIPE, SIG_IGN) early in the app in swift code (e.g., in the macOS
AppDelegate.applicationWillFinishLaunching) prevents the crash.Justification
Despite this not being able to be reproduced in Dart directly, and needing Flutter (so far), I believe this is a bug with Dart VM because the VM code is writing to a pipe it knows may have no reader. The comments even say so:
The issue is that the code handles the EPIPE but ignores that it will generate a SIGPIPE