forked from Manuel/meeting-assistant
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3aaab45b46 | ||
|
|
efc3fdc681 |
@@ -6,7 +6,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
macos-native-diagnostic:
|
macos-native-diagnostic:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 95
|
timeout-minutes: 25
|
||||||
env:
|
env:
|
||||||
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
|
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
|
||||||
DOTNET_NOLOGO: "1"
|
DOTNET_NOLOGO: "1"
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ The existing Intel Celeron 1037U has neither AVX nor AVX2. KVM run 4187 at `720a
|
|||||||
|
|
||||||
[CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/kern_start.cpp) selects the installed/updated Rosetta Cryptex and patches APFS hash checking; it does not replace the running Recovery cache or emulate instructions. macOS 13 is outside the [.NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This candidate therefore uses macOS 14 and software CPU emulation without Cryptex. It changes the compatibility profile, not one isolated causal variable; actual success must be measured.
|
[CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/kern_start.cpp) selects the installed/updated Rosetta Cryptex and patches APFS hash checking; it does not replace the running Recovery cache or emulate instructions. macOS 13 is outside the [.NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This candidate therefore uses macOS 14 and software CPU emulation without Cryptex. It changes the compatibility profile, not one isolated causal variable; actual success must be measured.
|
||||||
|
|
||||||
Earlier TCG run 4159 observed guest AVX2. Runs 4161/4163 measured slow native startup and reached the 40-minute host limit before readiness. They predated the UDIF CRC repair at `94a70b2`, reuse of successful sw_vers output and capturing the large Recovery hash only once. They do not qualify this candidate. Host/workflow limits are 90/95 minutes; a readiness pass does not establish that full installation/build/tests fit the pipeline.
|
Earlier TCG run 4159 observed guest AVX2 with the upstream-selected Skylake model. Runs 4161/4163 measured slow native startup and reached the 40-minute host limit before readiness. They predated the UDIF CRC repair at `94a70b2`, reuse of successful sw_vers output and capturing the large Recovery hash only once. They do not qualify this candidate.
|
||||||
|
|
||||||
|
Run 4188 at `c01ae13` passed the actual AVX/AVX2 ROM test (positive exit 33, negative exit 0), then recorded 93 UEFI starts and 87 XNU handoffs before its 90-minute deadline. Container restart count, CPU throttling and OOM events were zero; no guest hook proof appeared. This establishes a guest boot loop, without identifying its cause. The next diagnostic preserves the same CPU/OS profile and has host/workflow limits of 20/25 minutes. Its purpose is to capture the first failure, not qualify full-run performance.
|
||||||
|
|
||||||
|
Kernel arguments add `-v debug=0x108 serial=5 msgbuf=1048576` while preserving the other pinned arguments, following [OpenCore 1.0.7](https://raw.githubusercontent.com/acidanthera/OpenCorePkg/1.0.7/Docs/Configuration.tex). Actual VM arguments add `-no-reboot -no-shutdown` and `-d int,cpu_reset,guest_errors,unimp`. The [QEMU reset policy](https://github.com/qemu/qemu/blob/v11.1.1/system/runstate.c) pauses the VM after a requested reset so monitor/framebuffer evidence survives. Exception output uses two bounded 4-MiB Docker log files. CPU/staging markers and sparse kernel-handoff lines are retained separately; repeated handoff or halted VM status fails immediately. These diagnostics do not prove a particular panic, CPU deficiency, or completed native test.
|
||||||
|
|
||||||
|
Run 4189 at `efc3fdc` stopped the first reset within about six minutes of container startup, with `VM status: paused (shutdown)`, one handoff and successful cleanup. Its retained trace started at exception 9517 and showed repeated supervisor instruction-fetch pagefaults at RIP/CR2 `0x24b0`; the preceding cause was missing. The current producer therefore retains the first 2 MiB after the kernel handoff before Docker rotation, while passing all output onward. This small AWK filter is part of the existing container boot integration and runs before a guest SDK exists; orchestration remains C#/.NET. Final capture retrieves the available Docker log files, the separate first-context file and monitor register/stack state.
|
||||||
|
|
||||||
|
The current candidate restores `Skylake-Client-v4` and the upstream TCG `-spec-ctrl` mask used by run 4159. `enforce=on`, actual instruction preflight, macOS 14, resource budgets and Readiness gates remain. This tests a previously booted source-bound profile; it does not assert that Haswell caused the earlier fault.
|
||||||
|
|
||||||
## Entry points and dependencies
|
## Entry points and dependencies
|
||||||
|
|
||||||
@@ -27,7 +35,7 @@ The native diagnostic workflow is manual only. Temporary diagnostic branches are
|
|||||||
|
|
||||||
The existing daemon must be Linux/x64 with two CPUs and 6 GiB memory; the runner must have 5 GiB available memory and the Docker filesystem 8 GiB free. These checks do not reconfigure resources. Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, both imported QEMU image digests and original source seams remain pinned.
|
The existing daemon must be Linux/x64 with two CPUs and 6 GiB memory; the runner must have 5 GiB available memory and the Docker filesystem 8 GiB free. These checks do not reconfigure resources. Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, both imported QEMU image digests and original source seams remain pinned.
|
||||||
|
|
||||||
Actual `Haswell-noTSX` CPU flags under TCG use `enforce=on` to reject unsupported requests. The CPU preflight uses that same composed flag list and QEMU binary before Recovery download/boot. `tools/ci/macos-tcg-cpu-preflight.asm` enables long mode/YMM state, executes AVX and AVX2 integer arithmetic, and checks an Int32 from the upper 128-bit lane. Only the correct result reaches [QEMU debug-exit](https://github.com/qemu/qemu/blob/v11.1.1/hw/misc/debugexit.c) code 33. No disks/network attach; failure/timeout fails preflight. This tests that instruction chain, not the complete ISA or macOS.
|
Actual `Skylake-Client-v4` CPU flags under TCG use `enforce=on` to reject unsupported requests. The CPU preflight uses that same composed flag list and QEMU binary before Recovery download/boot. `tools/ci/macos-tcg-cpu-preflight.asm` enables long mode/YMM state, executes AVX and AVX2 integer arithmetic, and checks an Int32 from the upper 128-bit lane. Only the correct result reaches [QEMU debug-exit](https://github.com/qemu/qemu/blob/v11.1.1/hw/misc/debugexit.c) code 33. No disks/network attach; failure/timeout fails preflight. This tests that instruction chain, not the complete ISA or macOS.
|
||||||
|
|
||||||
The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549`. Assembly/static review does not prove remote execution.
|
The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549`. Assembly/static review does not prove remote execution.
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ return await NativeDiagnostic.Execute(args);
|
|||||||
static class NativeDiagnostic
|
static class NativeDiagnostic
|
||||||
{
|
{
|
||||||
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
|
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
|
||||||
const string Profile = "tcg-haswell-sonoma";
|
const string Profile = "tcg-skylake-sonoma";
|
||||||
const string CpuModel = "Haswell-noTSX";
|
const string CpuModel = "Skylake-Client-v4";
|
||||||
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 int DiagnosticMinutes = 20;
|
||||||
|
const string DiagnosticArguments = "-object iothread,id=io2 -no-reboot -no-shutdown -d int,cpu_reset,guest_errors,unimp";
|
||||||
|
const string CpuFlags = "Skylake-Client-v4,l3-cache=on,+hypervisor,vendor=GenuineIntel,vmx=off,vmware-cpuid-freq=on,-pdpe1gb,-spec-ctrl,-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";
|
||||||
@@ -100,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(90));
|
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;
|
||||||
@@ -112,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 = 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 = 90 });
|
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))
|
||||||
{
|
{
|
||||||
@@ -137,7 +139,7 @@ static class NativeDiagnostic
|
|||||||
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", "--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=-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=4m", "--log-opt", "max-file=2", "--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 };
|
||||||
@@ -146,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 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.");
|
Console.WriteLine("The owned unprivileged TCG/Skylake 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;
|
||||||
@@ -154,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))
|
||||||
{
|
{
|
||||||
@@ -181,7 +184,7 @@ static class NativeDiagnostic
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
error = exception is OperationCanceledException ? "The explicit 90-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
|
||||||
@@ -222,16 +225,17 @@ static class NativeDiagnostic
|
|||||||
}
|
}
|
||||||
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 = "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");
|
||||||
|
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") })
|
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=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}]}]
|
[{"Config":{"Labels":{"org.meeting-assistant.native-diagnostic":"validation"},"Env":["KVM=N","CPU_MODEL=Skylake-Client-v4","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("\"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") })
|
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=Skylake-Client-v4", "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.");
|
||||||
@@ -295,13 +299,37 @@ static class NativeDiagnostic
|
|||||||
var cpu = File.ReadAllText(cpuPath);
|
var cpu = File.ReadAllText(cpuPath);
|
||||||
if (Hash(Encoding.UTF8.GetBytes(cpu)) != "0f3e4b4e1c3e17743d3a8d27b77a76424ceebb576b269283d612c489bc70993e") throw new InvalidOperationException("Pinned CPU composition script hash mismatch.");
|
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\"");
|
cpu = ReplaceOnce(cpu, ",+movbe,+rdrand,check\"", ",+movbe,+rdrand,enforce=on\"");
|
||||||
|
// Explicit CPU_MODEL bypasses upstream selection, so restore its TCG mitigation mask.
|
||||||
|
cpu = ReplaceOnce(cpu, " DEFAULT_FLAGS+=\",-pcid,-invpcid,-tsc-deadline,-xsavec,-xsaves\"", " DEFAULT_FLAGS+=\",-spec-ctrl,-pcid,-invpcid,-tsc-deadline,-xsavec,-xsaves\"");
|
||||||
var preflight = File.ReadAllText(Path.Combine("tools", "ci", "macos-tcg-cpu-preflight.asm"));
|
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, ". cpu.sh # Configure CPU model\n", "");
|
entry = ReplaceOnce(entry, ". cpu.sh # Configure CPU model\n", "");
|
||||||
entry = ReplaceOnce(entry, ". proc.sh # Initialize processor\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, ". 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, "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=Skylake-Client-v4 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=Skylake-Client-v4 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");
|
||||||
|
// The producer must retain the first kernel context before Docker can rotate it.
|
||||||
|
// This tiny existing-runtime filter runs inside the VM container before any .NET SDK.
|
||||||
|
const string contextCapture = """
|
||||||
|
<"$pipe" | LC_ALL=C awk '
|
||||||
|
/^#\[EB\|LOG:HANDOFF TO XNU\] / { kernel_started=1 }
|
||||||
|
{
|
||||||
|
if (kernel_started && context_bytes < 2097152) {
|
||||||
|
remaining=2097152-context_bytes
|
||||||
|
piece=substr($0 "\n", 1, remaining)
|
||||||
|
printf "%s", piece > "/run/shm/first-kernel-context.log"
|
||||||
|
context_bytes+=length(piece)
|
||||||
|
fflush("/run/shm/first-kernel-context.log")
|
||||||
|
}
|
||||||
|
print
|
||||||
|
fflush()
|
||||||
|
}
|
||||||
|
' &
|
||||||
|
""";
|
||||||
|
entry = ReplaceOnce(entry, " <\"$pipe\" &", contextCapture);
|
||||||
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"));
|
||||||
@@ -386,9 +414,19 @@ static class NativeDiagnostic
|
|||||||
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 nvram = PlistValue(PlistValue(PlistValue(document.Root!.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82");
|
||||||
|
if (PlistValue(nvram, "boot-args").Value != "keepsyms=1 debug=0x100 -lilubeta -wegbeta vmhState=enabled") throw new InvalidOperationException("Pinned kernel diagnostic arguments differ.");
|
||||||
|
const string diagnosticBoot = """
|
||||||
|
local diagnostic_boot='-v keepsyms=1 debug=0x108 serial=5 msgbuf=1048576 -lilubeta -wegbeta vmhState=enabled'
|
||||||
|
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]"
|
||||||
|
xmlstarlet ed -P -L -u "$boot_args" -v "$diagnostic_boot" "$CFG" || exit 12
|
||||||
|
[ "$(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, " 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 \"PROFILE=tcg-haswell-sonoma\"\n");
|
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"PROFILE=tcg-skylake-sonoma\"\n");
|
||||||
return boot;
|
return boot;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +439,7 @@ static class NativeDiagnostic
|
|||||||
|
|
||||||
static readonly string TcgPreflight = """
|
static readonly string TcgPreflight = """
|
||||||
# Realize this exact TCG model and execute AVX/AVX2 before Apple downloads.
|
# Realize this exact TCG model and execute AVX/AVX2 before Apple downloads.
|
||||||
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; }
|
disabled "$KVM" && [[ "$ARCH" == amd64 && "$CPU_MODEL" == Skylake-Client-v4 && "$VERSION" == 14 && "$CPU_FLAGS" == '@@CPU_FLAGS@@' ]] || { error 'Supported probe requires the exact TCG/Skylake/macOS 14 profile.'; exit 1; }
|
||||||
[[ "$(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; }
|
[[ "$(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; }
|
||||||
printf '%s %s\n' c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549 /assets/ci-cpu-preflight.bin | sha256sum -c - || { error 'Compiled AVX/AVX2 preflight ROM hash mismatch.'; exit 1; }
|
printf '%s %s\n' c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549 /assets/ci-cpu-preflight.bin | sha256sum -c - || { error 'Compiled AVX/AVX2 preflight ROM hash mismatch.'; exit 1; }
|
||||||
probeTcgInstructions() {
|
probeTcgInstructions() {
|
||||||
@@ -417,6 +455,7 @@ static class NativeDiagnostic
|
|||||||
cat "$QEMU_DIR/cpu-preflight-negative.log"
|
cat "$QEMU_DIR/cpu-preflight-negative.log"
|
||||||
(( negative != 33 )) || { error 'AVX2-disabled negative control unexpectedly passed.'; exit 1; }
|
(( 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"
|
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);
|
""".Replace("@@CPU_FLAGS@@", CpuFlags, StringComparison.Ordinal);
|
||||||
|
|
||||||
static async Task ValidateTcgPreflight(string output, CancellationToken cancellation)
|
static async Task ValidateTcgPreflight(string output, CancellationToken cancellation)
|
||||||
@@ -454,7 +493,7 @@ static class NativeDiagnostic
|
|||||||
printf '[fixture-command] %s\n' "$*"
|
printf '[fixture-command] %s\n' "$*"
|
||||||
case "$*" in *,-avx2*) return @@NEGATIVE@@ ;; *) return @@POSITIVE@@ ;; esac
|
case "$*" in *,-avx2*) return @@NEGATIVE@@ ;; *) return @@POSITIVE@@ ;; esac
|
||||||
}
|
}
|
||||||
KVM=N; ARCH=amd64; CPU_MODEL=Haswell-noTSX; VERSION='@@VERSION@@'
|
KVM=N; ARCH=amd64; CPU_MODEL=Skylake-Client-v4; VERSION='@@VERSION@@'
|
||||||
CPU_FLAGS='@@FLAGS@@'; QEMU_DIR='@@WORK@@'
|
CPU_FLAGS='@@FLAGS@@'; QEMU_DIR='@@WORK@@'
|
||||||
""".Replace("@@HASH_EXIT@@", item.Item6 ? "0" : "1", StringComparison.Ordinal)
|
""".Replace("@@HASH_EXIT@@", item.Item6 ? "0" : "1", StringComparison.Ordinal)
|
||||||
.Replace("@@NEGATIVE@@", item.Item3.ToString(), StringComparison.Ordinal)
|
.Replace("@@NEGATIVE@@", item.Item3.ToString(), StringComparison.Ordinal)
|
||||||
@@ -516,13 +555,17 @@ static class NativeDiagnostic
|
|||||||
|| new[] { "KVM=N", "CPU_MODEL=" + CpuModel, "VERSION=14" }.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 unprivileged TCG/Haswell/macOS 14 boundary.");
|
throw new InvalidOperationException("Created container exceeds the owned unprivileged TCG/Skylake/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", final ? "all" : "3000", id], output, final ? "container-final" : "container", cancellation, requireSuccess: false);
|
||||||
|
if (final) await Command("docker", ["exec", id, "head", "-c", "2097152", "/run/shm/first-kernel-context.log"], output, "first-kernel-context", cancellation, requireSuccess: false, retainSuccessful: true);
|
||||||
|
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);
|
||||||
@@ -530,11 +573,51 @@ static class NativeDiagnostic
|
|||||||
}
|
}
|
||||||
// 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 the image on every twenty-second poll.
|
// Hash it once instead of rereading the image on every twenty-second poll.
|
||||||
if (logs.Output.Contains("[supported-profile] accelerator=tcg", 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/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);
|
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)
|
||||||
{
|
{
|
||||||
using var snapshotDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
using var snapshotDeadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation);
|
||||||
@@ -581,7 +664,7 @@ static class NativeDiagnostic
|
|||||||
var monitor = await Command("docker", ["exec", id, "sh", "-c", """
|
var monitor = await Command("docker", ["exec", id, "sh", "-c", """
|
||||||
test -S /run/shm/monitor.sock || exit 1
|
test -S /run/shm/monitor.sock || exit 1
|
||||||
rm -f -- "$1" || exit 1
|
rm -f -- "$1" || exit 1
|
||||||
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
|
printf 'info kvm\ninfo status\ninfo registers -a\nx/16gx $rsp\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=$?
|
monitor_exit=$?
|
||||||
printf '\n[monitor-exit] %s\n' "$monitor_exit"
|
printf '\n[monitor-exit] %s\n' "$monitor_exit"
|
||||||
[ "$monitor_exit" -eq 0 ] || exit "$monitor_exit"
|
[ "$monitor_exit" -eq 0 ] || exit "$monitor_exit"
|
||||||
|
|||||||
Reference in New Issue
Block a user