[Backtracing] Security improvements.

Use `task_read_for_pid()` rather than having the crashing program pass its
own task port through.  This opts us in to additional OS security measures
surrounding the use of this call.

rdar://107362003
This commit is contained in:
Alastair Houghton
2023-04-11 17:20:48 +01:00
parent ef46563686
commit e5680de071
6 changed files with 54 additions and 49 deletions

View File

@@ -252,6 +252,8 @@ _swift_backtrace_thread_get_state(__swift_thread_t target_act,
return thread_get_state(target_act, flavor, old_state, old_stateCnt);
}
extern __swift_kern_return_t task_read_for_pid(__swift_task_t task, int pid, __swift_task_t *ptask);
/* DANGER! These are SPI. They may change (or vanish) at short notice, may
not work how you expect, and are generally dangerous to use. */
struct dyld_process_cache_info {
@@ -270,6 +272,11 @@ extern void _dyld_process_info_get_cache(dyld_process_info info, dyld_process_c
extern void _dyld_process_info_for_each_image(dyld_process_info info, void (^callback)(__swift_uint64_t machHeaderAddress, const __swift_uuid_t uuid, const char* path));
extern void _dyld_process_info_for_each_segment(dyld_process_info info, __swift_uint64_t machHeaderAddress, void (^callback)(__swift_uint64_t segmentAddress, __swift_uint64_t segmentSize, const char* segmentName));
#define CS_OPS_STATUS 0
#define CS_PLATFORM_BINARY 0x04000000
#define CS_PLATFORM_PATH 0x08000000
extern int csops(int, unsigned int, void *, __swift_size_t);
/* DANGER! CoreSymbolication is a private framework. This is all SPI. */
typedef __swift_int32_t cpu_type_t;
typedef __swift_int32_t cpu_subtype_t;

View File

@@ -101,31 +101,13 @@ class Target {
var mcontext: MContext
static func getParentTask() -> task_t? {
var ports: mach_port_array_t? = nil
var portCount: mach_msg_type_number_t = 0
// For some reason, we can't pass a task read port this way, but we
// *can* pass the control port. So do that and then ask for a read port
// before immediately dropping the control port from this process.
let kr = mach_ports_lookup(mach_task_self_, &ports, &portCount)
static func getTask(pid: pid_t) -> task_t? {
var port: task_t = 0
let kr = task_read_for_pid(mach_task_self_, pid, &port)
if kr != KERN_SUCCESS {
return nil
}
if let ports = ports, portCount != 0 {
var taskPort: mach_port_t = 0
let kr = task_get_special_port(ports[0], TASK_READ_PORT, &taskPort)
if kr != KERN_SUCCESS {
mach_port_deallocate(mach_task_self_, ports[0])
return nil
}
mach_port_deallocate(mach_task_self_, ports[0])
return task_t(taskPort)
} else {
return nil
}
return port
}
static func getProcessName(pid: pid_t) -> String {
@@ -141,15 +123,40 @@ class Target {
}
}
static func isPlatformBinary(pid: pid_t) -> Bool {
var flags = UInt32(0)
return csops(pid,
UInt32(CS_OPS_STATUS),
&flags,
MemoryLayout<UInt32>.size) != 0 ||
(flags & UInt32(CS_PLATFORM_BINARY | CS_PLATFORM_PATH)) != 0
}
init(crashInfoAddr: UInt64, limit: Int?, top: Int, cache: Bool) {
pid = getppid()
if let parentTask = Self.getParentTask() {
task = parentTask
} else {
print("swift-backtrace: couldn't fetch parent task", to: &standardError)
if Self.isPlatformBinary(pid: pid) {
/* Exit silently in this case; either
1. We can't call csops(), because we're sandboxed, or
2. The target is a platform binary.
If we get killed, that is also fine. */
exit(1)
}
// This will normally only succeed if the parent process has
// the com.apple.security.get-task-allow privilege. That gets set
// automatically if you're developing in Xcode; if you're developing
// on the command line or using SwiftPM, you will need to code sign
// your binary with that entitlement to get this to work.
guard let parentTask = Self.getTask(pid: pid) else {
exit(1)
}
task = parentTask
reader = RemoteMemoryReader(task: __swift_task_t(task))
name = Self.getProcessName(pid: pid)

View File

@@ -342,26 +342,6 @@ trueOrFalse(OnOffTty oot) {
bool
run_backtracer()
{
// Forward our task port to the backtracer; we use the same technique that
// libxpc uses to forward one of its ports on fork(), except that we aren't
// going to call fork() so libxpc's atfork handler won't run and we'll get
// to send the task port to the child.
//
// I would very much like to send a task *read* port, but for some reason
// that doesn't work here. As a result, what we do instead is send the
// control port but have the backtracer use it to get the read port and
// immediately drop the control port.
//
// That *should* be safe enough in practice; if someone could replace the
// backtracer, then they can also replace libswiftCore, and since we do
// this early on in backtracer start-up, the control port won't be valid
// by the time anyone gets to try anything nefarious.
mach_port_t ports[] = {
mach_task_self(),
};
mach_ports_register(mach_task_self(), ports, 1);
// Set-up the backtracer's command line arguments
switch (_swift_backtraceSettings.algorithm) {
case UnwindAlgorithm::Fast:

View File

@@ -1024,7 +1024,8 @@ if run_vendor == 'apple':
if 'use_os_stdlib' not in lit_config.params:
config.target_codesign = make_path(config.swift_utils, "swift-darwin-postprocess.py")
else:
config.target_codesign = "codesign -f -s -"
config.target_codesign = "codesign -f -s - --options=runtime --entitlements {}".format(os.path.join(config.swift_utils, 'get-task-allow.plist'))
config.target_library_path_var = "DYLD_LIBRARY_PATH"
config.target_runtime = "objc"
config.target_sdk_libcxx_path = os.path.join(config.variant_sdk, 'usr', 'include', 'c++', 'v1')

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>

View File

@@ -10,7 +10,7 @@ import subprocess
import sys
utils = os.path.dirname(os.path.realpath(__file__))
get_task_allow_plist = os.path.join(utils, 'get-task-allow.plist')
def main(arguments):
parser = argparse.ArgumentParser(
@@ -91,7 +91,9 @@ def unrpathize(filename):
def codesign(filename):
# "-" is the signing identity for ad-hoc signing.
command = ["/usr/bin/codesign", "--force", "--sign", "-", filename]
command = ['/usr/bin/codesign', '--force', '--sign', '-',
'--options=runtime', '--entitlements', get_task_allow_plist,
filename]
subprocess.check_call(command)