ci: add gated native macOS guest build and tests on Ubuntu

This commit is contained in:
dh
2026-10-03 15:32:32 +02:00
parent 9a91a81992
commit db6b1b58e0
6 changed files with 1058 additions and 17 deletions
+344 -17
View File
@@ -16,6 +16,9 @@ static class NativeDiagnostic
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
const int MaximumCapturedCharacters = 8 * 1024 * 1024;
const string SdkVersion = "10.0.401";
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
const string FullState = "/storage/14/ci-state";
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";
@@ -47,15 +50,20 @@ static class NativeDiagnostic
{
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]");
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--full] [--output artifacts/native-macos] [--source existing-dockur-clone] [--compression-chunk readonly-qualified-chunk]");
return 0;
}
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
var full = args.Contains("--full");
if (args.Contains("--validate"))
{
ValidateContracts();
if (full) ValidateFullContracts();
if (Option(args, "--source") is { } source)
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full);
if (full) await ValidateDiskSerialParser(output);
if (Option(args, "--compression-chunk") is { } chunk)
await ValidateCompression(Path.GetFullPath(chunk), output);
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
return 0;
}
@@ -71,7 +79,9 @@ static class NativeDiagnostic
Save(statePath, state);
Directory.CreateDirectory(work);
File.WriteAllText(Path.Combine(work, "run.owner"), token);
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(40));
// Full execution leaves eight minutes within the existing 180-minute job for evidence/cleanup.
var deadlineMinutes = full ? 172 : 40;
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(deadlineMinutes));
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;
@@ -83,7 +93,7 @@ static class NativeDiagnostic
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 });
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, mode = full ? "full" : "readiness" });
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
using (var document = JsonDocument.Parse(info.Output))
{
@@ -102,13 +112,17 @@ static class NativeDiagnostic
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);
if (full) await PreparePayload(source, output, token, sourceCommit, deadline.Token);
await PrepareSource(source, output, token, true, deadline.Token, full);
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);
List<string> createArguments = ["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"];
if (full) createArguments.AddRange(["--env", "ALLOCATE=N", "--env", "DISK_OPTIONS=serial=" + DiskSerial(token)]);
createArguments.Add(state.ImageTag);
var create = await Command("docker", createArguments.ToArray(), 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 };
@@ -116,19 +130,58 @@ static class NativeDiagnostic
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.");
Console.WriteLine(full ? "The owned unprivileged TCG guest is starting. Installation requires a fresh native readiness receipt and an explicit owned-disk permit; success requires all 577 tests with zero skips." : "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 phaseStarted = Stopwatch.StartNew();
var phase = "recovery";
var phaseBudget = TimeSpan.FromMinutes(40);
var permitted = false;
while (true)
{
deadline.Token.ThrowIfCancellationRequested();
await CaptureGuest(id, output, deadline.Token);
await CaptureGuest(id, output, deadline.Token, full);
if (full && phaseStarted.Elapsed > phaseBudget)
throw new InvalidOperationException("The bounded native " + phase + " phase exceeded " + phaseBudget.TotalMinutes + " minutes.");
var resultPath = Path.Combine(output, "guest-result.json");
if (File.Exists(resultPath))
if (File.Exists(resultPath) && !permitted)
{
var result = File.ReadAllText(resultPath);
ValidateResult(result, token);
if (full)
{
await PermitInstallation(id, output, token, sourceCommit, result, deadline.Token);
permitted = true;
phase = "installation";
phaseBudget = TimeSpan.FromMinutes(80);
phaseStarted.Restart();
}
else
{
Console.WriteLine("Native Recovery readiness passed. This run has not installed macOS, .NET, CLT, or run Meeting Assistant tests.");
outcome = "readiness-passed";
break;
}
}
if (full && permitted)
{
var phasePath = Path.Combine(output, "guest-phase.json");
if (File.Exists(phasePath))
{
using var nativePhase = JsonDocument.Parse(File.ReadAllText(phasePath));
if (nativePhase.RootElement.GetProperty("token").GetString() != token) throw new InvalidOperationException("Stale native phase receipt.");
var current = nativePhase.RootElement.GetProperty("phase").GetString();
var next = current == "toolchain-installing" ? "toolchain" : current is "tests-running" or "tests-passed" ? "tests" : phase;
if (next != phase) { phase = next; phaseBudget = TimeSpan.FromMinutes(next == "toolchain" ? 30 : 25); phaseStarted.Restart(); Console.WriteLine("[native-diagnostic] phase: " + phase); }
if (current is "tests-failed" or "bootstrap-failed" or "installation-failed") throw new InvalidOperationException("Guest phase failed: " + current);
}
var fullResult = Path.Combine(output, "full-result.json");
if (File.Exists(fullResult))
{
ValidateFullResult(File.ReadAllText(fullResult), token, sourceCommit, File.ReadAllText(Path.Combine(output, "archive.sha256")).Trim());
ValidateTrx(File.ReadAllBytes(Path.Combine(output, "native.trx")), File.ReadAllText(fullResult));
outcome = "native-tests-passed";
Console.WriteLine("Native macOS 577/577 tests passed, including all five native tests, with fresh Mach-O/x86_64 and codesign evidence.");
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.");
@@ -137,20 +190,21 @@ static class NativeDiagnostic
}
catch (Exception exception)
{
error = exception is OperationCanceledException ? "The explicit 40-minute diagnostic deadline or cancellation was reached." : exception.Message;
error = exception is OperationCanceledException ? $"The explicit {deadlineMinutes}-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.ContainerName, output, captureDeadline.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); }
try { await CaptureGuest(state.ContainerName, output, captureDeadline.Token, full, true); } 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); }
if (full) try { PrintFullProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Full 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;
return outcome is "readiness-passed" or "native-tests-passed" ? 0 : 1;
}
static string? Option(string[] args, string name)
@@ -172,7 +226,7 @@ static class NativeDiagnostic
}
}
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
{
Directory.CreateDirectory(output);
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
@@ -184,12 +238,21 @@ static class NativeDiagnostic
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");
var dockerfile = ReplaceOnce(File.ReadAllText(dockerPath), "--from=qemux/qemu:7.50 ", "--from=qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df ");
// The actual remote BuildKit cannot checksum a dangling symlink during root COPY.
// Start directly from the same immutable image filesystem; its inspected Config is empty.
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);
if (full)
{
await PrepareFullSource(source, output, token, writeSource, cancellation);
hook = CreateFullReadiness(hook);
dockerfile += "\n# Payload stays inside this image and its owned anonymous storage volume.\nCOPY ci-payload/ /assets/ci-payload/\n";
entry = ReplaceOnce(entry, "free_kib >= 8 * 1024 * 1024", "free_kib >= 32 * 1024 * 1024").Replace("8-GiB diagnostic budget", "32-GiB full-run budget", StringComparison.Ordinal);
}
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", hook), ("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 "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "early-bootstrap.sh").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
@@ -203,6 +266,245 @@ static class NativeDiagnostic
}
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
static string DiskSerial(string token) => token[..20];
static async Task PreparePayload(string source, string output, string token, string commit, CancellationToken cancellation)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(commit, "^[0-9a-f]{40}$")) throw new InvalidOperationException("Candidate commit is not an exact Git SHA.");
var status = await Command("git", ["status", "--porcelain", "--untracked-files=all"], output, "source-cleanliness", cancellation);
if (!string.IsNullOrEmpty(status.Output)) throw new InvalidOperationException("Full CI requires a clean exact HEAD; commit the reviewable candidate before running it.");
var payload = Path.Combine(source, "ci-payload");
Directory.CreateDirectory(payload);
var archive = Path.Combine(payload, "source.tar");
await Command("git", ["archive", "--format=tar", "--output", archive, commit], output, "source-archive", cancellation);
var archiveHash = Hash(File.ReadAllBytes(archive));
File.WriteAllText(Path.Combine(output, "archive.sha256"), archiveHash + "\n");
File.WriteAllText(Path.Combine(payload, "source.commit"), commit + "\n");
Save(Path.Combine(payload, "payload.json"), new { runToken = token, sourceCommit = commit, archiveSha256 = archiveHash, sdkVersion = SdkVersion, sdkSha512 = SdkSha512, expectedTests = 577 });
File.Copy(Path.Combine(payload, "payload.json"), Path.Combine(output, "payload.json"));
foreach (var pair in new[] { ("MacOsNativeGuest.cs", "MacOsNativeGuest.cs"), ("macos-native-firstboot.sh", "native-firstboot-bootstrap.sh"), ("macos-native-disk-guard.sh", "macos-native-disk-guard.sh") })
File.Copy(Path.Combine("tools", "ci", pair.Item1), Path.Combine(payload, pair.Item2));
Console.WriteLine("[native-diagnostic] pinned-sdk-download");
using var downloadDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
downloadDeadline.CancelAfter(TimeSpan.FromMinutes(15));
using var client = new HttpClient { Timeout = Timeout.InfiniteTimeSpan };
using var response = await client.GetAsync($"https://builds.dotnet.microsoft.com/dotnet/Sdk/{SdkVersion}/dotnet-sdk-{SdkVersion}-osx-x64.tar.gz", HttpCompletionOption.ResponseHeadersRead, downloadDeadline.Token);
response.EnsureSuccessStatusCode();
var sdkPath = Path.Combine(payload, "sdk.tar.gz");
await using (var sdk = File.Create(sdkPath))
await using (var stream = await response.Content.ReadAsStreamAsync(downloadDeadline.Token))
await stream.CopyToAsync(sdk, downloadDeadline.Token);
await using (var sdk = File.OpenRead(sdkPath))
if (Convert.ToHexStringLower(await SHA512.HashDataAsync(sdk, downloadDeadline.Token)) != SdkSha512) throw new InvalidOperationException("Official macOS/x64 SDK SHA-512 mismatch.");
Save(Path.Combine(output, "payload-hashes.json"), Directory.GetFiles(payload).ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
}
static string ReadPinned(string source, string path, string expected)
{
var bytes = File.ReadAllBytes(Path.Combine(source, path));
if (Hash(bytes) != expected) throw new InvalidOperationException("Pinned full-install source hash mismatch: " + path);
return Encoding.UTF8.GetString(bytes);
}
static string CreateFullReadiness(string hook) => ReplaceOnce(hook,
" # Keep the service alive for the bounded host diagnostic to capture evidence.\n while :; do sleep 60; done",
"""
# Full mode waits for the host's independently validated fresh owned-disk permit.
if [ "$success" = true ]; then
permit_start=$SECONDS
while (( SECONDS - permit_start < 300 )); do
if [ -s "$STATE_DIR/install.permit" ]; then
exec /bin/bash "$STATE_DIR/full-install.sh"
fi
sleep 1
done
printf '[full-install] host permit was not received in five minutes\n' >> "$PROOF_LOG"
fi
while :; do sleep 60; done
""");
static async Task PrepareFullSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
{
var installer = ReadPinned(source, "src/install/recovery/launch.sh", "b44309d1056bbd0321251cc9f04a52cf6ad68209586f263b9619339bf7146b6c");
var selectorStart = installer.IndexOf("select_target_disk() {", StringComparison.Ordinal);
var selectorEnd = installer.IndexOf("find_startosinstall() {", StringComparison.Ordinal);
if (selectorStart < 0 || selectorEnd <= selectorStart) throw new InvalidOperationException("Pinned installer selector boundaries changed.");
var selector = """
select_target_disk() {
local permit_token permit_disk permit_commit extra
{ IFS= read -r permit_token; IFS= read -r permit_disk; IFS= read -r permit_commit; IFS= read -r extra || :; } < "$STATE_DIR/install.permit" || return 1
[ "$permit_token" = "$PROOF_TOKEN" ] && [ -z "${extra:-}" ] || return 1
[ "$permit_commit" = "$(cat "$STATE_DIR/source.commit")" ] || return 1
[[ "$permit_commit" =~ ^[0-9a-f]{40}$ ]] || return 1
. "$STATE_DIR/macos-native-disk-guard.sh"
verify_owned_disk "$permit_disk" "$STATE_DIR" "$PROOF_TOKEN" >&2 || return 1
printf '%s\n' "$permit_disk"
}
""" + "\n";
installer = ReplaceOnce(installer, installer[selectorStart..selectorEnd], selector);
installer = ReplaceOnce(installer, "MIN_TARGET_SIZE=$((16 * 1024 * 1024 * 1024))", "# Target policy is exclusively the own writable 64-GiB emulated disk.");
installer = ReplaceOnce(installer, "no writable installation disk of at least 16 GiB was found", "the run-owned writable 64-GiB installation disk was not proved");
installer = ReplaceAllExact(installer, "rm -f \"$STARTED\"", ": # Keep the owned erase guard on failure; never erase again.", 2);
installer = ReplaceOnce(installer, ": > \"$STARTED\" || fail \"failed to create installation guard\"", "( set -o noclobber; printf '%s:%s:%s\\n' \"$PROOF_TOKEN\" \"$(cat \"$STATE_DIR/source.commit\")\" \"$TARGET_DISK\" > \"$STARTED\" ) || fail \"failed to create the exclusive owned installation guard\"");
installer = ReplaceOnce(installer, "set -u\n", "set -u\nPROOF_TOKEN=\"" + token + "\"\n" + """
installer_parent=$$
# Installer watchdog: 80 minutes, also bounded by the host's 172-minute total.
(
trap 'kill "$sleeper" 2>/dev/null || :; exit 0' TERM INT
sleep 4800 & sleeper=$!; wait "$sleeper"; kill -TERM "$installer_parent" 2>/dev/null || :
) & install_watchdog=$!
trap 'kill -TERM "$install_watchdog" 2>/dev/null || :; wait "$install_watchdog" 2>/dev/null || :' EXIT
trap 'kill "${STARTOSINSTALL_PID:-}" "${BOOTSTRAPPER_PID:-}" 2>/dev/null || :; printf "{\"token\":\"%s\",\"phase\":\"installation-failed\"}\n" "$PROOF_TOKEN" > /Volumes/installstate/guest-phase.json; exit 1' TERM INT
""" + "\n");
var firstboot = ReadPinned(source, "src/install/firstboot/launch.sh", "d6b29bb42ffe99edda6b3be3faf6009c4e0b34e5b8bba6eb0855cf24a0c24307");
firstboot = ReplaceOnce(firstboot, "log \"prebuilt account package installed successfully\"\n", "log \"prebuilt account package installed successfully\"\n" + """
count=0
while [ ! -d /Volumes/installstate ] && (( count < 120 )); do
/sbin/mount_9p installstate >/dev/null 2>&1 || :
count=$((count + 1)); sleep 1
done
[ -d /Volumes/installstate ] || fail "native CI state share did not mount"
""" + "\n/bin/bash /Volumes/installstate/native-firstboot-bootstrap.sh \"" + token + "\" || fail \"native CI bootstrap or tests failed\"\n");
ReadPinned(source, "src/install/firstboot/com.dockur.macos.firstboot.plist", "29ef05388d962c236bdb87b2911e3b42e08c600035cfccf440d35ba64011c3d3");
ReadPinned(source, "src/image.sh", "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c");
var initialize = ReadPinned(source, "src/install.sh", "19b4b27187de85148ad1de1c40d75a54738eb9890f02dbb9445155bb5ec44f90");
initialize = ReplaceOnce(initialize, "INSTALL_STATE_DIR=\"$QEMU_DIR/installstate\"\nrm -rf \"$INSTALL_STATE_DIR\"", "INSTALL_STATE_DIR=\"$STORAGE/ci-state\"\n# Full CI preserves the own-volume erase guard and evidence across starts.");
initialize = ReplaceOnce(initialize, " if ! prepareInstallationState \"$INSTALL_STATE_DIR\"; then\n exit 34\n fi", """
if [ -e "$INSTALL_STATE_DIR/run.owner" ]; then
[ "$(cat "$INSTALL_STATE_DIR/run.owner")" = "@@OWNER@@" ] || { error "CI storage belongs to another run."; exit 34; }
else
prepareInstallationState "$INSTALL_STATE_DIR" || exit 34
cp -f /assets/ci-payload/* "$INSTALL_STATE_DIR/" || exit 34
cp -f "$IMAGE_TOOLS/recovery/full-install.sh" "$INSTALL_STATE_DIR/full-install.sh" || exit 34
cmp -s "$IMAGE_TOOLS/recovery/full-install.sh" "$INSTALL_STATE_DIR/full-install.sh" || exit 34
for file in /assets/ci-payload/*; do cmp -s "$file" "$INSTALL_STATE_DIR/${file##*/}" || exit 34; done
chmod 0755 "$INSTALL_STATE_DIR/full-install.sh" "$INSTALL_STATE_DIR/native-firstboot-bootstrap.sh" || exit 34
printf '%s\n' '@@OWNER@@' > "$INSTALL_STATE_DIR/run.owner" || exit 34
fi
""".Replace("@@OWNER@@", token, StringComparison.Ordinal));
foreach (var pair in new[] { ("full-install.sh", installer), ("full-firstboot.sh", firstboot), ("full-state-source.sh", initialize) })
{
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
await Command("bash", ["-n", Path.Combine(output, pair.Item1)], output, pair.Item1 + "-syntax", cancellation);
}
foreach (var name in new[] { "macos-native-firstboot.sh", "macos-native-disk-guard.sh" })
await Command("bash", ["-n", Path.Combine("tools", "ci", name)], output, name + "-syntax", cancellation);
Save(Path.Combine(output, "full-source-hashes.json"), new Dictionary<string, string> { ["full-install.sh"] = Hash(Encoding.UTF8.GetBytes(installer)), ["full-firstboot.sh"] = Hash(Encoding.UTF8.GetBytes(firstboot)), ["full-state-source.sh"] = Hash(Encoding.UTF8.GetBytes(initialize)), ["MacOsNativeGuest.cs"] = Hash(File.ReadAllBytes("tools/ci/MacOsNativeGuest.cs")), ["macos-native-firstboot.sh"] = Hash(File.ReadAllBytes("tools/ci/macos-native-firstboot.sh")), ["macos-native-disk-guard.sh"] = Hash(File.ReadAllBytes("tools/ci/macos-native-disk-guard.sh")) });
if (!writeSource) return;
File.WriteAllText(Path.Combine(source, "src/install/recovery/full-install.sh"), installer, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/firstboot/launch.sh"), firstboot, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install.sh"), initialize, new UTF8Encoding(false));
}
static async Task PermitInstallation(string id, string output, string token, string commit, string readiness, CancellationToken cancellation)
{
await Command("docker", ["inspect", id], output, "full-container-boundary", cancellation);
AssertContainer(File.ReadAllText(Path.Combine(output, "full-container-boundary.stdout.log")), token);
using (var document = JsonDocument.Parse(File.ReadAllText(Path.Combine(output, "full-container-boundary.stdout.log"))))
{
var mounts = document.RootElement[0].GetProperty("Mounts").EnumerateArray().ToArray();
if (mounts.Length != 1 || mounts[0].GetProperty("Type").GetString() != "volume" || mounts[0].GetProperty("Destination").GetString() != "/storage" || !mounts[0].GetProperty("RW").GetBoolean()) throw new InvalidOperationException("Full installer requires only the newly owned anonymous /storage volume.");
}
var drive = await Command("docker", ["exec", id, "qemu-img", "info", "--force-share", "--output=json", "/storage/14/data.img"], output, "owned-raw-disk", cancellation);
using (var document = JsonDocument.Parse(drive.Output))
if (document.RootElement.GetProperty("format").GetString() != "raw" || document.RootElement.GetProperty("virtual-size").GetInt64() != GuestDiskBytes) throw new InvalidOperationException("Owned VM raw-disk capacity/format mismatch.");
var attachment = await Command("docker", ["exec", id, "sh", "-c", "qemu_pid=$(cat /dev/shm/qemu.pid); tr '\\0' '\\n' < /proc/\"$qemu_pid\"/cmdline | sed -n '/^file=\\/storage\\/14\\/data\\.img,/p; /^ide-hd,drive=data3,/p; /^local,id=installstatefs,/p'"], output, "owned-disk-attachment", cancellation);
var lines = attachment.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length != 3 || !lines.Any(line => line.StartsWith("file=/storage/14/data.img,id=data3,format=raw,", StringComparison.Ordinal) && !line.Contains("readonly=on", StringComparison.Ordinal)) || !lines.Any(line => line.StartsWith("ide-hd,drive=data3,", StringComparison.Ordinal) && line.Contains("serial=" + DiskSerial(token), StringComparison.Ordinal)) || !lines.Contains("local,id=installstatefs,path=" + FullState + ",security_model=none")) throw new InvalidOperationException("QEMU did not attach the exact owned raw disk/serial and persistent state share.");
var owner = await Command("docker", ["exec", id, "cat", FullState + "/run.owner"], output, "full-share-owner", cancellation);
if (owner.Output.Trim() != token) throw new InvalidOperationException("Native state owner mismatch.");
using var receipt = JsonDocument.Parse(readiness);
var disk = receipt.RootElement.GetProperty("disk").GetString();
var permit = Path.Combine(output, "install.permit");
File.WriteAllText(permit, token + "\n" + disk + "\n" + commit + "\n");
await Command("docker", ["cp", permit, id + ":" + FullState + "/install.permit.tmp"], output, "stage-owned-install-permit", cancellation);
await Command("docker", ["exec", id, "mv", FullState + "/install.permit.tmp", FullState + "/install.permit"], output, "authorize-owned-guest-installation", cancellation);
}
static readonly string[] NativeFacts = [
"MeetingAssistant.Tests.MacOsMeetingAudioSourceTests.NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.MacOsCapabilityEndpointReportsEnabledRealProviders",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.NativeHelperCropsPngUsingOcrPixelCoordinates",
"MeetingAssistant.Tests.MacOsMeetingIntegrationTests.CalendarClientFallsBackToCalendarAutomationWhenEventKitIsDenied"];
static readonly string[] NativeArtifacts = ["MeetingAssistantAudioCapture.app/Contents/MacOS/macos-meeting-audio-capture", "macos-desktop-controls", "macos-meeting-integrations", "macos-meeting-assistant-launcher"];
static void ValidateFullResult(string json, string token, string commit, string archiveHash)
{
using var document = JsonDocument.Parse(json);
var result = document.RootElement;
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || result.GetProperty("sourceCommit").GetString() != commit || result.GetProperty("archiveSha256").GetString() != archiveHash || result.GetProperty("sdkVersion").GetString() != SdkVersion || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || new[] { "expectedTests", "total", "executed", "passed" }.Any(key => result.GetProperty(key).GetInt32() != 577) || new[] { "failed", "notExecuted", "audioCodeSignExit" }.Any(key => result.GetProperty(key).GetInt32() != 0) || !result.GetProperty("nativeTests").EnumerateArray().Select(value => value.GetString()).Order().SequenceEqual(NativeFacts.Order())) throw new InvalidOperationException("Native full receipt did not prove exact-source 577/577 with all five native tests and zero skips.");
var artifacts = result.GetProperty("nativeArtifacts").EnumerateArray().ToArray();
if (artifacts.Length != 4 || !artifacts.Select(item => item.GetProperty("name").GetString()).Order().SequenceEqual(NativeArtifacts.Order()) || artifacts.Any(item => item.GetProperty("architecture").GetString() != "x86_64" || !System.Text.RegularExpressions.Regex.IsMatch(item.GetProperty("sha256").GetString() ?? "", "^[0-9a-f]{64}$"))) throw new InvalidOperationException("Native Mach-O/x86_64 helper manifest is incomplete.");
}
static void ValidateTrx(byte[] bytes, string json)
{
using var receipt = JsonDocument.Parse(json);
if (Hash(bytes) != receipt.RootElement.GetProperty("trxSha256").GetString()) throw new InvalidOperationException("Returned native TRX SHA-256 mismatch.");
var document = XDocument.Load(new MemoryStream(bytes));
var counters = document.Descendants().Single(item => item.Name.LocalName == "Counters");
foreach (var key in new[] { "total", "executed", "passed" }) if ((int?)counters.Attribute(key) != 577) throw new InvalidOperationException("Native TRX count mismatch: " + key);
foreach (var key in new[] { "failed", "notExecuted" }) if ((int?)counters.Attribute(key) != 0) throw new InvalidOperationException("Native TRX failure/skip counter: " + key);
var results = document.Descendants().Where(item => item.Name.LocalName == "UnitTestResult").ToArray();
if (results.Length != 577 || results.Any(item => (string?)item.Attribute("outcome") != "Passed")) throw new InvalidOperationException("Native TRX contains missing, failed or skipped results.");
var identities = document.Descendants().Where(item => item.Name.LocalName == "UnitTest").ToDictionary(item => (string)item.Attribute("id")!, item => { var method = item.Elements().Single(child => child.Name.LocalName == "TestMethod"); var className = ((string?)method.Attribute("className"))?.Split(',')[0].Trim() ?? ""; var name = (string?)method.Attribute("name") ?? ""; return name.StartsWith(className + ".", StringComparison.Ordinal) ? name : className + "." + name; });
var passed = results.Select(item => identities[(string)item.Attribute("testId")!]).ToHashSet();
if (NativeFacts.Any(name => !passed.Contains(name))) throw new InvalidOperationException("Native TRX does not explicitly pass every required macOS fact.");
}
static void ValidateFullContracts()
{
var token = new string('a', 32); var commit = new string('b', 40); var hash = new string('c', 64);
var trx = "<TestRun><TestDefinitions>" + string.Concat(Enumerable.Range(0, 577).Select(index => { var identity = index < 5 ? NativeFacts[index] : "MeetingAssistant.Tests.Validation.Test" + index; var split = identity.LastIndexOf('.'); return $"<UnitTest id='t{index}'><TestMethod className='{identity[..split]}' name='{identity[(split + 1)..]}' /></UnitTest>"; })) + "</TestDefinitions><Results>" + string.Concat(Enumerable.Range(0, 577).Select(index => $"<UnitTestResult testId='t{index}' outcome='Passed' />")) + "</Results><ResultSummary><Counters total='577' executed='577' passed='577' failed='0' notExecuted='0' /></ResultSummary></TestRun>";
var good = JsonSerializer.Serialize(new { token, success = true, sourceCommit = commit, archiveSha256 = hash, sdkVersion = SdkVersion, osVersion = "14.6.1", architecture = "x86_64", expectedTests = 577, total = 577, executed = 577, passed = 577, failed = 0, notExecuted = 0, nativeTests = NativeFacts, nativeArtifacts = NativeArtifacts.Select(name => new { name, sha256 = hash, architecture = "x86_64" }), audioCodeSignExit = 0, trxSha256 = Hash(Encoding.UTF8.GetBytes(trx)) });
ValidateFullResult(good, token, commit, hash); ValidateTrx(Encoding.UTF8.GetBytes(trx), good);
byte[] bomTrx = [0xef, 0xbb, 0xbf, .. Encoding.UTF8.GetBytes(trx)];
ValidateTrx(bomTrx, good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(bomTrx), StringComparison.Ordinal));
var qualifiedTrx = trx;
foreach (var name in NativeFacts) qualifiedTrx = qualifiedTrx.Replace("name='" + name[(name.LastIndexOf('.') + 1)..] + "'", "name='" + name + "'", StringComparison.Ordinal);
ValidateTrx(Encoding.UTF8.GetBytes(qualifiedTrx), good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(Encoding.UTF8.GetBytes(qualifiedTrx)), StringComparison.Ordinal));
foreach (var invalid in new[] { good.Replace("\"success\":true", "\"success\":false"), good.Replace("\"passed\":577", "\"passed\":572"), good.Replace("\"notExecuted\":0", "\"notExecuted\":5"), good.Replace("\"audioCodeSignExit\":0", "\"audioCodeSignExit\":1"), good.Replace("x86_64", "arm64"), good.Replace(token, new string('d', 32)), good.Replace(commit, new string('e', 40)), good.Replace("NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures", "UnrelatedTest") })
{
try { ValidateFullResult(invalid, token, commit, hash); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Full native validator accepted an incomplete or stale proof.");
}
try { ValidateTrx(Encoding.UTF8.GetBytes(trx.Replace("outcome='Passed'", "outcome='NotExecuted'")), good); } catch (InvalidOperationException) { return; }
throw new InvalidOperationException("Full native validator accepted changed TRX bytes.");
}
static async Task ValidateCompression(string chunk, string output)
{
Directory.CreateDirectory(output);
var bytes = File.ReadAllBytes(chunk);
if (Hash(bytes) != "2770f06fe51f19ddc040cfad4ab870d7227f540d1ba4190a9ce631145c12fae0") throw new InvalidOperationException("Readonly retained Recovery qualification chunk hash mismatch.");
var text = Encoding.Latin1.GetString(bytes);
if (text.Split(DiagnosticDaemon, StringSplitOptions.None).Length != 2 || text.Split(MountOnlyBootstrap, StringSplitOptions.None).Length != 2) throw new InvalidOperationException("Qualification chunk does not contain this exact Recovery LaunchDaemon and mount-only patch.");
// Upstream UDIF recompression uses Python zlib; .NET's compressor differs even on baseline.
await Command("python3", ["-c", "import json,pathlib,sys,zlib; b=pathlib.Path(sys.argv[1]).read_bytes(); n=len(zlib.compress(b,9)); print(json.dumps({'runtime':zlib.ZLIB_RUNTIME_VERSION,'compressedBytes':n,'slotBytes':43266,'fits':n<=43266})); sys.exit(0 if n<=43266 else 1)", chunk], output, "readonly-recovery-compression", CancellationToken.None);
}
static async Task ValidateDiskSerialParser(string output)
{
Directory.CreateDirectory(output);
var guard = File.ReadAllText("tools/ci/macos-native-disk-guard.sh");
const string start = "-v disk=\"${disk#/dev/}\" '\n";
const string end = " ' \"$state/disk-ownership-ioreg.log\")";
var program = guard[(guard.IndexOf(start, StringComparison.Ordinal) + start.Length)..guard.IndexOf(end, StringComparison.Ordinal)];
var serial = new string('0', 20);
var own = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n";
var reversed = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n";
var splitRoots = "+-o QEMU HARDDISK <class IOAHCIBlockStorageDevice>\n | \"Device Characteristics\" = {\"Serial Number\"=\"" + serial + "\"}\n+-o Other <class IOAHCIBlockStorageDevice>\n +-o Media <class IOMedia>\n \"BSD Name\" = \"disk1\"\n";
foreach (var test in new[] { ("own", own, "1"), ("reversed-properties", reversed, "1"), ("split-roots", splitRoots, "0"), ("foreign-serial", own.Replace(serial, new string('a', 20)), "0"), ("foreign-disk", own.Replace("disk1", "disk2"), "0"), ("ambiguous", own + own, "2") })
{
var path = Path.Combine(output, "ioreg-" + test.Item1 + ".fixture");
File.WriteAllText(path, test.Item2);
var result = await Command("awk", ["-v", "expected=" + serial, "-v", "disk=disk1", program, path], output, "disk-serial-" + test.Item1, CancellationToken.None);
if (result.Output.Trim() != test.Item3) throw new InvalidOperationException("Actual boot-seam IORegistry parser fixture failed: " + test.Item1);
}
}
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
{
var count = text.Split(oldValue, StringSplitOptions.None).Length - 1;
@@ -227,14 +529,27 @@ static class NativeDiagnostic
throw new InvalidOperationException("Created container exceeds the owned/unprivileged diagnostic boundary.");
}
static async Task CaptureGuest(string id, string output, CancellationToken cancellation)
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool full = false, bool final = false)
{
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 files = new List<(string, string)> { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") };
if (full) files.AddRange([("guest-phase.json", "guest-phase.json"), ("full-result.json", "full-result.json"), ("firstboot.log", "firstboot.log"), ("install.log", "install.log"), ("apple.log", "apple.log"), ("disk-ownership-ioreg.log", "disk-ownership-ioreg.log"), ("installed-root.plist", "installed-root.plist"), ("clt-catalog.log", "clt-catalog.log"), ("clt-install.log", "clt-install.log")]);
foreach (var file in files)
{
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
var result = await Command("docker", ["exec", id, "cat", (full ? FullState : "/dev/shm/installstate") + "/" + file.Item1], output, "capture-" + file.Item1.Replace('/', '-'), cancellation, requireSuccess: false);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
}
if (full)
{
// Copy only this owned guest's diagnostic logs; no host paths or credentials.
var trx = await Command("docker", ["cp", id + ":" + FullState + "/test-results/native.trx", Path.Combine(output, "native.trx.tmp")], output, "capture-native-trx", cancellation, requireSuccess: false);
if (trx.ExitCode == 0) File.Move(Path.Combine(output, "native.trx.tmp"), Path.Combine(output, "native.trx"), overwrite: true);
if (final || File.Exists(Path.Combine(output, "full-result.json")))
{
Directory.CreateDirectory(Path.Combine(output, "guest-logs"));
await Command("docker", ["cp", id + ":" + FullState + "/guest-logs/.", Path.Combine(output, "guest-logs")], output, "capture-guest-logs", cancellation, requireSuccess: false);
}
}
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);
}
@@ -335,6 +650,18 @@ static class NativeDiagnostic
Console.WriteLine("[native-diagnostic] Final native guest proof:");
Console.Write(proof);
}
static void PrintFullProof(string output, string token)
{
foreach (var name in new[] { "full-result.json", "firstboot.log", "guest-logs/build.stdout.log", "guest-logs/build.stderr.log", "guest-logs/test.stdout.log", "guest-logs/test.stderr.log" })
{
var path = Path.Combine(output, name);
if (!File.Exists(path)) continue;
var proof = File.ReadAllText(path).Replace(token, "<run-id>", StringComparison.Ordinal);
if (proof.Length > 64 * 1024) proof = "[earlier output retained in artifact]\n" + proof[^(64 * 1024)..];
Console.WriteLine("[native-diagnostic] Final native evidence: " + name);
Console.WriteLine(proof);
}
}
static void Save(string path, object value)
{
var temporary = path + ".tmp";