Observe owned Recovery process without a guest SDK or task control rights

This commit is contained in:
dh committed 2026-10-04 19:32:31 +02:00
1 parent 81a3b9c7d2
commit 4b7fd160ef
7 files changed
+374 -11

No files matched your search

+111 -7
View File
@@ -30,6 +30,10 @@ static class NativeDiagnostic
const string NativeVersionSource = "/System/Library/CoreServices/SystemVersion.plist";
const string NativeVersionMethod = "guest-file/host-xml";
const int MaximumDiskStackBytes = 512 * 1024;
const string ProbeSourceHash = "38acf83b05694f9931c41ff1749e9b6b836f04c740b7f1a1c60e32332678cb1f";
const string ProbeDriverHash = "d9d87415c12398f29b35697109f18d9b16f121dae969a4a05d0ec6a8cb874d25";
const string ProbeBinaryHash = "b8d54e2945eeefb3e0c22468feb7aef7efa50813909058b1e4b40144dd7e3d3e";
const string ProbeManifestHash = "9e71d39e2dd65ab83d0827a467e85893f410ef03e34fb72e08daa27e74861ba3";
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
const string OriginalBootstrap = "[ ! -e /tmp/m ]&&{ /sbin/mount_9p installstate >/dev/null 2>&1;exec /Volumes/installstate/launch.sh;};: >/tmp/m\n";
const string MountOnlyBootstrap = "[ ! -e /tmp/m ]&& /sbin/mount_9p installstate >/dev/null 2>&1; : >/tmp/m\n";
@@ -75,6 +79,7 @@ static class NativeDiagnostic
ValidateContracts();
ValidateBootProgress();
ValidateDiskStackCapture(output);
ValidateProbeAssetFixtures(output);
await ValidateNativeSystemVersion(output);
if (full) ValidateFullContracts();
if (Option(args, "--source") is { } source)
@@ -98,7 +103,12 @@ static class NativeDiagnostic
nativeVersionFixtures = "native-system-version-fixtures.json", nativeProductVersionCommandRemoved = true,
diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 600, diskCommandExtendedForDiagnosticObservation = true,
diskObservationCommandLimitSeconds = 60, stackSamplingRequested = true, stackSamplingDurationSeconds = 1,
stackSamplingIntervalMilliseconds = 100, stackSamplingMayDie = true, stackSamplingRuntimeSucceeded = false,
stackSamplingIntervalMilliseconds = 100, stackSamplingMayDie = false, stackSamplingRuntimeSucceeded = false,
nativeProcessProbeBeforeAndAfter = true, nativeProcessProbeObservationPauseSeconds = 180, sampleUsageControlLimitSeconds = 45,
nativeProcessProbeSourceSha256 = ProbeSourceHash, nativeProcessProbeDriverSha256 = ProbeDriverHash,
nativeProcessProbeBinarySha256 = ProbeBinaryHash, nativeProcessProbeManifestSha256 = ProbeManifestHash,
nativeProcessProbeRecoveryPermissionProven = false, nativeProcessProbeAssetNegativeCases = 4,
nativeProcessProbePrivateApiWeakImported = true, nativeProcessProbeStagingFixtureVerified = true,
stackObservationIsQualifiedReadiness = false, diskStackMaximumCapturedBytes = MaximumDiskStackBytes,
diskManagementDiagnosticLabel = "com.apple.storagekitd", diskStackCaptureFixtureCases = 4,
resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12,
@@ -285,9 +295,10 @@ static class NativeDiagnostic
baseline = ReplaceOnce(baseline, "while (( attempt < 1 && SECONDS - readiness_start < 600 )); do", "while (( SECONDS - readiness_start < 600 )); do");
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, two explicit native SystemVersion getter blocks and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
foreach (var required in new[] { "command_limit=600; fi", "run_command management_before /bin/launchctl print system/com.apple.storagekitd", "observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd", "observe_command diskutil-stack /usr/bin/sample \"$disk_process\" 1 100 -mayDie -file \"$stack_output\"", "stack_output=\"$STATE_DIR/diskutil-stack.txt\"", "read -r -t 60 -u 9", "\"$BASH_VERSION\"" })
foreach (var required in new[] { "command_limit=600; fi", "run_command sample_usage /usr/bin/sample", "run_command management_before /bin/launchctl print system/com.apple.storagekitd", "observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd", "observe_command diskutil-stack /usr/bin/sample \"$disk_process\" 1 100 -file \"$stack_output\"", "observe_live_command diskutil-native-before \"$STATE_DIR/native-process-probe-x86_64\" \"$disk_process\" \"$$\"", "observe_live_command diskutil-native-after \"$STATE_DIR/native-process-probe-x86_64\" \"$disk_process\" \"$$\"", "read -r -t 180 -u 9", "stack_output=\"$STATE_DIR/diskutil-stack.txt\"", "read -r -t 60 -u 9", "\"$BASH_VERSION\"" })
if (!readiness.Contains(required, StringComparison.Ordinal)) throw new InvalidOperationException("Owned optional disk observation contract changed: " + required);
if (readiness.Contains("PENDING_OUTPUTS+=(\"$stack_output\")", StringComparison.Ordinal)) throw new InvalidOperationException("Stack reports must be bounded directly by the Linux host, without another guest copy.");
ReadProbeAssets();
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper.");
if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist.");
@@ -373,6 +384,10 @@ static class NativeDiagnostic
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
{
Directory.CreateDirectory(output);
var probe = ReadProbeAssets();
File.WriteAllBytes(Path.Combine(output, "native-process-probe-x86_64"), probe.Binary);
File.WriteAllBytes(Path.Combine(output, "native-process-probe-manifest.json"), probe.Manifest);
Save(Path.Combine(output, "native-process-probe-input-hashes.json"), new { sourceSha256 = ProbeSourceHash, driverSha256 = ProbeDriverHash, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, compilerExecutedInCI = false, qualifiedReadiness = false, recoveryPermissionProven = false });
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
var originalPatch = File.ReadAllText(patchPath);
if (Hash(Encoding.UTF8.GetBytes(originalPatch)) != "84f13db88c02edbf5ce21a39571fe58f12bebf5b0886c2d012f16ddbaed45323") throw new InvalidOperationException("Pinned Recovery patcher hash mismatch.");
@@ -413,9 +428,9 @@ static class NativeDiagnostic
var imagePath = Path.Combine(source, "src", "image.sh");
var originalImage = File.ReadAllText(imagePath);
if (Hash(Encoding.UTF8.GetBytes(originalImage)) != "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c") throw new InvalidOperationException("Pinned Recovery staging script hash mismatch.");
var image = ReplaceOnce(originalImage, " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\"; then\n", " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cp -f \"$IMAGE_TOOLS/recovery/readiness.sh\" \"${script%/*}/readiness.sh\"; then\n");
image = ReplaceOnce(image, " chmod 0755 \"$script\"\n", " chmod 0755 \"$script\"\n printf '%s\\n' '" + token + "' > \"${script%/*}/run.owner\" || return 1\n");
image = ReplaceOnce(image, " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n", " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cmp -s \"$IMAGE_TOOLS/recovery/readiness.sh\" \"$state/readiness.sh\" ||\n");
var image = ReplaceOnce(originalImage, " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\"; then\n", " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cp -f \"$IMAGE_TOOLS/recovery/readiness.sh\" \"${script%/*}/readiness.sh\" ||\n ! cp -f \"$IMAGE_ASSETS/native-process-probe/native-process-probe-x86_64\" \"${script%/*}/native-process-probe-x86_64\" ||\n ! cp -f \"$IMAGE_ASSETS/native-process-probe/build-manifest.json\" \"${script%/*}/native-process-probe-manifest.json\"; then\n");
image = ReplaceOnce(image, " chmod 0755 \"$script\"\n", " chmod 0755 \"$script\" \"${script%/*}/native-process-probe-x86_64\"\n printf '%s\\n' '" + token + "' > \"${script%/*}/run.owner\" || return 1\n");
image = ReplaceOnce(image, " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n", " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cmp -s \"$IMAGE_TOOLS/recovery/readiness.sh\" \"$state/readiness.sh\" ||\n ! cmp -s \"$IMAGE_ASSETS/native-process-probe/native-process-probe-x86_64\" \"$state/native-process-probe-x86_64\" ||\n ! cmp -s \"$IMAGE_ASSETS/native-process-probe/build-manifest.json\" \"$state/native-process-probe-manifest.json\" ||\n");
image = ReplaceOnce(image, " if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n", " info \"[recovery-original] bytes=$(stat -c%s -- \"$image\") sha256=$(sha256sum \"$image\" | awk '{print $1}')\"\n if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n");
if (full)
{
@@ -434,6 +449,7 @@ static class NativeDiagnostic
await Command("bash", ["-n", Path.Combine(output, "container-entry.sh")], output, "entry-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "cpu.sh.patched")], output, "cpu-composition-syntax", cancellation);
if (!writeSource) await ValidateProbeStaging(image, output, probe.Binary, probe.Manifest, cancellation);
if (full && !writeSource) await ValidateFullBootstrap(wrapper, output, token, cancellation);
if (!writeSource) return;
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
@@ -446,6 +462,72 @@ static class NativeDiagnostic
File.WriteAllText(Path.Combine(source, "src/boot.sh"), boot, new UTF8Encoding(false));
File.WriteAllText(cpuPath, cpu, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "assets/ci-cpu-preflight.asm"), preflight, new UTF8Encoding(false));
var probeAssets = Path.Combine(source, "assets/install/native-process-probe");
Directory.CreateDirectory(probeAssets);
File.WriteAllBytes(Path.Combine(probeAssets, "native-process-probe-x86_64"), probe.Binary);
File.WriteAllBytes(Path.Combine(probeAssets, "build-manifest.json"), probe.Manifest);
}
static (byte[] Binary, byte[] Manifest) ReadProbeAssets()
{
var folder = Path.Combine("tools", "ci", "native-process-probe");
var source = File.ReadAllBytes(Path.Combine(folder, "NativeProcessProbe.c"));
var driver = File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"));
var binary = File.ReadAllBytes(Path.Combine(folder, "native-process-probe-x86_64"));
var manifest = File.ReadAllBytes(Path.Combine(folder, "build-manifest.json"));
ValidateProbeAssets(source, driver, binary, manifest);
return (binary, manifest);
}
static void ValidateProbeAssets(byte[] source, byte[] driver, byte[] binary, byte[] manifest)
{
if (Hash(source) != ProbeSourceHash || Hash(driver) != ProbeDriverHash || Hash(binary) != ProbeBinaryHash || Hash(manifest) != ProbeManifestHash)
throw new InvalidOperationException("Disposable native diagnostic asset differs from its reviewed source/driver/binary/manifest pin.");
using var document = JsonDocument.Parse(manifest); var value = document.RootElement;
if (value.GetProperty("sourceSha256").GetString() != ProbeSourceHash || value.GetProperty("driverSha256").GetString() != ProbeDriverHash || value.GetProperty("binarySha256").GetString() != ProbeBinaryHash
|| value.GetProperty("architecture").GetString() != "x86_64" || value.GetProperty("minimumMacOS").GetString() != "14.0" || value.GetProperty("sdkVersion").GetString() != "27.0"
|| !value.GetProperty("importedLibraries").EnumerateArray().Select(item => item.GetString()).SequenceEqual(new[] { "/usr/lib/libSystem.B.dylib" })
|| value.GetProperty("signature").GetString() != "ad-hoc" || value.GetProperty("entitlements").GetBoolean() || !value.GetProperty("offlineVerified").GetBoolean()
|| !value.GetProperty("targetAndExpectedParentMandatory").GetBoolean() || !value.GetProperty("readOnlyTaskPortOnly").GetBoolean()
|| !value.GetProperty("taskReadWeakImportVerified").GetBoolean()
|| value.GetProperty("taskReadReturnContract").GetString() != "BSD int/errno" || value.GetProperty("outputMaximumBytes").GetInt32() != 32768
|| value.GetProperty("recoveryPermissionsProven").GetBoolean() || value.GetProperty("qualifiedReadiness").GetBoolean())
throw new InvalidOperationException("Disposable native diagnostic manifest claims another runtime, permissions or readiness.");
}
static void ValidateProbeAssetFixtures(string output)
{
ReadProbeAssets(); var folder = Path.Combine("tools", "ci", "native-process-probe");
var source = File.ReadAllBytes(Path.Combine(folder, "NativeProcessProbe.c")); var driver = File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"));
var binary = File.ReadAllBytes(Path.Combine(folder, "native-process-probe-x86_64")); var manifest = File.ReadAllBytes(Path.Combine(folder, "build-manifest.json"));
byte[] Changed(byte[] bytes) { var copy = bytes.ToArray(); copy[0] ^= 1; return copy; }
var cases = new[] { ("wrong-source", Changed(source), driver, binary, manifest), ("wrong-driver", source, Changed(driver), binary, manifest), ("wrong-binary", source, driver, Changed(binary), manifest), ("wrong-manifest-field", source, driver, binary, Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(manifest).Replace("\"recoveryPermissionsProven\": false", "\"recoveryPermissionsProven\": true", StringComparison.Ordinal))) };
foreach (var test in cases)
{
try { ValidateProbeAssets(test.Item2, test.Item3, test.Item4, test.Item5); }
catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Disposable native asset validator accepted " + test.Item1);
}
Directory.CreateDirectory(output);
Save(Path.Combine(output, "native-process-probe-assets-validation.json"), new { success = true, negativeCases = cases.Select(test => test.Item1), sourceSha256 = ProbeSourceHash, driverSha256 = ProbeDriverHash, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, nativeCompilerExecuted = false, nativeProbeExecuted = false, qualifiedReadiness = false });
}
static async Task ValidateProbeStaging(string image, string output, byte[] binary, byte[] manifest, CancellationToken cancellation)
{
var fixture = Path.Combine(output, "validation-probe-staging");
var tools = Path.Combine(fixture, "tools/recovery"); var assets = Path.Combine(fixture, "assets/install"); var state = Path.Combine(fixture, "state");
Directory.CreateDirectory(tools); Directory.CreateDirectory(Path.Combine(assets, "native-process-probe")); Directory.CreateDirectory(state);
File.WriteAllText(Path.Combine(tools, "launch.sh"), "# owned harmless wrapper fixture\n"); File.WriteAllText(Path.Combine(tools, "readiness.sh"), "# owned harmless readiness fixture\n");
File.WriteAllBytes(Path.Combine(assets, "native-process-probe/native-process-probe-x86_64"), binary);
File.WriteAllBytes(Path.Combine(assets, "native-process-probe/build-manifest.json"), manifest);
var begin = image.IndexOf("createAutomatedInstallationFiles() {", StringComparison.Ordinal); var end = image.IndexOf("prepareInstallationState() {", begin, StringComparison.Ordinal);
if (begin < 0 || end < 0) throw new InvalidOperationException("Actual staging producer missing.");
var script = "IMAGE_TOOLS=\"$1/tools\"\nIMAGE_ASSETS=\"$1/assets/install\"\nerror() { printf '%s\\n' \"$*\" >&2; }\n" + image[begin..end] + "\ncreateAutomatedInstallationFiles \"$1/state/launch.sh\"\n";
await Command("bash", ["-c", script, "diagnostic-staging-fixture", fixture], output, "native-probe-staging", cancellation);
if (Hash(File.ReadAllBytes(Path.Combine(state, "native-process-probe-x86_64"))) != ProbeBinaryHash || Hash(File.ReadAllBytes(Path.Combine(state, "native-process-probe-manifest.json"))) != ProbeManifestHash
|| Directory.GetFiles(state).Length != 5 || (File.GetUnixFileMode(Path.Combine(state, "native-process-probe-x86_64")) & UnixFileMode.UserExecute) == 0)
throw new InvalidOperationException("Actual staging paths/bytes/executable mode differ; C/driver must not be staged.");
Save(Path.Combine(fixture, "validation.json"), new { success = true, actualProducerSha256 = Hash(Encoding.UTF8.GetBytes(image[begin..end])), sourceAndDriverStaged = false, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, executable = true, nativeProbeExecuted = false, qualifiedReadiness = false });
}
static void ValidateDaemon(string xml, string processType, bool patched)
@@ -1538,16 +1620,38 @@ static class NativeDiagnostic
snapshotDeadline.CancelAfter(TimeSpan.FromSeconds(20));
const string snapshot = """
printf '[snapshot UTC]\n'; date -u '+%Y-%m-%dT%H:%M:%SZ'
for path in /proc/meminfo /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
for path in /proc/meminfo /proc/loadavg /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
/sys/fs/cgroup/cpu.max /sys/fs/cgroup/cpu.stat /sys/fs/cgroup/cpu.pressure \
/sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.peak \
/sys/fs/cgroup/memory.events /sys/fs/cgroup/memory.stat /sys/fs/cgroup/memory.pressure \
/sys/fs/cgroup/memory.swap.current; do
/sys/fs/cgroup/memory.swap.current /sys/fs/cgroup/io.stat /sys/fs/cgroup/io.pressure; do
printf '\n[%s]\n' "$path"
if [ -r "$path" ]; then cat "$path"; else printf 'unavailable\n'; fi
done
printf '\n[host paging counters]\n'
awk '/^(pgmajfault|pswpin|pswpout) / {print}' /proc/vmstat
printf '\n[owned QEMU process snapshot; counters are raw]\n'
qemu_pid=$(head -c 16 /run/shm/qemu.pid 2>/dev/null || true)
case "$qemu_pid" in ''|*[!0-9]*|0|1) printf 'owned QEMU PID unavailable\n' ;;
*)
qemu_exe=$(readlink "/proc/$qemu_pid/exe" 2>/dev/null || true)
if [ "$qemu_exe" = /usr/bin/qemu-system-x86_64 ]; then
printf 'pid=%s executable=%s\n' "$qemu_pid" "$qemu_exe"
for file in cmdline stat status; do
printf '\n[/proc/%s/%s]\n' "$qemu_pid" "$file"
head -c 4096 "/proc/$qemu_pid/$file" 2>/dev/null | tr '\000' '\n' || true
done
task_count=0
for file in /proc/"$qemu_pid"/task/*/stat; do
[ -r "$file" ] || continue
[ "$task_count" -lt 32 ] || { printf '[remaining own QEMU threads omitted]\n'; break; }
printf '\n[%s]\n' "$file"; head -c 4096 "$file" 2>/dev/null || true
task_count=$((task_count + 1))
done
else
printf 'owned QEMU executable identity unavailable; no process files read\n'
fi ;;
esac
""";
try
{
+6 -3
View File
@@ -196,7 +196,7 @@ observe_disk_query() {
}
trap cancel_observation TERM INT
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
observe_live_command diskutil-threads-before /bin/ps -M -p "$disk_process"
observe_live_command diskutil-native-before "$STATE_DIR/native-process-probe-x86_64" "$disk_process" "$$"
if [ ! -x /usr/bin/sample ]; then
printf '[disk-observation-unavailable] diskutil-stack: /usr/bin/sample is unavailable\n' >> "$output"
elif ! kill -0 "$disk_process" 2>/dev/null; then
@@ -205,13 +205,15 @@ observe_disk_query() {
printf '[disk-observation-unavailable] diskutil-stack: output already exists\n' >> "$output"
elif : > "$stack_output"; then
printf '[disk-stack-attempt] owned-diskutil-child=%s duration=1s interval=100ms limit=60s output=%s observation-only=true\n' "$disk_process" "$stack_output" >> "$output"
observe_command diskutil-stack /usr/bin/sample "$disk_process" 1 100 -mayDie -file "$stack_output"
observe_command diskutil-stack /usr/bin/sample "$disk_process" 1 100 -file "$stack_output"
printf '[disk-stack-result] status=%s observation-only=true; raw report is captured by the Linux host\n' "$OBSERVATION_EXIT" >> "$output"
else
printf '[disk-observation-unavailable] diskutil-stack: output cannot be created\n' >> "$output"
fi
observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd
observe_live_command diskutil-threads-after /bin/ps -M -p "$disk_process"
printf '[disk-observation-pause] limit=180s; canceled when owned query ends\n' >> "$output"
IFS= read -r -t 180 -u 9 unused || :
observe_live_command diskutil-native-after "$STATE_DIR/native-process-probe-x86_64" "$disk_process" "$$"
}
stop_disk_observation() {
@@ -335,6 +337,7 @@ flush_outputs || finish false diagnostic_log_budget_exceeded
# BEGIN disk IPC diagnostic
printf '[disk-diagnostic-runtime] bash=%s stack-observation-only=true\n' "$BASH_VERSION" >&3
run_command sample_usage /usr/bin/sample
run_command arbitration_before /bin/launchctl print system/com.apple.diskarbitrationd
run_command management_before /bin/launchctl print system/com.apple.storagekitd
run_command media_before /usr/sbin/ioreg -r -c IOMedia -l -w 0
@@ -0,0 +1,106 @@
// Disposable pre-SDK observation boundary. No control task port or process writes.
#include <errno.h>
#include <limits.h>
#include <libproc.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <mach/i386/thread_status.h>
#include <mach-o/dyld_images.h>
#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <unistd.h>
// Private exported BSD API/selector, verified against XNU10063.141.1. Its
// return is BSD int + errno, not a Mach kern_return_t. See manifest sources.
extern int task_read_for_pid(mach_port_name_t, int, mach_port_name_t *) __attribute__((weak_import));
#define READ_ONLY_DARWIN_ROLE 6
static unsigned output_bytes;
static void emit(const char *format, ...) {
char line[256]; va_list args; va_start(args, format);
int length = vsnprintf(line, sizeof line, format, args); va_end(args);
if (length < 0 || length >= (int)sizeof line || output_bytes + (unsigned)length > 32768) exit(70);
output_bytes += (unsigned)length; fwrite(line, 1, (size_t)length, stderr);
}
static void phase(const char *name, const char *point) { emit("[phase] %s %s\n", name, point); }
int main(int argc, char **argv) {
setvbuf(stderr, NULL, _IONBF, 0);
emit("[probe-entry] self=%d architecture=%s snapshot-only=true\n", getpid(),
#if defined(__x86_64__)
"x86_64"
#else
"other"
#endif
);
if (argc != 3) { emit("[invalid-pid] require target and expected-parent decimal PIDs greater than1\n"); return 64; }
long parsed[2];
for (int argument = 1; argument <= 2; ++argument) {
if (!argv[argument][0]) { emit("[invalid-pid] empty PID\n"); return 64; }
for (const char *p = argv[argument]; *p; ++p) if (*p < '0' || *p > '9') { emit("[invalid-pid] decimal digits required\n"); return 64; }
errno = 0; char *end; parsed[argument - 1] = strtol(argv[argument], &end, 10);
if (errno || *end || parsed[argument - 1] <= 1 || parsed[argument - 1] > INT_MAX) { emit("[invalid-pid] PID outside permitted numeric range\n"); return 64; }
}
int pid = (int)parsed[0], expected_parent = (int)parsed[1]; struct proc_bsdinfo bsd = {0};
phase("proc_pidinfo", "before"); errno = 0;
int bytes = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsd, sizeof bsd); int bsd_errno = errno;
emit("[phase] proc_pidinfo after return=%d errno=%d\n", bytes, bsd_errno);
if (bytes != sizeof bsd) { emit("[bsd-unavailable] identity was not established\n"); return 66; }
emit("[bsd] pid=%u ppid=%u flags=0x%x nice=%d status=%u\n", bsd.pbi_pid, bsd.pbi_ppid, bsd.pbi_flags, bsd.pbi_nice, bsd.pbi_status);
if (bsd.pbi_pid != (unsigned)pid || bsd.pbi_ppid != (unsigned)expected_parent) { emit("[ownership-rejected] target=%d expected-parent=%d actual-pid=%u actual-parent=%u\n", pid, expected_parent, bsd.pbi_pid, bsd.pbi_ppid); return 65; }
phase("getpriority-role", "before"); errno = 0;
int role = getpriority(READ_ONLY_DARWIN_ROLE, (id_t)pid); int role_errno = errno;
emit("[role] selector=6 value=%d errno=%d\n", role, role_errno); phase("getpriority-role", "after");
struct proc_taskinfo taskinfo = {0}; phase("proc_pidinfo-task", "before"); errno = 0;
bytes = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskinfo, sizeof taskinfo); int taskinfo_errno = errno;
emit("[phase] proc_pidinfo-task after return=%d errno=%d\n", bytes, taskinfo_errno);
if (bytes == sizeof taskinfo) {
emit("[bsd-task] total-user-raw=%llu total-system-raw=%llu threads-user-raw=%llu threads-system-raw=%llu\n", taskinfo.pti_total_user, taskinfo.pti_total_system, taskinfo.pti_threads_user, taskinfo.pti_threads_system);
emit("[bsd-task] threads=%d running=%d policy=%d priority=%d faults=%d pageins=%d virtual-bytes=%llu resident-bytes=%llu\n", taskinfo.pti_threadnum, taskinfo.pti_numrunning, taskinfo.pti_policy, taskinfo.pti_priority, taskinfo.pti_faults, taskinfo.pti_pageins, taskinfo.pti_virtual_size, taskinfo.pti_resident_size);
}
if (!task_read_for_pid) {
emit("[task-read-unavailable] optional private symbol absent; public BSD snapshot remains observational\n");
emit("[probe-end] snapshot-only=true qualified-readiness=false\n"); return 0;
}
mach_port_t task = MACH_PORT_NULL; phase("task_read_for_pid", "before"); errno = 0;
int read_result = task_read_for_pid(mach_task_self(), pid, &task); int read_errno = errno;
emit("[task-read] return=%d errno=%d port=0x%x\n", read_result, read_errno, task); phase("task_read_for_pid", "after");
if (read_result != 0 || task == MACH_PORT_NULL) {
emit("[mach-unavailable] read-only capability denied; BSD snapshot remains observational\n");
if (task != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), task);
emit("[probe-end] snapshot-only=true qualified-readiness=false\n");
return 0;
}
emit("[mach-capability] read-only=true; unsuspended snapshots may be incomplete\n");
task_dyld_info_data_t dyld = {0}; mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
phase("task_info-dyld", "before"); kern_return_t kr = task_info(task, TASK_DYLD_INFO, (task_info_t)&dyld, &count);
emit("[phase] task_info-dyld after kern=%d count=%u\n", kr, count);
size_t prefix = offsetof(struct dyld_all_image_infos, jitInfo);
if (kr == KERN_SUCCESS && count == TASK_DYLD_INFO_COUNT && dyld.all_image_info_format == TASK_DYLD_ALL_IMAGE_INFO_64 && dyld.all_image_info_size >= prefix) {
struct dyld_all_image_infos info = {0}; mach_vm_size_t received = 0; phase("dyld-prefix-read", "before");
kr = mach_vm_read_overwrite(task, dyld.all_image_info_addr, prefix, (mach_vm_address_t)(uintptr_t)&info, &received);
emit("[phase] dyld-prefix-read after kern=%d bytes=%llu\n", kr, (unsigned long long)received);
if (kr == KERN_SUCCESS && received == prefix) emit("[dyld] version=%u images=%u array=0x%llx libSystemInitialized=%d dyld=0x%llx array-null-is-pending=true\n", info.version, info.infoArrayCount, (unsigned long long)(uintptr_t)info.infoArray, info.version >= 2 ? info.libSystemInitialized : -1, info.version >= 2 ? (unsigned long long)(uintptr_t)info.dyldImageLoadAddress : 0);
}
thread_act_array_t threads = NULL; mach_msg_type_number_t thread_count = 0;
phase("task_threads", "before"); kr = task_threads(task, &threads, &thread_count);
emit("[phase] task_threads after kern=%d count=%u observed-limit=32\n", kr, thread_count);
if (kr == KERN_SUCCESS) {
for (unsigned i = 0; i < thread_count && i < 32; ++i) {
thread_basic_info_data_t basic = {0}; count = THREAD_BASIC_INFO_COUNT; phase("thread_info", "before");
kr = thread_info(threads[i], THREAD_BASIC_INFO, (thread_info_t)&basic, &count);
emit("[phase] thread_info after index=%u kern=%d count=%u\n", i, kr, count);
if (kr == KERN_SUCCESS && count == THREAD_BASIC_INFO_COUNT) emit("[thread-basic] index=%u run-state=%d policy=%d cpu=%d user=%d.%06d system=%d.%06d\n", i, basic.run_state, basic.policy, basic.cpu_usage, basic.user_time.seconds, basic.user_time.microseconds, basic.system_time.seconds, basic.system_time.microseconds);
x86_thread_state64_t registers = {0}; count = x86_THREAD_STATE64_COUNT; phase("thread_get_state-x86_64", "before");
kr = thread_get_state(threads[i], x86_THREAD_STATE64, (thread_state_t)&registers, &count);
emit("[phase] thread_get_state-x86_64 after index=%u kern=%d count=%u\n", i, kr, count);
if (kr == KERN_SUCCESS && count == x86_THREAD_STATE64_COUNT) emit("[thread-registers] index=%u rip=0x%llx rbp=0x%llx rsp=0x%llx\n", i, registers.__rip, registers.__rbp, registers.__rsp);
}
for (unsigned i = 0; i < thread_count; ++i) mach_port_deallocate(mach_task_self(), threads[i]);
vm_deallocate(mach_task_self(), (vm_address_t)threads, thread_count * sizeof *threads);
}
mach_port_deallocate(mach_task_self(), task);
emit("[probe-end] snapshot-only=true qualified-readiness=false\n"); return 0;
}
@@ -0,0 +1,116 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
// Offline build/owned-child validation only. No guest, Docker or services.
if (!OperatingSystem.IsMacOS()) throw new InvalidOperationException("The disposable build driver uses the existing Apple SDK on this local host.");
var folder = Path.GetFullPath(args.Single());
var source = Path.Combine(folder, "NativeProcessProbe.c");
var output = Path.Combine(folder, "artifacts"); Directory.CreateDirectory(output);
var sourceHash = Hash(File.ReadAllBytes(source));
var sdk = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "sdk-path")).Stdout.Trim();
var sdkVersion = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "sdk-version")).Stdout.Trim();
var compiler = await Run("/usr/bin/xcrun", ["--sdk", "macosx", "clang", "--version"], "compiler-version");
var binary = Path.Combine(output, "native-process-probe-x86_64");
string[] build = ["--sdk", "macosx", "clang", "-arch", "x86_64", "-mmacosx-version-min=14.0", "-std=c11", "-Os", "-Wall", "-Wextra", "-Werror", source, "-lproc", "-o", binary];
await Run("/usr/bin/xcrun", build, "build-x86_64");
await Run("/usr/bin/codesign", ["--force", "--sign", "-", binary], "adhoc-sign");
await Run("/usr/bin/codesign", ["--verify", "--strict", binary], "signature-verify");
var signature = await Run("/usr/bin/codesign", ["-d", "--verbose=4", "--entitlements", ":-", binary], "signature-details");
var architectures = await Run("/usr/bin/lipo", ["-archs", binary], "architectures");
var imports = await Run("/usr/bin/otool", ["-L", binary], "imports");
var loadCommands = await Run("/usr/bin/otool", ["-l", binary], "load-commands");
var symbols = await Run("/usr/bin/nm", ["-m", "-u", binary], "undefined-symbols");
if (architectures.Stdout.Trim() != "x86_64" || !Regex.IsMatch(loadCommands.Stdout, @"cmd LC_BUILD_VERSION\s+cmdsize [0-9]+\s+platform [0-9]+\s+minos 14\.0\b")) throw new Exception("Actual architecture/minimum OS differs.");
var importedLibraries = imports.Stdout.Split('\n').Skip(1).Where(line => line.Trim().Length != 0).Select(line => line.Trim().Split(' ')[0]).ToArray();
if (importedLibraries.Length != 1 || importedLibraries[0] != "/usr/lib/libSystem.B.dylib") throw new Exception("Probe imports another runtime/framework: " + string.Join(",", importedLibraries));
if (!Regex.IsMatch(symbols.Stdout, @"weak external _task_read_for_pid\b") || symbols.Stdout.Contains("_task_for_pid", StringComparison.Ordinal)
|| new[] { "_task_suspend", "_task_resume", "_thread_suspend", "_thread_resume", "_mach_vm_write" }.Any(symbols.Stdout.Contains)) throw new Exception("Read-only import contract failed.");
if (signature.Stdout.Contains("<dict>", StringComparison.Ordinal) || signature.Stderr.Contains("<dict>", StringComparison.Ordinal)) throw new Exception("Probe must not acquire entitlements.");
var cases = new List<object>(); bool x86Executed = false; string? executionUnavailable = null;
using var target = Process.Start(new ProcessStartInfo("/bin/sleep") { ArgumentList = { "30" }, UseShellExecute = false })!;
try
{
Result positive;
try { positive = await Run(binary, [target.Id.ToString(), Environment.ProcessId.ToString()], "owned-sleep", requireSuccess: false); x86Executed = true; }
catch (System.ComponentModel.Win32Exception exception) { executionUnavailable = exception.Message; positive = new(-1, "", ""); }
if (x86Executed)
{
var evidence = positive.Stdout + positive.Stderr;
if (positive.ExitCode != 0 || !evidence.Contains("[probe-entry]", StringComparison.Ordinal)
|| !Regex.IsMatch(evidence, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @" flags=0x[0-9a-f]+ nice=-?[0-9]+ status=[0-9]+")
|| !evidence.Contains("[role] selector=6", StringComparison.Ordinal) || !evidence.Contains("[probe-end] snapshot-only=true qualified-readiness=false", StringComparison.Ordinal)
|| !(Regex.IsMatch(evidence, @"\[task-read\] return=-?[0-9]+ errno=[0-9]+ port=0x[0-9a-f]+") || evidence.Contains("[task-read-unavailable] optional private symbol absent", StringComparison.Ordinal))) throw new Exception("Owned BSD snapshot/read-capability receipt failed: " + evidence);
var read = Regex.Match(evidence, @"\[task-read\] return=(-?[0-9]+) errno=([0-9]+) port=0x([0-9a-f]+)");
cases.Add(new { name = "owned-sleep", success = true, targetPid = target.Id, expectedParentPid = Environment.ProcessId, positive.ExitCode, outputBytes = Encoding.UTF8.GetByteCount(evidence), privateReadSymbolAvailable = read.Success, readReturn = read.Success ? int.Parse(read.Groups[1].Value) : (int?)null, readErrno = read.Success ? int.Parse(read.Groups[2].Value) : (int?)null, readPort = read.Success ? read.Groups[3].Value : null, targetIsApplePlatformBinary = true, targetArchitectureNotAsserted = true, recoveryPermissionProven = false });
var wrongParent = await Run(binary, [target.Id.ToString(), target.Id.ToString()], "owned-sleep-wrong-parent", requireSuccess: false);
var rejected = wrongParent.Stdout + wrongParent.Stderr;
if (wrongParent.ExitCode != 65 || !rejected.Contains("[ownership-rejected]", StringComparison.Ordinal)
|| !Regex.IsMatch(rejected, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @"\b")
|| rejected.Contains("getpriority-role", StringComparison.Ordinal) || rejected.Contains("proc_pidinfo-task", StringComparison.Ordinal) || rejected.Contains("task_read_for_pid", StringComparison.Ordinal)) throw new Exception("Wrong parent reached role/task/Mach observation.");
cases.Add(new { name = "owned-sleep-wrong-parent", success = true, targetPid = target.Id, suppliedExpectedParentPid = target.Id, actualParentPid = Environment.ProcessId, wrongParent.ExitCode, furtherReadsReached = false });
var ownedPid = target.Id.ToString(); var parentPid = Environment.ProcessId.ToString();
string[][] invalid = [[], [ownedPid], ["", parentPid], ["0", parentPid], ["1", parentPid], ["-1", parentPid], ["+2", parentPid], ["2x", parentPid], [" 2", parentPid], ["999999999999999999999999", parentPid],
[ownedPid, ""], [ownedPid, "0"], [ownedPid, "1"], [ownedPid, "-1"], [ownedPid, "+2"], [ownedPid, "2x"], [ownedPid, " 2"], [ownedPid, "999999999999999999999999"], [ownedPid, parentPid, "extra"]];
for (var i = 0; i < invalid.Length; i++)
{
var result = await Run(binary, invalid[i], "invalid-pid-" + i, requireSuccess: false);
var text = result.Stdout + result.Stderr;
if (result.ExitCode != 64 || !text.Contains("[probe-entry]", StringComparison.Ordinal) || !text.Contains("[invalid-pid]", StringComparison.Ordinal) || text.Contains("proc_pidinfo", StringComparison.Ordinal)) throw new Exception("Invalid PID reached observation.");
cases.Add(new { name = "invalid-pid-" + i, success = true, result.ExitCode, nativeProcessObserved = false });
}
if (target.HasExited) throw new Exception("Own sleep exited unexpectedly during the snapshot.");
}
}
finally
{
if (!target.HasExited) target.Kill();
await target.WaitForExitAsync();
}
if (!target.HasExited) throw new Exception("Owned local child was not cleaned up.");
var manifest = new
{
success = x86Executed, buildVerified = true, selftestPassed = x86Executed, purpose = "Disposable offline native read-only process diagnostic; no CI runner requirement", sourcePath = source, sourceSha256 = sourceHash,
driverSha256 = Hash(File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"))), binaryPath = binary, binarySha256 = Hash(File.ReadAllBytes(binary)),
hostArchitecture = RuntimeInformation.OSArchitecture.ToString(), compiler = compiler.Stdout.Trim(), sdk, sdkVersion, minimumMacOS = "14.0", architecture = "x86_64", compilerArguments = build,
importedLibraries, signature = "ad-hoc; no entitlements", outputMaximumBytes = 32768, threadObservationMaximum = 32, frameWalkUsed = false, imageArrayReadUsed = false,
readOnlyTaskPortOnly = true, taskReadWeakImportVerified = true, taskReadReturnContract = "BSD int/errno", darwinRolePrioritySelector = 6, targetAndExpectedParentMandatory = true, bsdIdentityRequiredBeforeFurtherReads = true, snapshotIsReadiness = false, qualifiedReadiness = false,
x86Executed, executionUnavailable, localOwnedChildCleanedUp = target.HasExited, selftests = cases, guestExecuted = false, dockerExecuted = false, recoveryPermissionsProven = false,
primarySources = new[] { "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/kern/kern_resource.c#L691-L721", "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/sys/resource.h", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/xnu-10063.141.1/bsd/vm/vm_unix.c", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/bsd/kern/syscalls.master" },
abiSourceIsExactGuestBinary = false, actualSdkExport = Path.Combine(sdk, "usr/lib/system/libsystem_kernel.tbd"), completedUtc = DateTimeOffset.UtcNow
};
File.WriteAllText(Path.Combine(output, "manifest.json"), JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine(JsonSerializer.Serialize(manifest));
async Task<Result> Run(string executable, string[] arguments, string label, bool requireSuccess = true)
{
using var process = new Process { StartInfo = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false } };
foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(argument);
process.Start();
async Task<string> Read(StreamReader reader)
{
var text = new StringBuilder(); var buffer = new char[2048];
while (await reader.ReadAsync(buffer) is var count && count != 0)
{
text.Append(buffer, 0, count);
if (Encoding.UTF8.GetByteCount(text.ToString()) > 32768) { if (!process.HasExited) process.Kill(); throw new Exception("Disposable probe/tool output exceeded32KiB."); }
}
return text.ToString();
}
var stdout = Read(process.StandardOutput); var stderr = Read(process.StandardError);
using var bound = new CancellationTokenSource(TimeSpan.FromSeconds(20));
try { await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(bound.Token)); }
catch { if (!process.HasExited) process.Kill(); await process.WaitForExitAsync(); throw; }
var result = new Result(process.ExitCode, await stdout, await stderr);
File.WriteAllText(Path.Combine(output, label + ".stdout.log"), result.Stdout);
File.WriteAllText(Path.Combine(output, label + ".stderr.log"), result.Stderr);
if (requireSuccess && result.ExitCode != 0) throw new Exception(label + " failed: " + result.Stderr);
return result;
}
static string Hash(byte[] value) => Convert.ToHexStringLower(SHA256.HashData(value));
sealed record Result(int ExitCode, string Stdout, string Stderr);
@@ -0,0 +1,26 @@
{
"purpose": "Disposable prebuilt diagnostic; no macOS compiler or runner is required by CI",
"sourceSha256": "38acf83b05694f9931c41ff1749e9b6b836f04c740b7f1a1c60e32332678cb1f",
"driverSha256": "d9d87415c12398f29b35697109f18d9b16f121dae969a4a05d0ec6a8cb874d25",
"binarySha256": "b8d54e2945eeefb3e0c22468feb7aef7efa50813909058b1e4b40144dd7e3d3e",
"compiler": "Apple clang 21.0.0 (clang-2100.3.34.2)",
"sdkVersion": "27.0",
"minimumMacOS": "14.0",
"architecture": "x86_64",
"importedLibraries": ["/usr/lib/libSystem.B.dylib"],
"signature": "ad-hoc",
"entitlements": false,
"offlineVerified": true,
"targetAndExpectedParentMandatory": true,
"readOnlyTaskPortOnly": true,
"taskReadWeakImportVerified": true,
"taskReadReturnContract": "BSD int/errno",
"darwinRolePrioritySelector": 6,
"outputMaximumBytes": 32768,
"threadObservationMaximum": 32,
"frameWalkUsed": false,
"imageArrayReadUsed": false,
"localPlatformReadPortDenied": true,
"recoveryPermissionsProven": false,
"qualifiedReadiness": false
}
Binary file not shown.