Compare commits

...
Author SHA1 Message Date
dh 4606de0696 ci: preserve Apple Recovery daemon during read-only readiness probe
PR and Push Build/Test / build-and-test (push) Canceled after 0s
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
2026-10-03 20:17:56 +02:00
dh 40281b57a5 Isolate measured UID watchdog failure in the native TCG diagnostic
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 1m33s
2026-10-03 18:58:10 +02:00
dh c92e62bdf5 Reduce native readiness probe overhead and preserve screenshot evidence
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 1m15s
2026-10-03 18:05:15 +02:00
dh b40234d3b5 ci: capture native recovery startup context before platform probe
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
PR and Push Build/Test / build-and-test (push) Canceled after 3m24s
2026-10-03 16:38:34 +02:00
dh 0a30a1ca5a ci: retain current build fixes in the readonly native diagnostic 2026-10-03 16:22:03 +02:00
dh 6b1896660f ci: derive the macOS probe from the pinned QEMU filesystem image 2026-10-03 15:01:00 +02:00
dh 9a91a81992 ci: probe native macOS Recovery readiness on the existing Ubuntu runner 2026-10-03 14:18:50 +02:00
6 changed files with 743 additions and 0 deletions
@@ -0,0 +1,36 @@
name: Native macOS Recovery diagnostic on Ubuntu
on:
workflow_dispatch:
jobs:
macos-native-diagnostic:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1"
steps:
- name: Checkout diagnostic source
uses: actions/checkout@v7
- name: Setup .NET for the diagnostic helper
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Probe native macOS Recovery with existing Docker resources
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources
if: always()
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
- name: Preserve native diagnostic evidence
if: always()
uses: actions/upload-artifact@v3
with:
name: native-macos-recovery-diagnostic
path: artifacts/native-macos/
if-no-files-found: error
retention-days: 7
+2
View File
@@ -175,6 +175,8 @@ Ubuntu does not compile the Swift helpers or execute Apple frameworks. Tests req
[Docker-OSX](https://github.com/sickcodes/Docker-OSX) runs a macOS VM rather than providing a Wine-style compatibility layer. Its launcher supports software emulation with `KVM=accel=tcg`, so KVM is not an absolute requirement. A supported .NET 10 guest needs macOS 14 or later plus the Swift build tools. The documented `auto` build downloads a preinstalled guest disk through `IMAGE_URL`; its documented ready-made tags and disk downloads were unavailable when checked on 2026-10-03. No verified native guest bootstrap is owned by this repository. CI validates source; it does not publish or deploy the workstation application.
The separate manual [native Recovery diagnostic](docs/macos-native-diagnostic.md) probes macOS startup and disk readiness through an unprivileged TCG guest on the existing Ubuntu Docker runner. It neither installs macOS nor runs application tests; its result is a prerequisite for a future native test job, not verification of macOS CI support.
## Operations And Limitations
- Treat recording, transcription drain, speaker finalization, OCR, and summarization as live user work. Never restart, kill, or clean runtime files until `/recording/status` is idle unless interruption is explicitly intended.
+47
View File
@@ -0,0 +1,47 @@
# Native macOS Recovery diagnostic on the existing Ubuntu runner
This manual diagnostic tests the unresolved Recovery startup boundary before adding a native macOS application test job. It does not install macOS, erase a guest disk, install .NET or Apple CLT, or run Meeting Assistant tests. A green diagnostic means only that a real macOS 14+ x86_64 Recovery guest has a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk.
The workflow `.gitea/workflows/macos-native-diagnostic.yaml` has only `workflow_dispatch`; it does not run on ordinary pushes or pull requests. It uses the same `ubuntu-latest` label and existing Docker daemon as the current builds. There are no runner changes, extra host devices, privileged containers, added capabilities, published ports, host networking or new secrets. It fails clearly if the existing Docker daemon cannot fit its bounded resource budget.
## Helper entry point and invocation
The orchestration is a .NET 10 file-based C# app at `tools/ci/MacOsNativeDiagnostic.cs`:
```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --help
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/pinned/dockur-clone --output artifacts/native-validation
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
```
Dependencies are the existing Linux/x64 runner, .NET 10 SDK, Git, Bash and Docker CLI/socket. The actual execution downloads public Dockur source, upstream build assets, Docker images and Apple Recovery; it does not use workstation credentials. The existing upstream Python UDIF patcher and the Bash hook are retained because they run inside the pinned Linux/macOS boot integration. Independent orchestration and validation remain C#.
The helper clones Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, verifies its exact Recovery patcher and staging-script hashes, and makes narrowly verified source edits. The early `rc.cdrom.sh` hook only mounts the existing state share and returns. A same-length XML replacement makes the existing `com.apple.recoveryosd` LaunchDaemon execute `/bin/bash /Volumes/installstate/launch.sh` after boot tasks. In this separate bootstrap A/B candidate, that file is the small `tools/ci/macos-native-bootstrap.sh` wrapper: it starts `/bin/bash /Volumes/installstate/readiness.sh` in the background, then `exec /usr/libexec/recoveryosd`. The original Apple executable therefore replaces the wrapper under the same launchd job/PID if exec succeeds. The staging copy and comparison transport the second script through the existing 9p share. All replacement counts are exact; an upstream mismatch fails. The two imported QEMU image digests are pinned and the final image/source/Recovery hashes are retained. Other upstream Dockerfile downloads are observed through the resulting image identity rather than asserted to be immutable.
The experiment starts from commit `40281b57a5e80bbe699ae50d8927e629d09d05ad`. The readiness script is byte-identical to that baseline; validation enforces SHA-256 `4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3`. This changes only whether Apple's original Recovery daemon runs alongside the same probe. The probe now has that daemon as its parent and competes with its work for guest CPU/I/O. If the job restarts, the wrapper could start another read-only probe. Those lifecycle and scheduling effects are part of the experiment, not proof of a CoreFoundation or DiskArbitration dependency. No original Apple daemon implementation or such dependency is established by the retained plist.
The VM uses TCG (`KVM=N`), slirp networking, a 4-GiB guest, two virtual CPUs and a sparse 64-GiB data disk. Its container has a 6-GiB memory/swap ceiling and a two-CPU limit. The existing Docker daemon must report at least two CPUs and 6 GiB total memory, the runner must have at least 5 GiB available memory, and the Docker filesystem must have at least 8 GiB free before Recovery downloads or boot. Native commands have 45-second watchdogs, except the single UID gate's targeted 180-second timing experiment. The ten-minute disk-readiness phase, 40-minute host deadline and 45-minute workflow limit remain unchanged.
Actual remote run 4155 stopped at the first `sw_vers` with exit 143. Run 4159 then proved native Darwin/x86_64, root identity and guest AVX2, but reached the host deadline before `sw_vers` or the service/disk gates. Its logged command durations included timer cleanup and output copying, so they did not isolate native execution time.
The next probe runs mandatory architecture, root identity and platform gates before optional process/CPU diagnostics. It keeps the proof log open, uses Bash 3.2's timed FIFO reads instead of starting a separate sleep process for every watchdog, and groups output copying and byte-limit checks. Separate markers record fork/exec/wait, timer cleanup and output flush durations. Raw output still fails above 512 KiB per command, proof above 4 MiB fails, and scalar gates reject hidden suffixes or multiline values. A local harmless-command harness verifies all 16 timeout, cancellation, output and scalar cases; this does not qualify macOS Recovery.
After an initial platform failure the hook collects native launchd context and repeats the identical `sw_vers` command once, with the same 45-second limit. Native product version and all original identity/service/disk gates remain required. Optional process and CPU diagnostics run only after a gate fails. The upstream AVX2 warning reads host flags; run 4159 observed AVX2 in the actual guest. No host or guest CPU settings change.
Actual run 4161 separated native wait from timer cleanup: architecture passed after 39 seconds, but the UID gate was terminated by its 45-second watchdog (51-second fork/exec/wait duration). Native ps commands passed after 34-42 seconds; output flushes took 289 and 76 seconds. Run 4163 used the targeted UID watchdog of 180 seconds but returned UID 0 with exit zero after 38 seconds, so it did not establish a need for that longer limit. Architecture passed after 46 seconds; the initial `sw_vers`, system-domain and DiskArbitration queries were terminated. The recovery-label query passed after 31 seconds but described the replacement Bash job, not Apple's executable. The identical warm `sw_vers` retry had no final exit before the 40-minute host deadline. This bootstrap A/B preserves all those limits and native pass conditions; no cause or native readiness is claimed. The earlier local 16-case harness qualified the previous 45-second timer/cancellation/output behavior, not this experiment or the actual emulated guest.
## Evidence and cleanup
Evidence is written under the requested output directory: run identity and candidate commit, Docker/runner resources, exact source patch artifacts and hashes, image/container inspection, Recovery hash, native platform/process/launchctl/diskutil logs, machine-readable guest result, outcome and cleanup receipt. `guest-launch.sh` records the wrapper and its original `recoveryosd` exec path; `guest-readiness.sh` records the separate token-bound probe; `image.sh.patched` records the copy/comparison seam. `source-hashes.json` distinguishes all three. These source artifacts alone do not prove that Apple's executable actually ran. The workflow retains these as a seven-day artifact. Phase names and up to 512 KiB of the final native proof also appear in CI stdout, on success or failure, with the run token replaced; no environment or credential dump is printed. A Docker start/build exit zero is not a successful native result. A missing, stale, unsupported-platform, read-only or wrong-size guest receipt fails.
While Recovery readiness is pending, a minute heartbeat reports elapsed guest time and the container's running state. Before final cleanup, an optional ten-second capture rechecks the saved container ID/ownership label and uses the pinned image's existing Unix HMP socket, `nc.openbsd` and a five-second `timeout` to collect only [`info status` and `screendump`](https://www.qemu.org/docs/master/system/monitor.html), retaining the command transcript, exit codes and fresh bounded PPM screenshot. Capture failure is visible and never changes native readiness success.
Run 4159 generated a 6,220,817-byte screenshot file under `/dev/shm`, but `docker cp` could not retrieve it. Screenshots now use the regular container path `/tmp/native-diagnostic-screen-<runToken>.ppm`, avoiding Docker's documented [`/dev`/tmpfs copy limitation](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases).
Every container/image has a random run token in its ownership label. `finally` cleanup and the workflow's `always()` step inspect that exact label before removing the matching container and its anonymous storage volume, then the matching image. They never remove an unrelated name or volume, prune Docker, modify host settings or restart Meeting Assistant. Temporary source files are deleted only when their local marker matches the same token. Evidence remains available after cleanup.
The earlier background-only local bootstrap never obtained DiskManagement readiness. This separate LaunchDaemon probe is still an experiment until the actual remote run produces the required native evidence. Full macOS CI support remains unverified until an installed guest subsequently compiles/signs the native helpers and passes all application tests, including all five native tests without skips.
Remote run 4152 passed Docker access and resource checks but failed before VM startup: the runner's BuildKit could not checksum a dangling `/etc/alternatives/awk.1.gz` link while copying the entire QEMU filesystem. The candidate now derives directly from the same pinned QEMU filesystem image and overwrites its QEMU executable as before. Inspection of that exact digest reports an empty image `Config`, so it adds no inherited environment, user, command or healthcheck. Actual run 4155 built that image and started QEMU/XNU successfully, then failed the first native `sw_vers` after its 45-second watchdog. It did not prove native readiness.
+403
View File
@@ -0,0 +1,403 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
// .NET 10 file-based CI diagnostic. See docs/macos-native-diagnostic.md.
return await NativeDiagnostic.Execute(args);
static class NativeDiagnostic
{
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
const int MaximumCapturedCharacters = 8 * 1024 * 1024;
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";
static readonly string OriginalDaemon = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
\t<key>Label</key>
\t<string>com.apple.recoveryosd</string>
\t<key>OnDemand</key>
\t<false/>
\t<key>ProcessType</key>
\t<string>App</string>
\t<key>EnablePressuredExit</key>
\t<false/>
\t<key>ProgramArguments</key>
\t<array>
\t\t<string>/usr/libexec/recoveryosd</string>
\t</array>
</dict>
</plist>
""".Replace("\\t", "\t", StringComparison.Ordinal);
static readonly string DiagnosticDaemon = (OriginalDaemon + "\n")
.Replace("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n", "", StringComparison.Ordinal)
.Replace("\t\t<string>/usr/libexec/recoveryosd</string>", "\t\t<string>/bin/bash</string>\n\t\t<string>/Volumes/installstate/launch.sh</string>", StringComparison.Ordinal);
public static async Task<int> Execute(string[] args)
{
if (args.Length == 0 || args.Contains("--help"))
{
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone]");
return 0;
}
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
if (args.Contains("--validate"))
{
ValidateContracts();
if (Option(args, "--source") is { } source)
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
return 0;
}
if (args.Contains("--cleanup"))
return await Cleanup(output) ? 0 : 1;
if (!args.Contains("--run")) throw new ArgumentException("Choose --run, --cleanup or --validate.");
Directory.CreateDirectory(output);
var statePath = Path.Combine(output, "owned-resources.json");
if (File.Exists(statePath)) throw new InvalidOperationException("Output already contains a run identity; choose a fresh directory or clean up its run first.");
var token = Guid.NewGuid().ToString("N");
var work = Path.Combine(Environment.GetEnvironmentVariable("RUNNER_TEMP") ?? Path.GetTempPath(), "meeting-assistant-native-" + token);
var state = new OwnedResources(token, "meeting-assistant-native-" + token, "meeting-assistant-native-diagnostic:" + token, work);
Save(statePath, state);
Directory.CreateDirectory(work);
File.WriteAllText(Path.Combine(work, "run.owner"), token);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(40));
using var signal = OperatingSystem.IsLinux() ? PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); }) : null;
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
Console.CancelKeyPress += cancelHandler;
var outcome = "failed";
string? error = null;
try
{
if (!OperatingSystem.IsLinux() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
ValidateContracts();
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
using (var document = JsonDocument.Parse(info.Output))
{
var data = document.RootElement;
if (data.GetProperty("OSType").GetString() != "linux" || data.GetProperty("Architecture").GetString() is not ("x86_64" or "amd64"))
throw new InvalidOperationException("The existing Docker daemon is not Linux/x64; this diagnostic does not reconfigure it.");
if (data.GetProperty("NCPU").GetInt32() < 2 || data.GetProperty("MemTotal").GetInt64() < ContainerMemoryBytes)
throw new InvalidOperationException("Existing Docker resources cannot fit this bounded 2-CPU/6-GiB diagnostic; no infrastructure change was requested.");
}
await Command("sh", ["-c", "cat /proc/meminfo; printf '\n[cgroup]\n'; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true; printf '\n[workspace disk]\n'; df -Pk ."], output, "runner-resources", deadline.Token);
var available = System.Text.RegularExpressions.Regex.Match(File.ReadAllText("/proc/meminfo"), @"(?m)^MemAvailable:\s+(\d+) kB$");
if (!available.Success || long.Parse(available.Groups[1].Value) < 5L * 1024 * 1024)
throw new InvalidOperationException("Existing runner memory has less than the 5-GiB available diagnostic budget; no infrastructure change was requested.");
var source = Path.Combine(work, "dockur");
await Command("git", ["clone", "--no-checkout", "https://github.com/dockur/macos.git", source], output, "dockur-clone", deadline.Token);
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
var actualCommit = (await Command("git", ["-C", source, "rev-parse", "HEAD"], output, "dockur-commit", deadline.Token)).Output.Trim();
if (actualCommit != DockurCommit) throw new InvalidOperationException("Dockur source pin mismatch.");
await PrepareSource(source, output, token, true, deadline.Token);
await Command("docker", ["build", "--platform", "linux/amd64", "--label", OwnerLabel + "=" + token, "--tag", state.ImageTag, source], output, "docker-build", deadline.Token, echo: true);
var imageInspect = await Command("docker", ["image", "inspect", state.ImageTag], output, "image-inspect", deadline.Token);
using (var image = JsonDocument.Parse(imageInspect.Output))
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
Save(statePath, state);
var create = await Command("docker", ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--env", "KVM=N", "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=14", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2", state.ImageTag], output, "docker-create", deadline.Token);
var id = create.Output.Trim();
if (!System.Text.RegularExpressions.Regex.IsMatch(id, "^[0-9a-f]{64}$")) throw new InvalidOperationException("Docker did not return a container identity.");
state = state with { ContainerId = id };
Save(statePath, state);
await Command("docker", ["inspect", id], output, "container-created", deadline.Token);
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
Console.WriteLine("The owned unprivileged TCG guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run.");
var recoveryStarted = Stopwatch.StartNew();
var heartbeat = Stopwatch.StartNew();
while (true)
{
deadline.Token.ThrowIfCancellationRequested();
await CaptureGuest(id, output, deadline.Token);
var resultPath = Path.Combine(output, "guest-result.json");
if (File.Exists(resultPath))
{
var result = File.ReadAllText(resultPath);
ValidateResult(result, token);
Console.WriteLine("Native Recovery readiness passed. This run has not installed macOS, .NET, CLT, or run Meeting Assistant tests.");
outcome = "readiness-passed";
break;
}
var running = await Command("docker", ["inspect", "--format", "{{.State.Running}}", id], output, "container-running", deadline.Token);
if (running.Output.Trim() != "true") throw new InvalidOperationException("Guest container exited before a native readiness result.");
if (heartbeat.Elapsed >= TimeSpan.FromSeconds(60))
{
Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1} minutes; container=running; readiness=pending");
heartbeat.Restart();
}
await Task.Delay(TimeSpan.FromSeconds(20), deadline.Token);
}
}
catch (Exception exception)
{
error = exception is OperationCanceledException ? "The explicit 40-minute diagnostic deadline or cancellation was reached." : exception.Message;
Console.Error.WriteLine(error);
}
finally
{
Console.CancelKeyPress -= cancelHandler;
using var captureDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(45));
try { await CaptureGuest(state.ContainerId ?? state.ContainerName, output, captureDeadline.Token, true, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
try { PrintGuestProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Native proof output: " + exception.Message); }
var clean = await Cleanup(output);
if (!clean) { outcome = "failed"; error = (error ?? "") + " Owned-resource cleanup failed; inspect cleanup evidence."; }
Save(Path.Combine(output, "outcome.json"), new { token, outcome, error, completedUtc = DateTimeOffset.UtcNow });
}
return outcome == "readiness-passed" ? 0 : 1;
}
static string? Option(string[] args, string name)
{
var index = Array.IndexOf(args, name);
return index < 0 ? null : index + 1 < args.Length ? args[index + 1] : throw new ArgumentException("Missing value for " + name);
}
static void ValidateContracts()
{
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-readiness.sh"))) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
throw new InvalidOperationException("Bootstrap A/B requires the unchanged 40281b readiness probe and limits.");
XDocument.Parse(DiagnosticDaemon);
if (Encoding.UTF8.GetByteCount(DiagnosticDaemon) > Encoding.UTF8.GetByteCount(OriginalDaemon + "\n")) throw new InvalidOperationException("Daemon replacement exceeds original file.");
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "14.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
ValidateResult(good, "validation");
foreach (var invalid in new[] { good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
{
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
}
}
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
{
Directory.CreateDirectory(output);
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.");
var patch = ReplaceOnce(originalPatch, OriginalBootstrap, MountOnlyBootstrap);
var oldConstants = "RECOVERY_ORIGINAL = b\"/usr/libexec/recoveryosd\"\nRECOVERY_REPLACEMENT = b\"/private/etc/rc.cdrom.sh\"";
var daemon = OriginalDaemon + "\n";
var constants = "RECOVERY_ORIGINAL = b'''" + daemon + "'''\nRECOVERY_REPLACEMENT = b'''" + DiagnosticDaemon + "'''.ljust(len(RECOVERY_ORIGINAL), b\" \")";
patch = ReplaceOnce(patch, oldConstants, constants);
var dockerPath = Path.Combine(source, "Dockerfile");
// The existing runner's BuildKit cannot checksum dangling manpage links during COPY /.
// This pinned filesystem image has an empty Config; FROM preserves the same runtime defaults.
var dockerfile = ReplaceOnce(File.ReadAllText(dockerPath), "FROM scratch AS base\nCOPY --from=qemux/qemu:7.50 --exclude=usr/bin/qemu-system-x86_64 / /\n", "FROM qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df AS base\n");
dockerfile = ReplaceAllExact(dockerfile, "--from=qemux/qemu-macos:latest ", "--from=qemux/qemu-macos:latest@sha256:af64297171228f27d5f616249e18f6ad5e2fbc79c1cc517252521e8bcd8eadaa ", 2);
var entryPath = Path.Combine(source, "src/entry.sh");
var entry = ReplaceOnce(File.ReadAllText(entryPath), "set -Eeuo pipefail\n", "set -Eeuo pipefail\n\n# Diagnostic budget: inspect existing Docker storage before Recovery download/boot.\ndf -Pk /storage\nfree_kib=$(df -Pk /storage | awk 'NR==2 {print $4}')\n[[ \"$free_kib\" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || { echo 'Existing Docker storage has less than the 8-GiB diagnostic budget.' >&2; exit 1; }\n");
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"));
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, " 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");
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", wrapper), ("guest-readiness.sh", hook), ("image.sh.patched", image), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("early-bootstrap.sh", MountOnlyBootstrap) })
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
Save(Path.Combine(output, "source-hashes.json"), Directory.GetFiles(output).Where(path => Path.GetFileName(path) is "recovery-patch.py" or "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "guest-readiness.sh" or "image.sh.patched" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "early-bootstrap.sh").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
await Command("bash", ["-n", Path.Combine(output, "guest-launch.sh")], output, "guest-hook-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "guest-readiness.sh")], output, "guest-readiness-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "image.sh.patched")], output, "guest-staging-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "container-entry.sh")], output, "entry-syntax", cancellation);
if (!writeSource) return;
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
File.WriteAllText(dockerPath, dockerfile, new UTF8Encoding(false));
File.WriteAllText(entryPath, entry, new UTF8Encoding(false));
File.WriteAllText(imagePath, image, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/launch.sh"), wrapper, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/readiness.sh"), hook, new UTF8Encoding(false));
}
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
{
var count = text.Split(oldValue, StringSplitOptions.None).Length - 1;
if (count != expected) throw new InvalidOperationException($"Pinned source contract expected {expected} match(es), found {count}: {oldValue.Split('\n')[0]}");
return text.Replace(oldValue, newValue, StringComparison.Ordinal);
}
static void ValidateResult(string json, string token)
{
using var document = JsonDocument.Parse(json);
var result = document.RootElement;
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || result.GetProperty("uid").GetInt32() != 0 || !System.Text.RegularExpressions.Regex.IsMatch(result.GetProperty("disk").GetString() ?? "", "^/dev/disk[0-9]+$") || result.GetProperty("diskBytes").GetInt64() != GuestDiskBytes || result.GetProperty("readOnly").GetBoolean() || new[] { "systemExit", "diskArbitrationExit", "recoveryExit", "diskListExit" }.Any(key => result.GetProperty(key).GetInt32() != 0))
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 14+/x86_64, service readiness and the writable 64-GiB disk.");
}
static void AssertContainer(string json, string token)
{
using var document = JsonDocument.Parse(json);
var container = document.RootElement[0];
var config = container.GetProperty("HostConfig");
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token || config.GetProperty("Privileged").GetBoolean() || config.GetProperty("NetworkMode").GetString() != "default" && config.GetProperty("NetworkMode").GetString() != "bridge" || config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes || new[] { "CapAdd", "Devices", "DeviceRequests", "Binds", "PortBindings" }.Any(key => config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null && (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any())))
throw new InvalidOperationException("Created container exceeds the owned/unprivileged diagnostic boundary.");
}
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool final = false, string? token = null)
{
if (final && token is not null) await CaptureMonitor(id, output, token, cancellation);
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
foreach (var file in new[] { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") })
{
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
}
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test ! -f /storage/14/setup.dmg || sha256sum /storage/14/setup.dmg; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false);
}
static async Task CaptureMonitor(string id, string output, string token, CancellationToken cancellation)
{
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
deadline.CancelAfter(TimeSpan.FromSeconds(10));
int? monitorExit = null, copyExit = null;
string? error = null;
try
{
if (!System.Text.RegularExpressions.Regex.IsMatch(id, "^[0-9a-f]{64}$") || !System.Text.RegularExpressions.Regex.IsMatch(token, "^[0-9a-f]{32}$")) throw new InvalidOperationException("No saved owned container identity for the optional monitor capture.");
var inspection = await Command("docker", ["inspect", id], output, "capture-monitor-container", deadline.Token);
AssertContainer(inspection.Output, token);
using (var document = JsonDocument.Parse(inspection.Output))
if (document.RootElement[0].GetProperty("Id").GetString() != id || !document.RootElement[0].GetProperty("State").GetProperty("Running").GetBoolean()) throw new InvalidOperationException("Owned guest container is no longer running for the optional monitor capture.");
var screen = "/tmp/native-diagnostic-screen-" + token + ".ppm";
var monitor = await Command("docker", ["exec", id, "sh", "-c", """
test -S /run/shm/monitor.sock || exit 1
rm -f -- "$1" || exit 1
printf 'info status\nscreendump %s\n' "$1" | /usr/bin/timeout -s KILL 5 /usr/bin/nc.openbsd -q 1 -w 2 -U /run/shm/monitor.sock
monitor_exit=$?
printf '\n[monitor-exit] %s\n' "$monitor_exit"
[ "$monitor_exit" -eq 0 ] || exit "$monitor_exit"
bytes=$(stat -c%s "$1") || exit 1
[ "$bytes" -gt 0 ] && [ "$bytes" -le 8388608 ] || exit 1
printf '[screen-bytes] %s\n' "$bytes"
""", "native-monitor", screen], output, "capture-monitor", deadline.Token, requireSuccess: false);
monitorExit = monitor.ExitCode;
if (monitorExit != 0) throw new InvalidOperationException("Optional monitor status/screenshot command exited " + monitorExit + ".");
var copy = await Command("docker", ["cp", id + ":" + screen, Path.Combine(output, "guest-screen-" + token + ".ppm")], output, "capture-monitor-screen", deadline.Token, requireSuccess: false);
copyExit = copy.ExitCode;
if (copyExit != 0) throw new InvalidOperationException("Optional monitor screenshot copy exited " + copyExit + ".");
}
catch (Exception exception) { error = exception.Message; Console.Error.WriteLine("Optional final monitor capture: " + error); }
finally { Save(Path.Combine(output, "monitor-capture.json"), new { token, monitorExit, copyExit, success = error is null, error, capturedUtc = DateTimeOffset.UtcNow }); }
}
static async Task<bool> Cleanup(string output)
{
var path = Path.Combine(output, "owned-resources.json");
if (!File.Exists(path)) return true;
var state = JsonSerializer.Deserialize<OwnedResources>(File.ReadAllText(path), JsonOptions) ?? throw new InvalidOperationException("Invalid owned-resource receipt.");
if (!System.Text.RegularExpressions.Regex.IsMatch(state.Token, "^[0-9a-f]{32}$") || state.ContainerName != "meeting-assistant-native-" + state.Token || state.ImageTag != "meeting-assistant-native-diagnostic:" + state.Token) throw new InvalidOperationException("Invalid cleanup ownership identity.");
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(90));
try
{
foreach (var kind in new[] { "container", "image" })
{
var name = kind == "container" ? state.ContainerName : state.ImageTag;
var inspect = await Command("docker", [kind, "inspect", name], output, "cleanup-" + kind + "-inspect", deadline.Token, requireSuccess: false);
if (inspect.ExitCode != 0)
{
if (inspect.Error.Contains("No such object", StringComparison.Ordinal) || inspect.Error.Contains("No such container", StringComparison.Ordinal) || inspect.Error.Contains("No such image", StringComparison.Ordinal)) continue;
throw new InvalidOperationException("Cannot establish owned " + kind + " absence: " + inspect.Error);
}
using var document = JsonDocument.Parse(inspect.Output);
var resource = document.RootElement[0];
if (resource.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != state.Token) throw new InvalidOperationException("Cleanup refuses a resource without this run's exact ownership label.");
var id = resource.GetProperty("Id").GetString()!;
var expectedId = kind == "container" ? state.ContainerId : state.ImageId;
if (expectedId is not null && expectedId != id) throw new InvalidOperationException("Cleanup refuses a resource whose ID changed after creation.");
await Command("docker", kind == "container" ? ["rm", "--force", "--volumes", id] : ["image", "rm", id], output, "cleanup-" + kind + "-remove", deadline.Token);
}
if (Path.GetFileName(state.WorkDirectory) == "meeting-assistant-native-" + state.Token && File.Exists(Path.Combine(state.WorkDirectory, "run.owner")) && File.ReadAllText(Path.Combine(state.WorkDirectory, "run.owner")) == state.Token) Directory.Delete(state.WorkDirectory, true);
Save(Path.Combine(output, "cleanup.json"), new { state.Token, success = true, completedUtc = DateTimeOffset.UtcNow });
return true;
}
catch (Exception exception)
{
Save(Path.Combine(output, "cleanup.json"), new { state.Token, success = false, error = exception.Message, completedUtc = DateTimeOffset.UtcNow });
Console.Error.WriteLine("Owned diagnostic cleanup failed: " + exception.Message);
return false;
}
}
static async Task<CommandResult> Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false)
{
if (!label.StartsWith("capture-", StringComparison.Ordinal) && label is not "container" and not "container-running" and not "guest-container-resources")
Console.WriteLine("[native-diagnostic] " + label);
using var commandCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
var commandToken = commandCancellation.Token;
var start = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false };
foreach (var argument in arguments) start.ArgumentList.Add(argument);
start.Environment["GIT_TERMINAL_PROMPT"] = "0";
using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start " + executable);
async Task<string> Read(StreamReader reader, string stream)
{
var captured = new StringBuilder();
var buffer = new char[8192];
using var log = new StreamWriter(Path.Combine(output, label + "." + stream + ".log"), false, new UTF8Encoding(false));
while (true)
{
var count = await reader.ReadAsync(buffer.AsMemory(), commandToken);
if (count == 0) break;
if (captured.Length + count > MaximumCapturedCharacters)
{
commandCancellation.Cancel();
throw new InvalidOperationException(label + " exceeded its bounded diagnostic log size.");
}
captured.Append(buffer, 0, count);
await log.WriteAsync(buffer.AsMemory(0, count), commandToken);
await log.FlushAsync(commandToken);
if (echo) Console.Write(new string(buffer, 0, count));
}
return captured.ToString();
}
var stdout = Read(process.StandardOutput, "stdout");
var stderr = Read(process.StandardError, "stderr");
try
{
await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(commandToken));
var result = new CommandResult(process.ExitCode, await stdout, await stderr);
if (requireSuccess && result.ExitCode != 0) throw new InvalidOperationException($"{label} exited {result.ExitCode}: {result.Error[..Math.Min(result.Error.Length, 1500)]}");
return result;
}
catch
{
try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { }
throw;
}
}
static string Hash(byte[] bytes) => Convert.ToHexStringLower(SHA256.HashData(bytes));
static void PrintGuestProof(string output, string token)
{
var path = Path.Combine(output, "guest-proof.log");
if (!File.Exists(path)) { Console.WriteLine("[native-diagnostic] No native guest proof was captured."); return; }
var proof = File.ReadAllText(path).Replace(token, "<run-id>", StringComparison.Ordinal);
const int budget = 512 * 1024;
if (proof.Length > budget)
proof = proof[..(64 * 1024)] + "\n[native-diagnostic] Middle of proof omitted from CI stdout; complete bounded proof is retained in the artifact.\n" + proof[^((budget - 64 * 1024))..];
Console.WriteLine("[native-diagnostic] Final native guest proof:");
Console.Write(proof);
}
static void Save(string path, object value)
{
var temporary = path + ".tmp";
File.WriteAllText(temporary, JsonSerializer.Serialize(value, JsonOptions), new UTF8Encoding(false));
File.Move(temporary, path, overwrite: true);
}
sealed record OwnedResources(string Token, string ContainerName, string ImageTag, string WorkDirectory, string? ContainerId = null, string? ImageId = null);
sealed record CommandResult(int ExitCode, string Output, string Error);
}
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Apple Recovery has Bash before any SDK is installed. Preserve the original
# daemon under its launchd label/PID while the unchanged read-only probe runs.
/bin/bash /Volumes/installstate/readiness.sh &
exec /usr/libexec/recoveryosd
+250
View File
@@ -0,0 +1,250 @@
#!/bin/bash
# Existing macOS Recovery/launchd runtime hook; never installs or erases anything.
set -u
PATH="/usr/bin:/bin:/usr/sbin:/sbin"
export PATH
PROOF_TOKEN="@@PROOF_TOKEN@@"
STATE_DIR="/Volumes/installstate"
PROOF_LOG="$STATE_DIR/proof.log"
RESULT="$STATE_DIR/result.json"
EXPECTED_BYTES=68719476736
MAX_LOG_BYTES=4194304
MAX_OUTPUT_BYTES=524288
TIMER_FIFO="/tmp/native-diagnostic-$PROOF_TOKEN-$$.fifo"
PENDING_OUTPUTS=()
ACTIVE_COMMAND=""
ACTIVE_TIMER=""
os_version=""
architecture=""
uid=-1
system_exit=-1
arbitration_exit=-1
recovery_exit=-1
disk_list_exit=-1
selected_disk=""
disk_bytes=0
count=0
while [ ! -d "$STATE_DIR" ] && (( count < 120 )); do
/sbin/mount_9p installstate >/dev/null 2>&1 || :
count=$((count + 1))
sleep 1
done
[ -d "$STATE_DIR" ] || exit 1
: > "$PROOF_LOG" || exit 1
exec 3>> "$PROOF_LOG" || exit 1
rm -f "$RESULT" "$RESULT.tmp"
printf '[proof-token] %s\n' "$PROOF_TOKEN" >&3
finish() {
local success="$1" reason="$2"
flush_outputs || { success=false; reason=diagnostic_log_budget_exceeded; }
printf '[proof-result] %s: %s\n' "$success" "$reason" >&3
printf '{"token":"%s","success":%s,"reason":"%s","osVersion":"%s","architecture":"%s","uid":%s,"systemExit":%s,"diskArbitrationExit":%s,"recoveryExit":%s,"diskListExit":%s,"disk":"%s","diskBytes":%s,"readOnly":false}\n' \
"$PROOF_TOKEN" "$success" "$reason" "$os_version" "$architecture" "$uid" \
"$system_exit" "$arbitration_exit" "$recovery_exit" "$disk_list_exit" \
"$selected_disk" "$disk_bytes" > "$RESULT.tmp"
/bin/mv -f "$RESULT.tmp" "$RESULT" || exit 1
exec 9>&-
[ ! -p "$TIMER_FIFO" ] || /bin/rm -f "$TIMER_FIFO"
# Keep the service alive for the bounded host diagnostic to capture evidence.
while :; do sleep 60; done
}
init_timer_fifo() {
# Recovery has Bash 3.2 before any SDK is installed. Its read timeout uses
# alarm(), avoiding a separate sleep process for every command and grace period.
[ ! -e "$TIMER_FIFO" ] || exit 1
/usr/bin/mkfifo -m 600 "$TIMER_FIFO" || exit 1
exec 9<> "$TIMER_FIFO" || exit 1
}
flush_outputs() {
(( ${#PENDING_OUTPUTS[@]} > 0 )) || return 0
local started=$SECONDS sizes="/tmp/native-diagnostic-$$.sizes" proof_size output_size raw_size
local raw_count=0 raw_valid=1 pending_count=${#PENDING_OUTPUTS[@]}
local bounded="/tmp/native-diagnostic-$$.flush"
# One bounded native copy per group, rather than tail/stat startup per command.
# Keep native byte-oriented copying: Bash 3.2 read -n would read large outputs
# one byte per system call. Small scalar reads below have a separate tight bound.
/usr/bin/tail -c "$MAX_OUTPUT_BYTES" "${PENDING_OUTPUTS[@]}" > "$bounded" || return 1
/usr/bin/stat -f '%z' "$PROOF_LOG" "$bounded" "${PENDING_OUTPUTS[@]}" > "$sizes" || return 1
{
IFS= read -r proof_size; IFS= read -r output_size
while IFS= read -r raw_size; do
raw_count=$((raw_count + 1))
[[ "$raw_size" =~ ^[0-9]+$ ]] && (( raw_size <= MAX_OUTPUT_BYTES )) || raw_valid=0
done
} < "$sizes"
PENDING_OUTPUTS=()
[[ "$proof_size" =~ ^[0-9]+$ && "$output_size" =~ ^[0-9]+$ ]] || return 1
(( raw_valid == 1 && raw_count == pending_count )) || return 1
(( proof_size + output_size + 1024 <= MAX_LOG_BYTES )) || return 1
/bin/cat "$bounded" >&3 || return 1
printf '\n[proof-flush] outputs-bytes=%s elapsed=%ss\n' "$output_size" "$((SECONDS - started))" >&3
}
read_scalar() {
local value status
# All three values are short native machine/uid/version scalars. Reject excess
# content instead of accepting a truncated first line as a successful gate.
IFS= read -r -n 65 -d '' value < "$LAST_OUTPUT"; status=$?
# EOF is mandatory: the byte bound or a NUL delimiter must never hide a suffix.
(( status == 1 && ${#value} < 65 )) || return 1
value=${value%$'\n'}
[[ "$value" != *$'\n'* ]] || return 1
SCALAR="$value"
}
cancel_probe() {
trap '' TERM INT
if [ -n "$ACTIVE_COMMAND" ]; then
kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$ACTIVE_COMMAND" 2>/dev/null || :
wait "$ACTIVE_COMMAND" 2>/dev/null || :
fi
[ -z "$ACTIVE_TIMER" ] || { kill -TERM "$ACTIVE_TIMER" 2>/dev/null || :; wait "$ACTIVE_TIMER" 2>/dev/null || :; }
ACTIVE_COMMAND=""; ACTIVE_TIMER=""
finish false probe_cancelled
}
run_command() {
local name="$1"
shift
local process timer exit_code started waited command_limit=45
# Run 4161: even native uname/ps startup took 34-42s under TCG.
# Isolate only the failed UID gate; every other watchdog remains unchanged.
[[ "$name" != uid ]] || command_limit=180
LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
printf '\n[proof-command] %s:' "$name" >&3
printf ' %s' "$@" >&3
printf '\n' >&3
started=$SECONDS
"$@" > "$LAST_OUTPUT" 2>&1 &
process=$!
ACTIVE_COMMAND="$process"
printf '[proof-start] %s child=%s shell=%s parent=%s seconds=%s\n' "$name" "$process" "$$" "$PPID" "$started" >&3
printf '[proof-limit] %s %ss\n' "$name" "$command_limit" >&3
(
trap 'exit 0' TERM INT
IFS= read -r -t "$command_limit" -u 9 unused || :
printf '[proof-timeout] %s child=%s elapsed=%ss signal=TERM\n' "$name" "$process" "$((SECONDS - started))" >&3
kill -TERM "$process" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$process" 2>/dev/null || :
) &
timer=$!
ACTIVE_TIMER="$timer"
wait "$process"
exit_code=$?
waited=$SECONDS
# Includes fork/exec/wait, but excludes timer cleanup and evidence copying.
printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3
kill -TERM "$timer" 2>/dev/null || :
wait "$timer" 2>/dev/null || :
ACTIVE_COMMAND=""; ACTIVE_TIMER=""
printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3
printf '[proof-exit] %s\n' "$exit_code" >&3
LAST_EXIT="$exit_code"
PENDING_OUTPUTS+=("$LAST_OUTPUT")
return 0
}
diagnose_failure() {
run_command kernel /usr/bin/uname -a
run_command account /usr/bin/id
run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features
run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm=
run_command processes /bin/ps -axo pid,ppid,comm
}
fail_probe() {
local reason="$1"
flush_outputs || finish false diagnostic_log_budget_exceeded
diagnose_failure
finish false "$reason"
}
init_timer_fifo
trap cancel_probe TERM INT
# Test the required native gates before optional process/CPU diagnostics.
run_command architecture /usr/bin/uname -m
(( LAST_EXIT == 0 )) || fail_probe architecture_probe_failed
read_scalar || fail_probe architecture_output_invalid
architecture="$SCALAR"
[ "$architecture" = x86_64 ] || fail_probe unexpected_guest_architecture
run_command uid /usr/bin/id -u
(( LAST_EXIT == 0 )) || fail_probe uid_probe_failed
read_scalar || fail_probe uid_output_invalid
uid="$SCALAR"
[ "$uid" = 0 ] || fail_probe recovery_account_not_root
run_command platform /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
flush_outputs || finish false diagnostic_log_budget_exceeded
if (( platform_exit != 0 )); then
run_command system /bin/launchctl print system
system_exit="$LAST_EXIT"
run_command arbitration /bin/launchctl print system/com.apple.diskarbitrationd
arbitration_exit="$LAST_EXIT"
run_command recovery /bin/launchctl print system/com.apple.recoveryosd
recovery_exit="$LAST_EXIT"
printf '[proof-retry] sw_vers once after native service context; same 45-second deadline\n' >&3
run_command platform-warm /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
fi
(( platform_exit == 0 )) || fail_probe sw_vers_failed
run_command version /usr/bin/sw_vers -productVersion
(( LAST_EXIT == 0 )) || fail_probe product_version_failed
read_scalar || fail_probe product_version_invalid
os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded
# Bound readiness independently of the host's 40-minute overall deadline.
readiness_start=$SECONDS
attempt=0
while (( SECONDS - readiness_start < 600 )); do
attempt=$((attempt + 1))
printf '\n[readiness-attempt] %s\n' "$attempt" >&3
run_command disks /usr/sbin/diskutil list physical
disk_list_exit="$LAST_EXIT"
if (( disk_list_exit == 0 )); then
disk_list=$(cat "$LAST_OUTPUT")
candidates=0
while IFS= read -r disk; do
[ -n "$disk" ] || continue
run_command "info-$disk" /usr/sbin/diskutil info "/dev/$disk"
(( LAST_EXIT == 0 )) || continue
info=$(cat "$LAST_OUTPUT")
if printf '%s\n' "$info" | grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*Yes'; then
continue
fi
printf '%s\n' "$info" | grep -Eq '^[[:space:]]*(Read-Only (Media|Device)|(Media|Device) Read-Only):[[:space:]]*No' || continue
size=$(printf '%s\n' "$info" | sed -nE 's/^[[:space:]]*Disk Size:.*\(([0-9]+) Bytes\).*/\1/p' | head -n 1)
[[ "$size" =~ ^[0-9]+$ ]] || continue
(( size == EXPECTED_BYTES )) || continue
candidates=$((candidates + 1))
selected_disk="/dev/$disk"
disk_bytes="$size"
printf '[writable-target] %s %s bytes\n' "$selected_disk" "$disk_bytes" >&3
done < <(printf '%s\n' "$disk_list" | sed -nE 's#^/dev/(disk[0-9]+).*#\1#p')
(( candidates <= 1 )) || fail_probe ambiguous_writable_64g_disks
if (( candidates == 1 )); then
# Re-probe live launchd domains after disk readiness, preserving native exits.
run_command system_ready /bin/launchctl print system
system_exit="$LAST_EXIT"
run_command arbitration_ready /bin/launchctl print system/com.apple.diskarbitrationd
arbitration_exit="$LAST_EXIT"
run_command recovery_ready /bin/launchctl print system/com.apple.recoveryosd
recovery_exit="$LAST_EXIT"
(( system_exit == 0 && arbitration_exit == 0 && recovery_exit == 0 )) || fail_probe service_domain_not_ready
finish true native_recovery_and_writable_64g_disk_ready
fi
fi
flush_outputs || finish false diagnostic_log_budget_exceeded
IFS= read -r -t 5 -u 9 unused || :
done
fail_probe disk_management_or_writable_target_not_ready