xnu: Allow same-executable inheritance of task exception ports through exec even when unsigned

Originator:mark
Number:rdar://26902656 Date Originated:2016-06-20
Status:Duplicate/19362779 Resolved:
Product:OS X Product Version:10.12dp1 16A201w
Classification:Serious Bug Reproducible:
 
Summary:
10.12dp1 seems to impose new rules on inheritance of task exception ports through the exec family of calls. Based on experimentation, it seems that task exception ports are cleared on exec unless the invoker is signed by Apple or the invoker and invokee are signed with the same team ID.

This is a bit frustrating for my crash reporting platform, which relies on the inheritance of exception ports. Only our release builds are ever signed. Ordinary developer builds are never signed, and in fact, most of our developers don’t even have their own signing identities.

This scheme would be more workable if task exception port inheritance were permitted in cases where the invoker and invokee are the same executable, even if unsigned. Because the signing requirement seems to be based on establishing equivalent identity (through the use of the team ID), it should also be possible to establish equivalent identity based on the invoker and invokee being identical executables.

The attached test program prints its task exception ports, re-execs itself, prints its task exception ports again, and then calls abort(). When run on 10.11, the set of exception ports are identical in both cases, and the abort() crash is handled by the appropriate task-level handler, normally ReportCrash. This also happens when signed and run on 10.12, but when unsigned and run on 10.12, the re-execed process has its task exception ports cleared, and no task-level handler is invoked for the abort() crash. (Instead, a host-level handler is).

In the example, I show the system’s crash reporter being used as the exception handler for brevity. In my actual environment, I provide my own exception handler, and not having crashes delivered to it is a big deal.

This is notably a problem in developer builds which are never signed. I envision that it’s also a problem in any instance where users build their own shells and wind up having any child process of their shell inadvertently lose access to the user-level ReportCrash. Furthermore, I am skeptical that this approach provides any real improved security when it’s possible to retain exception ports on exec from Apple-signed executables that can easily run user code such as Python. It’s incongruous to deny this longstanding ability to native code.

Steps to Reproduce:
$ clang++ -std=c++11 exec_exception_ports.cc -o exec_exception_ports
$ ./exec_exception_ports
$ codesign --sign 'your signing identity' exec_exception_ports
$ ./exec_exception_ports

Expected Results:
Whether unsigned or signed, exec_exception_ports should produce output similar to:

$ ./exec_exception_ports
2 task exception handlers:
mask 0x7fe: no handler
mask 0x3800: port 0xb03, behavior 0x80000003, flavor 7
re-execing
2 task exception handlers:
mask 0x7fe: no handler
mask 0x3800: port 0x10b, behavior 0x80000003, flavor 7
Abort trap: 6

The abort crash should be handled by the task-level exception handler, in this case the EXC_CORPSE_NOTIFY handler in the user com.apple.ReportCrash launch agent job. This produces a crash report in ~/Library/Logs/DiagnosticReports.

Actual Results:
When unsigned, exec_exception_ports produces output like this:

$ ./exec_exception_ports
2 task exception handlers:
mask 0x7fe: no handler
mask 0x3800: port 0xb03, behavior 0x80000003, flavor 7
re-execing
2 task exception handlers:
mask 0x7fe: no handler
mask 0x3800: no handler
Abort trap: 6

This indicates that although a task EXC_CORPSE_NOTIFY handler was registered, it has been cleared in the re-execed process. There is no task-level exception handler to pick up the abort crash, so no crash report is produced in ~/Library/Logs/DiagnosticReports. The host exception handler in the system com.apple.ReportCrash.Root launch daemon job will pick it up, producing a crash report in /Library/Logs/DiagnosticReports.

It’s a problem that there isn’t a way to inherit a user exception port at the task level across an exec. It ought to be possible to do this when equivalent identity is established, and having the same executable exec itself should be sufficient to establish equivalent identity.

Version:
10.12dp1 16A201w

--
exec_exception_ports.cc

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

#include <err.h>
#include <mach-o/dyld.h>
#include <mach/mach.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.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[]) {
  const size_t kMaxPorts = 32;
  mach_msg_type_number_t count = kMaxPorts;
  exception_mask_t masks[kMaxPorts];
  exception_handler_t ports[kMaxPorts];
  exception_behavior_t behaviors[kMaxPorts];
  thread_state_flavor_t flavors[kMaxPorts];

  const exception_mask_t kExceptionMask =
      EXC_MASK_ALL | EXC_MASK_CRASH | EXC_MASK_CORPSE_NOTIFY;
  kern_return_t kr = task_get_exception_ports(mach_task_self(),
                                              kExceptionMask,
                                              masks,
                                              &count,
                                              ports,
                                              behaviors,
                                              flavors);

  if (kr != KERN_SUCCESS) {
    errx(EXIT_FAILURE,
         "task_get_exception_ports: %s (0x%x)",
         mach_error_string(kr),
         kr);
  }

  printf("%d task exception handler%s%s\n",
         count,
         count == 1 ? "" : "s",
         count == 0 ? "" : ":");
  for (size_t i = 0; i < count; ++i) {
    if (ports[i] == MACH_PORT_NULL) {
      printf("mask 0x%x: no handler\n", masks[i]);
    } else {
      printf("mask 0x%x: port 0x%x, behavior 0x%x, flavor %d\n",
             masks[i],
             ports[i],
             behaviors[i],
             flavors[i]);
    }
  }
  const char kReexecArg[] = "exec";

  if (!(argc == 2 && strcmp(argv[1], kReexecArg) == 0)) {
    std::string executable_path = ExecutablePath();

    printf("re-execing\n");
    execl(executable_path.c_str(), argv[0], kReexecArg, nullptr);
    err(EXIT_FAILURE, "execl");
  } else {
    abort();
  }

  return EXIT_SUCCESS;
}

Comments

Note (not sent to Apple)

It appears that this was fixed in 10.12db2 16A239j. The new restrictions on inheriting exception ports were dropped from this build, which behaves as 10.11 did in this regard.

Apple Developer Relations12-Jul-2016 02:08 AM

Engineering has determined that your bug report is a duplicate of another issue and will be closed.

The open or closed status of the original bug report your issue was duplicated to appears in the yellow "Duplicate of XXXXXXXX" section of the bug reporter user interface. This section appears near the top of the right column's bug detail view just under the bug number, title, state, product and rank.


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!