mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-11 18:17:46 +00:00
Add bounded own-process Darwin SIGBUS capture controls
This commit is contained in:
1 parent
ce480d79d0
commit
5e15ffa8fd
3 files changed
+638
No files matched your search
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user