Compare commits

...
6 changed files with 341 additions and 176 deletions
@@ -1,4 +1,4 @@
name: macOS 13 KVM Cryptex Recovery compatibility diagnostic name: macOS 14 TCG Recovery prerequisite diagnostic
on: on:
workflow_dispatch: workflow_dispatch:
@@ -6,7 +6,7 @@ on:
jobs: jobs:
macos-native-diagnostic: macos-native-diagnostic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 45 timeout-minutes: 25
env: env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1" DOTNET_NOLOGO: "1"
@@ -19,7 +19,7 @@ jobs:
with: with:
dotnet-version: "10.0.x" dotnet-version: "10.0.x"
- name: Probe macOS 13 Recovery with existing KVM and host CPU - name: Verify actual AVX2 emulation then probe macOS 14 Recovery
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources - name: Always clean up only this diagnostic's owned resources
@@ -7,6 +7,7 @@ on:
# Its manual workflow provides that evidence; the PR branch still runs all jobs. # Its manual workflow provides that evidence; the PR branch still runs all jobs.
branches-ignore: branches-ignore:
- codex/macos-ci-kvm-compatibility - codex/macos-ci-kvm-compatibility
- codex/macos-ci-tcg-supported
workflow_dispatch: workflow_dispatch:
jobs: jobs:
+28 -52
View File
@@ -1,78 +1,54 @@
# macOS 13 KVM/Cryptex compatibility diagnostic # macOS 14 TCG prerequisite diagnostic
This separate manual candidate probes Recovery readiness on the existing Ubuntu Docker daemon with KVM, the real Intel host CPU and macOS 13. It does not install macOS, erase a disk, install .NET or Apple CLT, or run Meeting Assistant. Passing proves only a fresh macOS 13+ x86_64 Recovery guest with root identity, a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk. This manual candidate uses the existing Ubuntu/x64 Docker runner. Before downloading Apple Recovery it tests actual AVX/AVX2 instruction execution in the pinned QEMU binary, then probes a fresh macOS 14+ Recovery guest. It does not install macOS, erase a disk, provision .NET/CLT or run Meeting Assistant tests. Readiness is only a prerequisite for full native CI.
Baseline: bootstrap commit `4606de069678e8f95dfe3c7dad1bf5ce5384d30c`; separate branch `codex/macos-ci-kvm-compatibility`. KVM, CPU passthrough, Recovery major version and guest Cryptex staging change together. This is a compatibility experiment, not a causal single-variable A/B test. The TCG/bootstrap experiment remains separate. ## Profile and evidence
## Reasons and remaining gaps The existing Intel Celeron 1037U has neither AVX nor AVX2. KVM run 4187 at `720a431` reached macOS 13.6/x86_64/root and visible whole writable 64-GiB media. Diskutil timed out after 122 seconds; the sampler produced no report after 61 seconds. The screen remained at the Apple boot progress bar. CPU throttling, memory-limit/OOM events and container swap were zero; memory peaked at 2.67 GB. Host paging occurred. No unsupported-instruction crash or particular IPC wait is proved.
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. [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.
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. 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.
[Official CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/CryptexFixup/kern_start.cpp) activates without AVX2 and registers for normal, installer/Recovery and safe-mode boots. It redirects installer/updater ramrod to Apple Silicon's Rosetta Cryptex and bypasses APFS root-hash authentication on Ventura and newer. It does not emulate missing instructions. This kernel patch affects only the owned guest, never a host module. 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.
**Recovery cache gap:** CryptexFixup does not replace an already running Recovery BaseSystem shared cache. Its installer/update selector targets the installed Cryptex, but this readiness-only run invokes no installer. Staging or loading it therefore proves no Recovery userland compatibility. Actual CPU/kernel behavior, guest injection, all native gates and any later installed-Cryptex/build/test behavior remain unqualified until observed. 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.
Apple Recovery uses the pinned public InternetRecovery protocol with board ID and session/asset tokens, without Apple ID or workstation credentials. The macOS 13 selection, downloaded hash and actual guest version are retained; the hook downloads no full installer or SDK. 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.
## Entry point and dependencies 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.
Orchestration/validation remain the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Bash/Python stay only in the existing pinned Linux/macOS boot integration. ## Entry points and dependencies
~~~sh Orchestration remains the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Existing Bash/Python boot integration is necessary before a guest SDK exists. NASM assembles the CPU probe in the disposable image build, without host/runner installation. No new runner, device, capability, secret or service is used.
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --help
```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --cryptex-archive /path/to/CryptexFixup-1.0.5-RELEASE.zip --output /path/to/fresh/validation dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --output /path/to/fresh/validation
~~~
`--validate` checks result/container contracts without Docker. With `--source` it verifies the actual Cryptex ZIP/bundle, source seams, generated OpenCore configuration and staging/checksum contracts, checks Bash syntax, then exercises four raw/zlib Recovery fixtures and twelve rejection cases with independent C# CRC32 readback. It also checks preservation of a successful resource snapshot after a later failed capture, leaving the supplied source untouched. It does not download/extract the LongQT ISO, verify a complete Apple Recovery image or execute the active-Lilu runtime checks. The ISO checksum is enforced during the later Docker build; active Lilu and EFI-copy checks execute only during container boot. The optional local Cryptex ZIP must match the release size/hash; omitting it downloads only the public 69,703-byte release. Use a fresh output directory. Dependencies are .NET 10, Git, Bash and Python 3 with its standard library; manual execution also requires the existing Linux/x64 Docker daemon and its existing KVM device.
The manual-only workflow keeps these owned run/cleanup entry points; validation invokes neither:
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
~~~ ```
## Exact bootasset contract The native diagnostic workflow is manual only. Temporary diagnostic branches are excluded from ordinary push jobs to avoid repeating unchanged Wine/portable jobs. Remove this routing when integrating qualified CI into the actual PR.
Dockur stays pinned to `16a5b470cdd601bae8b05b02d748d7edfb36c12e`. Original Recovery patcher/staging, Dockerfile, OpenCore script and active config hashes are verified before edits. Both existing QEMU image digests remain pinned; other existing upstream downloads are observed through image identity. `source-hashes.json` includes the generated Recovery patcher, both original/replacement daemon variants and `udif_checksums.py`, staged from `tools/ci/macos-native-udif-checksums.py`. This small Python module belongs to the existing Linux UDIF runtime; C# supplies orchestration, validation fixtures and an independent CRC32 implementation. ## Before Apple downloads
Run 4173 at `45d7bde71f9f5a1f7121585fe3ee9fc81f7c585f` failed before QEMU started: the full macOS 14 plist pattern was absent from the macOS 13 download. The original image's hash was not retained. An independently downloaded comparison for the same board `Mac-4B682C642B45593E` is macOS 13.6/22G120, Apple product 042-23155, 710,918,897 bytes, SHA256 `c19bd12f5cb1651b87b74d04f02a636da762ea46b81c7ebc9f205fa2a976d599`. Its Apple chunklist signature and chunks verified before any changes. It is comparison evidence, not the missing run-4173 image identity. 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 HFS+ catalog identifies `/System/Library/LaunchDaemons/com.apple.recoveryosd.plist` as file ID 57231, logical size 465 bytes and one 4,096-byte allocated block. Its exact XML SHA256 is `af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6`. This variant has `ProcessType=Interactive`; the previous macOS 14 variant has `App`. The patch accepts only these two exact layouts with exactly one daemon label and original `ProgramArguments=[/usr/libexec/recoveryosd]`. It preserves each variant's fields and process type, removes only the XML doctype to fit the wrapper arguments, and pads to the original file size. Unknown, duplicate, malformed or wrong-argument layouts fail before image writes. The early rc.cdrom hook remains mount-only; the unchanged wrapper runs the Apple daemon. 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 checksum binding validates the original flattened UDIF boundaries and CRC32 values, stages every recompressed chunk before writing, then updates only the changed mish CRC32 and koly data-fork/master CRC32. [libdmg-hfsplus](https://github.com/planetbeing/libdmg-hfsplus/blob/master/dmg/dmglib.c) provides the checksum semantics; an independent C# reader matched all eight mish checksums on the unchanged comparison. Raw and inflated zlib bytes enter logical CRCs in run order; observed IGNORE runs are omitted. Unobserved ZERO runs, other compression/checksum types, overlaps and invalid boundaries are rejected. Base64 characters are replaced within the same metadata region, preserving its whitespace, length, partition tables and trailer offsets; the entire modified image is read again to verify CRCs. Apple chunklist authentication applies exclusively to the unchanged input, not the deliberately modified guest image. CRC integrity proves no Apple authenticity or native runtime gate. The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549`. Assembly/static review does not prove remote execution.
The [original LongQT v0.7 template](https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso), 15,884,288 bytes, is now Docker-ADD-checksummed to SHA256 `287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d`. Runtime copies actual `EFI_RELEASE/EFI/OC/Kexts`, including Lilu 1.7.1, even with official OpenCore DEBUG executables. Active Lilu: executable 526,984 bytes, SHA256 `0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52`; Info.plist SHA256 `fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a`. Staging checks both hashes and bundle version. Cryptex declares Lilu 1.4.7; [Lilu history](https://github.com/acidanthera/Lilu/blob/master/Changelog.md) includes Ventura/Sonoma installer/Recovery support before 1.7.1. Existing Lilu is kept. ## Native gates, bounds and cleanup
[CryptexFixup-1.0.5-RELEASE.zip](https://github.com/acidanthera/CryptexFixup/releases/download/1.0.5/CryptexFixup-1.0.5-RELEASE.zip), 69,703 bytes, SHA256 `25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30`, is checked in C#. Only expected Info.plist/executable files are accepted; identity/version/dependency and individual hashes are recorded. Runtime checks files before/after copying into fresh guest EFI. The original Apple recoveryosd runs under its existing job/PID beside the read-only probe. Exact known macOS 13/14 plist layouts and same-length replacements retain their allowlist. The patcher validates UDIF boundaries, updates changed mish/koly CRCs and reads back the image. Four raw/zlib positive and twelve rejection fixtures use an independent C# CRC32 reader. Apple chunklist authentication applies to the input, not the deliberately modified image.
Active `/assets/config.plist` receives exactly one enabled Cryptex immediately after enabled Lilu, preserving every other kext's order. Entry: `Arch=x86_64`, `BundlePath=CryptexFixup.kext`, `ExecutablePath=Contents/MacOS/CryptexFixup`, `PlistPath=Contents/Info.plist`, `MinKernel=22.0.0`, empty `MaxKernel`. [OpenCore Kernel.Add](https://github.com/acidanthera/OpenCorePkg/blob/1.0.7/Docs/Configuration.tex) requires dependencies first; bounds are Darwin versions. Runtime rechecks order/enabled/paths/architecture/bounds and rejects unverified `/custom.plist`. Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service queries and exactly one writable whole 64-GiB disk. The complete successful sw_vers output must contain one valid ProductVersion field and EOF within 1,024 bytes. The actual native diskutil query remains mandatory.
No new force/beta argument is needed for actual no-AVX2 CPUs. Baseline arguments remain. Validation rejects disabling arguments, `-crypt_allow_hash_validation` (disables the APFS patch) and unexpected Cryptex force/beta overrides. Manifest/profile enter the boot signature; this candidate always rebuilds `boot.img` and accepts no old cache as evidence. Required commands retain 45 seconds, UID 180 seconds and the single disk query 120 seconds. The owned observer uses `/bin/ps -M -p <diskutil-child>` with a separate 60-second limit and two-second TERM/KILL grace. It avoids stack symbolication; thread waiting states do not identify an IPC endpoint. Observation failure passes no gate. Owned children are stopped on completion/cancellation; output remains 512 KiB per command and 4 MiB proof.
## Gates, privileges and cleanup The container retains 6 GiB memory/swap, two-CPU limit, 512 MiB shared memory and a 4-GiB/two-vCPU guest. One fresh anonymous /storage volume holds the sparse 64-GiB target. Inspection rejects devices, capabilities, binds, ports, host networking and privileged mode. KVM is disabled with no /dev/kvm mapping; guest networking stays slirp.
The Apple wrapper is byte-identical to baseline: background `/Volumes/installstate/readiness.sh` then `exec /usr/libexec/recoveryosd` under the same launchd job/PID. Source evidence does not prove Apple's executable ran. Evidence retains run/source/profile identity, CPU preflight, original/patched Recovery identity, container/QEMU state, native proof/result and cleanup. Optional bounded before/during/after pressure snapshots record host/cgroup counters. The /storage/14/setup.dmg hash is captured once after staging; successful evidence survives later capture failure. Screenshots/pressure observations pass no gate.
The disk IPC continuation contains seven explicitly marked diagnostic blocks and limits disk enumeration to one attempt. Its disk query receives 120 seconds so the owned sample can finish while the query is still running. Validation removes only those marked blocks, including that command-budget exception, restores the former attempt condition and normalizes macOS 13 to 14 before requiring baseline SHA256 `4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3`. Two separate named version marker pairs exclude the new parser and explicitly restore the original three-line `sw_vers -productVersion` sequence for this baseline comparison. The receipt records this version-source exception, the 1,024-byte parser bound and all existing diagnostic budget exceptions. Architecture, UID, native service exits, minimum macOS version, disk size/writability/uniqueness, proof bounds and native-wait/cleanup/flush metrics remain identical. All other required native commands retain 45 seconds, UID retains 180 seconds, and outer limits remain ten minutes maximum disk readiness, 40 minutes host and 45 minutes workflow. Both cleanup paths verify exact token/label/ID before removing only the owned container, anonymous volume and image. No pruning, host changes, original checkout changes or Meeting Assistant restart occurs. Artifacts remain seven days. Full CI remains unverified until an installed supported guest builds/signs fresh helpers and passes all 577 tests, including the five native macOS tests, with zero skips.
Run 4186 at `227884723a25e703ec1be39f8600eeaae2085c4f` completed native `sw_vers` successfully after 43 seconds, reporting macOS 13.6 / 22G120. The redundant `sw_vers -productVersion` then timed out after 48 seconds before any disk query. The continuation extracts exactly one `ProductVersion` field from the entire already-successful `platform` or `platform-warm` output. It requires EOF within 1,024 bytes, rejects NUL delimiters, duplicate/missing fields and malformed version suffixes, and accepts only two or three numeric version components separated by dots with native tab/space padding. The existing macOS 13 minimum remains mandatory; no native timeout is relaxed by this reuse.
Run 4175 at `94a70b200508d3ba295124896d923fbb785d1658` reached macOS 13.6, x86_64 and UID 0 with KVM enabled; its nine `diskutil list physical` attempts timed out. Run 4185 at `25989cf0eb7205f30ac0bb44eb279aa3b463f12c` proved the whole writable 64-GiB target as IOMedia `disk2`; it measured approximately 14 seconds for the final native process listing and 33 seconds for IOMedia. Both Apple disk jobs were running; DiskManagement's endpoint was still inactive. The former eight-second observer allowance was shorter than observed native startup, so its killed sample did not establish an IPC wait point. A missing target is ruled out for that run; service initialization, IPC or resource delays remain unresolved.
Before the only disk attempt the continuation records bounded `launchctl print` output for `com.apple.diskarbitrationd` and `com.apple.diskmanagementd`, plus `ioreg -r -c IOMedia -l -w 0`. Its observer starts only `/usr/bin/sample <owned-diskutil-child-pid> 3 100 -file <owned-output>`, with no preceding process list or additional service query. Three seconds at a 100-millisecond interval reduces sampling overhead. The sample has 60 seconds for startup/reporting plus the existing two-second TERM/KILL grace; its separate report is flushed into the proof alongside command output. Missing sample tooling or an already completed diskutil is reported explicitly; nonzero observation exits are logged and cannot satisfy any native gate.
The observer owns its command/timer PIDs and is stopped when the disk query completes or the probe is canceled. Its output enters the existing 512-KiB per-output and 4-MiB proof budgets. The hook only reads media/service/process state and writes its existing diagnostic files: it does not load, restart, erase or modify any service or disk. Bash remains necessary because Apple Recovery runs this hook before a .NET SDK is installed. The CPU, Recovery, QEMU, Apple wrapper and container profile are unchanged. These observations are prepared diagnostics, not a new successful guest or full native CI receipt.
Container profile: `KVM=Y`, `CPU_MODEL=host`, `VERSION=13`, 4-GiB guest, two guest/host CPUs, 6-GiB memory/swap and 512-MiB shared memory. Fresh anonymous `/storage` holds the 64-GiB disk; evidence reads `/storage/13/setup.dmg`. Existing resource budget checks remain.
Only device mapping: exactly `/dev/kvm:/dev/kvm:rw`. Inspection rejects other devices/permissions, added capabilities, device requests/rules, binds, tmpfs overrides, published ports, host networking, privileged mode, wrong limits, unexpected persistent mounts or changed CPU/OS profile. No host modules, infrastructure, secrets, SSH or app lifecycle actions are involved. Guest slirp networking remains.
Evidence retains run/profile identity, source/assets, EFI staging, container/resources, macOS 13 Recovery hash, native proof/result/outcome and cleanup. `[recovery-original]` logs the exact download's size/SHA256 before modifying it, including when patch failure later deletes the source. `guest-container-resources.last-success.stdout.log` and its timestamp/hash receipt preserve the last successful resource snapshot independently of a later failed stopped-container `docker exec`. Optional final Unix HMP capture includes `info kvm`, `info status` and a bounded PPM exported from `/tmp`; capture success passes no native gate.
Optional 20-second resource snapshots before, during and after the guest probe retain cgroup CPU usage/throttling/pressure, memory events/pressure/statistics and host page-fault/swap counters. These observations test resource contention as a hypothesis; no resource failure is established by the existing guest timing alone. The large Recovery image hash is captured once after compatibility-profile staging is observed, with its successful receipt retained, instead of repeatedly hashing the image while collecting guest progress. Snapshot or hash observation failure cannot satisfy a native readiness gate.
Both cleanup paths keep exact token/label/ID checks. `docker rm --force --volumes` removes only the owned container and anonymous volume, then its exact image; no unrelated objects or pruning. Evidence stays seven days. Full native CI still needs a subsequent actual installed remote guest to build/sign helpers and pass the full suite, including five native tests without skips.
+220 -109
View File
@@ -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-skylake-sonoma";
const string CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30"; const string CpuModel = "Skylake-Client-v4";
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";
@@ -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=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 };
@@ -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/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;
@@ -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=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("\"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=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.");
@@ -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,44 @@ 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\"");
// 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 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=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"));
@@ -289,14 +339,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 +357,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 +405,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-skylake-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 +437,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" == Skylake-Client-v4 && "$VERSION" == 14 && "$CPU_FLAGS" == '@@CPU_FLAGS@@' ]] || { error 'Supported probe requires the exact TCG/Skylake/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=Skylake-Client-v4; 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 +525,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 +547,75 @@ 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/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);
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)
@@ -553,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"
+10 -12
View File
@@ -120,10 +120,10 @@ read_product_version() {
# END successful sw_vers version parser # END successful sw_vers version parser
# BEGIN disk IPC diagnostic # BEGIN disk IPC diagnostic
# Optional observations have their own child/timer ownership. No service is # Optional observations have their own child/timer ownership. Thread state/time
# loaded, restarted or changed, and samples target only this probe's diskutil. # targets only this probe's diskutil and does not request stack symbolication.
observe_disk_query() { observe_disk_query() {
local disk_process="$1" output="$2" sample_output="$3" observation_child="" observation_timer="" local disk_process="$1" output="$2" observation_child="" observation_timer=""
cancel_observation() { cancel_observation() {
trap '' TERM INT trap '' TERM INT
if [ -n "$observation_child" ]; then if [ -n "$observation_child" ]; then
@@ -161,14 +161,14 @@ observe_disk_query() {
} }
trap cancel_observation TERM INT trap cancel_observation TERM INT
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output" printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
if [ -x /usr/bin/sample ]; then if [ -x /bin/ps ]; then
if kill -0 "$disk_process" 2>/dev/null; then if kill -0 "$disk_process" 2>/dev/null; then
observe_command diskutil-sample /usr/bin/sample "$disk_process" 3 100 -file "$sample_output" observe_command diskutil-threads /bin/ps -M -p "$disk_process"
else else
printf '[disk-observation-unavailable] diskutil already exited before sample\n' >> "$output" printf '[disk-observation-unavailable] diskutil already exited before thread observation\n' >> "$output"
fi fi
else else
printf '[disk-observation-unavailable] /usr/bin/sample is unavailable\n' >> "$output" printf '[disk-observation-unavailable] /bin/ps is unavailable\n' >> "$output"
fi fi
} }
@@ -229,13 +229,11 @@ run_command() {
# BEGIN disk IPC diagnostic # BEGIN disk IPC diagnostic
if [[ "$name" == disks && "$attempt" == 1 ]]; then if [[ "$name" == disks && "$attempt" == 1 ]]; then
local observation_output="/tmp/native-diagnostic-disk-observation.out" local observation_output="/tmp/native-diagnostic-disk-observation.out"
local sample_output="/tmp/native-diagnostic-disk-sample.out"
: > "$observation_output" : > "$observation_output"
: > "$sample_output" observe_disk_query "$process" "$observation_output" &
observe_disk_query "$process" "$observation_output" "$sample_output" &
ACTIVE_OBSERVER=$! ACTIVE_OBSERVER=$!
printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3 printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3
PENDING_OUTPUTS+=("$observation_output" "$sample_output") PENDING_OUTPUTS+=("$observation_output")
fi fi
# END disk IPC diagnostic # END disk IPC diagnostic
wait "$process" wait "$process"
@@ -305,7 +303,7 @@ read_product_version || fail_probe product_version_invalid
# END successful sw_vers version extraction # END successful sw_vers version extraction
os_version="$SCALAR" os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid [[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version (( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded flush_outputs || finish false diagnostic_log_budget_exceeded
# BEGIN disk IPC diagnostic # BEGIN disk IPC diagnostic
+79
View File
@@ -0,0 +1,79 @@
; Bare 64-KiB BIOS for the existing Linux QEMU binary, not macOS firmware.
; Assemble: nasm -f bin -o ci-cpu-preflight.bin macos-tcg-cpu-preflight.asm
; No disks/network. isa-debug-exit returns 33 only after AVX + AVX2 execute
; and the upper 128-bit lane contains the expected arithmetic result.
; Unsupported instructions/triple faults cannot produce the success code.
BITS 16
ORG 0
start:
cli
cld
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000
; QEMU zeroes fresh RAM. Identity-map the first 2 MiB through three tables.
mov dword [0x1000], 0x2003
mov dword [0x2000], 0x3003
mov dword [0x3000], 0x0083
lgdt [cs:gdt_descriptor]
mov eax, 0x40620 ; PAE, OSFXSR, OSXMMEXCPT, OSXSAVE
mov cr4, eax
mov eax, 0x1000
mov cr3, eax
mov ecx, 0xc0000080 ; EFER.LME
rdmsr
or eax, 0x100
wrmsr
mov eax, cr0
and eax, ~0x0c ; clear EM and TS before vector instructions
or eax, 0x80000003 ; paging, protected mode, monitor coprocessor
mov cr0, eax
jmp dword 0x08:(0xf0000 + long_mode)
ALIGN 8
gdt:
dq 0
dq 0x00af9a000000ffff ; ring-0 long-mode code, base 0
dq 0x00cf92000000ffff ; ring-0 data, base 0
gdt_descriptor:
dw gdt_descriptor - gdt - 1
dd 0xf0000 + gdt
BITS 64
long_mode:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov rsp, 0x8000
xor ecx, ecx
mov eax, 7 ; XCR0 enables x87, SSE and AVX state
xor edx, edx
xsetbv
vxorps ymm0, ymm0, ymm0 ; AVX, including the upper YMM lane
vpcmpeqd ymm1, ymm1, ymm1 ; AVX2: all eight int32 lanes become -1
vpsrld ymm1, ymm1, 31 ; AVX2: all lanes become 1
vpaddd ymm2, ymm1, ymm1 ; AVX2: all lanes become 2
vextracti128 xmm3, ymm2, 1 ; AVX2: inspect the upper half, not only SSE
vmovd eax, xmm3
cmp eax, 2
jne fail
vzeroupper
mov eax, 0x10 ; QEMU debugexit computes (value << 1) | 1
jmp exit_qemu
fail:
mov eax, 0x11
exit_qemu:
mov dx, 0xf4
out dx, eax
hlt
jmp $
; CPU reset starts at the last 16 bytes; reload the real-mode CS base.
BITS 16
TIMES 0xfff0 - ($ - $$) db 0xff
jmp 0xf000:start
TIMES 0x10000 - ($ - $$) db 0xff