|
|
@@ -14,8 +14,11 @@ return await NativeDiagnostic.Execute(args);
|
|
|
|
static class NativeDiagnostic
|
|
|
|
static class NativeDiagnostic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
|
|
|
|
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 Profile = "tcg-haswell-sonoma";
|
|
|
|
const string CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30";
|
|
|
|
const string CpuModel = "Haswell-noTSX";
|
|
|
|
|
|
|
|
const int DiagnosticMinutes = 20;
|
|
|
|
|
|
|
|
const string DiagnosticArguments = "-object iothread,id=io2 -no-reboot -no-shutdown -d int,cpu_reset,guest_errors,unimp";
|
|
|
|
|
|
|
|
const string CpuFlags = "Haswell-noTSX,l3-cache=on,+hypervisor,vendor=GenuineIntel,vmx=off,vmware-cpuid-freq=on,-pdpe1gb,-pcid,-invpcid,-tsc-deadline,-xsavec,-xsaves,+ssse3,+sse4.2,+popcnt,+avx,+avx2,+aes,+fma,+bmi1,+bmi2,+smep,+xsave,+xsaveopt,+xgetbv1,+movbe,+rdrand,enforce=on";
|
|
|
|
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
|
|
|
|
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
|
|
|
|
const string UdifChecksumBindingHash = "6109d04619e800c483fdac363d593cd1cd69f34131d2521417334e11d41c8bfa";
|
|
|
|
const string UdifChecksumBindingHash = "6109d04619e800c483fdac363d593cd1cd69f34131d2521417334e11d41c8bfa";
|
|
|
|
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
|
|
|
|
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
|
|
|
@@ -57,7 +60,7 @@ static class NativeDiagnostic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (args.Length == 0 || args.Contains("--help"))
|
|
|
|
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] [--cryptex-archive verified-release.zip]");
|
|
|
|
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone]");
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
|
|
|
|
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
|
|
|
@@ -66,10 +69,23 @@ static class NativeDiagnostic
|
|
|
|
ValidateContracts();
|
|
|
|
ValidateContracts();
|
|
|
|
if (Option(args, "--source") is { } source)
|
|
|
|
if (Option(args, "--source") is { } source)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
await PrepareSource(Path.GetFullPath(source), output, "validation", false, Option(args, "--cryptex-archive"), CancellationToken.None);
|
|
|
|
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
|
|
|
|
await ValidateResourceRetention(output);
|
|
|
|
await ValidateResourceRetention(output);
|
|
|
|
await ValidateRecoveryPatch(output);
|
|
|
|
await ValidateRecoveryPatch(output);
|
|
|
|
Save(Path.Combine(output, "validation.json"), new { success = true, profile = "kvm-host-ventura-cryptex", helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))), udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))), baselineReadinessNormalized = true, readinessDiagnosticBlocksExcluded = 7, productVersionParserBlockExcluded = true, productVersionSequenceRestored = true, productVersionMaximumBytes = 1024, nativeProductVersionCommandRemoved = true, diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 120, diskSampleLimitSeconds = 60, diskSampleDurationSeconds = 3, diskSampleIntervalMilliseconds = 100, resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12, independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cryptexArchiveVerified = true, configurationAndStagingContractsVerified = true, templateIsoDownloaded = false, activeLiluRuntimeChecked = false, sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow });
|
|
|
|
await ValidateTcgPreflight(output, CancellationToken.None);
|
|
|
|
|
|
|
|
Save(Path.Combine(output, "validation.json"), new
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
success = true, profile = Profile,
|
|
|
|
|
|
|
|
helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))),
|
|
|
|
|
|
|
|
udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))),
|
|
|
|
|
|
|
|
baselineReadinessNormalized = true, readinessDiagnosticBlocksExcluded = 7,
|
|
|
|
|
|
|
|
productVersionParserBlockExcluded = true, productVersionSequenceRestored = true, productVersionMaximumBytes = 1024, nativeProductVersionCommandRemoved = true,
|
|
|
|
|
|
|
|
diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 120, diskThreadObservationLimitSeconds = 60, stackSamplingUsed = false,
|
|
|
|
|
|
|
|
resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12,
|
|
|
|
|
|
|
|
independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cpuProfileSourceContractsVerified = true,
|
|
|
|
|
|
|
|
preflightGateFixtureCases = 8, qemuRuntimePreflightExecuted = false, templateIsoDownloaded = false,
|
|
|
|
|
|
|
|
sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow
|
|
|
|
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
|
|
|
|
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
|
|
|
|
return 0;
|
|
|
|
return 0;
|
|
|
@@ -86,7 +102,7 @@ static class NativeDiagnostic
|
|
|
|
Save(statePath, state);
|
|
|
|
Save(statePath, state);
|
|
|
|
Directory.CreateDirectory(work);
|
|
|
|
Directory.CreateDirectory(work);
|
|
|
|
File.WriteAllText(Path.Combine(work, "run.owner"), token);
|
|
|
|
File.WriteAllText(Path.Combine(work, "run.owner"), token);
|
|
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(40));
|
|
|
|
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(DiagnosticMinutes));
|
|
|
|
using var signal = OperatingSystem.IsLinux() ? PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); }) : null;
|
|
|
|
using var signal = OperatingSystem.IsLinux() ? PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); }) : null;
|
|
|
|
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
|
|
|
|
ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); };
|
|
|
|
Console.CancelKeyPress += cancelHandler;
|
|
|
|
Console.CancelKeyPress += cancelHandler;
|
|
|
@@ -98,7 +114,7 @@ static class NativeDiagnostic
|
|
|
|
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
|
|
|
|
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
|
|
|
|
ValidateContracts();
|
|
|
|
ValidateContracts();
|
|
|
|
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
|
|
|
|
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, 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 = 40 });
|
|
|
|
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = Profile, causalSingleVariableTest = false, kvm = false, cpuModel = CpuModel, recoveryMajor = 14, cpuFlags = CpuFlags, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = DiagnosticMinutes });
|
|
|
|
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
|
|
|
|
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
|
|
|
|
using (var document = JsonDocument.Parse(info.Output))
|
|
|
|
using (var document = JsonDocument.Parse(info.Output))
|
|
|
|
{
|
|
|
|
{
|
|
|
@@ -117,13 +133,13 @@ static class NativeDiagnostic
|
|
|
|
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
|
|
|
|
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
|
|
|
|
var actualCommit = (await Command("git", ["-C", source, "rev-parse", "HEAD"], output, "dockur-commit", deadline.Token)).Output.Trim();
|
|
|
|
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 (actualCommit != DockurCommit) throw new InvalidOperationException("Dockur source pin mismatch.");
|
|
|
|
await PrepareSource(source, output, token, true, Option(args, "--cryptex-archive"), deadline.Token);
|
|
|
|
await PrepareSource(source, output, token, true, deadline.Token);
|
|
|
|
await Command("docker", ["build", "--platform", "linux/amd64", "--label", OwnerLabel + "=" + token, "--tag", state.ImageTag, source], output, "docker-build", deadline.Token, echo: true);
|
|
|
|
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);
|
|
|
|
var imageInspect = await Command("docker", ["image", "inspect", state.ImageTag], output, "image-inspect", deadline.Token);
|
|
|
|
using (var image = JsonDocument.Parse(imageInspect.Output))
|
|
|
|
using (var image = JsonDocument.Parse(imageInspect.Output))
|
|
|
|
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
|
|
|
|
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
|
|
|
|
Save(statePath, state);
|
|
|
|
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", "--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", state.ImageTag], output, "docker-create", deadline.Token);
|
|
|
|
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", "CPU_MODEL=" + CpuModel, "--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=" + DiagnosticArguments, state.ImageTag], output, "docker-create", deadline.Token);
|
|
|
|
var id = create.Output.Trim();
|
|
|
|
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.");
|
|
|
|
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 };
|
|
|
|
state = state with { ContainerId = id };
|
|
|
@@ -132,7 +148,7 @@ static class NativeDiagnostic
|
|
|
|
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
|
|
|
|
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
|
|
|
|
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
|
|
|
|
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
|
|
|
|
await CapturePressure(id, output, "before", deadline.Token);
|
|
|
|
await CapturePressure(id, output, "before", deadline.Token);
|
|
|
|
Console.WriteLine("The owned restricted KVM/host-CPU macOS 13 compatibility guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run. This is not a single-variable causal test.");
|
|
|
|
Console.WriteLine("The owned unprivileged TCG/Haswell macOS 14 guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run. This is not a single-variable causal test.");
|
|
|
|
var recoveryStarted = Stopwatch.StartNew();
|
|
|
|
var recoveryStarted = Stopwatch.StartNew();
|
|
|
|
var heartbeat = Stopwatch.StartNew();
|
|
|
|
var heartbeat = Stopwatch.StartNew();
|
|
|
|
var diskPressureCaptured = false;
|
|
|
|
var diskPressureCaptured = false;
|
|
|
@@ -140,6 +156,7 @@ static class NativeDiagnostic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
deadline.Token.ThrowIfCancellationRequested();
|
|
|
|
deadline.Token.ThrowIfCancellationRequested();
|
|
|
|
await CaptureGuest(id, output, deadline.Token);
|
|
|
|
await CaptureGuest(id, output, deadline.Token);
|
|
|
|
|
|
|
|
await CheckRecoveryBootProgress(id, output, deadline.Token);
|
|
|
|
var proofPath = Path.Combine(output, "guest-proof.log");
|
|
|
|
var proofPath = Path.Combine(output, "guest-proof.log");
|
|
|
|
if (!diskPressureCaptured && File.Exists(proofPath) && File.ReadAllText(proofPath).Contains("[proof-start] disks", StringComparison.Ordinal))
|
|
|
|
if (!diskPressureCaptured && File.Exists(proofPath) && File.ReadAllText(proofPath).Contains("[proof-start] disks", StringComparison.Ordinal))
|
|
|
|
{
|
|
|
|
{
|
|
|
@@ -167,7 +184,7 @@ static class NativeDiagnostic
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception exception)
|
|
|
|
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 {DiagnosticMinutes}-minute diagnostic deadline or cancellation was reached." : exception.Message;
|
|
|
|
Console.Error.WriteLine(error);
|
|
|
|
Console.Error.WriteLine(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
finally
|
|
|
|
finally
|
|
|
@@ -194,10 +211,9 @@ static class NativeDiagnostic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
|
|
|
|
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
|
|
|
|
var baseline = NormalizeReadinessDiagnostics(readiness);
|
|
|
|
var baseline = NormalizeReadinessDiagnostics(readiness);
|
|
|
|
baseline = ReplaceOnce(baseline, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
|
|
|
|
|
|
|
|
baseline = ReplaceOnce(baseline, "while (( attempt < 1 && SECONDS - readiness_start < 600 )); do", "while (( SECONDS - readiness_start < 600 )); do");
|
|
|
|
baseline = ReplaceOnce(baseline, "while (( attempt < 1 && SECONDS - readiness_start < 600 )); do", "while (( SECONDS - readiness_start < 600 )); do");
|
|
|
|
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
|
|
|
|
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
|
|
|
|
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, the successful sw_vers version parser/sequence, the macOS minimum and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
|
|
|
|
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, the successful sw_vers version parser/sequence and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
|
|
|
|
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
|
|
|
|
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.");
|
|
|
|
throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper.");
|
|
|
|
if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist.");
|
|
|
|
if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist.");
|
|
|
@@ -207,18 +223,19 @@ static class NativeDiagnostic
|
|
|
|
ValidateDaemon(variant.Item2, variant.Item3, true);
|
|
|
|
ValidateDaemon(variant.Item2, variant.Item3, true);
|
|
|
|
if (Encoding.UTF8.GetByteCount(variant.Item2) > Encoding.UTF8.GetByteCount(variant.Item1)) throw new InvalidOperationException("Daemon replacement exceeds original file.");
|
|
|
|
if (Encoding.UTF8.GetByteCount(variant.Item2) > Encoding.UTF8.GetByteCount(variant.Item1)) throw new InvalidOperationException("Daemon replacement exceeds original file.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
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 });
|
|
|
|
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "14.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
|
|
|
|
ValidateResult(good, "validation");
|
|
|
|
ValidateResult(good, "validation");
|
|
|
|
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") })
|
|
|
|
ValidateBootProgress();
|
|
|
|
|
|
|
|
foreach (var invalid in new[] { good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
|
|
|
|
{
|
|
|
|
{
|
|
|
|
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
|
|
|
|
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
|
|
|
|
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
|
|
|
|
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var boundary = """
|
|
|
|
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}]}]
|
|
|
|
[{"Config":{"Labels":{"org.meeting-assistant.native-diagnostic":"validation"},"Env":["KVM=N","CPU_MODEL=Haswell-noTSX","VERSION=14"]},"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":[]},"Mounts":[{"Type":"volume","Destination":"/storage","RW":true}]}]
|
|
|
|
""";
|
|
|
|
""";
|
|
|
|
AssertContainer(boundary, "validation");
|
|
|
|
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") })
|
|
|
|
foreach (var invalid in new[] { boundary.Replace("\"Privileged\":false", "\"Privileged\":true"), boundary.Replace("\"Devices\":[]", "\"Devices\":[{\"PathOnHost\":\"/dev/kvm\",\"PathInContainer\":\"/dev/kvm\",\"CgroupPermissions\":\"rw\"}]"), boundary.Replace("KVM=N", "KVM=Y"), boundary.Replace("CPU_MODEL=Haswell-noTSX", "CPU_MODEL=host"), boundary.Replace("VERSION=14", "VERSION=13"), 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"), boundary.Replace("\"NanoCpus\":2000000000", "\"NanoCpus\":4000000000") })
|
|
|
|
{
|
|
|
|
{
|
|
|
|
try { AssertContainer(invalid, "validation"); } catch (InvalidOperationException) { continue; }
|
|
|
|
try { AssertContainer(invalid, "validation"); } catch (InvalidOperationException) { continue; }
|
|
|
|
throw new InvalidOperationException("Diagnostic validator accepted an excessive/wrong-profile container boundary.");
|
|
|
|
throw new InvalidOperationException("Diagnostic validator accepted an excessive/wrong-profile container boundary.");
|
|
|
@@ -257,7 +274,7 @@ static class NativeDiagnostic
|
|
|
|
return source.Remove(from, to + end.Length - from).Insert(from, originalSequence);
|
|
|
|
return source.Remove(from, to + end.Length - from).Insert(from, originalSequence);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static async Task PrepareSource(string source, string output, string token, bool writeSource, string? cryptexArchive, CancellationToken cancellation)
|
|
|
|
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
Directory.CreateDirectory(output);
|
|
|
|
Directory.CreateDirectory(output);
|
|
|
|
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
|
|
|
|
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
|
|
|
@@ -275,11 +292,23 @@ static class NativeDiagnostic
|
|
|
|
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");
|
|
|
|
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 = 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");
|
|
|
|
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);
|
|
|
|
dockerfile = ReplaceOnce(dockerfile, " gzip \\\n", " gzip \\\n nasm \\\n");
|
|
|
|
|
|
|
|
dockerfile = ReplaceOnce(dockerfile, "COPY --chmod=755 ./assets /assets/\n", "COPY --chmod=755 ./assets /assets/\nRUN nasm -f bin /assets/ci-cpu-preflight.asm -o /assets/ci-cpu-preflight.bin && test \"$(stat -c%s /assets/ci-cpu-preflight.bin)\" = 65536\n");
|
|
|
|
|
|
|
|
var boot = PrepareTcgBoot(source);
|
|
|
|
|
|
|
|
var cpuPath = Path.Combine(source, "src/cpu.sh");
|
|
|
|
|
|
|
|
var cpu = File.ReadAllText(cpuPath);
|
|
|
|
|
|
|
|
if (Hash(Encoding.UTF8.GetBytes(cpu)) != "0f3e4b4e1c3e17743d3a8d27b77a76424ceebb576b269283d612c489bc70993e") throw new InvalidOperationException("Pinned CPU composition script hash mismatch.");
|
|
|
|
|
|
|
|
cpu = ReplaceOnce(cpu, ",+movbe,+rdrand,check\"", ",+movbe,+rdrand,enforce=on\"");
|
|
|
|
|
|
|
|
var preflight = File.ReadAllText(Path.Combine("tools", "ci", "macos-tcg-cpu-preflight.asm"));
|
|
|
|
var entryPath = Path.Combine(source, "src/entry.sh");
|
|
|
|
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 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, ". cpu.sh # Configure CPU model\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");
|
|
|
|
entry = ReplaceOnce(entry, ". proc.sh # Initialize processor\n", "");
|
|
|
|
|
|
|
|
entry = ReplaceOnce(entry, ". init.sh # Initialize system\n", ". init.sh # Initialize system\n. cpu.sh # Compose the exact guest CPU before any Apple download\n. proc.sh # Compose the actual accelerator/CPU_FLAGS once\n" + TcgPreflight + "\n");
|
|
|
|
|
|
|
|
entry = ReplaceOnce(entry, "trap - ERR\n", "[[ \"$KVM_OPTS\" == ' -accel tcg,thread=multi' && \"$CPU_FLAGS\" == '" + CpuFlags + "' && \"$CPU_OPTS\" == \"-cpu $CPU_FLAGS -smp $SMP\" ]] || { error 'Supported profile refuses a CPU/accelerator fallback.'; exit 1; }\ninfo '[supported-profile] accelerator=tcg cpu=Haswell-noTSX recovery=14; AVX/AVX2 preflight passed; native guest gates still pending'\n\ntrap - ERR\n");
|
|
|
|
|
|
|
|
entry = ReplaceOnce(entry, "\ntrap - ERR\n", "\nprintf '%s\\n' '[supported-profile] accelerator=tcg cpu=Haswell-noTSX recovery=14; AVX/AVX2 preflight passed; native guest gates still pending' >> \"$QEMU_DIR/native-stage.log\"\ntrap - ERR\n");
|
|
|
|
|
|
|
|
// Preserve sparse boot markers independently of the bounded, verbose Docker trace.
|
|
|
|
|
|
|
|
entry = ReplaceOnce(entry, " -e 's/failed to load Boot/skipped Boot/g' \\\n", " -e 's/failed to load Boot/skipped Boot/g' \\\n -e '/^#\\[EB|LOG:HANDOFF TO XNU\\] /w /run/shm/kernel-handoffs.log' \\\n");
|
|
|
|
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
|
|
|
|
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
|
|
|
|
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
|
|
|
|
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
|
|
|
|
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"));
|
|
|
|
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"));
|
|
|
@@ -289,14 +318,16 @@ static class NativeDiagnostic
|
|
|
|
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");
|
|
|
|
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");
|
|
|
|
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");
|
|
|
|
image = ReplaceOnce(image, " if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n", " info \"[recovery-original] bytes=$(stat -c%s -- \"$image\") sha256=$(sha256sum \"$image\" | awk '{print $1}')\"\n if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n");
|
|
|
|
image = ReplaceOnce(image, " if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n", " info \"[recovery-original] bytes=$(stat -c%s -- \"$image\") sha256=$(sha256sum \"$image\" | awk '{print $1}')\"\n if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n");
|
|
|
|
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), ("recoveryosd-13-original.plist", OriginalDaemon13), ("recoveryosd-13-diagnostic.plist", DiagnosticDaemon13), ("early-bootstrap.sh", MountOnlyBootstrap), ("boot.sh.patched", compatibility.Boot), ("opencore-config.plist", compatibility.Config) })
|
|
|
|
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), ("recoveryosd-13-original.plist", OriginalDaemon13), ("recoveryosd-13-diagnostic.plist", DiagnosticDaemon13), ("early-bootstrap.sh", MountOnlyBootstrap), ("boot.sh.patched", boot), ("opencore-config.plist", File.ReadAllText(Path.Combine(source, "assets/config.plist"))), ("cpu.sh.patched", cpu), ("ci-cpu-preflight.asm", preflight) })
|
|
|
|
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
|
|
|
|
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 "udif_checksums.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 "recoveryosd-13-original.plist" or "recoveryosd-13-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))));
|
|
|
|
Save(Path.Combine(output, "source-hashes.json"), Directory.GetFiles(output).Where(path => Path.GetFileName(path) is "recovery-patch.py" or "udif_checksums.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 "recoveryosd-13-original.plist" or "recoveryosd-13-diagnostic.plist" or "early-bootstrap.sh" or "boot.sh.patched" or "opencore-config.plist" or "cpu.sh.patched" or "ci-cpu-preflight.asm").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
|
|
|
|
|
|
|
|
Save(Path.Combine(output, "cpu-preflight-source.json"), new { profile = Profile, cpuModel = CpuModel, cpuFlags = CpuFlags, expectedExitCode = 33, instructionProbeExecuted = false, qemuBinaryExecuted = false, sourceSha256 = Hash(Encoding.UTF8.GetBytes(preflight)) });
|
|
|
|
await Command("bash", ["-n", Path.Combine(output, "guest-launch.sh")], output, "guest-hook-syntax", cancellation);
|
|
|
|
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, "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, "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, "container-entry.sh")], output, "entry-syntax", cancellation);
|
|
|
|
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
|
|
|
|
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
|
|
|
|
|
|
|
|
await Command("bash", ["-n", Path.Combine(output, "cpu.sh.patched")], output, "cpu-composition-syntax", cancellation);
|
|
|
|
if (!writeSource) return;
|
|
|
|
if (!writeSource) return;
|
|
|
|
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
|
|
|
|
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
|
|
|
|
File.WriteAllText(Path.Combine(source, "src/install/recovery/udif_checksums.py"), checksumBinding, new UTF8Encoding(false));
|
|
|
|
File.WriteAllText(Path.Combine(source, "src/install/recovery/udif_checksums.py"), checksumBinding, new UTF8Encoding(false));
|
|
|
@@ -305,16 +336,9 @@ static class NativeDiagnostic
|
|
|
|
File.WriteAllText(imagePath, image, 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/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/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, "src/boot.sh"), boot, new UTF8Encoding(false));
|
|
|
|
File.WriteAllText(Path.Combine(source, "assets/config.plist"), compatibility.Config, new UTF8Encoding(false));
|
|
|
|
File.WriteAllText(cpuPath, cpu, new UTF8Encoding(false));
|
|
|
|
var target = Path.Combine(source, "assets", "native-compatibility");
|
|
|
|
File.WriteAllText(Path.Combine(source, "assets/ci-cpu-preflight.asm"), preflight, new UTF8Encoding(false));
|
|
|
|
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 void ValidateDaemon(string xml, string processType, bool patched)
|
|
|
|
static void ValidateDaemon(string xml, string processType, bool patched)
|
|
|
@@ -360,53 +384,29 @@ static class NativeDiagnostic
|
|
|
|
return string.Join("\n", lines.Select(line => new string(' ', spaces) + (line.Length > 0 ? line[common..] : "")));
|
|
|
|
return string.Join("\n", lines.Select(line => new string(' ', spaces) + (line.Length > 0 ? line[common..] : "")));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, CancellationToken cancellation)
|
|
|
|
static string PrepareTcgBoot(string source)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
|
|
|
|
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
|
|
|
|
var config = File.ReadAllText(Path.Combine(source, "assets", "config.plist"));
|
|
|
|
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.");
|
|
|
|
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 document = XDocument.Parse(config, LoadOptions.PreserveWhitespace);
|
|
|
|
var add = PlistValue(PlistValue(document.Root!.Element("dict")!, "Kernel"), "Add");
|
|
|
|
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" };
|
|
|
|
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.");
|
|
|
|
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>");
|
|
|
|
var nvram = PlistValue(PlistValue(PlistValue(document.Root!.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82");
|
|
|
|
add.Elements("dict").First().AddAfterSelf(cryptex);
|
|
|
|
if (PlistValue(nvram, "boot-args").Value != "keepsyms=1 debug=0x100 -lilubeta -wegbeta vmhState=enabled") throw new InvalidOperationException("Pinned kernel diagnostic arguments differ.");
|
|
|
|
var bootArguments = PlistValue(PlistValue(PlistValue(PlistValue(document.Root.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82"), "boot-args").Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
const string diagnosticBoot = """
|
|
|
|
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.");
|
|
|
|
local diagnostic_boot='-v keepsyms=1 debug=0x108 serial=5 msgbuf=1048576 -lilubeta -wegbeta vmhState=enabled'
|
|
|
|
boot = ReplaceOnce(boot, " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n", " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n" + CompatibilityStaging + "\n");
|
|
|
|
local boot_args="/plist/dict/key[.='NVRAM']/following-sibling::dict[1]/key[.='Add']/following-sibling::dict[1]/key[.='7C436110-AB2A-4BBB-A880-FE41995C9F82']/following-sibling::dict[1]/key[.='boot-args']/following-sibling::string[1]"
|
|
|
|
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");
|
|
|
|
xmlstarlet ed -P -L -u "$boot_args" -v "$diagnostic_boot" "$CFG" || exit 12
|
|
|
|
boot = ReplaceOnce(boot, " checkOpenCoreConfig\n addVmHideKext\n", " checkOpenCoreConfig\n" + CompatibilityConfigCheck + "\n addVmHideKext\n");
|
|
|
|
[ "$(xmlstarlet sel -t -v "$boot_args" "$CFG")" = "$diagnostic_boot" ] || { error 'Kernel diagnostic arguments were not installed.'; exit 12; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
""";
|
|
|
|
|
|
|
|
boot = ReplaceOnce(boot, " # DEBUG logging goes only to the OpenCore log file on the EFI partition.\n", diagnosticBoot + " # DEBUG logging goes only to the OpenCore log file on the EFI partition.\n");
|
|
|
|
|
|
|
|
boot = ReplaceOnce(boot, " PLIST=\"/assets/config.plist\"\n", " [ ! -e /custom.plist ] || { error 'Supported profile refuses an unverified custom OpenCore config!'; exit 12; }\n PLIST=\"/assets/config.plist\"\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, " 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");
|
|
|
|
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"PROFILE=tcg-haswell-sonoma\"\n");
|
|
|
|
return (boot, document.ToString(), assets);
|
|
|
|
return boot;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static XElement PlistValue(XElement dictionary, string key)
|
|
|
|
static XElement PlistValue(XElement dictionary, string key)
|
|
|
@@ -416,32 +416,81 @@ static class NativeDiagnostic
|
|
|
|
return value;
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const string CompatibilityStaging = """
|
|
|
|
static readonly string TcgPreflight = """
|
|
|
|
# Only the freshly extracted, owned guest EFI is changed; never the host.
|
|
|
|
# Realize this exact TCG model and execute AVX/AVX2 before Apple downloads.
|
|
|
|
local lilu="$EFI_DIR/OC/Kexts/Lilu.kext/Contents"
|
|
|
|
disabled "$KVM" && [[ "$ARCH" == amd64 && "$CPU_MODEL" == Haswell-noTSX && "$VERSION" == 14 && "$CPU_FLAGS" == '@@CPU_FLAGS@@' ]] || { error 'Supported probe requires the exact TCG/Haswell/macOS 14 profile.'; exit 1; }
|
|
|
|
printf '%s %s\n' \
|
|
|
|
[[ "$(qemu-system-x86_64 --version | head -n 1)" == 'QEMU emulator version 11.1.1 (Reims 11.1.3)' ]] || { error 'Pinned QEMU runtime version mismatch.'; exit 1; }
|
|
|
|
fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a "$lilu/Info.plist" \
|
|
|
|
printf '%s %s\n' c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549 /assets/ci-cpu-preflight.bin | sha256sum -c - || { error 'Compiled AVX/AVX2 preflight ROM hash mismatch.'; exit 1; }
|
|
|
|
0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52 "$lilu/MacOS/Lilu" | sha256sum -c - || { error "Pinned active Lilu files mismatch!"; exit 12; }
|
|
|
|
probeTcgInstructions() {
|
|
|
|
[ "$(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; }
|
|
|
|
/usr/bin/timeout --signal=TERM --kill-after=2 10 qemu-system-x86_64 \
|
|
|
|
[ ! -e "$EFI_DIR/OC/Kexts/CryptexFixup.kext" ] || { error "Unexpected pre-existing Cryptex kext!"; exit 12; }
|
|
|
|
-machine q35 -accel tcg,thread=multi -cpu "$1" -smp 2 -m 64 -bios /assets/ci-cpu-preflight.bin \
|
|
|
|
(cd /assets/native-compatibility && sha256sum -c SHA256SUMS) || { error "Pinned Cryptex staging files mismatch!"; exit 12; }
|
|
|
|
-nodefaults -display none -serial none -monitor none -nographic -no-reboot \
|
|
|
|
cp -a /assets/native-compatibility/CryptexFixup.kext "$EFI_DIR/OC/Kexts/"
|
|
|
|
-device isa-debug-exit,iobase=0xf4,iosize=0x04
|
|
|
|
(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"
|
|
|
|
if probeTcgInstructions "$CPU_FLAGS" > "$QEMU_DIR/cpu-preflight-positive.log" 2>&1; then positive=0; else positive=$?; fi
|
|
|
|
""";
|
|
|
|
cat "$QEMU_DIR/cpu-preflight-positive.log"
|
|
|
|
|
|
|
|
(( positive == 33 )) || { error "Actual TCG AVX/AVX2 instruction preflight failed: exit=$positive"; exit 1; }
|
|
|
|
|
|
|
|
if probeTcgInstructions "$CPU_FLAGS,-avx2" > "$QEMU_DIR/cpu-preflight-negative.log" 2>&1; then negative=0; else negative=$?; fi
|
|
|
|
|
|
|
|
cat "$QEMU_DIR/cpu-preflight-negative.log"
|
|
|
|
|
|
|
|
(( negative != 33 )) || { error 'AVX2-disabled negative control unexpectedly passed.'; exit 1; }
|
|
|
|
|
|
|
|
info "[cpu-preflight] positive=$positive negative=$negative cpu=$CPU_FLAGS; actual AVX/AVX2 executed before Apple download"
|
|
|
|
|
|
|
|
printf '[cpu-preflight] positive=%s negative=%s cpu=%s; actual AVX/AVX2 executed before Apple download\n' "$positive" "$negative" "$CPU_FLAGS" > "$QEMU_DIR/native-stage.log"
|
|
|
|
|
|
|
|
""".Replace("@@CPU_FLAGS@@", CpuFlags, StringComparison.Ordinal);
|
|
|
|
|
|
|
|
|
|
|
|
const string CompatibilityConfigCheck = """
|
|
|
|
static async Task ValidateTcgPreflight(string output, CancellationToken cancellation)
|
|
|
|
local kernel='/plist/dict/key[.="Kernel"]/following-sibling::dict[1]/key[.="Add"]/following-sibling::array[1]'
|
|
|
|
{
|
|
|
|
local actual expected
|
|
|
|
var fixture = Path.Combine(output, "validation-cpu-preflight-gate");
|
|
|
|
actual=$(xmlstarlet sel -T -t -m "$kernel/dict" -v 'key[.="BundlePath"]/following-sibling::string[1]' -n "$CFG") || exit 12
|
|
|
|
Directory.CreateDirectory(fixture);
|
|
|
|
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
|
|
|
|
var entry = File.ReadAllText(Path.Combine(output, "container-entry.sh"));
|
|
|
|
[ "$actual" = "$expected" ] || { error "Active Kernel.Add order mismatch!"; exit 12; }
|
|
|
|
if (entry.IndexOf(TcgPreflight, StringComparison.Ordinal) >= entry.IndexOf(". download.sh", StringComparison.Ordinal)
|
|
|
|
[ "$(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; }
|
|
|
|
|| entry.Split(". cpu.sh", StringSplitOptions.None).Length != 2
|
|
|
|
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
|
|
|
|
|| entry.Split(". proc.sh", StringSplitOptions.None).Length != 2)
|
|
|
|
expected=$(printf '%s\n' x86_64 Contents/MacOS/CryptexFixup Contents/Info.plist 22.0.0 '')
|
|
|
|
throw new InvalidOperationException("Actual composed CPU preflight must execute once before any Apple download.");
|
|
|
|
[ "$actual" = "$expected" ] || { error "Active Cryptex Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
|
|
|
|
var cases = new[]
|
|
|
|
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
|
|
|
|
{
|
|
|
|
""";
|
|
|
|
("positive-negative-exit", 33, 0, "14", CpuFlags, true, true),
|
|
|
|
|
|
|
|
("positive-negative-timeout", 33, 124, "14", CpuFlags, true, true),
|
|
|
|
|
|
|
|
("positive-normal-exit", 0, 0, "14", CpuFlags, true, false),
|
|
|
|
|
|
|
|
("positive-timeout", 124, 0, "14", CpuFlags, true, false),
|
|
|
|
|
|
|
|
("negative-passed", 33, 33, "14", CpuFlags, true, false),
|
|
|
|
|
|
|
|
("wrong-recovery", 33, 0, "13", CpuFlags, true, false),
|
|
|
|
|
|
|
|
("wrong-cpu-flags", 33, 0, "14", CpuFlags.Replace("enforce=on", "check"), true, false),
|
|
|
|
|
|
|
|
("wrong-rom-hash", 33, 0, "14", CpuFlags, false, false)
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
foreach (var item in cases)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var work = Path.Combine(fixture, item.Item1);
|
|
|
|
|
|
|
|
Directory.CreateDirectory(work);
|
|
|
|
|
|
|
|
var script = """
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
disabled() { [ "$1" = N ]; }
|
|
|
|
|
|
|
|
error() { printf '%s\n' "$*" >&2; }
|
|
|
|
|
|
|
|
info() { printf '%s\n' "$*"; }
|
|
|
|
|
|
|
|
qemu-system-x86_64() { printf '%s\n' 'QEMU emulator version 11.1.1 (Reims 11.1.3)'; }
|
|
|
|
|
|
|
|
sha256sum() { cat >/dev/null; return "@@HASH_EXIT@@"; }
|
|
|
|
|
|
|
|
mock_timeout() {
|
|
|
|
|
|
|
|
printf '[fixture-command] %s\n' "$*"
|
|
|
|
|
|
|
|
case "$*" in *,-avx2*) return @@NEGATIVE@@ ;; *) return @@POSITIVE@@ ;; esac
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
KVM=N; ARCH=amd64; CPU_MODEL=Haswell-noTSX; VERSION='@@VERSION@@'
|
|
|
|
|
|
|
|
CPU_FLAGS='@@FLAGS@@'; QEMU_DIR='@@WORK@@'
|
|
|
|
|
|
|
|
""".Replace("@@HASH_EXIT@@", item.Item6 ? "0" : "1", StringComparison.Ordinal)
|
|
|
|
|
|
|
|
.Replace("@@NEGATIVE@@", item.Item3.ToString(), StringComparison.Ordinal)
|
|
|
|
|
|
|
|
.Replace("@@POSITIVE@@", item.Item2.ToString(), StringComparison.Ordinal)
|
|
|
|
|
|
|
|
.Replace("@@VERSION@@", item.Item4, StringComparison.Ordinal)
|
|
|
|
|
|
|
|
.Replace("@@FLAGS@@", item.Item5, StringComparison.Ordinal)
|
|
|
|
|
|
|
|
.Replace("@@WORK@@", work.Replace("'", "'\\''", StringComparison.Ordinal), StringComparison.Ordinal)
|
|
|
|
|
|
|
|
+ "\n" + TcgPreflight.Replace("/usr/bin/timeout", "mock_timeout", StringComparison.Ordinal)
|
|
|
|
|
|
|
|
+ "\nprintf '[fixture] Apple download reached after gate\\n'\n";
|
|
|
|
|
|
|
|
var path = Path.Combine(work, "fixture.sh");
|
|
|
|
|
|
|
|
File.WriteAllText(path, script, new UTF8Encoding(false));
|
|
|
|
|
|
|
|
var result = await Command("bash", [path], work, "gate", cancellation, requireSuccess: false);
|
|
|
|
|
|
|
|
var reached = result.Output.Contains("Apple download reached after gate", StringComparison.Ordinal);
|
|
|
|
|
|
|
|
Save(Path.Combine(work, "receipt.json"), new { success = (result.ExitCode == 0) == item.Item7 && reached == item.Item7, result.ExitCode, reachedAppleDownloadSeam = reached, qemuExecuted = false });
|
|
|
|
|
|
|
|
if ((result.ExitCode == 0) != item.Item7 || reached != item.Item7)
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Actual TCG preflight gate fixture failed: " + item.Item1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
|
|
|
|
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
|
|
|
|
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
|
|
|
|
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
|
|
|
@@ -455,8 +504,8 @@ static class NativeDiagnostic
|
|
|
|
{
|
|
|
|
{
|
|
|
|
using var document = JsonDocument.Parse(json);
|
|
|
|
using var document = JsonDocument.Parse(json);
|
|
|
|
var result = document.RootElement;
|
|
|
|
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 < 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))
|
|
|
|
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 13+/x86_64, service readiness and the writable 64-GiB disk.");
|
|
|
|
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 14+/x86_64, service readiness and the writable 64-GiB disk.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void AssertContainer(string json, string token)
|
|
|
|
static void AssertContainer(string json, string token)
|
|
|
@@ -477,34 +526,74 @@ static class NativeDiagnostic
|
|
|
|
|| new[] { "CapAdd", "DeviceRequests", "Binds", "PortBindings", "DeviceCgroupRules", "Tmpfs" }.Any(key =>
|
|
|
|
|| new[] { "CapAdd", "DeviceRequests", "Binds", "PortBindings", "DeviceCgroupRules", "Tmpfs" }.Any(key =>
|
|
|
|
config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null
|
|
|
|
config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null
|
|
|
|
&& (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any()))
|
|
|
|
&& (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any()))
|
|
|
|
|| devices.GetArrayLength() != 1
|
|
|
|
|| devices.GetArrayLength() != 0
|
|
|
|
|| devices[0].GetProperty("PathOnHost").GetString() != "/dev/kvm"
|
|
|
|
|
|
|
|
|| devices[0].GetProperty("PathInContainer").GetString() != "/dev/kvm"
|
|
|
|
|
|
|
|
|| devices[0].GetProperty("CgroupPermissions").GetString() != "rw"
|
|
|
|
|
|
|
|
|| mounts.GetArrayLength() != 1
|
|
|
|
|| mounts.GetArrayLength() != 1
|
|
|
|
|| mounts[0].GetProperty("Type").GetString() != "volume"
|
|
|
|
|| mounts[0].GetProperty("Type").GetString() != "volume"
|
|
|
|
|| mounts[0].GetProperty("Destination").GetString() != "/storage"
|
|
|
|
|| mounts[0].GetProperty("Destination").GetString() != "/storage"
|
|
|
|
|| !mounts[0].GetProperty("RW").GetBoolean()
|
|
|
|
|| !mounts[0].GetProperty("RW").GetBoolean()
|
|
|
|
|| new[] { "KVM=Y", "CPU_MODEL=host", "VERSION=13" }.Any(expected =>
|
|
|
|
|| new[] { "KVM=N", "CPU_MODEL=" + CpuModel, "VERSION=14" }.Any(expected =>
|
|
|
|
environment.Count(value => value is not null && value.StartsWith(expected.Split('=')[0] + "=", StringComparison.Ordinal)) != 1
|
|
|
|
environment.Count(value => value is not null && value.StartsWith(expected.Split('=')[0] + "=", StringComparison.Ordinal)) != 1
|
|
|
|
|| !environment.Contains(expected)))
|
|
|
|
|| !environment.Contains(expected)))
|
|
|
|
throw new InvalidOperationException("Created container exceeds the owned restricted KVM/host-CPU compatibility boundary.");
|
|
|
|
throw new InvalidOperationException("Created container exceeds the owned unprivileged TCG/Haswell/macOS 14 boundary.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool final = false, string? token = null)
|
|
|
|
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool final = false, string? token = null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (final && token is not null) await CaptureMonitor(id, output, token, cancellation);
|
|
|
|
if (final && token is not null) await CaptureMonitor(id, output, token, cancellation);
|
|
|
|
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
|
|
|
|
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
|
|
|
|
|
|
|
|
var stage = await Command("docker", ["exec", id, "cat", "/run/shm/native-stage.log"], output, "capture-native-stage", cancellation, requireSuccess: false);
|
|
|
|
|
|
|
|
ReportCpuPreflight(output, stage.ExitCode == 0 ? stage.Output : logs.Output);
|
|
|
|
|
|
|
|
await Command("docker", ["exec", id, "head", "-c", "4096", "/run/shm/kernel-handoffs.log"], output, "capture-kernel-handoffs", cancellation, requireSuccess: false, retainSuccessful: true);
|
|
|
|
foreach (var file in new[] { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") })
|
|
|
|
foreach (var file in new[] { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") })
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
|
|
|
|
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
|
|
|
|
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
|
|
|
|
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// The immutable Recovery image is complete only after this staging marker.
|
|
|
|
// The immutable Recovery image is complete only after this staging marker.
|
|
|
|
// Hash it once instead of rereading 710 MB on every twenty-second poll.
|
|
|
|
// Hash it once instead of rereading the image on every twenty-second poll.
|
|
|
|
if (logs.Output.Contains("[compatibility-profile] accelerator=kvm", StringComparison.Ordinal)
|
|
|
|
if ((logs.Output + stage.Output).Contains("[supported-profile] accelerator=tcg", StringComparison.Ordinal)
|
|
|
|
&& !File.Exists(Path.Combine(output, "guest-container-resources.last-success.json")))
|
|
|
|
&& !File.Exists(Path.Combine(output, "guest-container-resources.last-success.json")))
|
|
|
|
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 || exit 1; 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, retainSuccessful: true);
|
|
|
|
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 || exit 1; 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, retainSuccessful: true);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void ReportCpuPreflight(string output, string logs)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var receipt = Path.Combine(output, "cpu-preflight-runtime.json");
|
|
|
|
|
|
|
|
if (File.Exists(receipt)) return;
|
|
|
|
|
|
|
|
var expectedSuffix = " cpu=" + CpuFlags + "; actual AVX/AVX2 executed before Apple download";
|
|
|
|
|
|
|
|
foreach (var line in logs.Split('\n'))
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var match = System.Text.RegularExpressions.Regex.Match(line, @"\[cpu-preflight\] positive=33 negative=([0-9]{1,3})");
|
|
|
|
|
|
|
|
if (!match.Success || match.Groups[1].Value == "33" || !line[(match.Index + match.Length)..].StartsWith(expectedSuffix, StringComparison.Ordinal)) continue;
|
|
|
|
|
|
|
|
var sourceMarker = match.Value + expectedSuffix;
|
|
|
|
|
|
|
|
var marker = "[cpu-preflight] positive=33 negative=" + match.Groups[1].Value + " accelerator=tcg cpu=" + CpuModel + " instructions=AVX/AVX2";
|
|
|
|
|
|
|
|
Console.WriteLine(marker);
|
|
|
|
|
|
|
|
Save(receipt, new { marker, markerSha256 = Hash(Encoding.UTF8.GetBytes(sourceMarker)), capturedUtc = DateTimeOffset.UtcNow, readinessGateSatisfied = false });
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static int KernelHandoffs(string logs) => logs.Split('\n').Count(line => line.Trim().StartsWith("#[EB|LOG:HANDOFF TO XNU] ", StringComparison.Ordinal));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static void ValidateBootProgress()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
const string handoff = "#[EB|LOG:HANDOFF TO XNU] _\r\n";
|
|
|
|
|
|
|
|
foreach (var (logs, count) in new[] { ("", 0), ("BdsDxe: starting Boot0002\n", 0), (handoff, 1), (handoff + handoff, 2), ("source says \"" + handoff, 0), ("#[EB|LOG:HANDOFF TO XNU-ish] _\n", 0) })
|
|
|
|
|
|
|
|
if (KernelHandoffs(logs) != count) throw new InvalidOperationException("Recovery boot-progress parser accepted missing, quoted or malformed markers.");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async Task CheckRecoveryBootProgress(string id, string output, CancellationToken cancellation)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
var retained = Path.Combine(output, "capture-kernel-handoffs.last-success.stdout.log");
|
|
|
|
|
|
|
|
var count = KernelHandoffs(File.ReadAllText(File.Exists(retained) ? retained : Path.Combine(output, "container.stdout.log")));
|
|
|
|
|
|
|
|
Save(Path.Combine(output, "recovery-boot-progress.json"), new { kernelHandoffs = count, unexpectedRepeat = count >= 2, capturedUtc = DateTimeOffset.UtcNow });
|
|
|
|
|
|
|
|
if (count >= 2) throw new InvalidOperationException("Recovery returned to kernel boot before readiness; repeated handoff detected. See serial/exception logs; this does not identify the reset cause.");
|
|
|
|
|
|
|
|
if (!File.ReadAllText(Path.Combine(output, "capture-native-stage.stdout.log")).Contains("[supported-profile] accelerator=tcg", StringComparison.Ordinal)) return;
|
|
|
|
|
|
|
|
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
|
|
|
|
|
|
|
deadline.CancelAfter(TimeSpan.FromSeconds(8));
|
|
|
|
|
|
|
|
var monitor = await Command("docker", ["exec", id, "sh", "-c", "test -S /run/shm/monitor.sock || exit 1; printf 'info status\\n' | /usr/bin/timeout -s KILL 5 /usr/bin/nc.openbsd -q 1 -w 2 -U /run/shm/monitor.sock"], output, "recovery-vm-status", deadline.Token, requireSuccess: false);
|
|
|
|
|
|
|
|
if (monitor.ExitCode == 0 && System.Text.RegularExpressions.Regex.IsMatch(monitor.Output, @"(?m)^VM status: (shutdown|paused|internal-error|guest-panicked)(?:\s+\([^\r\n]*\))?\r?$"))
|
|
|
|
|
|
|
|
throw new InvalidOperationException("Recovery VM halted before readiness. The first reset was retained for exception/monitor evidence.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static async Task CapturePressure(string id, string output, string phase, CancellationToken cancellation)
|
|
|
|
static async Task CapturePressure(string id, string output, string phase, CancellationToken cancellation)
|
|
|
|