Skip to content

Process.start with non-existent command delivers SIGPIPE, killing Flutter apps on macOS #62679

Description

@zafnz

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:

  1. ProcessStarter::Start() error path closes the read end of the exit code pipe.
  2. ExitCodeHandler::ExitCodeHandlerEntry reaps the child via wait(), then calls FDUtils::WriteToBlocking to write the exit code to the write end of the same pipe.
  3. 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

Metadata

Metadata

Labels

area-vmUse area-vm for VM related issues, including code coverage, and the AOT and JIT backends.triagedIssue has been triaged by sub team

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions