From b15670cc6221b00d6ea4bd576d7d4e7d0f64dad4 Mon Sep 17 00:00:00 2001 From: dh Date: Tue, 6 Oct 2026 08:02:17 +0200 Subject: [PATCH] Observe owned macOS guest resource use and loaded recovery kexts --- docs/macos-native-diagnostic.md | 2 + tools/ci/MacOsNativeDiagnostic.cs | 181 ++++++++++++++++++++++++++++- tools/ci/macos-native-readiness.sh | 1 + 3 files changed, 180 insertions(+), 4 deletions(-) diff --git a/docs/macos-native-diagnostic.md b/docs/macos-native-diagnostic.md index cdf01e6..8d9ca35 100644 --- a/docs/macos-native-diagnostic.md +++ b/docs/macos-native-diagnostic.md @@ -20,6 +20,8 @@ Memory admission requires the unchanged 4-GiB guest plus 512 MiB QEMU overhead. ## Reasons and remaining gaps +The runtime-observation candidate compares against `96755c704e63884e9c45e8ebbb18b7b12e4bdd83` without changing VM parameters or native gates. After the existing KVM staging marker, the host samples only the owned container's QEMU `stat`, `status`, `io`, `wchan` and cgroup CPU/memory/I/O counters and pressure, at most once per 60 seconds and 173 attempts within the unchanged 172-minute outer budget. Ownership inspection and reads share a ten-second deadline; the exec also has a nine-second internal timeout. Missing files are optional and each successful UTC-stamped snapshot is limited to 16 KiB and appended to `runtime-resources.log`. Completion prints the complete history, bounded to 173 × 16 KiB, plus the existing resource limits bounded to 16 KiB, allowing counter deltas even when artifact retrieval fails. Command lines and environments are never read. Failure diagnostics additionally invoke `kmutil showloaded --list-only` under the existing 45-second watchdog; absent, empty or failed output does not establish whether the kexts loaded. The original gate failure is preserved. These observations provide performance evidence; they prove no bottleneck or fix by themselves. Local `--validate` covers marker retention, scheduling, PID/executable rejection, byte budgets, UTC retention and a real ten-second timeout without Docker or macOS. + 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. All four Swift helpers target `x86_64-apple-macos13.0`; the macOS 14 EventKit call has an existing macOS 13 fallback. Inspected native Mach-O files in pinned .NET SDK 10.0.401 x64 declare `minos 12.0`. These source/binary minima are not runtime qualification or vendor support: macOS 13 is outside [Microsoft's current .NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This probe does not install that SDK, compile helpers or test calendar/audio permissions. diff --git a/tools/ci/MacOsNativeDiagnostic.cs b/tools/ci/MacOsNativeDiagnostic.cs index 363ed42..b1c54e1 100644 --- a/tools/ci/MacOsNativeDiagnostic.cs +++ b/tools/ci/MacOsNativeDiagnostic.cs @@ -24,6 +24,10 @@ static class NativeDiagnostic const long GuestDiskBytes = 64L * 1024 * 1024 * 1024; const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024; const int MaximumCapturedCharacters = 8 * 1024 * 1024; + const int MaximumRuntimeSnapshotBytes = 16 * 1024; + const int MaximumRuntimeSnapshots = 173; + static readonly TimeSpan RuntimeSnapshotInterval = TimeSpan.FromSeconds(60); + static readonly TimeSpan RuntimeSnapshotTimeout = TimeSpan.FromSeconds(10); const string SdkVersion = "10.0.401"; const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0"; const string FullState = "/storage/13/ci-state"; @@ -71,6 +75,7 @@ static class NativeDiagnostic if (recoveryFormat is not ("dmg" or "raw")) throw new ArgumentException("Recovery format must be dmg or raw."); if (args.Contains("--validate")) { + await ValidateRuntimeObservation(output); ValidateRunnerMemoryGate(); ValidateGuestProgress(output); ValidateContracts(); @@ -106,6 +111,8 @@ static class NativeDiagnostic // Full execution leaves eight minutes within the existing 180-minute job for evidence/cleanup. var deadlineMinutes = full ? 172 : 40; using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(deadlineMinutes)); + var runtimeElapsed = Stopwatch.StartNew(); + var runtimeObservation = new RuntimeObservation(); using var signal = OperatingSystem.IsLinux() ? PosixSignalRegistration.Create(PosixSignal.SIGTERM, context => { context.Cancel = true; deadline.Cancel(); }) : null; ConsoleCancelEventHandler cancelHandler = (_, context) => { context.Cancel = true; deadline.Cancel(); }; Console.CancelKeyPress += cancelHandler; @@ -117,7 +124,7 @@ static class NativeDiagnostic throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only."); ValidateContracts(); var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim(); - Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex-noavx", recoveryFormat, causalSingleVariableTest = false, comparisonBaselineCommit = "d1594e90b3fa9c6f3bb52f12b754ae933105b8c8", changedGuestVariable = recoveryFormat == "raw" ? "none" : "recovery-disk-backend", changedHostVariable = "recovery-hash-capture-frequency", kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", noAvxBaseVersion = "12.6", noAvxSha256 = NoAvxHash, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes, mode = full ? "full" : "readiness" }); + Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex-noavx", recoveryFormat, causalSingleVariableTest = false, comparisonBaselineCommit = "96755c704e63884e9c45e8ebbb18b7b12e4bdd83", changedGuestVariable = "loaded-kexts-diagnostic", changedHostVariable = "runtime-resource-observation", kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", noAvxBaseVersion = "12.6", noAvxSha256 = NoAvxHash, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes, mode = full ? "full" : "readiness" }); var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token); using (var document = JsonDocument.Parse(info.Output)) { @@ -163,6 +170,7 @@ static class NativeDiagnostic { deadline.Token.ThrowIfCancellationRequested(); await CaptureGuest(id, output, deadline.Token, full); + await CaptureRuntimeResources(state, output, runtimeObservation, runtimeElapsed.Elapsed, deadline.Token); if (full && phaseStarted.Elapsed > phaseBudget) throw new InvalidOperationException("The bounded native " + phase + " phase exceeded " + phaseBudget.TotalMinutes + " minutes."); var resultPath = Path.Combine(output, "guest-result.json"); @@ -229,6 +237,7 @@ static class NativeDiagnostic using var captureDeadline = new CancellationTokenSource(TimeSpan.FromSeconds(45)); try { await CaptureGuest(state.ContainerId ?? state.ContainerName, output, captureDeadline.Token, full, true, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Final evidence capture: " + exception.Message); } try { PrintGuestProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Native proof output: " + exception.Message); } + try { PrintRuntimeResources(output); } catch (Exception exception) { Console.Error.WriteLine("Runtime resource output: " + exception.GetType().Name); } if (full) try { PrintFullProof(output, state.Token); } catch (Exception exception) { Console.Error.WriteLine("Full native proof output: " + exception.Message); } var clean = await Cleanup(output); if (!clean) { outcome = "failed"; error = (error ?? "") + " Owned-resource cleanup failed; inspect cleanup evidence."; } @@ -247,8 +256,9 @@ static class NativeDiagnostic { var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh")); var baseline = ReplaceOnce(readiness, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))"); + baseline = ReplaceOnce(baseline, " run_command processes /bin/ps -axo pid,ppid,comm\n run_command loaded_kexts /usr/bin/kmutil showloaded --list-only\n", " run_command processes /bin/ps -axo pid,ppid,comm\n"); if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3") - throw new InvalidOperationException("Compatibility readiness may change only the baseline's macOS minimum to 13; identity, services, disk and limits must remain identical."); + throw new InvalidOperationException("Readiness may change only the macOS minimum and optional failure-time loaded-kext diagnostic; gates and watchdogs must remain identical."); if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5") throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper."); if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist."); @@ -1281,6 +1291,93 @@ static class NativeDiagnostic 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; if test -f /storage/13/setup.dmg.raw.json; then printf '[RAW Recovery equality receipt]\n'; cat /storage/13/setup.dmg.raw.json; fi; 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); } + // Runtime arguments are fixed container paths; fixtures substitute only their own temporary tree. + const string RuntimeResourceScript = """ + { + proc=$1; cgroup=$2; pid_file=$3 + qemu_pid=$(head -c 32 "$pid_file" 2>/dev/null || true) + valid_pid=false + case "$qemu_pid" in ''|*[!0-9]*|0|1) ;; *) + if [ "${#qemu_pid}" -le 10 ] && [ "$(readlink "$proc/$qemu_pid/exe" 2>/dev/null)" = /usr/bin/qemu-system-x86_64 ]; then valid_pid=true; fi ;; + esac + if [ "$valid_pid" = true ]; then + printf '[owned QEMU pid=%s]\n' "$qemu_pid" + for name in stat status io wchan; do + printf '\n[qemu/%s]\n' "$name" + if [ -r "$proc/$qemu_pid/$name" ]; then head -c 2048 "$proc/$qemu_pid/$name" 2>/dev/null || true; else printf 'unavailable\n'; fi + done + else + printf '[owned QEMU unavailable; no process counters read]\n' + fi + for name in cpu.stat memory.current memory.events cpu.pressure memory.pressure io.pressure io.stat; do + printf '\n[cgroup/%s]\n' "$name" + if [ -r "$cgroup/$name" ]; then head -c 1024 "$cgroup/$name" 2>/dev/null || true; else printf 'unavailable\n'; fi + done + } | head -c 16384 + """; + + sealed class RuntimeObservation + { + public bool Staged { get; set; } + public int Attempts { get; set; } + public TimeSpan? LastAttempt { get; set; } + } + + static bool StartRuntimeObservation(RuntimeObservation observation, bool staged, TimeSpan elapsed) + { + observation.Staged |= staged; + if (!observation.Staged || observation.Attempts >= MaximumRuntimeSnapshots || elapsed >= TimeSpan.FromMinutes(172) + || observation.LastAttempt is { } previous && elapsed - previous < RuntimeSnapshotInterval) return false; + observation.LastAttempt = elapsed; + observation.Attempts++; + return true; + } + + static async Task CaptureRuntimeResources(OwnedResources state, string output, RuntimeObservation observation, TimeSpan elapsed, CancellationToken cancellation) + { + var log = Path.Combine(output, "container.stdout.log"); + var staged = File.Exists(log) && File.ReadLines(log).Any(line => line.Contains("[compatibility-profile] accelerator=kvm", StringComparison.Ordinal)); + if (!StartRuntimeObservation(observation, staged, elapsed)) return; + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); + deadline.CancelAfter(RuntimeSnapshotTimeout); + try + { + if (state.ContainerId is not { } id || !System.Text.RegularExpressions.Regex.IsMatch(id, "^[0-9a-f]{64}$")) + throw new InvalidOperationException("Missing owned container ID."); + var owner = await Command("docker", ["inspect", "--format", "{{index .Config.Labels \"" + OwnerLabel + "\"}}", id], output, "capture-runtime-owner", deadline.Token, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); + if (owner.Output.Trim() != state.Token) throw new InvalidOperationException("Runtime observation refuses a foreign container."); + // The internal timeout also bounds the exec process if the Docker client is interrupted. + var result = await Command("docker", ["exec", id, "timeout", "9", "sh", "-c", RuntimeResourceScript, "runtime-resources", "/proc", "/sys/fs/cgroup", "/dev/shm/qemu.pid"], output, "capture-runtime-resources", deadline.Token, requireSuccess: false, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); + if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) AppendRuntimeSnapshot(output, result.Output, DateTimeOffset.UtcNow); + } + catch (Exception exception) + { + // This optional probe must neither qualify nor fail a native gate or prevent cleanup. + Console.Error.WriteLine("[native-diagnostic] Runtime resource observation unavailable: " + exception.GetType().Name); + cancellation.ThrowIfCancellationRequested(); + } + } + + static void AppendRuntimeSnapshot(string output, string snapshot, DateTimeOffset capturedUtc) + { + var record = "[runtime-resources UTC " + capturedUtc.ToUniversalTime().ToString("O") + "]\n" + snapshot + "\n"; + if (Encoding.UTF8.GetByteCount(record) > MaximumRuntimeSnapshotBytes) throw new InvalidOperationException("Runtime snapshot exceeds its byte budget."); + File.AppendAllText(Path.Combine(output, "runtime-resources.log"), record, new UTF8Encoding(false)); + File.WriteAllText(Path.Combine(output, "runtime-resources.last-success.log"), record, new UTF8Encoding(false)); + } + + static void PrintRuntimeResources(string output) + { + foreach (var (name, budget) in new[] { ("runtime-resources.log", MaximumRuntimeSnapshots * MaximumRuntimeSnapshotBytes), ("guest-container-resources.last-success.stdout.log", MaximumRuntimeSnapshotBytes) }) + { + var path = Path.Combine(output, name); + if (!File.Exists(path)) continue; + if (new FileInfo(path).Length > budget) throw new InvalidOperationException("Runtime console evidence exceeds its byte budget."); + Console.WriteLine("[native-diagnostic] Final resource evidence: " + name); + Console.WriteLine(File.ReadAllText(path)); + } + } + static async Task CaptureMonitor(string id, string output, string token, CancellationToken cancellation) { using var deadline = CancellationTokenSource.CreateLinkedTokenSource(cancellation); @@ -1498,6 +1595,80 @@ static class NativeDiagnostic if (Crc32(image.AsSpan(0, length)) != BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(trailer + 88)) || Crc32(decoded) != BinaryPrimitives.ReadUInt32BigEndian(mish.AsSpan(72)) || Crc32(mish.AsSpan(72, 4)) != BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(trailer + 360))) throw new InvalidOperationException("Independent C# fixture CRC32 readback failed."); } + static async Task ValidateRuntimeObservation(string output) + { + var fixture = Path.Combine(output, "validation-runtime-observation"); + Directory.CreateDirectory(fixture); + var observation = new RuntimeObservation(); + if (StartRuntimeObservation(observation, false, TimeSpan.Zero) + || !StartRuntimeObservation(observation, true, TimeSpan.Zero) + || StartRuntimeObservation(observation, false, TimeSpan.FromSeconds(59)) + || !StartRuntimeObservation(observation, false, TimeSpan.FromSeconds(60)) + || !StartRuntimeObservation(observation, false, TimeSpan.FromSeconds(130)) + || observation.Attempts != 3) throw new InvalidOperationException("Runtime marker retention or monotonic interval failed."); + observation.Attempts = MaximumRuntimeSnapshots - 1; + if (!StartRuntimeObservation(observation, false, TimeSpan.FromSeconds(190)) + || StartRuntimeObservation(observation, false, TimeSpan.FromSeconds(250)) + || StartRuntimeObservation(new RuntimeObservation(), true, TimeSpan.FromMinutes(172))) + throw new InvalidOperationException("Runtime attempt/deadline bounds failed."); + + var proc = Path.Combine(fixture, "proc"); + var cgroup = Path.Combine(fixture, "cgroup"); + var pidFile = Path.Combine(fixture, "qemu.pid"); + Directory.CreateDirectory(Path.Combine(proc, "123")); + Directory.CreateDirectory(cgroup); + var exe = Path.Combine(proc, "123", "exe"); + File.Delete(exe); + File.CreateSymbolicLink(exe, "/usr/bin/qemu-system-x86_64"); + File.WriteAllText(pidFile, "123\n"); + File.WriteAllText(Path.Combine(proc, "123", "stat"), "123 (qemu-system-x86) S 1 2 3\n"); + File.WriteAllText(Path.Combine(proc, "123", "io"), "read_bytes: 4096\n"); + File.WriteAllText(Path.Combine(cgroup, "cpu.stat"), "usage_usec 100\nnr_throttled 2\n"); + var result = await Command("sh", ["-c", RuntimeResourceScript, "runtime-fixture", proc, cgroup, pidFile], fixture, "runtime-valid", CancellationToken.None, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); + if (!result.Output.Contains("[owned QEMU pid=123]") || !result.Output.Contains("read_bytes: 4096") + || !result.Output.Contains("nr_throttled 2") || !result.Output.Contains("unavailable") || result.Error.Length != 0) + throw new InvalidOperationException("Owned process/cgroup fixture or optional-file handling failed."); + File.WriteAllText(pidFile, "../123\n"); + var invalid = await Command("sh", ["-c", RuntimeResourceScript, "runtime-fixture", proc, cgroup, pidFile], fixture, "runtime-invalid-pid", CancellationToken.None, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); + if (!invalid.Output.Contains("no process counters read") || invalid.Output.Contains("read_bytes: 4096")) throw new InvalidOperationException("Invalid QEMU PID reached process counters."); + File.WriteAllText(pidFile, "123\n"); + File.Delete(exe); + File.CreateSymbolicLink(exe, "/usr/bin/other-process"); + var foreign = await Command("sh", ["-c", RuntimeResourceScript, "runtime-fixture", proc, cgroup, pidFile], fixture, "runtime-foreign-executable", CancellationToken.None, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); + if (foreign.Output.Contains("read_bytes: 4096")) throw new InvalidOperationException("Foreign executable reached process counters."); + + foreach (var (label, script) in new[] { ("runtime-ascii-budget", "head -c 20000 /dev/zero | tr '\\000' x"), ("runtime-utf8-budget", "i=0; while [ \"$i\" -lt 5000 ]; do printf 'éé'; i=$((i+1)); done") }) + { + var rejected = false; + try { await Command("sh", ["-c", script], fixture, label, CancellationToken.None, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); } + catch (Exception exception) when (exception is InvalidOperationException or OperationCanceledException) { rejected = true; } + var loggedBytes = new FileInfo(Path.Combine(fixture, label + ".stdout.log")).Length + new FileInfo(Path.Combine(fixture, label + ".stderr.log")).Length; + if (!rejected || loggedBytes > MaximumRuntimeSnapshotBytes) throw new InvalidOperationException("Runtime output exceeded its shared UTF-8 byte budget."); + } + var captured = DateTimeOffset.Parse("2026-10-06T00:00:00Z"); + File.Delete(Path.Combine(fixture, "runtime-resources.log")); + AppendRuntimeSnapshot(fixture, result.Output, captured); + AppendRuntimeSnapshot(fixture, "last successful snapshot", captured.AddMinutes(1)); + var retained = File.ReadAllText(Path.Combine(fixture, "runtime-resources.last-success.log")); + if (!retained.Contains("2026-10-06T00:01:00.0000000+00:00") || !retained.Contains("last successful snapshot") + || !File.ReadAllText(Path.Combine(fixture, "runtime-resources.log")).Contains(result.Output)) + throw new InvalidOperationException("Runtime UTC append or last-success retention failed."); + var oversizedRejected = false; + try { AppendRuntimeSnapshot(fixture, new string('é', MaximumRuntimeSnapshotBytes), captured); } + catch (InvalidOperationException) { oversizedRejected = true; } + if (!oversizedRejected || File.ReadAllText(Path.Combine(fixture, "runtime-resources.last-success.log")) != retained) + throw new InvalidOperationException("Oversized snapshot replaced last successful evidence."); + + using var deadline = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken.None); + deadline.CancelAfter(RuntimeSnapshotTimeout); + var elapsed = Stopwatch.StartNew(); + var timedOut = false; + try { await Command("sh", ["-c", "sleep 30"], fixture, "runtime-timeout", deadline.Token, maximumCapturedBytes: MaximumRuntimeSnapshotBytes); } + catch (OperationCanceledException) { timedOut = true; } + if (!timedOut || elapsed.Elapsed > RuntimeSnapshotTimeout + TimeSpan.FromSeconds(5)) throw new InvalidOperationException("Runtime command escaped its ten-second linked deadline."); + Save(Path.Combine(fixture, "validation.json"), new { success = true, intervalSeconds = RuntimeSnapshotInterval.TotalSeconds, maximumAttempts = MaximumRuntimeSnapshots, maximumBytes = MaximumRuntimeSnapshotBytes, timeoutSeconds = RuntimeSnapshotTimeout.TotalSeconds, observedTimeoutSeconds = elapsed.Elapsed.TotalSeconds, dockerExecuted = false, guestExecuted = false }); + } + static async Task ValidateResourceRetention(string output) { var fixture = Path.Combine(output, "validation-resource-retention"); @@ -1513,7 +1684,7 @@ static class NativeDiagnostic if (receipt.RootElement.GetProperty("stdoutSha256").GetString() != Hash(Encoding.UTF8.GetBytes(successful)) || receipt.RootElement.GetProperty("exitCode").GetInt32() != 0) throw new InvalidOperationException("Last successful snapshot receipt does not identify the retained bytes."); } - static async Task Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false, bool retainSuccessful = false) + static async Task Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false, bool retainSuccessful = false, int? maximumCapturedBytes = null) { if (!label.StartsWith("capture-", StringComparison.Ordinal) && label is not "container" and not "container-running" and not "guest-container-resources") Console.WriteLine("[native-diagnostic] " + label); @@ -1523,6 +1694,7 @@ static class NativeDiagnostic foreach (var argument in arguments) start.ArgumentList.Add(argument); start.Environment["GIT_TERMINAL_PROMPT"] = "0"; using var process = Process.Start(start) ?? throw new InvalidOperationException("Cannot start " + executable); + var capturedBytes = 0; async Task Read(StreamReader reader, string stream) { var captured = new StringBuilder(); @@ -1532,7 +1704,8 @@ static class NativeDiagnostic { var count = await reader.ReadAsync(buffer.AsMemory(), commandToken); if (count == 0) break; - if (captured.Length + count > MaximumCapturedCharacters) + if (captured.Length + count > MaximumCapturedCharacters + || maximumCapturedBytes is { } byteBudget && Interlocked.Add(ref capturedBytes, Encoding.UTF8.GetByteCount(buffer.AsSpan(0, count))) > byteBudget) { commandCancellation.Cancel(); throw new InvalidOperationException(label + " exceeded its bounded diagnostic log size."); diff --git a/tools/ci/macos-native-readiness.sh b/tools/ci/macos-native-readiness.sh index 5c68ae2..fa70693 100644 --- a/tools/ci/macos-native-readiness.sh +++ b/tools/ci/macos-native-readiness.sh @@ -157,6 +157,7 @@ diagnose_failure() { run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm= run_command processes /bin/ps -axo pid,ppid,comm + run_command loaded_kexts /usr/bin/kmutil showloaded --list-only } fail_probe() {