diff --git a/.gitea/workflows/pr-push-build-and-test.yaml b/.gitea/workflows/pr-push-build-and-test.yaml index 5e93b7d..ef30e45 100644 --- a/.gitea/workflows/pr-push-build-and-test.yaml +++ b/.gitea/workflows/pr-push-build-and-test.yaml @@ -7,6 +7,7 @@ on: # Its manual workflow provides that evidence; the PR branch still runs all jobs. branches-ignore: - codex/macos-ci-kvm-compatibility + - codex/macos-kvm-noavx-recovery workflow_dispatch: jobs: diff --git a/docs/macos-native-diagnostic.md b/docs/macos-native-diagnostic.md index 4ec1cac..0de38ff 100644 --- a/docs/macos-native-diagnostic.md +++ b/docs/macos-native-diagnostic.md @@ -6,6 +6,10 @@ Baseline: bootstrap commit `4606de069678e8f95dfe3c7dad1bf5ce5384d30c`; separate The NoAVX continuation compares against KVM Recovery commit `720a431`. Its only guest change is adding `NoAVXFSCompressionTypeZlib-AVXpel.kext` to the existing OpenCore overlay and `Kernel.Add`. Existing Lilu/CryptexFixup, CPU passthrough, macOS 13 Recovery, disk, probes and deadlines are preserved. The hypothesis is that an AVX-dependent filesystem decompression path blocks native file loading on the Celeron; this has not been established as the cause of the disk-readiness hang. Application source is unchanged, and this candidate has only offline validation evidence. +The host-memory admission check is copied unchanged from RAW candidate `ec5508e`: the unchanged 4-GiB guest plus 512 MiB QEMU overhead requires 4.5 GiB available. Offline validation accepts the captured run-4204 value of 5,138,696 KiB and rejects 4 GiB, missing and invalid values. Guest RAM and the 6-GiB container cap are unchanged. This admission budget reserves no host memory against other workloads. The `codex/macos-kvm-noavx-recovery` branch skips only the PR/Push workflow's push trigger; pull requests and manual workflows retain their existing triggers. + +The existing one-minute heartbeat prints at most the last two captured `[proof-start]`, `[proof-done]`, `[proof-native-wait]`, `[proof-result]` or `[native-version]` lines, each capped at 256 characters. `[proof-native-wait]` is this candidate's existing command-completion marker. It reads only the already retained `guest-proof.log`; no additional Docker/guest query or polling timer is added. Five offline fixture cases verify marker selection, missing/unrelated output and the bounds. The controlled 40-minute host deadline and existing cleanup are preserved. + ## Reasons and remaining gaps The existing daemon's Intel Celeron 1037U lacks AVX/AVX2; a separate diagnostic proved KVM enabled/paused state and clean exit. `CPU_MODEL=host` preserves actual instruction availability rather than advertising AVX2 through emulated Skylake. This candidate refuses a TCG or CPU-model fallback. diff --git a/tools/ci/MacOsNativeDiagnostic.cs b/tools/ci/MacOsNativeDiagnostic.cs index ea8573f..7d58c82 100644 --- a/tools/ci/MacOsNativeDiagnostic.cs +++ b/tools/ci/MacOsNativeDiagnostic.cs @@ -65,6 +65,8 @@ static class NativeDiagnostic var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos"); if (args.Contains("--validate")) { + ValidateRunnerMemoryGate(); + ValidateGuestProgress(output); ValidateContracts(); if (Option(args, "--source") is { } source) { @@ -113,9 +115,8 @@ static class NativeDiagnostic throw new InvalidOperationException("Existing Docker resources cannot fit this bounded 2-CPU/6-GiB diagnostic; no infrastructure change was requested."); } await Command("sh", ["-c", "cat /proc/meminfo; printf '\n[cgroup]\n'; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true; printf '\n[workspace disk]\n'; df -Pk ."], output, "runner-resources", deadline.Token); - var available = System.Text.RegularExpressions.Regex.Match(File.ReadAllText("/proc/meminfo"), @"(?m)^MemAvailable:\s+(\d+) kB$"); - if (!available.Success || long.Parse(available.Groups[1].Value) < 5L * 1024 * 1024) - throw new InvalidOperationException("Existing runner memory has less than the 5-GiB available diagnostic budget; no infrastructure change was requested."); + if (!HasAvailableGuestMemory(File.ReadAllText("/proc/meminfo"))) + throw new InvalidOperationException("Existing runner memory cannot fit the 4-GiB guest plus its 512-MiB QEMU overhead budget; no infrastructure change was requested."); var source = Path.Combine(work, "dockur"); await Command("git", ["clone", "--no-checkout", "https://github.com/dockur/macos.git", source], output, "dockur-clone", deadline.Token); await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token); @@ -136,7 +137,7 @@ static class NativeDiagnostic AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token); await Command("docker", ["start", id], output, "docker-start", 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 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. The additional NoAVX boot kext is the only guest variable against KVM baseline 720a431."); var recoveryStarted = Stopwatch.StartNew(); var heartbeat = Stopwatch.StartNew(); var diskPressureCaptured = false; @@ -163,7 +164,8 @@ static class NativeDiagnostic if (running.Output.Trim() != "true") throw new InvalidOperationException("Guest container exited before a native readiness result."); if (heartbeat.Elapsed >= TimeSpan.FromSeconds(60)) { - Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1} minutes; container=running; readiness=pending"); + Console.WriteLine($"[native-diagnostic] phase=recovery; elapsed={recoveryStarted.Elapsed.TotalMinutes:F1}/40 minutes; container=running; readiness=pending"); + foreach (var progress in LastGuestProgress(proofPath)) Console.WriteLine("[native-diagnostic] guest-progress=" + progress); heartbeat.Restart(); } await Task.Delay(TimeSpan.FromSeconds(20), deadline.Token); @@ -430,6 +432,53 @@ static class NativeDiagnostic return (boot, document.ToString(), assets); } + static bool HasAvailableGuestMemory(string meminfo) + { + var available = System.Text.RegularExpressions.Regex.Match(meminfo, @"(?m)^MemAvailable:\s+(\d+) kB$"); + return available.Success && long.TryParse(available.Groups[1].Value, out var kib) && kib >= 4L * 1024 * 1024 + 512L * 1024; + } + + static string[] LastGuestProgress(string path) + { + if (!File.Exists(path)) return []; + var markers = new[] { "[proof-start]", "[proof-done]", "[proof-native-wait]", "[proof-result]", "[native-version]" }; + return File.ReadLines(path).Where(line => markers.Any(marker => line.StartsWith(marker, StringComparison.Ordinal))) + .TakeLast(2).Select(line => line[..Math.Min(line.Length, 256)]).ToArray(); + } + + static void ValidateGuestProgress(string output) + { + var fixture = Path.Combine(output, "validation-guest-progress"); + Directory.CreateDirectory(fixture); + var path = Path.Combine(fixture, "guest-proof.log"); + File.Delete(path); + if (LastGuestProgress(path).Length != 0) throw new InvalidOperationException("Missing guest progress was fabricated."); + File.WriteAllText(path, "[proof-start] platform child=12\nignored native output\n[proof-native-wait] platform exit=0\n[proof-start] uid child=13\n", new UTF8Encoding(false)); + if (!LastGuestProgress(path).SequenceEqual(new[] { "[proof-native-wait] platform exit=0", "[proof-start] uid child=13" })) throw new InvalidOperationException("Heartbeat must show the last two captured native progress markers."); + File.WriteAllText(path, "[proof-done] uid\n[native-version] 13.6\n[proof-result] true: readiness\n", new UTF8Encoding(false)); + if (!LastGuestProgress(path).SequenceEqual(new[] { "[native-version] 13.6", "[proof-result] true: readiness" })) throw new InvalidOperationException("Heartbeat lost native version/result progress."); + File.WriteAllText(path, "[proof-start] " + new string('x', 1024) + "\n", new UTF8Encoding(false)); + if (LastGuestProgress(path).Single().Length != 256) throw new InvalidOperationException("Heartbeat progress line exceeded its output bound."); + File.WriteAllText(path, "unrelated output\n", new UTF8Encoding(false)); + if (LastGuestProgress(path).Length != 0) throw new InvalidOperationException("Heartbeat selected unrelated guest output."); + Save(Path.Combine(fixture, "receipt.json"), new { success = true, fixtureCases = 5, maximumLines = 2, maximumLineCharacters = 256, existingProofOnly = true, dockerExecuted = false, guestExecuted = false }); + } + + static void ValidateRunnerMemoryGate() + { + // Run 4204: 4-GiB guest plus 512-MiB QEMU overhead fits its captured available memory. + var cases = new[] + { + ("captured-run4204", "MemTotal: 16281732 kB\nMemAvailable: 5138696 kB\n", true), + ("below-guest-plus-overhead", "MemAvailable: 4194304 kB\n", false), + ("missing", "MemTotal: 16281732 kB\n", false), + ("invalid", "MemAvailable: unavailable kB\n", false) + }; + foreach (var (name, meminfo, expected) in cases) + if (HasAvailableGuestMemory(meminfo) != expected) + throw new InvalidOperationException("Existing runner memory admission failed: " + name); + } + static Dictionary ReadNoAvxArchive(byte[] bytes) { if (bytes.Length != 98356 || Hash(bytes) != NoAvxHash) throw new InvalidOperationException("Pinned OCLP NoAVX archive size/hash mismatch.");