dyld fatal errors via abort_with_payload are indistinguishable from normal process exit(1)

Originator:mark
Number:rdar://26894758 Date Originated:2016-06-20
Status:Duplicate/26829150 Resolved:
Product:OS X Product Version:10.12dp1 16A201w
Classification:Serious Bug Reproducible:Always
 
Summary:
Background: a new system call, abort_with_payload, was introduced in macOS 10.12. Along with a wrapper function, abort_with_reason, and variants that can target another process, terminate_with_payload and terminate_with_reason, it allows a process to be killed while providing diagnostic information that can be included in a crash report.

dyld uses this new abort_with_payload system call from dyld::halt() when it encounters a fatal error. Prior to macOS 10.12, dyld would execute an int3 instruction (on x86 and x86-64, or the ARM equivalent on ARM) to raise a user trap, which would show up in user space to a parent process waiting for its child to exit as SIGTRAP. (10.11.2 dyld-360.18/src/dyldStartup.s _dyld_fatal_error.) The exit status is visible to the parent process via a wait-family call such as waitpid().

Having a process that exits by abnormal means be reaped with a “killed by signal” status is desirable. This is not a normal exit status, it indicates fairly unambiguously that something went wrong in the process.

abort_with_payload causes the process to exit with an observed exit status of 1. Thus, processes that experience a dyld fatal error do not show up as “killed by signal.” 1 is a normal exit status ordinarily used to indicate an error, so this exit status is indistinguishable from a typical “process error” condition. The parent process has no reason to believe that the process didn’t start, do some of its own processing, and decide to exit with this status on its own.

Please revise the abort_with_payload and terminate_with_payload system calls to not cause processes to exit with an apparent normal exit status. I recommend making them appear to exit with SIGABRT for abort_with_payload, and SIGKILL for terminate_with_payload. SIGABRT best matches the intent of an in-process abort as would occur when calling abort(). SIGKILL best represents the idea of an uncatchable kill originating from out-of-process.

If it makes more sense to do so, making both abort_with_payload and terminate_with_payload exit with SIGKILL would also be viable, because SIGKILL represents the “uncatchable” concept, and both of these syscalls result in a termination that’s not catchable in-process as a signal.

Steps to Reproduce:
Trigger a dyld fatal error and observe the exit status. An easy way to do this is to set DYLD_INSERT_LIBRARIES to a path that doesn’t exist and to try to launch something. Because dyld ignores environment variables for restricted processes, it’s best to test this with an unsigned executable. This can also be done with a simple executable on the system if SIP is disabled or if a copy of the executable is made in a non-restricted location.

Either compile and run the attached test program:

$ clang++ -std=c++11 dyld_fatal_error_test.cc -o dyld_fatal_error_test
$ ./dyld_fatal_error_test

or run any executable with DYLD_INSERT_LIBRARIES set to a value that causes a dyld fatal error (here, a copy of /usr/bin/true is made in /tmp to exclude it from SIP enforcement):

$ cp /usr/bin/true /tmp/true
$ DYLD_INSERT_LIBRARIES=/var/empty/NoDirectory/NoFile /tmp/true

Expected Results:
$ ./dyld_fatal_error_test
dyld: could not load inserted library '/var/empty/NoDirectory/NoFile' because image not found

pid 12345 terminated by signal 6 (Abort trap: 6)

or

$ DYLD_INSERT_LIBRARIES=/var/empty/NoDirectory/NoFile /tmp/true
dyld: could not load inserted library '/var/empty/NoDirectory/NoFile' because image not found

Abort trap: 6
$ echo $?
134

Actual Results:
$ ./dyld_fatal_error_test
dyld: could not load inserted library '/var/empty/NoDirectory/NoFile' because image not found

pid 12345 exited with code 1

or

$ DYLD_INSERT_LIBRARIES=/var/empty/NoDirectory/NoFile /tmp/true
dyld: could not load inserted library '/var/empty/NoDirectory/NoFile' because image not found

$ echo $?
1

Version:
10.12dp1 16A201w

--

dyld_fatal_error_test.cc

// clang++ -std=c++11 dyld_fatal_error_test.cc -o dyld_fatal_error_test

#include <err.h>
#include <mach-o/dyld.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>

#include <string>

namespace {

std::string ExecutablePath() {
  uint32_t executable_length = 0;
  _NSGetExecutablePath(nullptr, &executable_length);
  std::string executable_path(executable_length - 1, std::string::value_type());
  int rv = _NSGetExecutablePath(&executable_path[0], &executable_length);
  if (rv != 0) {
    errx(EXIT_FAILURE, "_NSGetExecutablePath");
  }

  return executable_path;
}

}  // namespace

int main(int argc, char* argv[]) {
  if (argc == 2 && strcmp(argv[1], "child") == 0) {
    return EXIT_SUCCESS;
  }

  std::string executable_path = ExecutablePath();
  int rv = setenv("DYLD_INSERT_LIBRARIES", "/var/empty/NoDirectory/NoFile", 1);
  if (rv == -1) {
    err(EXIT_FAILURE, "setenv");
  }

  pid_t pid = fork();
  if (pid == -1) {
    err(EXIT_FAILURE, "fork");
  }

  if (pid == 0) {
    // Child
    execl(executable_path.c_str(), argv[0], "child", nullptr);
    err(EXIT_FAILURE, "execl");
  }

  int status;
  pid_t waitpid_rv = waitpid(pid, &status, 0);
  if (waitpid_rv == -1) {
    err(EXIT_FAILURE, "waitpid");
  }

  if (WIFEXITED(status)) {
    printf("pid %d exited with code %d\n", pid, WEXITSTATUS(status));
  } else if (WIFSIGNALED(status)) {
    int sig = WTERMSIG(status);
    printf("pid %d terminated by signal code %d (%s)%s\n",
           pid,
           sig,
           strsignal(sig),
           WCOREDUMP(status) ? " (core dumped)" : "");
  } else {
    printf("pid %d unknown status 0x%x\n", pid, status);
  }

  return EXIT_SUCCESS;
}

Comments

04-Aug-2016 02:23 PM

This appears to have been fixed in 10.12db4 16A270f. The testcase appears to its parent as though it terminated on SIGABRT.


Please note: Reports posted here will not necessarily be seen by Apple. All problems should be submitted at bugreport.apple.com before they are posted here. Please only post information for Radars that you have filed yourself, and please do not include Apple confidential information in your posts. Thank you!