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

This commit is contained in:
dh
2026-10-04 19:32:31 +02:00
parent 81a3b9c7d2
commit 4b7fd160ef
7 changed files with 374 additions and 11 deletions
+9 -1
View File
@@ -20,10 +20,18 @@ Run 4192 at `6122be2` captured the actual 603-byte guest file and strictly parse
Run 4193 at `9164fe4` successfully derived the same current-file version in both guest and host. Its single `diskutil list physical` process was stopped at 124 seconds by the 120-second watchdog without output. The observer saw one runnable row and 3.18 seconds of accumulated CPU time, without a stack or proven IPC endpoint. `ioreg` was also stopped before producing output. The post-disk service gates, permit, installation and tests were not reached. A prior modified Recovery14 reference image has byte-identical SystemVersion contents but a different image hash; it contains StorageKit with the `com.apple.storagekitd` and `com.apple.storagekitd.dm` MachServices, not `diskmanagementd`. The next observation targets that actual service family and the own diskutil stack; this reference does not establish the live service state of run 4193.
Run 4194 at `81a3b9c` stopped the same single query after 606 seconds without output. StorageKit exists but was not running and had never started in the before/live snapshots; the live snapshot covers approximately 103–160 seconds of the query, not its entire lifetime. `sample` hit its own watchdog without even its sampling-start message or report. This does not establish symbolication as the cause. The observed `1T` is a current Timeshare priority, not a thread count or proof of background policy. Neither installation nor tests ran.
## Entry points and dependencies
Orchestration remains the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Existing Bash/Python boot integration is necessary before a guest SDK exists. NASM assembles the CPU probe in the disposable image build, without host/runner installation. No new runner, device, capability, secret or service is used.
The disposable native process diagnostic uses a small C boundary linked only to libSystem, so it can record entry and kernel observations before the guest has .NET or CLT. Its C# file-based build driver and retained source/compiler/SDK/minimum-OS/import/signature/hash receipt describe the one-time local build. The pipeline receives this diagnostic asset and does not invoke an Apple compiler or request a macOS runner. This asset is not one of the application's four freshly built helpers and cannot qualify a test or readiness gate.
The probe validates both the target PID and expected parent before reading role/task data. Public BSD observations include background flags, Nice, role and raw CPU/page-in counters. A read-only Mach port is attempted separately, with return and errno recorded before any DYLD/thread reads. The local Apple-sleep fixture returned EPERM; actual Recovery rights remain unknown. The probe has no control-port fallback, process suspension, remote writes, extra entitlements or SIP change. Unsuspended snapshots may be incomplete.
For local maintenance with an existing Apple SDK, run `dotnet run --file tools/ci/native-process-probe/ProbeDriver.cs -- tools/ci/native-process-probe`. It builds and signs into that folder's `artifacts/`, observes and cleans up its own temporary sleep child, and retains the build/self-test receipt there. Publishing a changed asset requires reviewing that receipt, refreshing the portable `build-manifest.json` and updating all four controller hash pins. CI verifies those pins before staging the binary and manifest into the owned Recovery state; it never runs the build driver.
```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --output /path/to/fresh/validation
@@ -49,7 +57,7 @@ Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service q
The raw file, exact source path, length, SHA256 and parsing receipt are retained and bound to the current token. This version evidence travels only from guest to host and requires no reply. The guest uses its existing Bash before any SDK exists; authoritative XML logic stays in C#/.NET. A Bash candidate alone cannot authorize installation or qualify readiness. This method establishes the current guest version, not successful execution of `sw_vers`.
Required commands retain 45 seconds and UID 180 seconds. The single disk query receives a 600-second diagnostic window under the existing 90-minute Recovery deadline. The owned observer captures StorageKit state, thread CPU snapshots and an optional one-second/100-ms `sample -mayDie` stack of only that live diskutil child. Each observation retains its own 60-second watchdog and two-second TERM/KILL grace. Missing tools, failed or timed-out samples remain explicit missing evidence; thread states alone do not identify an IPC endpoint. Stack output is captured directly from the owned state with a 512-KiB bound, without another native copy command. Observation failure passes no gate. Owned children are stopped on query completion/cancellation; output remains 512 KiB per command and 4 MiB proof. No service is started or restarted by the observer.
Required commands retain 45 seconds and UID 180 seconds. The single disk query retains its 600-second diagnostic window under the existing 90-minute Recovery deadline. An optional no-target `sample` CLI control precedes it. The owned observer takes an early native process snapshot, requests an ordinary one-second/100-ms `sample` without eager `-mayDie` symbol loading, and records live StorageKit state. After a cancelable 180-second builtin pause it takes a late snapshot of the same live disk child and expected parent. Each observation retains its own 60-second watchdog and two-second TERM/KILL grace. Missing tools, denied reads, failures and timed-out samples remain explicit missing evidence. Stack output is captured directly from the owned state with a 512-KiB bound, without another native copy command. Observation failure passes no gate. Owned children are stopped on query completion/cancellation; output remains 512 KiB per command and 4 MiB proof. No service is started or restarted by the observer.
The container retains 6 GiB memory/swap, two-CPU limit, 512 MiB shared memory and a 4-GiB/two-vCPU guest. One fresh anonymous /storage volume holds the sparse 64-GiB target. Inspection rejects devices, capabilities, binds, ports, host networking and privileged mode. KVM is disabled with no /dev/kvm mapping; guest networking stays slirp.
+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.