Add bounded own-process Darwin SIGBUS capture controls

This commit is contained in:
Moritz Angermann committed 2026-10-17 21:58:59 +09:00
1 parent ce480d79d0
commit 5e15ffa8fd
3 files changed
+638

No files matched your search

+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env python3
"""Owned native Darwin controls; cross-compile x86_64 without executing it."""
import importlib.util
import hashlib
import json
import os
from pathlib import Path
import platform
import signal
import struct
import subprocess
import tempfile
import unittest
from unittest import mock
HERE = Path(__file__).resolve().parent
SPEC = importlib.util.spec_from_file_location("sigbus", HERE / "trace-ghc-sigbus.py")
SIGBUS = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(SIGBUS)
FIXTURE = r'''
#define _DARWIN_C_SOURCE 1
#include <fcntl.h>
#include <mach-o/dyld.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
static int environment_ok(const char *expected)
{
if (getenv("SIMPLEX_GHC_BUS_RECORD") != NULL ||
getenv("SIMPLEX_GHC_BUS_DYLD_PRESENT") != NULL ||
getenv("SIMPLEX_GHC_BUS_DYLD_VALUE") != NULL) return 0;
const char *value = getenv("DYLD_INSERT_LIBRARIES");
return strcmp(expected, "<unset>") == 0 ? value == NULL
: value != NULL && strcmp(expected, value) == 0;
}
int main(int argc, char **argv)
{
if (argc < 2) return 120;
if (strcmp(argv[1], "0") == 0 || strcmp(argv[1], "7") == 0) {
printf("argv0=%s argc=%d last=%s\n", argv[0], argc, argv[argc - 1]);
return atoi(argv[1]);
}
if (strcmp(argv[1], "raise") == 0) {
if (raise(SIGBUS) != 0) return 121;
return 122;
}
if (strcmp(argv[1], "mmap") == 0 && argc == 3) {
long page = sysconf(_SC_PAGESIZE);
int descriptor = open(argv[2], O_RDWR | O_CREAT | O_EXCL, 0600);
if (page <= 0 || descriptor < 0 || ftruncate(descriptor, page) != 0) return 123;
void *memory = mmap(NULL, (size_t)page, PROT_READ, MAP_SHARED, descriptor, 0);
if (memory == MAP_FAILED || ftruncate(descriptor, 0) != 0) return 124;
volatile const unsigned char *bytes = memory;
return bytes[0];
}
if (strcmp(argv[1], "child-check") == 0 && argc == 3) {
if (!environment_ok(argv[2])) return 126;
for (uint32_t index = 0; index < _dyld_image_count(); ++index) {
const char *name = _dyld_get_image_name(index);
if (name != NULL && strstr(name, "trace-arm64.dylib") != NULL) return 127;
}
puts("CHILD_ENV_OK");
return 0;
}
if (strcmp(argv[1], "child-env") == 0 && argc == 3) {
if (!environment_ok(argv[2])) return 126;
pid_t child = fork();
if (child < 0) return 128;
if (child == 0) {
execl(argv[0], argv[0], "child-check", argv[2], (char *)NULL);
_exit(129);
}
int status = 0;
if (waitpid(child, &status, 0) != child || !WIFEXITED(status)) return 130;
return WEXITSTATUS(status);
}
if (strcmp(argv[1], "sleep") == 0) { for (;;) pause(); }
return 131;
}
'''
class ParserControls(unittest.TestCase):
def test_bounded_regular_record_reads(self):
with tempfile.TemporaryDirectory(prefix="ghc-sigbus-parser-") as directory:
root = Path(directory)
record = root / "record.bin"
words = [SIGBUS.MAGIC, 1, 1, SIGBUS.CPUS["arm64"]] + [0] * 60
record.write_bytes(struct.pack("<64Q", *words))
with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("Unbounded read")):
self.assertEqual(SIGBUS.decode(record)["bytes"], 512)
record.write_bytes(bytes(4 * 512 + 1))
with self.assertRaisesRegex(ValueError, "byte limit"):
SIGBUS.decode(record)
link = root / "record-link"
link.symlink_to(record)
with self.assertRaises(OSError):
SIGBUS.decode(link)
fifo = root / "record-fifo"
os.mkfifo(fifo)
with self.assertRaisesRegex(ValueError, "regular file"):
SIGBUS.decode(fifo)
def test_bounded_immutable_wrapper_reads(self):
with tempfile.TemporaryDirectory(prefix="ghc-sigbus-parser-") as directory:
root = Path(directory)
binary, tracer = root / "ghc-fixture", root / "tracer.dylib"
for path, kind in ((binary, 2), (tracer, 6)):
path.write_bytes(struct.pack("<4I", 0xFEEDFACF, SIGBUS.CPUS["arm64"], 0, kind))
source = root / "ghc-source"
source.write_text('#!/bin/sh\nexec ' + str(binary) + ' "$@"\n')
original_hash = hashlib.sha256(source.read_bytes()).hexdigest()
with mock.patch.object(Path, "read_bytes", side_effect=AssertionError("Unbounded read")):
report = SIGBUS.prepare(source, root / "copy", tracer, root / "record.bin",
"arm64", binary)
self.assertEqual(report["sourceSHA256"], original_hash)
source.write_bytes(bytes(64 * 1024 + 1))
with self.assertRaisesRegex(ValueError, "byte limit"):
SIGBUS.prepare(source, root / "large-copy", tracer, root / "large.bin",
"arm64", binary)
link = root / "source-link"
link.symlink_to(source)
with self.assertRaises(OSError):
SIGBUS.prepare(link, root / "link-copy", tracer, root / "link.bin", "arm64", binary)
class Controls(unittest.TestCase):
@classmethod
def setUpClass(cls):
if platform.system() != "Darwin" or platform.machine() != "arm64":
raise unittest.SkipTest("This prototype control runs only native arm64 Darwin")
cls.directory = tempfile.TemporaryDirectory(prefix="ghc-sigbus-controls-")
cls.root = Path(cls.directory.name)
cls.compiler = subprocess.check_output(["xcrun", "--find", "clang"], text=True).strip()
cls.sdk = subprocess.check_output(["xcrun", "--show-sdk-path"], text=True).strip()
cls.commands = []
flags = ["-isysroot", cls.sdk, "-std=c11", "-Werror=vla", "-pedantic",
"-Wall", "-Wextra", "-Werror", "-O0", "-g"]
for architecture in ("arm64", "x86_64"):
command = [cls.compiler, *flags, "-arch", architecture, "-dynamiclib",
str(HERE / "trace_ghc_sigbus.c"), "-o",
str(cls.root / f"trace-{architecture}.dylib")]
result = subprocess.run(command, capture_output=True, text=True, timeout=30)
cls.commands.append({"argv": command, "exit": result.returncode, "stderr": result.stderr})
if result.returncode != 0:
raise AssertionError(result.stderr)
fixture = cls.root / "Fixture.c"
fixture.write_text(FIXTURE)
cls.binary = cls.root / "ghc-fixture"
subprocess.run([cls.compiler, *flags, "-arch", "arm64", str(fixture),
"-o", str(cls.binary)], check=True, capture_output=True, timeout=30)
binary_data = cls.binary.read_bytes()
command_count = struct.unpack_from("<I", binary_data, 16)[0]
offset = 32
cls.binary_uuid = None
for _ in range(command_count):
kind, size = struct.unpack_from("<II", binary_data, offset)
if kind == 0x1B:
cls.binary_uuid = binary_data[offset + 8:offset + 24].hex()
offset += size
if cls.binary_uuid is None:
raise AssertionError("Fixture executable must contain LC_UUID")
cls.source = cls.root / "ghc-original"
cls.source.write_text('#!/bin/sh\nexec ' + str(cls.binary) + ' \\\n "$@"\n')
cls.source.chmod(0o755)
cls.source_hash = hashlib.sha256(cls.source.read_bytes()).hexdigest()
benign_source = cls.root / "Benign.c"
benign_source.write_text("int diagnostic_control(void) { return 0; }\n")
cls.benign = cls.root / "benign.dylib"
subprocess.run([cls.compiler, *flags, "-arch", "arm64", "-dynamiclib",
str(benign_source), "-o", str(cls.benign)], check=True,
capture_output=True, timeout=30)
cls.results = []
@classmethod
def tearDownClass(cls):
output = os.environ.get("SIGBUS_CONTROL_RECEIPT")
if output:
Path(output).write_text(json.dumps({"compile": cls.commands,
"runtime": cls.results}, indent=2) + "\n")
cls.directory.cleanup()
def run_owned(self, argv, env=None, timeout=10):
process = subprocess.Popen(argv, env=env, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, start_new_session=True)
try:
stdout, stderr = process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
os.killpg(process.pid, signal.SIGKILL)
stdout, stderr = process.communicate(timeout=5)
self.results.append({"argv": list(map(str, argv)), "pid": process.pid,
"exit": process.returncode, "boundedTimeout": True})
raise
self.results.append({"argv": list(map(str, argv)), "pid": process.pid,
"exit": process.returncode, "stdout": stdout.decode(),
"stderr": stderr.decode()})
return process.returncode, stdout, stderr
def wrapped(self, name):
output, record = self.root / (name + ".sh"), self.root / (name + ".bin")
SIGBUS.prepare(self.source, output, self.root / "trace-arm64.dylib",
record, "arm64", self.binary)
return output, record
def test_normal_status_and_original_argv(self):
for status in (0, 7):
wrapper, record = self.wrapped("normal" + str(status))
argv = [str(status), "space and ' quote"]
original = self.run_owned([self.source, *argv])
traced = self.run_owned([wrapper, *argv])
self.assertEqual(original, traced)
self.assertEqual(hashlib.sha256(self.source.read_bytes()).hexdigest(), self.source_hash)
self.assertEqual(traced[0], status)
decoded = SIGBUS.decode(record)
self.assertEqual([r["kind"] for r in decoded["records"]], ["startup"])
self.assertEqual(decoded["records"][0]["mainImageUUID"], self.binary_uuid)
def test_hardware_and_raised_sigbus_preserve_signal(self):
for mode in ("mmap", "raise"):
wrapper, record = self.wrapped(mode)
arguments = [mode] + ([str(self.root / "empty-mapping")] if mode == "mmap" else [])
status, _, _ = self.run_owned([wrapper, *arguments])
decoded = SIGBUS.decode(record)
self.results[-1]["decoded"] = decoded
self.assertEqual(status, -signal.SIGBUS)
fault = next(r for r in decoded["records"] if r["kind"] == "fault")
self.assertEqual(fault["signal"], signal.SIGBUS)
self.assertNotEqual(int(fault["pc"], 16), 0)
self.assertNotEqual(int(fault["sp"], 16), 0)
def test_child_environment_none_and_empty(self):
for index, previous in enumerate((None, "", str(self.benign))):
record = self.root / f"child-{index}.bin"
env = os.environ.copy()
for key in (*SIGBUS.PRIVATE, "DYLD_INSERT_LIBRARIES"):
env.pop(key, None)
env.update(SIMPLEX_GHC_BUS_RECORD=str(record),
SIMPLEX_GHC_BUS_DYLD_PRESENT="0" if previous is None else "1",
DYLD_INSERT_LIBRARIES=str(self.root / "trace-arm64.dylib") +
(":" + previous if previous else ""))
if previous is not None:
env["SIMPLEX_GHC_BUS_DYLD_VALUE"] = previous
self.assertEqual(self.run_owned([self.binary, "child-env",
"<unset>" if previous is None else previous], env)[0], 0)
self.assertEqual(len(SIGBUS.decode(record)["records"]), 1)
def test_shape_cpu_and_record_rejections(self):
bad = self.root / "bad.sh"
bad.write_text(self.source.read_text() + "echo unexpected\n")
with self.assertRaises(ValueError):
SIGBUS.prepare(bad, self.root / "bad-copy", self.root / "trace-arm64.dylib",
self.root / "bad.bin", "arm64", self.binary)
bad.write_text('#!/bin/sh\nexec ' + str(self.binary) + '; echo injected "$@"\n')
with self.assertRaises(ValueError):
SIGBUS.prepare(bad, self.root / "separator-copy", self.root / "trace-arm64.dylib",
self.root / "separator.bin", "arm64", self.binary)
with self.assertRaises(ValueError):
SIGBUS.prepare(self.source, self.root / "wrong-cpu", self.root / "trace-x86_64.dylib",
self.root / "wrong.bin", "arm64", self.binary)
truncated = self.root / "truncated.bin"
truncated.write_bytes(b"not a record")
with self.assertRaises(ValueError):
SIGBUS.decode(truncated)
def test_owned_timeout_cleanup(self):
wrapper, _ = self.wrapped("timeout")
with self.assertRaises(subprocess.TimeoutExpired):
self.run_owned([wrapper, "sleep"], timeout=1)
pid = self.results[-1]["pid"]
with self.assertRaises(ProcessLookupError):
os.kill(pid, 0)
if __name__ == "__main__":
unittest.main()
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""Prepare a private compiler wrapper and decode its bounded SIGBUS record."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import shlex
import stat
import struct
MAGIC = 0x5349474255533031
WORDS = 64
CPUS = {"x86_64": 0x01000007, "arm64": 0x0100000C}
PRIVATE = (
"SIMPLEX_GHC_BUS_RECORD",
"SIMPLEX_GHC_BUS_DYLD_PRESENT",
"SIMPLEX_GHC_BUS_DYLD_VALUE",
)
def read_regular(path, limit):
descriptor = os.open(path, os.O_RDONLY | os.O_CLOEXEC | os.O_NOFOLLOW | os.O_NONBLOCK)
with os.fdopen(descriptor, "rb") as stream:
if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
raise ValueError("Diagnostic input must be a regular file")
data = stream.read(limit + 1)
if len(data) > limit:
raise ValueError("Diagnostic input exceeds its byte limit")
return data
def cpu(path, file_type=None):
with Path(path).open("rb") as stream:
header = stream.read(16)
if len(header) != 16 or struct.unpack("<I", header[:4])[0] != 0xFEEDFACF:
raise ValueError("Expected a thin little-endian 64-bit Mach-O executable")
if file_type is not None and struct.unpack_from("<I", header, 12)[0] != file_type:
raise ValueError("Unexpected Mach-O file type")
return struct.unpack_from("<I", header, 4)[0]
def prepare(source, output, tracer, record, architecture, expected_binary):
source, output, tracer, record = map(Path, (source, output, tracer, record))
expected_binary = Path(expected_binary)
if not all(path.is_absolute() for path in (source, output, tracer, record, expected_binary)):
raise ValueError("Every diagnostic path must be absolute")
original = read_regular(source, 64 * 1024)
text = original.decode("utf-8")
# The pinned compiler factory emits only this shebang and a continued exec.
# Fail closed on other script logic rather than bypassing any wrapper setup.
if not text.startswith("#!/bin/sh\nexec ") or text.count("\nexec ") != 1:
raise ValueError("Compiler wrapper must contain one final exec after its shebang")
body = text[len("#!/bin/sh\n"):]
logical = body.replace("\\\n", "").rstrip("\n")
if re.fullmatch(r'exec [A-Za-z0-9_./:+,=@% -]+ "\$@"', logical) is None:
raise ValueError("Compiler wrapper contains unsupported shell syntax")
argv = shlex.split(logical)
if (len(argv) < 3 or argv[0] != "exec" or argv[-1] != "$@"
or "\n" in body.replace("\\\n", "").rstrip("\n")):
raise ValueError("Unsupported compiler wrapper exec shape")
if Path(argv[1]) != expected_binary or not expected_binary.name.startswith("ghc"):
raise ValueError("Compiler executable does not match the expected wrapper target")
if cpu(expected_binary, 2) != CPUS[architecture] or cpu(tracer, 6) != CPUS[architecture]:
raise ValueError("Compiler or diagnostic dylib has the wrong Mach-O CPU")
# These builtins run only after /bin/sh has started. Setting DYLD before
# launching that protected shell would not reliably inject the compiler.
insertion = """if [ "${SIMPLEX_GHC_BUS_RECORD+x}" = x ] ||
[ "${SIMPLEX_GHC_BUS_DYLD_PRESENT+x}" = x ] ||
[ "${SIMPLEX_GHC_BUS_DYLD_VALUE+x}" = x ]; then
echo 'GHC SIGBUS diagnostic: private environment key already exists' >&2
exit 125
fi
if [ "${DYLD_INSERT_LIBRARIES+x}" = x ]; then
export SIMPLEX_GHC_BUS_DYLD_PRESENT=1
export SIMPLEX_GHC_BUS_DYLD_VALUE="$DYLD_INSERT_LIBRARIES"
else
export SIMPLEX_GHC_BUS_DYLD_PRESENT=0
fi
export SIMPLEX_GHC_BUS_RECORD=RECORD
export DYLD_INSERT_LIBRARIES=TRACER${DYLD_INSERT_LIBRARIES:+:$DYLD_INSERT_LIBRARIES}
""".replace("RECORD\n", shlex.quote(str(record)) + "\n").replace(
"TRACER${", shlex.quote(str(tracer)) + "${")
prefix = b"#!/bin/sh\n"
changed = prefix + insertion.encode() + original[len(prefix):]
if changed.replace(insertion.encode(), b"", 1) != original:
raise ValueError("Private wrapper failed its reverse byte comparison")
with output.open("xb") as stream:
stream.write(changed)
output.chmod(source.stat().st_mode & 0o777)
return {
"source": str(source), "sourceSHA256": hashlib.sha256(original).hexdigest(),
"copy": str(output), "copySHA256": hashlib.sha256(changed).hexdigest(),
"expectedBinary": str(expected_binary), "cpu": architecture,
"record": str(record), "originalExecAndArgumentsPreserved": True,
}
def decode(path):
data = read_regular(path, 4 * WORDS * 8)
size = WORDS * 8
if not data or len(data) > 4 * size or len(data) % size:
raise ValueError("Missing, truncated, or oversized SIGBUS record")
result = []
kinds = set()
for offset in range(0, len(data), size):
words = struct.unpack_from("<" + "Q" * WORDS, data, offset)
if words[0] != MAGIC or words[1] != 1 or words[2] not in (1, 2):
raise ValueError("Unknown SIGBUS record format")
if words[2] in kinds or words[3] not in CPUS.values():
raise ValueError("Duplicate record or unsupported CPU")
kinds.add(words[2])
architecture = next(name for name, value in CPUS.items() if value == words[3])
if words[2] == 1:
result.append({
"kind": "startup", "cpu": architecture, "pid": words[4],
"mainImageBase": hex(words[5]), "mainImageSlide": hex(words[6]),
"mainImageUUID": data[offset + 64:offset + 80].hex() if words[7] else None,
})
else:
names = (["rax", "rbx", "rcx", "rdx", "rdi", "rsi", "r8", "r9",
"r10", "r11", "r12", "r13", "r14", "r15"]
if architecture == "x86_64" else [f"x{index}" for index in range(29)])
result.append({
"kind": "fault", "cpu": architecture, "signal": words[4],
"code": struct.unpack("<q", struct.pack("<Q", words[5]))[0],
"faultAddress": hex(words[6]), "pc": hex(words[7]),
"sp": hex(words[8]), "bp": hex(words[9]), "lr": hex(words[10]),
"flags": hex(words[11]),
"registers": {name: hex(words[12 + index]) for index, name in enumerate(names)},
})
return {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest(), "records": result}
def main():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
create = commands.add_parser("prepare")
for name in ("source", "output", "tracer", "record", "expected-binary"):
create.add_argument("--" + name, required=True)
create.add_argument("--cpu", choices=CPUS, required=True)
read = commands.add_parser("decode")
read.add_argument("record")
arguments = parser.parse_args()
if arguments.command == "prepare":
report = prepare(arguments.source, arguments.output, arguments.tracer,
arguments.record, arguments.cpu, arguments.expected_binary)
else:
report = decode(arguments.record)
print(json.dumps(report, indent=2))
if __name__ == "__main__":
main()
+197
View File
@@ -0,0 +1,197 @@
/* Diagnostic-only Darwin SIGBUS record. Never load this in a canonical build. */
#define _DARWIN_C_SOURCE 1
#include <errno.h>
#include <fcntl.h>
#include <mach-o/dyld.h>
#include <mach-o/loader.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/ucontext.h>
#include <unistd.h>
#define RECORD_MAGIC UINT64_C(0x5349474255533031)
#define RECORD_WORDS 64
#if defined(__x86_64__)
#define RECORD_CPU CPU_TYPE_X86_64
#elif defined(__aarch64__) || defined(__arm64__)
#define RECORD_CPU CPU_TYPE_ARM64
#else
#error "The SIGBUS diagnostic requires 64-bit Darwin"
#endif
/* This is the only static value read from the signal handler. */
_Static_assert(ATOMIC_INT_LOCK_FREE == 2, "The handler requires lock-free int atomics");
static atomic_int record_fd = ATOMIC_VAR_INIT(-1);
static void fail(void)
{
static const char message[] = "GHC SIGBUS diagnostic: initialization failed\n";
(void)write(STDERR_FILENO, message, sizeof(message) - 1);
_exit(125);
}
static void fault_record(int signal_number, siginfo_t *information, void *context)
{
/* No formatter, allocator, unwinder, dyld call, or fault-address read here.
* A short/interrupted write is reported as a truncated record by the reader.
*/
uint64_t record[RECORD_WORDS] = {0};
record[0] = RECORD_MAGIC;
record[1] = 1;
record[2] = 2;
record[3] = RECORD_CPU;
record[4] = (uint64_t)signal_number;
if (information != NULL) {
record[5] = (uint64_t)(int64_t)information->si_code;
record[6] = (uint64_t)(uintptr_t)information->si_addr;
}
ucontext_t *machine = context;
if (machine != NULL && machine->uc_mcontext != NULL) {
#if defined(__x86_64__)
const struct __darwin_x86_thread_state64 *state = &machine->uc_mcontext->__ss;
record[7] = state->__rip;
record[8] = state->__rsp;
record[9] = state->__rbp;
record[11] = state->__rflags;
record[12] = state->__rax;
record[13] = state->__rbx;
record[14] = state->__rcx;
record[15] = state->__rdx;
record[16] = state->__rdi;
record[17] = state->__rsi;
record[18] = state->__r8;
record[19] = state->__r9;
record[20] = state->__r10;
record[21] = state->__r11;
record[22] = state->__r12;
record[23] = state->__r13;
record[24] = state->__r14;
record[25] = state->__r15;
#else
const struct __darwin_arm_thread_state64 *state = &machine->uc_mcontext->__ss;
record[7] = state->__pc;
record[8] = state->__sp;
record[9] = state->__fp;
record[10] = state->__lr;
record[11] = state->__cpsr;
for (unsigned int index = 0; index < 29; ++index) {
record[12 + index] = state->__x[index];
}
#endif
}
(void)write(atomic_load_explicit(&record_fd, memory_order_relaxed), record, sizeof(record));
/* SA_RESETHAND restored SIG_DFL. Queue the same signal while it is blocked,
* then return for default termination. Darwin raise() can report the same
* si_code as a hardware fault, so returning alone could swallow that signal.
* The record above retains the original fault address and machine context.
*/
(void)kill(getpid(), signal_number);
}
__attribute__((constructor)) static void initialize(void)
{
const char *path = getenv("SIMPLEX_GHC_BUS_RECORD");
const char *present = getenv("SIMPLEX_GHC_BUS_DYLD_PRESENT");
const char *previous = getenv("SIMPLEX_GHC_BUS_DYLD_VALUE");
if (path == NULL || path[0] != '/' || present == NULL ||
(strcmp(present, "0") != 0 && strcmp(present, "1") != 0) ||
(strcmp(present, "1") == 0 && previous == NULL)) {
fail();
}
const int had_previous = strcmp(present, "1") == 0;
char *previous_copy = had_previous ? strdup(previous) : NULL;
if (had_previous && previous_copy == NULL) {
fail();
}
int descriptor = open(path, O_WRONLY | O_CREAT | O_EXCL | O_APPEND | O_CLOEXEC, 0600);
if (descriptor < 0) {
fail();
}
atomic_store_explicit(&record_fd, descriptor, memory_order_relaxed);
int restored = had_previous ? setenv("DYLD_INSERT_LIBRARIES", previous_copy, 1)
: unsetenv("DYLD_INSERT_LIBRARIES");
free(previous_copy);
if (restored != 0 || unsetenv("SIMPLEX_GHC_BUS_RECORD") != 0 ||
unsetenv("SIMPLEX_GHC_BUS_DYLD_PRESENT") != 0 ||
unsetenv("SIMPLEX_GHC_BUS_DYLD_VALUE") != 0) {
fail();
}
struct rlimit core_limit;
if (getrlimit(RLIMIT_CORE, &core_limit) != 0) {
fail();
}
core_limit.rlim_cur = 0;
if (setrlimit(RLIMIT_CORE, &core_limit) != 0) {
fail();
}
struct sigaction previous_action;
if (sigaction(SIGBUS, NULL, &previous_action) != 0 || previous_action.sa_handler != SIG_DFL) {
fail();
}
/* Startup metadata is captured outside the signal handler. */
uint64_t record[RECORD_WORDS] = {0};
record[0] = RECORD_MAGIC;
record[1] = 1;
record[2] = 1;
record[3] = RECORD_CPU;
record[4] = (uint64_t)getpid();
const struct mach_header_64 *header = NULL;
intptr_t slide = 0;
uint32_t image_count = _dyld_image_count();
if (image_count > 1024) {
fail();
}
for (uint32_t index = 0; index < image_count; ++index) {
const struct mach_header *image = _dyld_get_image_header(index);
if (image != NULL && image->filetype == MH_EXECUTE) {
if (header != NULL) {
fail();
}
header = (const struct mach_header_64 *)image;
slide = _dyld_get_image_vmaddr_slide(index);
}
}
if (header == NULL || header->magic != MH_MAGIC_64 ||
header->cputype != RECORD_CPU || header->sizeofcmds > 1024 * 1024 ||
header->ncmds > 1024) {
fail();
}
record[5] = (uint64_t)(uintptr_t)header;
record[6] = (uint64_t)slide;
const unsigned char *commands = (const unsigned char *)(header + 1);
size_t offset = 0;
for (uint32_t index = 0; index < header->ncmds; ++index) {
if (offset > header->sizeofcmds || header->sizeofcmds - offset < sizeof(struct load_command)) {
fail();
}
const struct load_command *command = (const struct load_command *)(commands + offset);
if (command->cmdsize < sizeof(*command) || command->cmdsize > header->sizeofcmds - offset) {
fail();
}
if (command->cmd == LC_UUID) {
if (command->cmdsize != sizeof(struct uuid_command) || record[7] != 0) {
fail();
}
const struct uuid_command *uuid = (const struct uuid_command *)command;
memcpy(&record[8], uuid->uuid, sizeof(uuid->uuid));
record[7] = 1;
}
offset += command->cmdsize;
}
if (write(descriptor, record, sizeof(record)) != (ssize_t)sizeof(record)) {
fail();
}
struct sigaction action = {0};
action.sa_sigaction = fault_record;
action.sa_flags = SA_SIGINFO | SA_RESETHAND;
if (sigemptyset(&action.sa_mask) != 0 || sigaction(SIGBUS, &action, NULL) != 0) {
fail();
}
}