forked from Manuel/meeting-assistant
Integrate existing-runner KVM profile with full native CI gates
This commit is contained in:
commit
8d2014dcbc
11 files changed
+523
-78
No files matched your search
@@ -1,5 +1,6 @@
|
||||
#:property PublishAot=false
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
@@ -12,13 +13,16 @@ return await NativeDiagnostic.Execute(args);
|
||||
static class NativeDiagnostic
|
||||
{
|
||||
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
|
||||
const string CryptexUrl = "https://github.com/acidanthera/CryptexFixup/releases/download/1.0.5/CryptexFixup-1.0.5-RELEASE.zip";
|
||||
const string CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30";
|
||||
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
|
||||
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;
|
||||
const string SdkVersion = "10.0.401";
|
||||
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
|
||||
const string FullState = "/storage/14/ci-state";
|
||||
const string FullState = "/storage/13/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";
|
||||
@@ -50,7 +54,7 @@ static class NativeDiagnostic
|
||||
{
|
||||
if (args.Length == 0 || args.Contains("--help"))
|
||||
{
|
||||
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]");
|
||||
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--full] [--output artifacts/native-macos] [--source existing-dockur-clone] [--cryptex-archive verified-release.zip] [--compression-chunk readonly-qualified-chunk]");
|
||||
return 0;
|
||||
}
|
||||
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
|
||||
@@ -60,7 +64,7 @@ static class NativeDiagnostic
|
||||
ValidateContracts();
|
||||
if (full) ValidateFullContracts();
|
||||
if (Option(args, "--source") is { } source)
|
||||
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full);
|
||||
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full, Option(args, "--cryptex-archive"));
|
||||
if (full) await ValidateDiskSerialParser(output);
|
||||
if (Option(args, "--compression-chunk") is { } chunk)
|
||||
await ValidateCompression(Path.GetFullPath(chunk), output);
|
||||
@@ -93,7 +97,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, mode = full ? "full" : "readiness" });
|
||||
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex", causalSingleVariableTest = false, kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", 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))
|
||||
{
|
||||
@@ -113,13 +117,13 @@ static class NativeDiagnostic
|
||||
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.");
|
||||
if (full) await PreparePayload(source, output, token, sourceCommit, deadline.Token);
|
||||
await PrepareSource(source, output, token, true, deadline.Token, full);
|
||||
await PrepareSource(source, output, token, true, deadline.Token, full, Option(args, "--cryptex-archive"));
|
||||
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);
|
||||
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"];
|
||||
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", "--device", "/dev/kvm:/dev/kvm:rw", "--env", "KVM=Y", "--env", "CPU_MODEL=host", "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=13", "--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);
|
||||
@@ -130,7 +134,7 @@ 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(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.");
|
||||
Console.WriteLine(full ? "The owned restricted KVM/host-CPU macOS 13 guest is starting. Installation requires fresh native readiness and an owned-disk permit; success requires all 577 tests with zero skips." : "The owned restricted KVM/host-CPU macOS 13 guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run.");
|
||||
var phaseStarted = Stopwatch.StartNew();
|
||||
var phase = "recovery";
|
||||
var phaseBudget = TimeSpan.FromMinutes(40);
|
||||
@@ -162,7 +166,7 @@ static class NativeDiagnostic
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (full && permitted)
|
||||
if (full)
|
||||
{
|
||||
var phasePath = Path.Combine(output, "guest-phase.json");
|
||||
if (File.Exists(phasePath))
|
||||
@@ -170,12 +174,12 @@ static class NativeDiagnostic
|
||||
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;
|
||||
var next = !permitted ? phase : 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))
|
||||
if (permitted && 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));
|
||||
@@ -221,18 +225,33 @@ static class NativeDiagnostic
|
||||
|
||||
static void ValidateContracts()
|
||||
{
|
||||
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
|
||||
var baseline = ReplaceOnce(readiness, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
|
||||
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
|
||||
throw new InvalidOperationException("Compatibility readiness may change only the baseline's macOS minimum to 13; identity, services, disk and limits must remain identical.");
|
||||
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
|
||||
throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper.");
|
||||
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 });
|
||||
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "13.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") })
|
||||
foreach (var invalid in new[] { good.Replace("13.6.1", "12.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.");
|
||||
}
|
||||
var boundary = """
|
||||
[{"Config":{"Labels":{"org.meeting-assistant.native-diagnostic":"validation"},"Env":["KVM=Y","CPU_MODEL=host","VERSION=13"]},"HostConfig":{"Privileged":false,"NetworkMode":"default","Memory":6442450944,"MemorySwap":6442450944,"NanoCpus":2000000000,"ShmSize":536870912,"CapAdd":null,"DeviceRequests":null,"Binds":null,"PortBindings":{},"DeviceCgroupRules":null,"Tmpfs":null,"Devices":[{"PathOnHost":"/dev/kvm","PathInContainer":"/dev/kvm","CgroupPermissions":"rw"}]},"Mounts":[{"Type":"volume","Destination":"/storage","RW":true}]}]
|
||||
""";
|
||||
AssertContainer(boundary, "validation");
|
||||
foreach (var invalid in new[] { boundary.Replace("\"Privileged\":false", "\"Privileged\":true"), boundary.Replace("\"CgroupPermissions\":\"rw\"", "\"CgroupPermissions\":\"rwm\""), boundary.Replace("/dev/kvm", "/dev/other"), boundary.Replace("KVM=Y", "KVM=N"), boundary.Replace("CPU_MODEL=host", "CPU_MODEL=Skylake-Client-v4"), boundary.Replace("VERSION=13", "VERSION=14"), boundary.Replace("6442450944", "8589934592"), boundary.Replace("\"NetworkMode\":\"default\"", "\"NetworkMode\":\"host\""), boundary.Replace("\"CapAdd\":null", "\"CapAdd\":[\"NET_ADMIN\"]"), boundary.Replace("\"Type\":\"volume\"", "\"Type\":\"bind\""), boundary.Replace("/storage", "/host") })
|
||||
{
|
||||
try { AssertContainer(invalid, "validation"); } catch (InvalidOperationException) { continue; }
|
||||
throw new InvalidOperationException("Diagnostic validator accepted an excessive/wrong-profile container boundary.");
|
||||
}
|
||||
}
|
||||
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false, string? cryptexArchive = null)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
|
||||
@@ -244,14 +263,26 @@ 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");
|
||||
// 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.
|
||||
if (Hash(File.ReadAllBytes(dockerPath)) != "a0e804235967400eb70e755d63eff8a33a7761922ddd6e9723faa8e828fd8aa3") throw new InvalidOperationException("Pinned Dockerfile hash mismatch.");
|
||||
// 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);
|
||||
dockerfile = ReplaceOnce(dockerfile, "ADD $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso", "ADD --checksum=sha256:" + OpenCoreTemplateHash + " $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso");
|
||||
var compatibility = await PrepareCompatibility(source, output, cryptexArchive, cancellation);
|
||||
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");
|
||||
entry = ReplaceOnce(entry, ". init.sh # Initialize system\n", ". init.sh # Initialize system\n# Fail before Apple downloads if the existing daemon cannot retain this profile.\nenabled \"$KVM\" && [[ \"$CPU_MODEL\" == host && \"$VERSION\" == 13 ]] && grep -Eq '^vendor_id[[:space:]]*:[[:space:]]*GenuineIntel$' /proc/cpuinfo || { error 'Compatibility probe requires existing Intel KVM and the exact host/13 profile.'; exit 1; }\n");
|
||||
entry = ReplaceOnce(entry, "trap - ERR\n", "[[ \"$KVM_OPTS\" == *'accel=kvm'* || \"$KVM_OPTS\" == *'-accel kvm'* ]] && [[ \"$KVM_OPTS\" != *tcg* && \"$CPU_MODEL\" == host ]] || { error 'Compatibility profile refuses a TCG/CPU fallback.'; exit 1; }\ninfo '[compatibility-profile] accelerator=kvm cpu=host recovery=13; actual guest gates still pending'\n\ntrap - ERR\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", full ? "macos-native-full-bootstrap.sh" : "macos-native-bootstrap.sh"));
|
||||
if (full) wrapper = ReplaceOnce(wrapper, "@@PROOF_TOKEN@@", token);
|
||||
var imagePath = Path.Combine(source, "src", "image.sh");
|
||||
// Read immutable staging source before PrepareFullSource can write any seam.
|
||||
var originalImage = ReadPinned(source, "src/image.sh", "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c");
|
||||
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");
|
||||
if (full)
|
||||
{
|
||||
await PrepareFullSource(source, output, token, writeSource, cancellation);
|
||||
@@ -259,22 +290,198 @@ static class NativeDiagnostic
|
||||
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) })
|
||||
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), ("boot.sh.patched", compatibility.Boot), ("opencore-config.plist", compatibility.Config) })
|
||||
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))));
|
||||
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" or "boot.sh.patched" or "opencore-config.plist" or "compatibility-boot-assets.json").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);
|
||||
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
|
||||
if (full && !writeSource) await ValidateFullBootstrap(wrapper, output, token, 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(Path.Combine(source, "src/install/recovery/launch.sh"), hook, 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));
|
||||
File.WriteAllText(Path.Combine(source, "src/boot.sh"), compatibility.Boot, new UTF8Encoding(false));
|
||||
File.WriteAllText(Path.Combine(source, "assets/config.plist"), compatibility.Config, new UTF8Encoding(false));
|
||||
var target = Path.Combine(source, "assets", "native-compatibility");
|
||||
if (Directory.Exists(target)) throw new InvalidOperationException("Refusing an existing compatibility asset overlay.");
|
||||
foreach (var file in Directory.GetFiles(compatibility.Assets, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var destination = Path.Combine(target, Path.GetRelativePath(compatibility.Assets, file));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
File.Copy(file, destination, false);
|
||||
}
|
||||
}
|
||||
|
||||
static async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, CancellationToken cancellation)
|
||||
{
|
||||
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
|
||||
var config = File.ReadAllText(Path.Combine(source, "assets", "config.plist"));
|
||||
if (Hash(Encoding.UTF8.GetBytes(boot)) != "82b56525707a8f586e040f56108b5034c02e7fecfea071f1857e596cba10cbed" || Hash(Encoding.UTF8.GetBytes(config)) != "3b0ec58b693cfa0fadf3e3f952486e87af8c27d504f9545d1e90ae2dc3777096") throw new InvalidOperationException("Pinned OpenCore staging/config hashes mismatch.");
|
||||
byte[] bytes;
|
||||
if (archivePath is not null) bytes = await File.ReadAllBytesAsync(archivePath, cancellation);
|
||||
else
|
||||
{
|
||||
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30), MaxResponseContentBufferSize = 2 * 1024 * 1024 };
|
||||
bytes = await client.GetByteArrayAsync(CryptexUrl, cancellation);
|
||||
}
|
||||
if (bytes.Length != 69703 || Hash(bytes) != CryptexHash) throw new InvalidOperationException("Official CryptexFixup release size/hash mismatch.");
|
||||
File.WriteAllBytes(Path.Combine(output, "CryptexFixup-1.0.5-RELEASE.zip"), bytes);
|
||||
var assets = Path.Combine(output, "compatibility-assets");
|
||||
if (Directory.Exists(assets)) throw new InvalidOperationException("Compatibility validation requires a fresh output directory.");
|
||||
Directory.CreateDirectory(assets);
|
||||
using var archive = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read);
|
||||
var required = new[] { "CryptexFixup.kext/Contents/Info.plist", "CryptexFixup.kext/Contents/MacOS/CryptexFixup" };
|
||||
var entries = archive.Entries.Where(entry => entry.FullName.StartsWith("CryptexFixup.kext/", StringComparison.Ordinal) && !entry.FullName.EndsWith('/')).ToArray();
|
||||
if (entries.Length != 2 || required.Any(name => entries.Count(entry => entry.FullName == name) != 1)) throw new InvalidOperationException("Cryptex bundle has an unexpected file layout.");
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (entry.Length <= 0 || entry.Length > 1024 * 1024) throw new InvalidOperationException("Cryptex bundle file exceeded the staging bound.");
|
||||
var destination = Path.Combine(assets, entry.FullName);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
|
||||
entry.ExtractToFile(destination, false);
|
||||
}
|
||||
var info = XDocument.Load(Path.Combine(assets, required[0])).Root!.Element("dict")!;
|
||||
if (PlistValue(info, "CFBundleIdentifier").Value != "com.khronokernel.CryptexFixup" || PlistValue(info, "CFBundleVersion").Value != "1.0.5" || PlistValue(info, "CFBundleExecutable").Value != "CryptexFixup" || PlistValue(PlistValue(info, "OSBundleLibraries"), "as.vit9696.Lilu").Value != "1.4.7") throw new InvalidOperationException("Cryptex bundle identity/version/Lilu dependency mismatch.");
|
||||
var fileHashes = required.ToDictionary(name => name, name => Hash(File.ReadAllBytes(Path.Combine(assets, name))));
|
||||
File.WriteAllText(Path.Combine(assets, "SHA256SUMS"), string.Concat(fileHashes.Select(pair => pair.Value + " " + pair.Key + "\n")), new UTF8Encoding(false));
|
||||
Save(Path.Combine(output, "compatibility-boot-assets.json"), new { cryptexUrl = CryptexUrl, cryptexSha256 = CryptexHash, cryptexBytes = bytes.Length, cryptexFiles = fileHashes, templateUrl = "https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso", templateSha256 = OpenCoreTemplateHash, templateBytes = 15884288, liluVersion = "1.7.1", liluBinarySha256 = "0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52", liluBinaryBytes = 526984, liluInfoSha256 = "fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a", causalSingleVariableTest = false });
|
||||
var document = XDocument.Parse(config, LoadOptions.PreserveWhitespace);
|
||||
var add = PlistValue(PlistValue(document.Root!.Element("dict")!, "Kernel"), "Add");
|
||||
var expected = new[] { "Lilu.kext", "VMHide.kext", "VirtualSMC.kext", "WhateverGreen.kext", "VoodooPS2Controller.kext", "VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext", "AppleMCEReporterDisabler.kext" };
|
||||
if (!add.Elements("dict").Select(dict => PlistValue(dict, "BundlePath").Value).SequenceEqual(expected) || add.Elements("dict").Any(dict => PlistValue(dict, "Enabled").Name != "true")) throw new InvalidOperationException("Pinned Kernel.Add order/enabled contract mismatch.");
|
||||
var cryptex = XElement.Parse("<dict><key>Arch</key><string>x86_64</string><key>BundlePath</key><string>CryptexFixup.kext</string><key>Comment</key><string>Official CryptexFixup 1.0.5; owned compatibility guest only</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/CryptexFixup</string><key>MaxKernel</key><string></string><key>MinKernel</key><string>22.0.0</string><key>PlistPath</key><string>Contents/Info.plist</string></dict>");
|
||||
add.Elements("dict").First().AddAfterSelf(cryptex);
|
||||
var bootArguments = PlistValue(PlistValue(PlistValue(PlistValue(document.Root.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82"), "boot-args").Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (bootArguments.Intersect(new[] { "-cryptoff", "-liluoff", "-crypt_allow_hash_validation", "-crypt_force_avx", "-cryptbeta", "-lilubetaall" }).Any()) throw new InvalidOperationException("Unexpected Cryptex/Lilu disabling or forcing boot argument.");
|
||||
boot = ReplaceOnce(boot, " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n", " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n" + CompatibilityStaging + "\n");
|
||||
boot = ReplaceOnce(boot, " PLIST=\"/assets/config.plist\"\n", " [ ! -e /custom.plist ] || { error 'Compatibility profile refuses an unverified custom OpenCore config!'; exit 12; }\n PLIST=\"/assets/config.plist\"\n");
|
||||
boot = ReplaceOnce(boot, " checkOpenCoreConfig\n addVmHideKext\n", " checkOpenCoreConfig\n" + CompatibilityConfigCheck + "\n addVmHideKext\n");
|
||||
boot = ReplaceOnce(boot, " if [ -s \"$target\" ] && [ \"$previous\" = \"$current\" ]; then\n IMG=\"$target\"\n return 0\n fi\n", " # This owned compatibility probe always rebuilds; never trust a cached boot.img.\n");
|
||||
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"COMPATIBILITY=kvm-host-ventura-cryptex\"\n sha256sum /assets/native-compatibility/SHA256SUMS\n");
|
||||
return (boot, document.ToString(), assets);
|
||||
}
|
||||
|
||||
static XElement PlistValue(XElement dictionary, string key)
|
||||
{
|
||||
var keys = dictionary.Elements("key").Where(element => element.Value == key).ToArray();
|
||||
if (keys.Length != 1 || keys[0].ElementsAfterSelf().FirstOrDefault() is not { } value) throw new InvalidOperationException("Missing/duplicate plist key: " + key);
|
||||
return value;
|
||||
}
|
||||
|
||||
const string CompatibilityStaging = """
|
||||
# Only the freshly extracted, owned guest EFI is changed; never the host.
|
||||
local lilu="$EFI_DIR/OC/Kexts/Lilu.kext/Contents"
|
||||
printf '%s %s\n' \
|
||||
fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a "$lilu/Info.plist" \
|
||||
0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52 "$lilu/MacOS/Lilu" | sha256sum -c - || { error "Pinned active Lilu files mismatch!"; exit 12; }
|
||||
[ "$(xmlstarlet sel -T -t -v '/plist/dict/key[.="CFBundleVersion"]/following-sibling::string[1]' "$lilu/Info.plist")" = 1.7.1 ] || { error "Active Lilu version mismatch!"; exit 12; }
|
||||
[ ! -e "$EFI_DIR/OC/Kexts/CryptexFixup.kext" ] || { error "Unexpected pre-existing Cryptex kext!"; exit 12; }
|
||||
(cd /assets/native-compatibility && sha256sum -c SHA256SUMS) || { error "Pinned Cryptex staging files mismatch!"; exit 12; }
|
||||
cp -a /assets/native-compatibility/CryptexFixup.kext "$EFI_DIR/OC/Kexts/"
|
||||
(cd "$EFI_DIR/OC/Kexts" && sha256sum -c /assets/native-compatibility/SHA256SUMS) || { error "Active Cryptex copy mismatch!"; exit 12; }
|
||||
info "[compatibility-boot] Lilu=1.7.1 CryptexFixup=1.0.5 files=verified; guest injection and Recovery readiness remain unproved"
|
||||
""";
|
||||
|
||||
const string CompatibilityConfigCheck = """
|
||||
local kernel='/plist/dict/key[.="Kernel"]/following-sibling::dict[1]/key[.="Add"]/following-sibling::array[1]'
|
||||
local actual expected
|
||||
actual=$(xmlstarlet sel -T -t -m "$kernel/dict" -v 'key[.="BundlePath"]/following-sibling::string[1]' -n "$CFG") || exit 12
|
||||
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
|
||||
[ "$actual" = "$expected" ] || { error "Active Kernel.Add order mismatch!"; exit 12; }
|
||||
[ "$(xmlstarlet sel -T -t -v "name($kernel/dict[1]/key[.='Enabled']/following-sibling::*[1])" -v "name($kernel/dict[2]/key[.='Enabled']/following-sibling::*[1])" "$CFG")" = truetrue ] || { error "Active Lilu/Cryptex must both be enabled!"; exit 12; }
|
||||
actual=$(xmlstarlet sel -T -t -m "$kernel/dict[2]" -v 'key[.="Arch"]/following-sibling::string[1]' -n -v 'key[.="ExecutablePath"]/following-sibling::string[1]' -n -v 'key[.="PlistPath"]/following-sibling::string[1]' -n -v 'key[.="MinKernel"]/following-sibling::string[1]' -n -v 'key[.="MaxKernel"]/following-sibling::string[1]' "$CFG") || exit 12
|
||||
expected=$(printf '%s\n' x86_64 Contents/MacOS/CryptexFixup Contents/Info.plist 22.0.0 '')
|
||||
[ "$actual" = "$expected" ] || { error "Active Cryptex Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
|
||||
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
|
||||
""";
|
||||
|
||||
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
|
||||
|
||||
static string DiskSerial(string token) => token[..20];
|
||||
|
||||
static async Task ValidateFullBootstrap(string wrapper, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
// Execute the complete generated shell with only its external filesystem,
|
||||
// Apple daemon and probe-process boundaries mapped to harmless fixtures.
|
||||
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
var ownMarker = token + ":" + commit + "\n";
|
||||
var cases = new[]
|
||||
{
|
||||
(Name: "restart", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "already-owned", Initial: ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: false, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "foreign-token", Initial: ownMarker.Replace(token, new string('f', 32)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "foreign-commit", Initial: ownMarker.Replace(commit, new string('f', 40)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "empty", Initial: "", ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "extra-record", Initial: ownMarker + ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "unterminated", Initial: ownMarker.TrimEnd('\n'), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "write-failure", Initial: (string?)null, ChildExit: 0, WriteFailure: true, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "first-child-failure", Initial: (string?)null, ChildExit: 1, WriteFailure: false, Children: 1, Failure: true, MountAfter: 0, Owner: (string?)token),
|
||||
(Name: "delayed-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 3, Owner: (string?)token),
|
||||
(Name: "missing-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: -1, Owner: (string?)null),
|
||||
(Name: "foreign-owner", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)new string('f', 32))
|
||||
};
|
||||
foreach (var test in cases)
|
||||
{
|
||||
var state = Path.Combine(output, "full-bootstrap-" + test.Name + "-fixture");
|
||||
if (Directory.Exists(state)) throw new InvalidOperationException("Full bootstrap fixtures require fresh validation output: " + state);
|
||||
Directory.CreateDirectory(state);
|
||||
if (test.MountAfter == 0 && test.Owner is not null) File.WriteAllText(Path.Combine(state, "run.owner"), test.Owner + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
|
||||
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
|
||||
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
|
||||
var marker = Path.Combine(state, "probe.started");
|
||||
if (test.Initial is not null) File.WriteAllText(marker, test.Initial);
|
||||
var mapped = wrapper.Replace("/Volumes/installstate", state, StringComparison.Ordinal);
|
||||
if (test.WriteFailure) mapped = ReplaceOnce(mapped, "MARKER=\"$STATE_DIR/probe.started\"", "MARKER=\"$STATE_DIR/absent-parent/probe.started\"");
|
||||
var script = """
|
||||
STATE_TEST="$1"; export STATE_TEST
|
||||
CHILD_EXIT="$2"; export CHILD_EXIT
|
||||
MOUNT_AFTER="$3"; MOUNT_OWNER="$4"; MOUNT_COMMIT="$5"
|
||||
/sbin/mount_9p() {
|
||||
local attempts=0
|
||||
[ ! -f "$STATE_TEST/mount-attempts" ] || attempts=$(cat "$STATE_TEST/mount-attempts")
|
||||
attempts=$((attempts + 1)); printf '%s\n' "$attempts" > "$STATE_TEST/mount-attempts"
|
||||
if (( MOUNT_AFTER > 0 && attempts >= MOUNT_AFTER )); then
|
||||
printf '%s\n' "$MOUNT_OWNER" > "$STATE_TEST/run.owner"
|
||||
printf '%s\n' "$MOUNT_COMMIT" > "$STATE_TEST/source.commit"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
sleep() { [ "$1" = 1 ] || return 1; printf 'sleep\n' >> "$STATE_TEST/mount-sleeps.log"; }
|
||||
/bin/bash() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/child-marker.log"; printf 'child\n' >> "$STATE_TEST/children.log"; return "$CHILD_EXIT"; }
|
||||
exec() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/apple-marker.log" 2>/dev/null || :; printf '%s\n' "$*" >> "$STATE_TEST/apple-exec.log"; return 0; }
|
||||
""" + "\n" + mapped + "\nwait\n";
|
||||
var errors = "";
|
||||
for (var start = 0; start < 2; start++)
|
||||
errors += (await Command("bash", ["-c", script, "full-bootstrap-contract", state, test.ChildExit.ToString(), test.MountAfter.ToString(), test.Owner ?? "", commit], output, "full-bootstrap-" + test.Name + "-start-" + start, cancellation)).Error;
|
||||
var childLog = Path.Combine(state, "children.log");
|
||||
var children = File.Exists(childLog) ? File.ReadAllLines(childLog).Length : 0;
|
||||
var appleExecutions = File.ReadAllLines(Path.Combine(state, "apple-exec.log"));
|
||||
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
var phaseUnchanged = phase == active;
|
||||
using var receipt = JsonDocument.Parse(phase);
|
||||
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "bootstrap-failed";
|
||||
var markerPreserved = test.Initial is not null ? File.ReadAllText(marker) == test.Initial : test.Children == 0 ? !File.Exists(marker) : File.Exists(marker) && File.ReadAllText(marker) == ownMarker;
|
||||
var completeBeforeExec = test.Initial is null && test.Children == 1 ? File.Exists(Path.Combine(state, "child-marker.log")) && File.ReadAllText(Path.Combine(state, "apple-marker.log")) == ownMarker + ownMarker && File.ReadAllText(Path.Combine(state, "child-marker.log")) == ownMarker : true;
|
||||
var mountAttemptsPath = Path.Combine(state, "mount-attempts");
|
||||
var mountAttempts = File.Exists(mountAttemptsPath) ? int.Parse(File.ReadAllText(mountAttemptsPath)) : 0;
|
||||
var sleepPath = Path.Combine(state, "mount-sleeps.log");
|
||||
var mountSleeps = File.Exists(sleepPath) ? File.ReadAllLines(sleepPath).Length : 0;
|
||||
var expectedMounts = test.MountAfter < 0 ? 240 : test.MountAfter;
|
||||
var failureReported = test.Owner == token ? realFailure : phaseUnchanged && errors.Contains("[full-bootstrap] ERROR:", StringComparison.Ordinal);
|
||||
Save(Path.Combine(output, "full-bootstrap-" + test.Name + ".json"), new { children, appleExecutions = appleExecutions.Length, phaseUnchanged, realFailure, failureReported, markerPreserved, completeBeforeExec, mountAttempts, mountSleeps });
|
||||
if (children != test.Children || appleExecutions.Length != 2 || appleExecutions.Any(value => value != "/usr/libexec/recoveryosd") || test.Failure && !failureReported || !test.Failure && !phaseUnchanged || !markerPreserved || !completeBeforeExec || mountAttempts != expectedMounts || mountSleeps != expectedMounts)
|
||||
throw new InvalidOperationException($"Full bootstrap contract failed ({test.Name}): children={children}, appleExecutions={appleExecutions.Length}, phaseUnchanged={phaseUnchanged}, failureReported={failureReported}, markerPreserved={markerPreserved}, completeBeforeExec={completeBeforeExec}, mountAttempts={mountAttempts}, mountSleeps={mountSleeps}.");
|
||||
}
|
||||
}
|
||||
|
||||
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.");
|
||||
@@ -355,7 +562,38 @@ static class NativeDiagnostic
|
||||
installer = ReplaceOnce(installer, " local message=\"$1\"\n\n echo \"[log] ERROR: $message\"", " local message=\"$1\"\n\n mark_installation_failed || :\n echo \"[log] ERROR: $message\"");
|
||||
installer = ReplaceOnce(installer, "if (( rc != 0 )); then\n", "if (( rc != 0 )); then\n mark_installation_failed || :\n");
|
||||
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, "select_target_disk() {", """
|
||||
owns_started_guard() {
|
||||
local permit_token permit_disk permit_commit extra record
|
||||
[ -f "$STARTED" ] && [ ! -L "$STARTED" ] || return 1
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
|
||||
{ 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" =~ ^[0-9a-f]{40}$ && "$permit_disk" =~ ^/dev/disk[0-9]+$ ]] || return 1
|
||||
[ "$permit_commit" = "$(cat "$STATE_DIR/source.commit")" ] || return 1
|
||||
[ -z "${TARGET_DISK:-}" ] || [ "$TARGET_DISK" = "$permit_disk" ] || return 1
|
||||
{
|
||||
IFS= read -r record || return 1
|
||||
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
|
||||
} < "$STARTED"
|
||||
[ "$record" = "$PROOF_TOKEN:$permit_commit:$permit_disk" ]
|
||||
}
|
||||
|
||||
select_target_disk() {
|
||||
""");
|
||||
installer = ReplaceOnce(installer, " echo \"[log] installation was already started; refusing to erase the target disk again\"\n exec /usr/libexec/recoveryosd\n exit 1", " owns_started_guard || fail \"existing installation guard is not owned by this token, commit and disk\"\n echo \"[log] owned installation is already running; duplicate child exits without changing its phase\"\n exit 0");
|
||||
installer = ReplaceOnce(installer, ": > \"$STARTED\" || fail \"failed to create installation guard\"", """
|
||||
if ! ( set -o noclobber; printf '%s:%s:%s\n' "$PROOF_TOKEN" "$(cat "$STATE_DIR/source.commit")" "$TARGET_DISK" > "$STARTED" ); then
|
||||
if owns_started_guard; then
|
||||
echo "[log] another owned child claimed installation; duplicate exits without changing its phase"
|
||||
exit 0
|
||||
fi
|
||||
fail "failed to create the exclusive owned installation guard"
|
||||
fi
|
||||
""");
|
||||
// The Full bootstrap wrapper keeps Apple's original daemon running.
|
||||
// An installer child must terminate rather than launch another copy.
|
||||
installer = ReplaceAllExact(installer, " exec /usr/libexec/recoveryosd\n", "", 2);
|
||||
installer = ReplaceOnce(installer, "set -u\n", "set -u\nPROOF_TOKEN=\"" + token + "\"\n" + """
|
||||
mark_installation_failed() {
|
||||
local state="${STATE_DIR:-/Volumes/installstate}" temporary
|
||||
@@ -410,6 +648,7 @@ static class NativeDiagnostic
|
||||
if (!writeSource)
|
||||
{
|
||||
await ValidateInstallerFailureReceipt(installer, output, token, cancellation);
|
||||
await ValidateInstallGuardChild(installer, output, token, cancellation);
|
||||
return;
|
||||
}
|
||||
File.WriteAllText(Path.Combine(source, "src/install/recovery/full-install.sh"), installer, new UTF8Encoding(false));
|
||||
@@ -441,6 +680,76 @@ static class NativeDiagnostic
|
||||
throw new InvalidOperationException("Installer terminal phase overwrote foreign run-owned state.");
|
||||
}
|
||||
|
||||
static async Task ValidateInstallGuardChild(string installer, string output, string token, CancellationToken cancellation)
|
||||
{
|
||||
// Exercise the generated Recovery shell's actual pre-erase control path.
|
||||
// Apple exec and rolling log snapshots are external boundaries; no VM,
|
||||
// native disk command, installer, daemon or application is invoked here.
|
||||
static string Between(string text, string start, string end)
|
||||
{
|
||||
var first = text.IndexOf(start, StringComparison.Ordinal);
|
||||
var last = first < 0 ? -1 : text.IndexOf(end, first, StringComparison.Ordinal);
|
||||
if (first < 0 || last <= first) throw new InvalidOperationException("Generated installer guard validation boundary changed: " + start);
|
||||
return text[first..last];
|
||||
}
|
||||
var functions = Between(installer, "mark_installation_failed() {", "installer_parent=$$") +
|
||||
Between(installer, "fail() {", "select_target_disk() {");
|
||||
var existing = Between(installer, "if [ -e \"$STARTED\" ]; then", "[ -s \"$ADMIN_PACKAGE\" ]");
|
||||
var claim = Between(installer, "# Everything needed for the unattended install", "if ! /usr/sbin/diskutil eraseDisk");
|
||||
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
|
||||
const string disk = "/dev/disk1";
|
||||
var ownGuard = token + ":" + commit + ":" + disk + "\n";
|
||||
var cases = new[]
|
||||
{
|
||||
(Name: "race", Initial: (string?)null, Race: true, WriteFailure: false, Success: true, Erase: false),
|
||||
(Name: "fresh-winner", Initial: (string?)null, Race: false, WriteFailure: false, Success: true, Erase: true),
|
||||
(Name: "already-owned", Initial: ownGuard, Race: false, WriteFailure: false, Success: true, Erase: false),
|
||||
(Name: "foreign-token", Initial: ownGuard.Replace(token, new string('f', 32)), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "foreign-commit", Initial: ownGuard.Replace(commit, new string('f', 40)), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "foreign-disk", Initial: ownGuard.Replace(disk, "/dev/disk2"), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "empty", Initial: "", Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "extra-record", Initial: ownGuard + ownGuard, Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "unterminated", Initial: ownGuard.TrimEnd('\n'), Race: false, WriteFailure: false, Success: false, Erase: false),
|
||||
(Name: "write-failure", Initial: (string?)null, Race: false, WriteFailure: true, Success: false, Erase: false)
|
||||
};
|
||||
foreach (var test in cases)
|
||||
{
|
||||
var state = Path.Combine(output, "installer-guard-" + test.Name + "-fixture");
|
||||
if (Directory.Exists(state)) throw new InvalidOperationException("Install-guard fixtures require a fresh validation output: " + state);
|
||||
Directory.CreateDirectory(state);
|
||||
File.WriteAllText(Path.Combine(state, "run.owner"), token + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
|
||||
File.WriteAllText(Path.Combine(state, "install.permit"), token + "\n" + disk + "\n" + commit + "\n");
|
||||
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
|
||||
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
|
||||
var guard = Path.Combine(state, test.WriteFailure ? "absent-parent/started" : "started");
|
||||
if (test.Initial is not null) File.WriteAllText(guard, test.Initial);
|
||||
var script = """
|
||||
set -u
|
||||
STATE_DIR="$1"; PROOF_TOKEN="$2"; TARGET_DISK="$3"
|
||||
STARTED="$4"; LOCAL_LOG="$STATE_DIR/local.log"
|
||||
STATE_LOG="$STATE_DIR/install.log"; STATE_LOG_LIMIT=1024
|
||||
APPLE_INSTALL_LOG="$STATE_DIR/apple.log"; APPLE_INSTALL_LOG_LIMIT=1024
|
||||
snapshot_log() { :; }
|
||||
exec() { printf '%s\n' "$*" >> "$STATE_DIR/apple-exec.log"; return 0; }
|
||||
""" + "\n" + functions + "\n" + existing + "\n" +
|
||||
// Publish another child's complete guard after the initial check.
|
||||
(test.Race ? "printf '%s:%s:%s\\n' \"$PROOF_TOKEN\" \"$(cat \"$STATE_DIR/source.commit\")\" \"$TARGET_DISK\" > \"$STARTED\"\n" : "") + claim +
|
||||
"printf 'guard-acquired\\n' > \"$STATE_DIR/erase-boundary-reached\"\n";
|
||||
var command = await Command("bash", ["-c", script, "generated-install-guard-validation", state, token, disk, guard], output, "installer-guard-" + test.Name, cancellation, requireSuccess: false);
|
||||
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
|
||||
var phaseUnchanged = phase == active;
|
||||
using var receipt = JsonDocument.Parse(phase);
|
||||
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "installation-failed";
|
||||
var appleExec = File.Exists(Path.Combine(state, "apple-exec.log"));
|
||||
var eraseReached = File.Exists(Path.Combine(state, "erase-boundary-reached"));
|
||||
var guardPreserved = test.Initial is not null ? File.ReadAllText(guard) == test.Initial : test.WriteFailure ? !File.Exists(guard) : File.ReadAllText(guard) == ownGuard;
|
||||
Save(Path.Combine(output, "installer-guard-" + test.Name + ".json"), new { command.ExitCode, phaseUnchanged, realFailure, appleExec, eraseReached, guardPreserved });
|
||||
if ((command.ExitCode == 0) != test.Success || test.Success && !phaseUnchanged || !test.Success && !realFailure || appleExec || eraseReached != test.Erase || !guardPreserved)
|
||||
throw new InvalidOperationException($"Generated install child contract failed ({test.Name}): exit={command.ExitCode}, phaseUnchanged={phaseUnchanged}, realFailure={realFailure}, appleExec={appleExec}, eraseReached={eraseReached}, guardPreserved={guardPreserved}.");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -450,12 +759,12 @@ static class NativeDiagnostic
|
||||
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);
|
||||
var drive = await Command("docker", ["exec", id, "qemu-img", "info", "--force-share", "--output=json", "/storage/13/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 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\\/13\\/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.");
|
||||
if (lines.Length != 3 || !lines.Any(line => line.StartsWith("file=/storage/13/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);
|
||||
@@ -478,7 +787,7 @@ static class NativeDiagnostic
|
||||
{
|
||||
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.");
|
||||
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 < 13 || 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.");
|
||||
}
|
||||
@@ -502,14 +811,14 @@ static class NativeDiagnostic
|
||||
{
|
||||
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)) });
|
||||
var good = JsonSerializer.Serialize(new { token, success = true, sourceCommit = commit, archiveSha256 = hash, sdkVersion = SdkVersion, osVersion = "13.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") })
|
||||
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("13.6.1", "12.6.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.");
|
||||
@@ -559,8 +868,8 @@ static class NativeDiagnostic
|
||||
{
|
||||
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.");
|
||||
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 13 || 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 13+/x86_64, service readiness and the writable 64-GiB disk.");
|
||||
}
|
||||
|
||||
static void AssertContainer(string json, string token)
|
||||
@@ -568,8 +877,31 @@ static class NativeDiagnostic
|
||||
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.");
|
||||
var devices = config.GetProperty("Devices");
|
||||
var mounts = container.GetProperty("Mounts");
|
||||
var environment = container.GetProperty("Config").GetProperty("Env").EnumerateArray().Select(value => value.GetString()).ToArray();
|
||||
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token
|
||||
|| config.GetProperty("Privileged").GetBoolean()
|
||||
|| config.GetProperty("NetworkMode").GetString() is not ("default" or "bridge")
|
||||
|| config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes
|
||||
|| config.GetProperty("MemorySwap").GetInt64() != ContainerMemoryBytes
|
||||
|| config.GetProperty("NanoCpus").GetInt64() != 2000000000
|
||||
|| config.GetProperty("ShmSize").GetInt64() != 536870912
|
||||
|| new[] { "CapAdd", "DeviceRequests", "Binds", "PortBindings", "DeviceCgroupRules", "Tmpfs" }.Any(key =>
|
||||
config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null
|
||||
&& (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any()))
|
||||
|| devices.GetArrayLength() != 1
|
||||
|| devices[0].GetProperty("PathOnHost").GetString() != "/dev/kvm"
|
||||
|| devices[0].GetProperty("PathInContainer").GetString() != "/dev/kvm"
|
||||
|| devices[0].GetProperty("CgroupPermissions").GetString() != "rw"
|
||||
|| mounts.GetArrayLength() != 1
|
||||
|| mounts[0].GetProperty("Type").GetString() != "volume"
|
||||
|| mounts[0].GetProperty("Destination").GetString() != "/storage"
|
||||
|| !mounts[0].GetProperty("RW").GetBoolean()
|
||||
|| new[] { "KVM=Y", "CPU_MODEL=host", "VERSION=13" }.Any(expected =>
|
||||
environment.Count(value => value is not null && value.StartsWith(expected.Split('=')[0] + "=", StringComparison.Ordinal)) != 1
|
||||
|| !environment.Contains(expected)))
|
||||
throw new InvalidOperationException("Created container exceeds the owned restricted KVM/host-CPU compatibility boundary.");
|
||||
}
|
||||
|
||||
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool full = false, bool final = false, string? token = null)
|
||||
@@ -578,6 +910,7 @@ static class NativeDiagnostic
|
||||
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
|
||||
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"), ("apfs-containers.plist", "apfs-containers.plist"), ("physical-store.plist", "physical-store.plist"), ("clt-catalog.log", "clt-catalog.log"), ("clt-install.log", "clt-install.log")]);
|
||||
if (full) files.Add(("clt-sdk.log", "clt-sdk.log"));
|
||||
foreach (var file in files)
|
||||
{
|
||||
var result = await Command("docker", ["exec", id, "cat", (full ? FullState : "/dev/shm/installstate") + "/" + file.Item1], output, "capture-" + file.Item1.Replace('/', '-'), cancellation, requireSuccess: false);
|
||||
@@ -594,7 +927,7 @@ static class NativeDiagnostic
|
||||
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);
|
||||
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/13/setup.dmg || sha256sum /storage/13/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)
|
||||
@@ -614,7 +947,7 @@ static class NativeDiagnostic
|
||||
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
|
||||
printf 'info kvm\ninfo 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"
|
||||
|
||||
@@ -126,6 +126,8 @@ static class NativeGuest
|
||||
await Cmd("/usr/bin/xcode-select", ["-p"], "clt-location");
|
||||
await Cmd("/usr/sbin/pkgutil", ["--pkg-info", "com.apple.pkg.CLTools_Executables"], "clt-package");
|
||||
await Cmd("/usr/bin/xcrun", ["swiftc", "--version"], "swift-version");
|
||||
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "apple-sdk-version");
|
||||
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "apple-sdk-path");
|
||||
RequireNoLinks(dotnet);
|
||||
actualSdk = (await Cmd(dotnet, ["--version"], "sdk-version")).Output.Trim();
|
||||
var sdkInfo = (await Cmd(dotnet, ["--info"], "sdk-info")).Output;
|
||||
@@ -294,14 +296,14 @@ static class NativeGuest
|
||||
|
||||
static void ValidateResult(FullResult result, Payload payload)
|
||||
{
|
||||
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 14 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests != ExpectedTests || result.Total != ExpectedTests || result.Executed != ExpectedTests || result.Passed != ExpectedTests || result.Failed != 0 || result.NotExecuted != 0 || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
|
||||
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 13 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests != ExpectedTests || result.Total != ExpectedTests || result.Executed != ExpectedTests || result.Passed != ExpectedTests || result.Failed != 0 || result.NotExecuted != 0 || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
|
||||
throw new InvalidOperationException("Native guest receipt does not prove this source, toolchain, signed artifacts and all expected tests.");
|
||||
}
|
||||
|
||||
static void RequirePlatform(string version, string architecture, string uid)
|
||||
{
|
||||
if (!Version.TryParse(version, out var parsed) || parsed.Major < 14 || architecture != "x86_64" || uid != "0")
|
||||
throw new InvalidOperationException("Native build requires installed macOS 14+/x86_64 running as root.");
|
||||
if (!Version.TryParse(version, out var parsed) || parsed.Major < 13 || architecture != "x86_64" || uid != "0")
|
||||
throw new InvalidOperationException("Native build requires installed macOS 13+/x86_64 running as root.");
|
||||
}
|
||||
static void RequireApfs(string xml)
|
||||
{
|
||||
@@ -430,8 +432,8 @@ static class NativeGuest
|
||||
Reject(() => ReadPayload(json.Replace("577", "572", StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal)));
|
||||
Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal)));
|
||||
RequirePlatform("14.6.1", "x86_64", "0");
|
||||
Reject(() => RequirePlatform("13.6.1", "x86_64", "0"));
|
||||
RequirePlatform("13.6.1", "x86_64", "0");
|
||||
Reject(() => RequirePlatform("12.6.1", "x86_64", "0"));
|
||||
Reject(() => RequirePlatform("14.6.1", "arm64", "0"));
|
||||
Reject(() => RequirePlatform("14.6.1", "x86_64", "501"));
|
||||
RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>");
|
||||
@@ -484,9 +486,9 @@ static class NativeGuest
|
||||
aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted");
|
||||
Reject(() => ValidateTrx(aborted.ToString(), ExpectedTests, started));
|
||||
var artifacts = NativeNames.Select(name => new NativeArtifact(name, new string('d', 64), "x86_64")).ToArray();
|
||||
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "14.6.1", "x86_64", SdkVersion, ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, new string('e', 64), "", started, DateTimeOffset.UtcNow);
|
||||
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "13.6.1", "x86_64", SdkVersion, ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, new string('e', 64), "", started, DateTimeOffset.UtcNow);
|
||||
ValidateResult(result, payload);
|
||||
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "13.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
|
||||
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "12.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
|
||||
Reject(() => ValidateResult(invalid, payload));
|
||||
var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N"));
|
||||
try
|
||||
|
||||
@@ -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
|
||||
@@ -67,6 +67,10 @@ printf '[firstboot] selected CLT: %s\n' "$label"
|
||||
/usr/sbin/pkgutil --pkg-info=com.apple.pkg.CLTools_Executables || fail clt_receipt_failed
|
||||
/usr/bin/xcrun --find swiftc || fail swiftc_missing
|
||||
/usr/bin/xcrun swiftc --version || fail swiftc_version_failed
|
||||
{
|
||||
/usr/bin/xcrun --sdk macosx --show-sdk-version &&
|
||||
/usr/bin/xcrun --sdk macosx --show-sdk-path
|
||||
} > "$STATE_DIR/clt-sdk.log" 2>&1 || fail clt_sdk_identity_failed
|
||||
printf 'import AppKit\nimport AVFoundation\nimport ScreenCaptureKit\nimport EventKit\nimport WebKit\nprint("native SDK ready")\n' > "$WORK/toolchain-smoke.swift"
|
||||
/usr/bin/xcrun swiftc -target x86_64-apple-macos13.0 "$WORK/toolchain-smoke.swift" -o "$WORK/toolchain-smoke" || fail native_framework_compile_failed
|
||||
"$WORK/toolchain-smoke" || fail native_framework_execution_failed
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# Full-only pre-.NET seam. Claim one persistent owned probe before forking;
|
||||
# launchd then execs the original Apple daemon, including on a real failure.
|
||||
set -u
|
||||
PROOF_TOKEN="@@PROOF_TOKEN@@"
|
||||
STATE_DIR="/Volumes/installstate"
|
||||
MARKER="$STATE_DIR/probe.started"
|
||||
|
||||
bootstrap_failed() {
|
||||
printf '[full-bootstrap] ERROR: %s\n' "$1" >&2
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
|
||||
printf '{"token":"%s","phase":"bootstrap-failed","reason":"%s"}\n' "$PROOF_TOKEN" "$1" > "$STATE_DIR/guest-phase.bootstrap.$$.tmp" &&
|
||||
/bin/mv -f "$STATE_DIR/guest-phase.bootstrap.$$.tmp" "$STATE_DIR/guest-phase.json"
|
||||
}
|
||||
|
||||
wait_for_owned_state() {
|
||||
local count=0
|
||||
while :; do
|
||||
if [ -e "$STATE_DIR/run.owner" ] || [ -L "$STATE_DIR/run.owner" ]; then
|
||||
[ -f "$STATE_DIR/run.owner" ] && [ ! -L "$STATE_DIR/run.owner" ] &&
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || {
|
||||
bootstrap_failed foreign_state_owner
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
fi
|
||||
if (( count >= 120 )); then
|
||||
bootstrap_failed state_share_mount_timeout
|
||||
return 1
|
||||
fi
|
||||
/sbin/mount_9p installstate >/dev/null 2>&1 || :
|
||||
count=$((count + 1))
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
owns_probe_marker() {
|
||||
local record extra
|
||||
[ -f "$MARKER" ] && [ ! -L "$MARKER" ] || return 1
|
||||
{
|
||||
IFS= read -r record || return 1
|
||||
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
|
||||
} < "$MARKER"
|
||||
[ "$record" = "$PROOF_TOKEN:$source_commit" ]
|
||||
}
|
||||
|
||||
claim_probe() {
|
||||
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || { bootstrap_failed invalid_probe_token; return 1; }
|
||||
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || { bootstrap_failed foreign_state_owner; return 1; }
|
||||
source_commit=$(cat "$STATE_DIR/source.commit" 2>/dev/null) || { bootstrap_failed source_commit_missing; return 1; }
|
||||
[[ "$source_commit" =~ ^[0-9a-f]{40}$ ]] || { bootstrap_failed source_commit_invalid; return 1; }
|
||||
if [ -e "$MARKER" ] || [ -L "$MARKER" ]; then
|
||||
owns_probe_marker || { bootstrap_failed foreign_or_invalid_probe_marker; return 1; }
|
||||
return 2
|
||||
fi
|
||||
# Keep partial/failed markers: an incomplete first start is an error, no retry.
|
||||
if ! ( set -o noclobber; printf '%s:%s\n' "$PROOF_TOKEN" "$source_commit" > "$MARKER" ); then
|
||||
bootstrap_failed probe_marker_write_failed
|
||||
return 1
|
||||
fi
|
||||
owns_probe_marker || { bootstrap_failed probe_marker_incomplete; return 1; }
|
||||
return 0
|
||||
}
|
||||
|
||||
if wait_for_owned_state && claim_probe; then
|
||||
(
|
||||
/bin/bash "$STATE_DIR/readiness.sh"
|
||||
child_exit=$?
|
||||
(( child_exit == 0 )) || bootstrap_failed first_probe_child_failed
|
||||
) &
|
||||
fi
|
||||
exec /usr/libexec/recoveryosd
|
||||
@@ -112,7 +112,10 @@ cancel_probe() {
|
||||
run_command() {
|
||||
local name="$1"
|
||||
shift
|
||||
local process timer exit_code started waited
|
||||
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
|
||||
@@ -122,9 +125,10 @@ run_command() {
|
||||
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 45 -u 9 unused || :
|
||||
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 || :
|
||||
@@ -196,7 +200,7 @@ run_command version /usr/bin/sw_vers -productVersion
|
||||
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
|
||||
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version
|
||||
flush_outputs || finish false diagnostic_log_budget_exceeded
|
||||
|
||||
# Bound readiness independently of the host's 40-minute overall deadline.
|
||||
|
||||
Reference in new issue
Block a user