Integrate existing-runner KVM profile with full native CI gates

This commit is contained in:
dh committed 2026-10-03 21:43:04 +02:00
commit 8d2014dcbc
11 files changed
+523 -78

No files matched your search

@@ -1,4 +1,4 @@
name: Native macOS Recovery diagnostic on Ubuntu
name: macOS 13 KVM Cryptex Recovery compatibility diagnostic
on:
workflow_dispatch:
@@ -19,7 +19,7 @@ jobs:
with:
dotnet-version: "10.0.x"
- name: Probe native macOS Recovery with existing Docker resources
- name: Probe macOS 13 Recovery with existing KVM and host CPU
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Run owned software macOS guest and all native tests
- name: Run owned macOS 13 KVM guest and all native tests
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full
- name: Always remove only this run's owned resources
if: always()
+1 -1
View File
@@ -145,7 +145,7 @@ jobs:
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Run owned software macOS guest and all native tests
- name: Run owned macOS 13 KVM guest and all native tests
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full
- name: Always remove only this run's owned resources
if: always()
+2 -2
View File
@@ -169,9 +169,9 @@ Detailed workflow syntax and extension guidance live in `docs/meeting-workflow-e
Behavior changes are OpenSpec-driven and test-first: update the relevant requirement/scenario, add a failing public behavior test, implement the smallest passing change, run focused tests and then the justified broader suite, and validate the active change with `openspec validate <change-id> --strict`. Documentation-only maintenance does not need a new OpenSpec change.
The Gitea workflow runs for pull requests, pushes, and manual dispatch on the existing `ubuntu-latest` runners. One job explicitly builds the Windows desktop target, installs Wine plus a matching Windows .NET SDK, and runs the portable test project through the Windows host under Wine. Another job builds and tests `net10.0` on Ubuntu, including the managed macOS audio, calendar, screenshot, registration, and desktop-control behavior tests. Its `TZ=Europe/Berlin` setting also exercises the calendar daylight-saving regression. After both jobs pass, the workflow requires a native macOS Full job on the same existing runner label. No additional runner labels or host devices are required.
The Gitea workflow runs for pull requests, pushes, and manual dispatch on the existing `ubuntu-latest` runners. One job explicitly builds the Windows desktop target, installs Wine plus a matching Windows .NET SDK, and runs the portable test project through the Windows host under Wine. Another job builds and tests `net10.0` on Ubuntu, including the managed macOS audio, calendar, screenshot, registration, and desktop-control behavior tests. Its `TZ=Europe/Berlin` setting also exercises the calendar daylight-saving regression. After both jobs pass, the workflow requires a native macOS Full job on the same existing runner label, using the runner's existing `/dev/kvm` device and Docker daemon.
The Ubuntu managed-test job does not compile the Swift helpers or execute Apple frameworks; its native macOS tests report an explicit skip through `MacOsFact`. The prepared Full job runs an owned macOS 14+ x86_64 guest through unprivileged TCG on the existing Ubuntu Docker runner, builds and signs the native helpers, and requires all 577 tests to pass with zero skips, including all five native macOS tests. It has a 180-minute job limit with `always()` steps for retained evidence and ownership-checked cleanup. This CI path remains unqualified until an actual remote installed guest produces every required receipt. Native tests can also be run on a supported Mac with `dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release -p:EnableWindowsTargeting=true`; real microphone/system-audio capture and privacy permissions still require the operational checks described above.
The Ubuntu managed-test job does not compile the Swift helpers or execute Apple frameworks; its native macOS tests report an explicit skip through `MacOsFact`. The prepared Full job runs an owned macOS 13+ x86_64 guest with KVM, the real host CPU and pinned CryptexFixup on the existing Ubuntu Docker runner. It builds all four native helpers, verifies the audio app's signature, and requires all 577 tests to pass with zero skips, including all five native macOS tests. It has a 180-minute job limit with `always()` steps for retained evidence and ownership-checked cleanup. Recovery compatibility, installed toolchain operation and this CI path remain unqualified until actual remote guests produce every required receipt; .NET 10's binary minimum does not establish vendor support for macOS 13. Native tests can also be run on a supported Mac with `dotnet test MeetingAssistant.Tests/MeetingAssistant.Tests.csproj -f net10.0 -c Release -p:EnableWindowsTargeting=true`; real microphone/system-audio capture and privacy permissions still require the operational checks described above.
The separate manual `.gitea/workflows/macos-native-full.yaml` retains the same Full flow as a diagnostic entry point. CI validates source; it does not publish or deploy the workstation application.
+56 -31
View File
@@ -1,66 +1,91 @@
# Native macOS Recovery diagnostic on the existing Ubuntu runner
# macOS 13 KVM/Cryptex diagnostic and prepared Full flow
This manual diagnostic tests the unresolved Recovery startup boundary before qualifying the prepared native macOS application build/test flow. It does not install macOS, erase a guest disk, install .NET or Apple CLT, or run Meeting Assistant tests. A green diagnostic means only that a real macOS 14+ x86_64 Recovery guest has a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk.
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.
The workflow `.gitea/workflows/macos-native-diagnostic.yaml` has only `workflow_dispatch`; it does not run on ordinary pushes or pull requests. It uses the same `ubuntu-latest` label and existing Docker daemon as the current builds. There are no runner changes, extra host devices, privileged containers, added capabilities, published ports, host networking or new secrets. It fails clearly if the existing Docker daemon cannot fit its bounded resource budget.
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.
## Helper entry point and invocation
## Reasons and remaining gaps
The orchestration is a .NET 10 file-based C# app at `tools/ci/MacOsNativeDiagnostic.cs`:
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.
```sh
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.
[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.
**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.
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.
## Entry point and dependencies
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.
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --help
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/pinned/dockur-clone --output artifacts/native-validation
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
~~~
`--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, then checks Bash syntax, leaving the supplied source untouched. It does not download/extract the LongQT ISO 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 and Bash; 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 -- --cleanup --output artifacts/native-macos
```
~~~
Dependencies are the existing Linux/x64 runner, .NET 10 SDK, Git, Bash and Docker CLI/socket. The actual execution downloads public Dockur source, upstream build assets, Docker images and Apple Recovery; it does not use workstation credentials. The existing upstream Python UDIF patcher and the Bash hook are retained because they run inside the pinned Linux/macOS boot integration. Independent orchestration and validation remain C#.
## Exact bootasset contract
The helper clones Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, verifies its exact Recovery patcher hash, and makes three narrowly verified source edits. The early `rc.cdrom.sh` hook only mounts the existing state share and returns. A same-length XML replacement makes the existing `com.apple.recoveryosd` LaunchDaemon execute `/bin/bash /Volumes/installstate/launch.sh` after boot tasks. The staged `launch.sh` is replaced entirely by the checked-in read-only readiness probe. All replacement counts are exact; an upstream mismatch fails. The two imported QEMU image digests are pinned and the final image/source/Recovery hashes are retained. Other upstream Dockerfile downloads are observed through the resulting image identity rather than asserted to be immutable.
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.
The VM uses TCG (`KVM=N`), slirp networking, a 4-GiB guest, two virtual CPUs and a sparse 64-GiB data disk. Its container has a 6-GiB memory/swap ceiling and a two-CPU limit. The existing Docker daemon must report at least two CPUs and 6 GiB total memory, the runner must have at least 5 GiB available memory, and the Docker filesystem must have at least 8 GiB free before Recovery downloads or boot. Its own native commands retain 45-second watchdogs and a ten-minute readiness phase; the host orchestrator has a 40-minute deadline and the workflow a 45-minute limit.
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.
Actual remote run 4155 stopped at the first `sw_vers` with exit 143. Run 4159 then proved native Darwin/x86_64, root identity and guest AVX2, but reached the host deadline before `sw_vers` or the service/disk gates. Its logged command durations included timer cleanup and output copying, so they did not isolate native execution time.
[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 next probe runs mandatory architecture, root identity and platform gates before optional process/CPU diagnostics. It keeps the proof log open, uses Bash 3.2's timed FIFO reads instead of starting a separate sleep process for every watchdog, and groups output copying and byte-limit checks. Separate markers record fork/exec/wait, timer cleanup and output flush durations. Raw output still fails above 512 KiB per command, proof above 4 MiB fails, and scalar gates reject hidden suffixes or multiline values. A local harmless-command harness verifies all 16 timeout, cancellation, output and scalar cases; this does not qualify macOS Recovery.
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`.
After an initial platform failure the hook collects native launchd context and repeats the identical `sw_vers` command once, with the same 45-second limit. Native product version and all original identity/service/disk gates remain required. Optional process and CPU diagnostics run only after a gate fails. The upstream AVX2 warning reads host flags; run 4159 observed AVX2 in the actual guest. No host or guest CPU settings change.
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.
## Evidence and cleanup
## Gates, privileges and cleanup
Evidence is written under the requested output directory: run identity and candidate commit, Docker/runner resources, exact source patch artifacts and hashes, image/container inspection, Recovery hash, native platform/process/launchctl/diskutil logs, machine-readable guest result, outcome and cleanup receipt. The workflow retains these as a seven-day artifact. Phase names and up to 512 KiB of the final native proof also appear in CI stdout, on success or failure, with the run token replaced; no environment or credential dump is printed. A Docker start/build exit zero is not a successful native result. A missing, stale, unsupported-platform, read-only or wrong-size guest receipt fails.
The read-only Apple wrapper is byte-identical to baseline: background `/Volumes/installstate/readiness.sh` then `exec /usr/libexec/recoveryosd` under the same launchd job/PID. Full mode uses the separate guarded wrapper described below. Source evidence does not prove Apple's executable ran.
Before final cleanup, an optional ten-second capture rechecks the saved container ID/ownership label and uses the pinned image's existing Unix HMP socket, `nc.openbsd` and a five-second `timeout` to collect only [`info status` and `screendump`](https://www.qemu.org/docs/master/system/monitor.html), retaining the command transcript, exit codes and fresh bounded PPM screenshot; capture failure is visible and never changes native readiness or test success.
Readiness changes only minimum macOS 14 to 13. Validation normalizes that gate to 14 and requires baseline SHA256 `4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3`. Architecture, UID, services, disk size/writability/uniqueness, retries, proof bounds, timers and native-wait/cleanup/flush metrics remain identical. Limits stay 45 seconds per native command, 180 seconds for UID, ten minutes disk readiness, 40 minutes host and 45 minutes workflow.
Run 4159 generated a 6,220,817-byte screenshot file under `/dev/shm`, but `docker cp` could not retrieve it. Screenshots now use the regular container path `/tmp/native-diagnostic-screen-<runToken>.ppm`, avoiding Docker's documented [`/dev`/tmpfs copy limitation](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases).
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.
Every container/image has a random run token in its ownership label. `finally` cleanup and the workflow's `always()` step inspect that exact label before removing the matching container and its anonymous storage volume, then the matching image. They never remove an unrelated name or volume, prune Docker, modify host settings or restart Meeting Assistant. Temporary source files are deleted only when their local marker matches the same token. Evidence remains available after cleanup.
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.
The earlier background-only local bootstrap never obtained DiskManagement readiness. This separate LaunchDaemon probe is still an experiment until the actual remote run produces the required native evidence. Full macOS CI support remains unverified until an installed guest subsequently compiles/signs the native helpers and passes all application tests, including all five native tests without skips.
Evidence retains run/profile identity, source/assets, EFI staging, container/resources, macOS 13 Recovery hash, native proof/result/outcome and cleanup. Optional final Unix HMP capture includes `info kvm`, `info status` and a bounded PPM exported from `/tmp`; capture success passes no native 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.
## Experimental full guest flow
The prepared `.gitea/workflows/pr-push-build-and-test.yaml` requires the `macos-native-full` job after both existing Wine and portable jobs succeed, using `ubuntu-latest`, a 180-minute limit and the same checkout/SDK/Full-run/always-cleanup/artifact steps as the separate manual `.gitea/workflows/macos-native-full.yaml`. The manual Full workflow remains a diagnostic entry point. This automatic CI path is unqualified until an actual remote installed guest provides the required native build and test receipts. Before publishing or attempting it, inspect the candidate and its ownership boundaries and wait for the actual remote Recovery probe to qualify. The full acceptance review follows actual remote build/test verification. Each Full run repeats Recovery readiness in its own VM; it does not accept another run's disk receipt. The ordinary diagnostic workflow and checked-in read-only hook keep their read-only behavior.
The prepared `.gitea/workflows/pr-push-build-and-test.yaml` requires `macos-native-full` after both existing Wine and portable jobs succeed, using `ubuntu-latest`, a 180-minute limit and the same checkout/SDK/Full-run/always-cleanup/artifact steps as the separate manual `.gitea/workflows/macos-native-full.yaml`. Both use the existing KVM device, host CPU and macOS 13/Cryptex profile above. Recovery compatibility and the installed build/test path remain unqualified. A Full run must wait for actual remote Recovery qualification, then repeat readiness in its own VM; it cannot accept another run's disk receipt. The full acceptance review follows actual remote build/test verification. The ordinary diagnostic workflow remains read-only.
```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --full --source /path/to/pinned/dockur-clone --output artifacts/native-full-validation
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --full --source /path/to/clean/pinned/dockur-clone --cryptex-archive /path/to/CryptexFixup-1.0.5-RELEASE.zip --output /path/to/fresh/full-validation
dotnet run --file tools/ci/MacOsNativeGuest.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos-full
```
~~~
The host requires a clean exact Git HEAD, creates its Git/PAX source archive and SHA-256, and downloads macOS/x64 SDK `10.0.401` from Microsoft's release URL with the fixed official SHA-512 recorded in both helpers. Source archive, SDK and helper files are copied inside the image into the newly owned anonymous `/storage` volume; no workstation bind mount is introduced. The full flow uses persistent `/storage/14/ci-state` as its existing 9p share, with a run-owner marker and an erase guard that is never removed on installer failure. It never automatically restarts a container or retries erasure.
The host requires a clean exact Git HEAD, creates its Git/PAX source archive and SHA-256, and downloads macOS/x64 SDK `10.0.401` from Microsoft's release URL with the fixed official SHA-512 recorded in both helpers. Source archive, SDK and helper files enter the image and newly owned anonymous `/storage` volume; no workstation bind mount is introduced. The persistent state is `/storage/13/ci-state` through the existing guest 9p share. An uncommitted integration cannot be qualified through `git archive HEAD`: only an archive of the final reviewed commit binds the actual integrated source.
Before the only guest `eraseDisk`, C# revalidates the owned Docker boundary, sole anonymous storage mount, exact 64-GiB raw image, live QEMU attachment and per-run emulated disk serial. Only after a valid native Recovery receipt does it atomically provide the run/commit/disk permit. The guarded Apple installer rechecks `diskutil` and the corresponding IORegistry serial; missing or ambiguous identity fails. With mounted run-owned state, `fail()`, nonzero `startosinstall` and TERM/INT atomically publish a token-bound `installation-failed` phase for the next host poll, preserving the erase guard. Upstream `startosinstall`, USR1 bootstrap staging, Setup Assistant/admin packages and byte-for-byte staging checks remain in use. Installer reboots preserve the same QEMU process, disk, NVRAM and share. The readonly Recovery media stays attached.
The Full-only `macos-native-full-bootstrap.sh` waits up to 120 one-second mount attempts for the share's own `run.owner`; a present foreign or invalid owner fails immediately. Before backgrounding the probe, it exclusively creates and validates the complete one-record `probe.started` marker bound to run token and source commit. A subsequent launchd wrapper start with the same complete owned marker starts no second child and always execs the original Apple `recoveryosd`. Foreign, empty, partial, extra-record or unterminated markers and genuine write failures remain errors; markers are preserved. An initial child failure remains a token-bound bootstrap failure, without silent retry. If the share never appears, the wrapper reports a bounded mount failure to stderr without writing to foreign state, then still execs Apple. This contract relies on completing the marker before the wrapper's first Apple exec; it does not prove arbitrary simultaneous installer starts or actual guest 9p atomicity.
The existing firstboot LaunchDaemon invokes `tools/ci/macos-native-firstboot.sh` before its staging cleanup. This Bash seam is required because the guest has Apple boot tools but no .NET SDK yet. It proves installed APFS `/` belongs to the same owned 64-GiB physical disk, mounts the state share, installs a compatible Apple CLT catalog label through headless `softwareupdate`, verifies the CLT package/compiler and builds a framework smoke program. There is no GUI fallback, Apple account or new secret. `macos-native-disk-guard.sh` holds the shared pre-.NET Apple disk/IORegistry check. The existing upstream Python UDIF patcher remains the image-format runtime binding; both its exact patch matches and compressed-slot checks remain enforced.
Before the only guest `eraseDisk`, C# revalidates the owned Docker boundary, sole anonymous storage mount, exact writable 64-GiB raw image, live QEMU attachment and per-run emulated disk serial. Only after a valid fresh native Recovery receipt does it atomically provide the run/commit/disk permit. The guarded Apple installer rechecks `diskutil` and the corresponding IORegistry serial; missing or ambiguous identity fails. Its exclusive persistent `started` marker binds token, commit and disk. A duplicate child with a complete matching marker exits neutrally, preserving the active installation phase. Foreign or invalid guards and real write errors publish installation failure. The installer child never starts an extra Apple daemon; it terminates while the Full wrapper preserves the original one. The local losing-claim fixture publishes a complete record between checks; the low-level create-before-printf window is not claimed to be a general concurrent-race solution.
After verifying and extracting the SDK on the guest's own APFS work directory, `tools/ci/MacOsNativeGuest.cs` takes over. This .NET10 file-based slice validates payload hashes and safe Git tar paths/PAX commit, then performs restore/build/test for `net10.0` with `TZ=Europe/Berlin`. It requires fresh outputs for all four Swift helpers, actual Mach-O/x86_64 tools output and strict audio-app codesign verification. It accepts only fresh TRX with 577 total/executed/passed results, zero failures/skips and all five named macOS tests explicitly passed. TRX SHA-256 uses the same raw-byte snapshot as parsing, including any UTF-8 BOM.
With mounted run-owned state, `fail()`, nonzero `startosinstall` and TERM/INT atomically publish a token-bound `installation-failed` phase for the next host poll, preserving the erase guard. The host observes Full bootstrap failure even before the install permit. Upstream `startosinstall`, USR1 bootstrap staging, Setup Assistant/admin packages and byte-for-byte staging checks remain in use. Installer reboots preserve the same QEMU process, disk, NVRAM and share. The read-only Recovery media stays attached. There is no automatic container restart or erase retry.
The full helper's outer deadline is 172 minutes; the job declares 180 minutes within the existing three-hour server limit, leaving time for evidence and owned-resource cleanup. Independent budgets are Recovery 40 minutes, installer 80, firstboot/CLT 30 and guest checks/restore/build/tests 25; the outer deadline also bounds their combined runtime and preparation. The same 4-GiB/two-CPU guest and 6-GiB container remain, with explicit sparse allocation. Full execution checks 32 GiB of existing Docker free space before Recovery downloads/boot and 8 GiB of guest free space before toolchain work. Insufficient resources, networking, Apple catalog availability, disk ownership, installer progress or test proof fail clearly without changing infrastructure.
The existing firstboot LaunchDaemon invokes `macos-native-firstboot.sh` before staging cleanup. This Bash seam is required because the guest has Apple boot tools but no .NET SDK yet. It proves installed APFS `/` maps through one APFS container and physical store to the same owned 64-GiB whole disk, mounts the state share, installs a compatible Apple CLT catalog label through headless `softwareupdate`, and verifies the CLT package/compiler. It records the actual `xcrun` SDK version/path and compiles and runs a macOS 13-targeted smoke program importing AppKit, AVFoundation, ScreenCaptureKit, EventKit and WebKit. CLT compatibility is established by these actual compiler/framework gates, rather than a guessed catalog version. There is no GUI fallback, Apple account or new secret. `macos-native-disk-guard.sh` holds the shared pre-.NET Apple disk/IORegistry check. The pinned upstream Python UDIF patcher remains the image-format runtime binding; exact patch matches and compressed-slot checks fail closed.
Artifacts add source/SDK hashes, generated pinned boot-source patches, installer/Apple/firstboot logs, installed-root/disk identity (including the APFS container and physical-store mapping plists), native tool logs, guest phase and full-result receipts, native helper hashes and binary-preserved TRX. The polling loop prints a bounded heartbeat after each elapsed minute with the current phase, its elapsed/budget time, container liveness and readiness state, without dumping environment variables or download URLs. Bounded final build/test output and the full-result receipt are also printed in CI. Cleanup uses the same exact saved resource ID/ownership label through `finally` and workflow `always()`; it removes only this run's container/image/anonymous volume. Installed guest files disappear with that volume and retained CI evidence stays outside it. Shared Docker build cache is not pruned.
After verifying and extracting the SDK on the guest's own APFS work directory, `MacOsNativeGuest.cs` validates payload hashes, safe Git tar paths and PAX commit, then restores/builds/tests `net10.0` with `TZ=Europe/Berlin`. It records the actual SDK/compiler environment and requires installed macOS 13+ x86_64 with guest root identity. It requires fresh outputs for all four Swift helpers, actual Mach-O/x86_64 tools output and strict audio-app codesign verification. Only fresh TRX with 577 total/executed/passed results, zero failures/skips and all five named macOS tests explicitly passed is accepted. TRX SHA-256 uses the same raw-byte snapshot as parsing, including any UTF-8 BOM. Binary minima and local parser fixtures do not demonstrate .NET vendor support or installed runtime compatibility.
Local `--validate` creates only patch/fixture evidence; it never starts Docker, installs an OS/toolchain, erases a disk, builds the application or runs native tests. With `--compression-chunk` it can read the retained qualified Recovery raw chunk and validate the unchanged LaunchDaemon/mount patch against Python zlib's real compressed slot, without changing the DMG. The IORegistry parser's root association and installed-APFS mapping still require the actual emulated guest's output; synthetic fixtures do not qualify that disk identity. This is a candidate until an actual remote installed guest produces every required native receipt.
The Full outer deadline is 172 minutes; the workflow declares 180 minutes within the existing three-hour server limit, leaving time for evidence and owned-resource cleanup. Independent budgets are Recovery 40 minutes, installer 80, firstboot/CLT 30 and guest checks/restore/build/tests 25; the outer deadline also bounds their combined runtime and preparation. The same 4-GiB/two-CPU guest and 6-GiB container remain, with `ALLOCATE=N`. Full execution checks 32 GiB of existing Docker free space before downloads/boot and 8 GiB of guest free space before toolchain work. Insufficient resources, networking, Apple catalog availability, disk ownership, installer progress or test proof fail without changing infrastructure.
Artifacts include source/SDK hashes, pinned boot patches, installer/Apple/firstboot logs, CLT SDK identity, installed-root/disk identity with APFS mapping plists, native tool logs, guest phase and Full-result receipts, helper hashes and binary-preserved TRX. Polling prints a bounded heartbeat each elapsed minute with phase/time/budget, liveness and readiness. Cleanup retains exact saved resource ID/ownership checks through `finally` and workflow `always()`; only the owned container/image/anonymous volume are removed. Installed files disappear with that volume; retained CI evidence remains outside it. Shared Docker build cache is not pruned.
Local `--validate --full` generates source/fixture evidence and executes the generated installer guard and complete Full wrapper with harmless filesystem/mount, child-process and Apple-exec boundary fixtures. It covers owned/foreign/invalid/write-failed installer and probe markers, restart, first-child failure and delayed/missing/foreign-owner shares. It starts no Docker or VM, erases no disk, installs no OS/toolchain, builds no application and runs no native test. Bash syntax, synthetic disk parser and receipt tests do not qualify actual Apple exec, guest storage or native CI. Read-only `--compression-chunk` evidence applies only to its retained Recovery chunk, not the new Full wrapper or a different downloaded macOS image. This remains a locally prepared candidate until actual remote guests satisfy every native gate.
+364 -31
View File
@@ -1,5 +1,6 @@
#:property PublishAot=false
using System.Diagnostics;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -12,13 +13,16 @@ return await NativeDiagnostic.Execute(args);
static class NativeDiagnostic
{
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 CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30";
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
const int MaximumCapturedCharacters = 8 * 1024 * 1024;
const string SdkVersion = "10.0.401";
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
const string FullState = "/storage/14/ci-state";
const string FullState = "/storage/13/ci-state";
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
const string OriginalBootstrap = "[ ! -e /tmp/m ]&&{ /sbin/mount_9p installstate >/dev/null 2>&1;exec /Volumes/installstate/launch.sh;};: >/tmp/m\n";
const string MountOnlyBootstrap = "[ ! -e /tmp/m ]&& /sbin/mount_9p installstate >/dev/null 2>&1; : >/tmp/m\n";
@@ -50,7 +54,7 @@ static class NativeDiagnostic
{
if (args.Length == 0 || args.Contains("--help"))
{
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--full] [--output artifacts/native-macos] [--source existing-dockur-clone] [--compression-chunk readonly-qualified-chunk]");
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--full] [--output artifacts/native-macos] [--source existing-dockur-clone] [--cryptex-archive verified-release.zip] [--compression-chunk readonly-qualified-chunk]");
return 0;
}
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
@@ -60,7 +64,7 @@ static class NativeDiagnostic
ValidateContracts();
if (full) ValidateFullContracts();
if (Option(args, "--source") is { } source)
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full);
await PrepareSource(Path.GetFullPath(source), output, full ? new string('0', 32) : "validation", false, CancellationToken.None, full, Option(args, "--cryptex-archive"));
if (full) await ValidateDiskSerialParser(output);
if (Option(args, "--compression-chunk") is { } chunk)
await ValidateCompression(Path.GetFullPath(chunk), output);
@@ -93,7 +97,7 @@ static class NativeDiagnostic
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
ValidateContracts();
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes, mode = full ? "full" : "readiness" });
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex", 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, mode = full ? "full" : "readiness" });
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
using (var document = JsonDocument.Parse(info.Output))
{
@@ -113,13 +117,13 @@ static class NativeDiagnostic
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 (full) await PreparePayload(source, output, token, sourceCommit, deadline.Token);
await PrepareSource(source, output, token, true, deadline.Token, full);
await PrepareSource(source, output, token, true, deadline.Token, full, Option(args, "--cryptex-archive"));
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);
using (var image = JsonDocument.Parse(imageInspect.Output))
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
Save(statePath, state);
List<string> createArguments = ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--env", "KVM=N", "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=14", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2"];
List<string> createArguments = ["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"];
if (full) createArguments.AddRange(["--env", "ALLOCATE=N", "--env", "DISK_OPTIONS=serial=" + DiskSerial(token)]);
createArguments.Add(state.ImageTag);
var create = await Command("docker", createArguments.ToArray(), output, "docker-create", deadline.Token);
@@ -130,7 +134,7 @@ static class NativeDiagnostic
await Command("docker", ["inspect", id], output, "container-created", deadline.Token);
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
Console.WriteLine(full ? "The owned unprivileged TCG guest is starting. Installation requires a fresh native readiness receipt and an explicit owned-disk permit; success requires all 577 tests with zero skips." : "The owned unprivileged TCG guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run.");
Console.WriteLine(full ? "The owned restricted KVM/host-CPU macOS 13 guest is starting. Installation requires fresh native readiness and an owned-disk permit; success requires all 577 tests with zero skips." : "The owned restricted KVM/host-CPU macOS 13 guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run.");
var phaseStarted = Stopwatch.StartNew();
var phase = "recovery";
var phaseBudget = TimeSpan.FromMinutes(40);
@@ -162,7 +166,7 @@ static class NativeDiagnostic
break;
}
}
if (full && permitted)
if (full)
{
var phasePath = Path.Combine(output, "guest-phase.json");
if (File.Exists(phasePath))
@@ -170,12 +174,12 @@ static class NativeDiagnostic
using var nativePhase = JsonDocument.Parse(File.ReadAllText(phasePath));
if (nativePhase.RootElement.GetProperty("token").GetString() != token) throw new InvalidOperationException("Stale native phase receipt.");
var current = nativePhase.RootElement.GetProperty("phase").GetString();
var next = current == "toolchain-installing" ? "toolchain" : current is "tests-running" or "tests-passed" ? "tests" : phase;
var next = !permitted ? phase : current == "toolchain-installing" ? "toolchain" : current is "tests-running" or "tests-passed" ? "tests" : phase;
if (next != phase) { phase = next; phaseBudget = TimeSpan.FromMinutes(next == "toolchain" ? 30 : 25); phaseStarted.Restart(); Console.WriteLine("[native-diagnostic] phase: " + phase); }
if (current is "tests-failed" or "bootstrap-failed" or "installation-failed") throw new InvalidOperationException("Guest phase failed: " + current);
}
var fullResult = Path.Combine(output, "full-result.json");
if (File.Exists(fullResult))
if (permitted && File.Exists(fullResult))
{
ValidateFullResult(File.ReadAllText(fullResult), token, sourceCommit, File.ReadAllText(Path.Combine(output, "archive.sha256")).Trim());
ValidateTrx(File.ReadAllBytes(Path.Combine(output, "native.trx")), File.ReadAllText(fullResult));
@@ -221,18 +225,33 @@ static class NativeDiagnostic
static void ValidateContracts()
{
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
var baseline = ReplaceOnce(readiness, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
throw new InvalidOperationException("Compatibility readiness may change only the baseline's macOS minimum to 13; identity, services, disk and limits must remain identical.");
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.");
XDocument.Parse(DiagnosticDaemon);
if (Encoding.UTF8.GetByteCount(DiagnosticDaemon) > Encoding.UTF8.GetByteCount(OriginalDaemon + "\n")) throw new InvalidOperationException("Daemon replacement exceeds original file.");
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "14.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "13.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");
foreach (var invalid in new[] { good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
foreach (var invalid in new[] { good.Replace("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") })
{
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
}
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}]}]
""";
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") })
{
try { AssertContainer(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an excessive/wrong-profile container boundary.");
}
}
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false, string? cryptexArchive = null)
{
Directory.CreateDirectory(output);
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
@@ -244,14 +263,26 @@ static class NativeDiagnostic
var constants = "RECOVERY_ORIGINAL = b'''" + daemon + "'''\nRECOVERY_REPLACEMENT = b'''" + DiagnosticDaemon + "'''.ljust(len(RECOVERY_ORIGINAL), b\" \")";
patch = ReplaceOnce(patch, oldConstants, constants);
var dockerPath = Path.Combine(source, "Dockerfile");
// The actual remote BuildKit cannot checksum a dangling symlink during root COPY.
// Start directly from the same immutable image filesystem; its inspected Config is empty.
if (Hash(File.ReadAllBytes(dockerPath)) != "a0e804235967400eb70e755d63eff8a33a7761922ddd6e9723faa8e828fd8aa3") throw new InvalidOperationException("Pinned Dockerfile hash mismatch.");
// The existing runner's BuildKit cannot checksum dangling manpage links during COPY /.
// This pinned filesystem image has an empty Config; FROM preserves the same runtime defaults.
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 = 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);
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");
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, "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");
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", full ? "macos-native-full-bootstrap.sh" : "macos-native-bootstrap.sh"));
if (full) wrapper = ReplaceOnce(wrapper, "@@PROOF_TOKEN@@", token);
var imagePath = Path.Combine(source, "src", "image.sh");
// Read immutable staging source before PrepareFullSource can write any seam.
var originalImage = ReadPinned(source, "src/image.sh", "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c");
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");
if (full)
{
await PrepareFullSource(source, output, token, writeSource, cancellation);
@@ -259,22 +290,198 @@ static class NativeDiagnostic
dockerfile += "\n# Payload stays inside this image and its owned anonymous storage volume.\nCOPY ci-payload/ /assets/ci-payload/\n";
entry = ReplaceOnce(entry, "free_kib >= 8 * 1024 * 1024", "free_kib >= 32 * 1024 * 1024").Replace("8-GiB diagnostic budget", "32-GiB full-run budget", StringComparison.Ordinal);
}
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", hook), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("early-bootstrap.sh", MountOnlyBootstrap) })
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), ("early-bootstrap.sh", MountOnlyBootstrap), ("boot.sh.patched", compatibility.Boot), ("opencore-config.plist", compatibility.Config) })
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 "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "early-bootstrap.sh").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 "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 "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))));
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, "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, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
if (full && !writeSource) await ValidateFullBootstrap(wrapper, output, token, cancellation);
if (!writeSource) return;
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
File.WriteAllText(dockerPath, dockerfile, new UTF8Encoding(false));
File.WriteAllText(entryPath, entry, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/launch.sh"), hook, 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/readiness.sh"), hook, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/boot.sh"), compatibility.Boot, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "assets/config.plist"), compatibility.Config, new UTF8Encoding(false));
var target = Path.Combine(source, "assets", "native-compatibility");
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 async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, CancellationToken cancellation)
{
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
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.");
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 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" };
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>");
add.Elements("dict").First().AddAfterSelf(cryptex);
var bootArguments = PlistValue(PlistValue(PlistValue(PlistValue(document.Root.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82"), "boot-args").Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
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.");
boot = ReplaceOnce(boot, " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n", " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n" + CompatibilityStaging + "\n");
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");
boot = ReplaceOnce(boot, " checkOpenCoreConfig\n addVmHideKext\n", " checkOpenCoreConfig\n" + CompatibilityConfigCheck + "\n addVmHideKext\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");
return (boot, document.ToString(), assets);
}
static XElement PlistValue(XElement dictionary, string key)
{
var keys = dictionary.Elements("key").Where(element => element.Value == key).ToArray();
if (keys.Length != 1 || keys[0].ElementsAfterSelf().FirstOrDefault() is not { } value) throw new InvalidOperationException("Missing/duplicate plist key: " + key);
return value;
}
const string CompatibilityStaging = """
# Only the freshly extracted, owned guest EFI is changed; never the host.
local lilu="$EFI_DIR/OC/Kexts/Lilu.kext/Contents"
printf '%s %s\n' \
fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a "$lilu/Info.plist" \
0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52 "$lilu/MacOS/Lilu" | sha256sum -c - || { error "Pinned active Lilu files mismatch!"; exit 12; }
[ "$(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; }
[ ! -e "$EFI_DIR/OC/Kexts/CryptexFixup.kext" ] || { error "Unexpected pre-existing Cryptex kext!"; exit 12; }
(cd /assets/native-compatibility && sha256sum -c SHA256SUMS) || { error "Pinned Cryptex staging files mismatch!"; exit 12; }
cp -a /assets/native-compatibility/CryptexFixup.kext "$EFI_DIR/OC/Kexts/"
(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"
""";
const string CompatibilityConfigCheck = """
local kernel='/plist/dict/key[.="Kernel"]/following-sibling::dict[1]/key[.="Add"]/following-sibling::array[1]'
local actual expected
actual=$(xmlstarlet sel -T -t -m "$kernel/dict" -v 'key[.="BundlePath"]/following-sibling::string[1]' -n "$CFG") || exit 12
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
[ "$actual" = "$expected" ] || { error "Active Kernel.Add order mismatch!"; exit 12; }
[ "$(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; }
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
expected=$(printf '%s\n' x86_64 Contents/MacOS/CryptexFixup Contents/Info.plist 22.0.0 '')
[ "$actual" = "$expected" ] || { error "Active Cryptex Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
""";
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
static string DiskSerial(string token) => token[..20];
static async Task ValidateFullBootstrap(string wrapper, string output, string token, CancellationToken cancellation)
{
// Execute the complete generated shell with only its external filesystem,
// Apple daemon and probe-process boundaries mapped to harmless fixtures.
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
var ownMarker = token + ":" + commit + "\n";
var cases = new[]
{
(Name: "restart", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 0, Owner: (string?)token),
(Name: "already-owned", Initial: ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: false, MountAfter: 0, Owner: (string?)token),
(Name: "foreign-token", Initial: ownMarker.Replace(token, new string('f', 32)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "foreign-commit", Initial: ownMarker.Replace(commit, new string('f', 40)), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "empty", Initial: "", ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "extra-record", Initial: ownMarker + ownMarker, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "unterminated", Initial: ownMarker.TrimEnd('\n'), ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "write-failure", Initial: (string?)null, ChildExit: 0, WriteFailure: true, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "first-child-failure", Initial: (string?)null, ChildExit: 1, WriteFailure: false, Children: 1, Failure: true, MountAfter: 0, Owner: (string?)token),
(Name: "delayed-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 1, Failure: false, MountAfter: 3, Owner: (string?)token),
(Name: "missing-share", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: -1, Owner: (string?)null),
(Name: "foreign-owner", Initial: (string?)null, ChildExit: 0, WriteFailure: false, Children: 0, Failure: true, MountAfter: 0, Owner: (string?)new string('f', 32))
};
foreach (var test in cases)
{
var state = Path.Combine(output, "full-bootstrap-" + test.Name + "-fixture");
if (Directory.Exists(state)) throw new InvalidOperationException("Full bootstrap fixtures require fresh validation output: " + state);
Directory.CreateDirectory(state);
if (test.MountAfter == 0 && test.Owner is not null) File.WriteAllText(Path.Combine(state, "run.owner"), test.Owner + "\n");
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
var marker = Path.Combine(state, "probe.started");
if (test.Initial is not null) File.WriteAllText(marker, test.Initial);
var mapped = wrapper.Replace("/Volumes/installstate", state, StringComparison.Ordinal);
if (test.WriteFailure) mapped = ReplaceOnce(mapped, "MARKER=\"$STATE_DIR/probe.started\"", "MARKER=\"$STATE_DIR/absent-parent/probe.started\"");
var script = """
STATE_TEST="$1"; export STATE_TEST
CHILD_EXIT="$2"; export CHILD_EXIT
MOUNT_AFTER="$3"; MOUNT_OWNER="$4"; MOUNT_COMMIT="$5"
/sbin/mount_9p() {
local attempts=0
[ ! -f "$STATE_TEST/mount-attempts" ] || attempts=$(cat "$STATE_TEST/mount-attempts")
attempts=$((attempts + 1)); printf '%s\n' "$attempts" > "$STATE_TEST/mount-attempts"
if (( MOUNT_AFTER > 0 && attempts >= MOUNT_AFTER )); then
printf '%s\n' "$MOUNT_OWNER" > "$STATE_TEST/run.owner"
printf '%s\n' "$MOUNT_COMMIT" > "$STATE_TEST/source.commit"
return 0
fi
return 1
}
sleep() { [ "$1" = 1 ] || return 1; printf 'sleep\n' >> "$STATE_TEST/mount-sleeps.log"; }
/bin/bash() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/child-marker.log"; printf 'child\n' >> "$STATE_TEST/children.log"; return "$CHILD_EXIT"; }
exec() { cat "$STATE_TEST/probe.started" >> "$STATE_TEST/apple-marker.log" 2>/dev/null || :; printf '%s\n' "$*" >> "$STATE_TEST/apple-exec.log"; return 0; }
""" + "\n" + mapped + "\nwait\n";
var errors = "";
for (var start = 0; start < 2; start++)
errors += (await Command("bash", ["-c", script, "full-bootstrap-contract", state, test.ChildExit.ToString(), test.MountAfter.ToString(), test.Owner ?? "", commit], output, "full-bootstrap-" + test.Name + "-start-" + start, cancellation)).Error;
var childLog = Path.Combine(state, "children.log");
var children = File.Exists(childLog) ? File.ReadAllLines(childLog).Length : 0;
var appleExecutions = File.ReadAllLines(Path.Combine(state, "apple-exec.log"));
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
var phaseUnchanged = phase == active;
using var receipt = JsonDocument.Parse(phase);
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "bootstrap-failed";
var markerPreserved = test.Initial is not null ? File.ReadAllText(marker) == test.Initial : test.Children == 0 ? !File.Exists(marker) : File.Exists(marker) && File.ReadAllText(marker) == ownMarker;
var completeBeforeExec = test.Initial is null && test.Children == 1 ? File.Exists(Path.Combine(state, "child-marker.log")) && File.ReadAllText(Path.Combine(state, "apple-marker.log")) == ownMarker + ownMarker && File.ReadAllText(Path.Combine(state, "child-marker.log")) == ownMarker : true;
var mountAttemptsPath = Path.Combine(state, "mount-attempts");
var mountAttempts = File.Exists(mountAttemptsPath) ? int.Parse(File.ReadAllText(mountAttemptsPath)) : 0;
var sleepPath = Path.Combine(state, "mount-sleeps.log");
var mountSleeps = File.Exists(sleepPath) ? File.ReadAllLines(sleepPath).Length : 0;
var expectedMounts = test.MountAfter < 0 ? 240 : test.MountAfter;
var failureReported = test.Owner == token ? realFailure : phaseUnchanged && errors.Contains("[full-bootstrap] ERROR:", StringComparison.Ordinal);
Save(Path.Combine(output, "full-bootstrap-" + test.Name + ".json"), new { children, appleExecutions = appleExecutions.Length, phaseUnchanged, realFailure, failureReported, markerPreserved, completeBeforeExec, mountAttempts, mountSleeps });
if (children != test.Children || appleExecutions.Length != 2 || appleExecutions.Any(value => value != "/usr/libexec/recoveryosd") || test.Failure && !failureReported || !test.Failure && !phaseUnchanged || !markerPreserved || !completeBeforeExec || mountAttempts != expectedMounts || mountSleeps != expectedMounts)
throw new InvalidOperationException($"Full bootstrap contract failed ({test.Name}): children={children}, appleExecutions={appleExecutions.Length}, phaseUnchanged={phaseUnchanged}, failureReported={failureReported}, markerPreserved={markerPreserved}, completeBeforeExec={completeBeforeExec}, mountAttempts={mountAttempts}, mountSleeps={mountSleeps}.");
}
}
static async Task PreparePayload(string source, string output, string token, string commit, CancellationToken cancellation)
{
if (!System.Text.RegularExpressions.Regex.IsMatch(commit, "^[0-9a-f]{40}$")) throw new InvalidOperationException("Candidate commit is not an exact Git SHA.");
@@ -355,7 +562,38 @@ static class NativeDiagnostic
installer = ReplaceOnce(installer, " local message=\"$1\"\n\n echo \"[log] ERROR: $message\"", " local message=\"$1\"\n\n mark_installation_failed || :\n echo \"[log] ERROR: $message\"");
installer = ReplaceOnce(installer, "if (( rc != 0 )); then\n", "if (( rc != 0 )); then\n mark_installation_failed || :\n");
installer = ReplaceAllExact(installer, "rm -f \"$STARTED\"", ": # Keep the owned erase guard on failure; never erase again.", 2);
installer = ReplaceOnce(installer, ": > \"$STARTED\" || fail \"failed to create installation guard\"", "( set -o noclobber; printf '%s:%s:%s\\n' \"$PROOF_TOKEN\" \"$(cat \"$STATE_DIR/source.commit\")\" \"$TARGET_DISK\" > \"$STARTED\" ) || fail \"failed to create the exclusive owned installation guard\"");
installer = ReplaceOnce(installer, "select_target_disk() {", """
owns_started_guard() {
local permit_token permit_disk permit_commit extra record
[ -f "$STARTED" ] && [ ! -L "$STARTED" ] || return 1
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
{ IFS= read -r permit_token; IFS= read -r permit_disk; IFS= read -r permit_commit; IFS= read -r extra || :; } < "$STATE_DIR/install.permit" || return 1
[ "$permit_token" = "$PROOF_TOKEN" ] && [ -z "${extra:-}" ] || return 1
[[ "$permit_commit" =~ ^[0-9a-f]{40}$ && "$permit_disk" =~ ^/dev/disk[0-9]+$ ]] || return 1
[ "$permit_commit" = "$(cat "$STATE_DIR/source.commit")" ] || return 1
[ -z "${TARGET_DISK:-}" ] || [ "$TARGET_DISK" = "$permit_disk" ] || return 1
{
IFS= read -r record || return 1
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
} < "$STARTED"
[ "$record" = "$PROOF_TOKEN:$permit_commit:$permit_disk" ]
}
select_target_disk() {
""");
installer = ReplaceOnce(installer, " echo \"[log] installation was already started; refusing to erase the target disk again\"\n exec /usr/libexec/recoveryosd\n exit 1", " owns_started_guard || fail \"existing installation guard is not owned by this token, commit and disk\"\n echo \"[log] owned installation is already running; duplicate child exits without changing its phase\"\n exit 0");
installer = ReplaceOnce(installer, ": > \"$STARTED\" || fail \"failed to create installation guard\"", """
if ! ( set -o noclobber; printf '%s:%s:%s\n' "$PROOF_TOKEN" "$(cat "$STATE_DIR/source.commit")" "$TARGET_DISK" > "$STARTED" ); then
if owns_started_guard; then
echo "[log] another owned child claimed installation; duplicate exits without changing its phase"
exit 0
fi
fail "failed to create the exclusive owned installation guard"
fi
""");
// The Full bootstrap wrapper keeps Apple's original daemon running.
// An installer child must terminate rather than launch another copy.
installer = ReplaceAllExact(installer, " exec /usr/libexec/recoveryosd\n", "", 2);
installer = ReplaceOnce(installer, "set -u\n", "set -u\nPROOF_TOKEN=\"" + token + "\"\n" + """
mark_installation_failed() {
local state="${STATE_DIR:-/Volumes/installstate}" temporary
@@ -410,6 +648,7 @@ static class NativeDiagnostic
if (!writeSource)
{
await ValidateInstallerFailureReceipt(installer, output, token, cancellation);
await ValidateInstallGuardChild(installer, output, token, cancellation);
return;
}
File.WriteAllText(Path.Combine(source, "src/install/recovery/full-install.sh"), installer, new UTF8Encoding(false));
@@ -441,6 +680,76 @@ static class NativeDiagnostic
throw new InvalidOperationException("Installer terminal phase overwrote foreign run-owned state.");
}
static async Task ValidateInstallGuardChild(string installer, string output, string token, CancellationToken cancellation)
{
// Exercise the generated Recovery shell's actual pre-erase control path.
// Apple exec and rolling log snapshots are external boundaries; no VM,
// native disk command, installer, daemon or application is invoked here.
static string Between(string text, string start, string end)
{
var first = text.IndexOf(start, StringComparison.Ordinal);
var last = first < 0 ? -1 : text.IndexOf(end, first, StringComparison.Ordinal);
if (first < 0 || last <= first) throw new InvalidOperationException("Generated installer guard validation boundary changed: " + start);
return text[first..last];
}
var functions = Between(installer, "mark_installation_failed() {", "installer_parent=$$") +
Between(installer, "fail() {", "select_target_disk() {");
var existing = Between(installer, "if [ -e \"$STARTED\" ]; then", "[ -s \"$ADMIN_PACKAGE\" ]");
var claim = Between(installer, "# Everything needed for the unattended install", "if ! /usr/sbin/diskutil eraseDisk");
const string commit = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const string disk = "/dev/disk1";
var ownGuard = token + ":" + commit + ":" + disk + "\n";
var cases = new[]
{
(Name: "race", Initial: (string?)null, Race: true, WriteFailure: false, Success: true, Erase: false),
(Name: "fresh-winner", Initial: (string?)null, Race: false, WriteFailure: false, Success: true, Erase: true),
(Name: "already-owned", Initial: ownGuard, Race: false, WriteFailure: false, Success: true, Erase: false),
(Name: "foreign-token", Initial: ownGuard.Replace(token, new string('f', 32)), Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "foreign-commit", Initial: ownGuard.Replace(commit, new string('f', 40)), Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "foreign-disk", Initial: ownGuard.Replace(disk, "/dev/disk2"), Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "empty", Initial: "", Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "extra-record", Initial: ownGuard + ownGuard, Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "unterminated", Initial: ownGuard.TrimEnd('\n'), Race: false, WriteFailure: false, Success: false, Erase: false),
(Name: "write-failure", Initial: (string?)null, Race: false, WriteFailure: true, Success: false, Erase: false)
};
foreach (var test in cases)
{
var state = Path.Combine(output, "installer-guard-" + test.Name + "-fixture");
if (Directory.Exists(state)) throw new InvalidOperationException("Install-guard fixtures require a fresh validation output: " + state);
Directory.CreateDirectory(state);
File.WriteAllText(Path.Combine(state, "run.owner"), token + "\n");
File.WriteAllText(Path.Combine(state, "source.commit"), commit + "\n");
File.WriteAllText(Path.Combine(state, "install.permit"), token + "\n" + disk + "\n" + commit + "\n");
var active = JsonSerializer.Serialize(new { token, phase = "installation" });
File.WriteAllText(Path.Combine(state, "guest-phase.json"), active);
var guard = Path.Combine(state, test.WriteFailure ? "absent-parent/started" : "started");
if (test.Initial is not null) File.WriteAllText(guard, test.Initial);
var script = """
set -u
STATE_DIR="$1"; PROOF_TOKEN="$2"; TARGET_DISK="$3"
STARTED="$4"; LOCAL_LOG="$STATE_DIR/local.log"
STATE_LOG="$STATE_DIR/install.log"; STATE_LOG_LIMIT=1024
APPLE_INSTALL_LOG="$STATE_DIR/apple.log"; APPLE_INSTALL_LOG_LIMIT=1024
snapshot_log() { :; }
exec() { printf '%s\n' "$*" >> "$STATE_DIR/apple-exec.log"; return 0; }
""" + "\n" + functions + "\n" + existing + "\n" +
// Publish another child's complete guard after the initial check.
(test.Race ? "printf '%s:%s:%s\\n' \"$PROOF_TOKEN\" \"$(cat \"$STATE_DIR/source.commit\")\" \"$TARGET_DISK\" > \"$STARTED\"\n" : "") + claim +
"printf 'guard-acquired\\n' > \"$STATE_DIR/erase-boundary-reached\"\n";
var command = await Command("bash", ["-c", script, "generated-install-guard-validation", state, token, disk, guard], output, "installer-guard-" + test.Name, cancellation, requireSuccess: false);
var phase = File.ReadAllText(Path.Combine(state, "guest-phase.json"));
var phaseUnchanged = phase == active;
using var receipt = JsonDocument.Parse(phase);
var realFailure = receipt.RootElement.GetProperty("token").GetString() == token && receipt.RootElement.GetProperty("phase").GetString() == "installation-failed";
var appleExec = File.Exists(Path.Combine(state, "apple-exec.log"));
var eraseReached = File.Exists(Path.Combine(state, "erase-boundary-reached"));
var guardPreserved = test.Initial is not null ? File.ReadAllText(guard) == test.Initial : test.WriteFailure ? !File.Exists(guard) : File.ReadAllText(guard) == ownGuard;
Save(Path.Combine(output, "installer-guard-" + test.Name + ".json"), new { command.ExitCode, phaseUnchanged, realFailure, appleExec, eraseReached, guardPreserved });
if ((command.ExitCode == 0) != test.Success || test.Success && !phaseUnchanged || !test.Success && !realFailure || appleExec || eraseReached != test.Erase || !guardPreserved)
throw new InvalidOperationException($"Generated install child contract failed ({test.Name}): exit={command.ExitCode}, phaseUnchanged={phaseUnchanged}, realFailure={realFailure}, appleExec={appleExec}, eraseReached={eraseReached}, guardPreserved={guardPreserved}.");
}
}
static async Task PermitInstallation(string id, string output, string token, string commit, string readiness, CancellationToken cancellation)
{
await Command("docker", ["inspect", id], output, "full-container-boundary", cancellation);
@@ -450,12 +759,12 @@ static class NativeDiagnostic
var mounts = document.RootElement[0].GetProperty("Mounts").EnumerateArray().ToArray();
if (mounts.Length != 1 || mounts[0].GetProperty("Type").GetString() != "volume" || mounts[0].GetProperty("Destination").GetString() != "/storage" || !mounts[0].GetProperty("RW").GetBoolean()) throw new InvalidOperationException("Full installer requires only the newly owned anonymous /storage volume.");
}
var drive = await Command("docker", ["exec", id, "qemu-img", "info", "--force-share", "--output=json", "/storage/14/data.img"], output, "owned-raw-disk", cancellation);
var drive = await Command("docker", ["exec", id, "qemu-img", "info", "--force-share", "--output=json", "/storage/13/data.img"], output, "owned-raw-disk", cancellation);
using (var document = JsonDocument.Parse(drive.Output))
if (document.RootElement.GetProperty("format").GetString() != "raw" || document.RootElement.GetProperty("virtual-size").GetInt64() != GuestDiskBytes) throw new InvalidOperationException("Owned VM raw-disk capacity/format mismatch.");
var attachment = await Command("docker", ["exec", id, "sh", "-c", "qemu_pid=$(cat /dev/shm/qemu.pid); tr '\\0' '\\n' < /proc/\"$qemu_pid\"/cmdline | sed -n '/^file=\\/storage\\/14\\/data\\.img,/p; /^ide-hd,drive=data3,/p; /^local,id=installstatefs,/p'"], output, "owned-disk-attachment", cancellation);
var attachment = await Command("docker", ["exec", id, "sh", "-c", "qemu_pid=$(cat /dev/shm/qemu.pid); tr '\\0' '\\n' < /proc/\"$qemu_pid\"/cmdline | sed -n '/^file=\\/storage\\/13\\/data\\.img,/p; /^ide-hd,drive=data3,/p; /^local,id=installstatefs,/p'"], output, "owned-disk-attachment", cancellation);
var lines = attachment.Output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
if (lines.Length != 3 || !lines.Any(line => line.StartsWith("file=/storage/14/data.img,id=data3,format=raw,", StringComparison.Ordinal) && !line.Contains("readonly=on", StringComparison.Ordinal)) || !lines.Any(line => line.StartsWith("ide-hd,drive=data3,", StringComparison.Ordinal) && line.Contains("serial=" + DiskSerial(token), StringComparison.Ordinal)) || !lines.Contains("local,id=installstatefs,path=" + FullState + ",security_model=none")) throw new InvalidOperationException("QEMU did not attach the exact owned raw disk/serial and persistent state share.");
if (lines.Length != 3 || !lines.Any(line => line.StartsWith("file=/storage/13/data.img,id=data3,format=raw,", StringComparison.Ordinal) && !line.Contains("readonly=on", StringComparison.Ordinal)) || !lines.Any(line => line.StartsWith("ide-hd,drive=data3,", StringComparison.Ordinal) && line.Contains("serial=" + DiskSerial(token), StringComparison.Ordinal)) || !lines.Contains("local,id=installstatefs,path=" + FullState + ",security_model=none")) throw new InvalidOperationException("QEMU did not attach the exact owned raw disk/serial and persistent state share.");
var owner = await Command("docker", ["exec", id, "cat", FullState + "/run.owner"], output, "full-share-owner", cancellation);
if (owner.Output.Trim() != token) throw new InvalidOperationException("Native state owner mismatch.");
using var receipt = JsonDocument.Parse(readiness);
@@ -478,7 +787,7 @@ static class NativeDiagnostic
{
using var document = JsonDocument.Parse(json);
var result = document.RootElement;
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || result.GetProperty("sourceCommit").GetString() != commit || result.GetProperty("archiveSha256").GetString() != archiveHash || result.GetProperty("sdkVersion").GetString() != SdkVersion || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || new[] { "expectedTests", "total", "executed", "passed" }.Any(key => result.GetProperty(key).GetInt32() != 577) || new[] { "failed", "notExecuted", "audioCodeSignExit" }.Any(key => result.GetProperty(key).GetInt32() != 0) || !result.GetProperty("nativeTests").EnumerateArray().Select(value => value.GetString()).Order().SequenceEqual(NativeFacts.Order())) throw new InvalidOperationException("Native full receipt did not prove exact-source 577/577 with all five native tests and zero skips.");
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || result.GetProperty("sourceCommit").GetString() != commit || result.GetProperty("archiveSha256").GetString() != archiveHash || result.GetProperty("sdkVersion").GetString() != SdkVersion || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 13 || result.GetProperty("architecture").GetString() != "x86_64" || new[] { "expectedTests", "total", "executed", "passed" }.Any(key => result.GetProperty(key).GetInt32() != 577) || new[] { "failed", "notExecuted", "audioCodeSignExit" }.Any(key => result.GetProperty(key).GetInt32() != 0) || !result.GetProperty("nativeTests").EnumerateArray().Select(value => value.GetString()).Order().SequenceEqual(NativeFacts.Order())) throw new InvalidOperationException("Native full receipt did not prove exact-source 577/577 with all five native tests and zero skips.");
var artifacts = result.GetProperty("nativeArtifacts").EnumerateArray().ToArray();
if (artifacts.Length != 4 || !artifacts.Select(item => item.GetProperty("name").GetString()).Order().SequenceEqual(NativeArtifacts.Order()) || artifacts.Any(item => item.GetProperty("architecture").GetString() != "x86_64" || !System.Text.RegularExpressions.Regex.IsMatch(item.GetProperty("sha256").GetString() ?? "", "^[0-9a-f]{64}$"))) throw new InvalidOperationException("Native Mach-O/x86_64 helper manifest is incomplete.");
}
@@ -502,14 +811,14 @@ static class NativeDiagnostic
{
var token = new string('a', 32); var commit = new string('b', 40); var hash = new string('c', 64);
var trx = "<TestRun><TestDefinitions>" + string.Concat(Enumerable.Range(0, 577).Select(index => { var identity = index < 5 ? NativeFacts[index] : "MeetingAssistant.Tests.Validation.Test" + index; var split = identity.LastIndexOf('.'); return $"<UnitTest id='t{index}'><TestMethod className='{identity[..split]}' name='{identity[(split + 1)..]}' /></UnitTest>"; })) + "</TestDefinitions><Results>" + string.Concat(Enumerable.Range(0, 577).Select(index => $"<UnitTestResult testId='t{index}' outcome='Passed' />")) + "</Results><ResultSummary><Counters total='577' executed='577' passed='577' failed='0' notExecuted='0' /></ResultSummary></TestRun>";
var good = JsonSerializer.Serialize(new { token, success = true, sourceCommit = commit, archiveSha256 = hash, sdkVersion = SdkVersion, osVersion = "14.6.1", architecture = "x86_64", expectedTests = 577, total = 577, executed = 577, passed = 577, failed = 0, notExecuted = 0, nativeTests = NativeFacts, nativeArtifacts = NativeArtifacts.Select(name => new { name, sha256 = hash, architecture = "x86_64" }), audioCodeSignExit = 0, trxSha256 = Hash(Encoding.UTF8.GetBytes(trx)) });
var good = JsonSerializer.Serialize(new { token, success = true, sourceCommit = commit, archiveSha256 = hash, sdkVersion = SdkVersion, osVersion = "13.6.1", architecture = "x86_64", expectedTests = 577, total = 577, executed = 577, passed = 577, failed = 0, notExecuted = 0, nativeTests = NativeFacts, nativeArtifacts = NativeArtifacts.Select(name => new { name, sha256 = hash, architecture = "x86_64" }), audioCodeSignExit = 0, trxSha256 = Hash(Encoding.UTF8.GetBytes(trx)) });
ValidateFullResult(good, token, commit, hash); ValidateTrx(Encoding.UTF8.GetBytes(trx), good);
byte[] bomTrx = [0xef, 0xbb, 0xbf, .. Encoding.UTF8.GetBytes(trx)];
ValidateTrx(bomTrx, good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(bomTrx), StringComparison.Ordinal));
var qualifiedTrx = trx;
foreach (var name in NativeFacts) qualifiedTrx = qualifiedTrx.Replace("name='" + name[(name.LastIndexOf('.') + 1)..] + "'", "name='" + name + "'", StringComparison.Ordinal);
ValidateTrx(Encoding.UTF8.GetBytes(qualifiedTrx), good.Replace(Hash(Encoding.UTF8.GetBytes(trx)), Hash(Encoding.UTF8.GetBytes(qualifiedTrx)), StringComparison.Ordinal));
foreach (var invalid in new[] { good.Replace("\"success\":true", "\"success\":false"), good.Replace("\"passed\":577", "\"passed\":572"), good.Replace("\"notExecuted\":0", "\"notExecuted\":5"), good.Replace("\"audioCodeSignExit\":0", "\"audioCodeSignExit\":1"), good.Replace("x86_64", "arm64"), good.Replace(token, new string('d', 32)), good.Replace(commit, new string('e', 40)), good.Replace("NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures", "UnrelatedTest") })
foreach (var invalid in new[] { good.Replace("\"success\":true", "\"success\":false"), good.Replace("\"passed\":577", "\"passed\":572"), good.Replace("\"notExecuted\":0", "\"notExecuted\":5"), good.Replace("\"audioCodeSignExit\":0", "\"audioCodeSignExit\":1"), good.Replace("13.6.1", "12.6.1"), good.Replace("x86_64", "arm64"), good.Replace(token, new string('d', 32)), good.Replace(commit, new string('e', 40)), good.Replace("NativeHelperAdvertisesCalendarPromptAndScreenshotFeatures", "UnrelatedTest") })
{
try { ValidateFullResult(invalid, token, commit, hash); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Full native validator accepted an incomplete or stale proof.");
@@ -559,8 +868,8 @@ static class NativeDiagnostic
{
using var document = JsonDocument.Parse(json);
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 < 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 14+/x86_64, service readiness and the writable 64-GiB disk.");
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))
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 13+/x86_64, service readiness and the writable 64-GiB disk.");
}
static void AssertContainer(string json, string token)
@@ -568,8 +877,31 @@ static class NativeDiagnostic
using var document = JsonDocument.Parse(json);
var container = document.RootElement[0];
var config = container.GetProperty("HostConfig");
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token || config.GetProperty("Privileged").GetBoolean() || config.GetProperty("NetworkMode").GetString() != "default" && config.GetProperty("NetworkMode").GetString() != "bridge" || config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes || new[] { "CapAdd", "Devices", "DeviceRequests", "Binds", "PortBindings" }.Any(key => config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null && (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any())))
throw new InvalidOperationException("Created container exceeds the owned/unprivileged diagnostic boundary.");
var devices = config.GetProperty("Devices");
var mounts = container.GetProperty("Mounts");
var environment = container.GetProperty("Config").GetProperty("Env").EnumerateArray().Select(value => value.GetString()).ToArray();
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token
|| config.GetProperty("Privileged").GetBoolean()
|| config.GetProperty("NetworkMode").GetString() is not ("default" or "bridge")
|| config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes
|| config.GetProperty("MemorySwap").GetInt64() != ContainerMemoryBytes
|| config.GetProperty("NanoCpus").GetInt64() != 2000000000
|| config.GetProperty("ShmSize").GetInt64() != 536870912
|| new[] { "CapAdd", "DeviceRequests", "Binds", "PortBindings", "DeviceCgroupRules", "Tmpfs" }.Any(key =>
config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null
&& (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any()))
|| devices.GetArrayLength() != 1
|| devices[0].GetProperty("PathOnHost").GetString() != "/dev/kvm"
|| devices[0].GetProperty("PathInContainer").GetString() != "/dev/kvm"
|| devices[0].GetProperty("CgroupPermissions").GetString() != "rw"
|| mounts.GetArrayLength() != 1
|| mounts[0].GetProperty("Type").GetString() != "volume"
|| mounts[0].GetProperty("Destination").GetString() != "/storage"
|| !mounts[0].GetProperty("RW").GetBoolean()
|| new[] { "KVM=Y", "CPU_MODEL=host", "VERSION=13" }.Any(expected =>
environment.Count(value => value is not null && value.StartsWith(expected.Split('=')[0] + "=", StringComparison.Ordinal)) != 1
|| !environment.Contains(expected)))
throw new InvalidOperationException("Created container exceeds the owned restricted KVM/host-CPU compatibility boundary.");
}
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool full = false, bool final = false, string? token = null)
@@ -578,6 +910,7 @@ static class NativeDiagnostic
var logs = await Command("docker", ["logs", "--tail", "3000", id], output, "container", cancellation, requireSuccess: false);
var files = new List<(string, string)> { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") };
if (full) files.AddRange([("guest-phase.json", "guest-phase.json"), ("full-result.json", "full-result.json"), ("firstboot.log", "firstboot.log"), ("install.log", "install.log"), ("apple.log", "apple.log"), ("disk-ownership-ioreg.log", "disk-ownership-ioreg.log"), ("installed-root.plist", "installed-root.plist"), ("apfs-containers.plist", "apfs-containers.plist"), ("physical-store.plist", "physical-store.plist"), ("clt-catalog.log", "clt-catalog.log"), ("clt-install.log", "clt-install.log")]);
if (full) files.Add(("clt-sdk.log", "clt-sdk.log"));
foreach (var file in files)
{
var result = await Command("docker", ["exec", id, "cat", (full ? FullState : "/dev/shm/installstate") + "/" + file.Item1], output, "capture-" + file.Item1.Replace('/', '-'), cancellation, requireSuccess: false);
@@ -594,7 +927,7 @@ static class NativeDiagnostic
await Command("docker", ["cp", id + ":" + FullState + "/guest-logs/.", Path.Combine(output, "guest-logs")], output, "capture-guest-logs", cancellation, requireSuccess: false);
}
}
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; 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);
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; 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);
}
static async Task CaptureMonitor(string id, string output, string token, CancellationToken cancellation)
@@ -614,7 +947,7 @@ static class NativeDiagnostic
var monitor = await Command("docker", ["exec", id, "sh", "-c", """
test -S /run/shm/monitor.sock || exit 1
rm -f -- "$1" || exit 1
printf 'info 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\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=$?
printf '\n[monitor-exit] %s\n' "$monitor_exit"
[ "$monitor_exit" -eq 0 ] || exit "$monitor_exit"
+9 -7
View File
@@ -126,6 +126,8 @@ static class NativeGuest
await Cmd("/usr/bin/xcode-select", ["-p"], "clt-location");
await Cmd("/usr/sbin/pkgutil", ["--pkg-info", "com.apple.pkg.CLTools_Executables"], "clt-package");
await Cmd("/usr/bin/xcrun", ["swiftc", "--version"], "swift-version");
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "apple-sdk-version");
await Cmd("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "apple-sdk-path");
RequireNoLinks(dotnet);
actualSdk = (await Cmd(dotnet, ["--version"], "sdk-version")).Output.Trim();
var sdkInfo = (await Cmd(dotnet, ["--info"], "sdk-info")).Output;
@@ -294,14 +296,14 @@ static class NativeGuest
static void ValidateResult(FullResult result, Payload payload)
{
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 14 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests != ExpectedTests || result.Total != ExpectedTests || result.Executed != ExpectedTests || result.Passed != ExpectedTests || result.Failed != 0 || result.NotExecuted != 0 || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
if (result.Token != payload.RunToken || !result.Success || result.SourceCommit != payload.SourceCommit || result.ArchiveSha256 != payload.ArchiveSha256 || !Version.TryParse(result.OsVersion, out var version) || version.Major < 13 || result.Architecture != "x86_64" || result.SdkVersion != SdkVersion || result.ExpectedTests != ExpectedTests || result.Total != ExpectedTests || result.Executed != ExpectedTests || result.Passed != ExpectedTests || result.Failed != 0 || result.NotExecuted != 0 || !result.NativeTests.Order().SequenceEqual(RequiredNativeTests.Order()) || result.AudioCodeSignExit != 0 || !Hex(result.TrxSha256, 64) || result.NativeArtifacts.Length != NativeNames.Length || !result.NativeArtifacts.Select(artifact => artifact.Name).Order().SequenceEqual(NativeNames.Order()) || result.NativeArtifacts.Any(artifact => artifact.Architecture != "x86_64" || !Hex(artifact.Sha256, 64)) || result.CompletedUtc < result.StartedUtc || result.CompletedUtc - result.StartedUtc > TimeSpan.FromMinutes(25).Add(TimeSpan.FromSeconds(15)) || result.CompletedUtc > DateTimeOffset.UtcNow.AddSeconds(30) || result.CompletedUtc < DateTimeOffset.UtcNow.AddMinutes(-1) || result.Reason.Length != 0)
throw new InvalidOperationException("Native guest receipt does not prove this source, toolchain, signed artifacts and all expected tests.");
}
static void RequirePlatform(string version, string architecture, string uid)
{
if (!Version.TryParse(version, out var parsed) || parsed.Major < 14 || architecture != "x86_64" || uid != "0")
throw new InvalidOperationException("Native build requires installed macOS 14+/x86_64 running as root.");
if (!Version.TryParse(version, out var parsed) || parsed.Major < 13 || architecture != "x86_64" || uid != "0")
throw new InvalidOperationException("Native build requires installed macOS 13+/x86_64 running as root.");
}
static void RequireApfs(string xml)
{
@@ -430,8 +432,8 @@ static class NativeGuest
Reject(() => ReadPayload(json.Replace("577", "572", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal)));
RequirePlatform("14.6.1", "x86_64", "0");
Reject(() => RequirePlatform("13.6.1", "x86_64", "0"));
RequirePlatform("13.6.1", "x86_64", "0");
Reject(() => RequirePlatform("12.6.1", "x86_64", "0"));
Reject(() => RequirePlatform("14.6.1", "arm64", "0"));
Reject(() => RequirePlatform("14.6.1", "x86_64", "501"));
RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>");
@@ -484,9 +486,9 @@ static class NativeGuest
aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted");
Reject(() => ValidateTrx(aborted.ToString(), ExpectedTests, started));
var artifacts = NativeNames.Select(name => new NativeArtifact(name, new string('d', 64), "x86_64")).ToArray();
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "14.6.1", "x86_64", SdkVersion, ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, new string('e', 64), "", started, DateTimeOffset.UtcNow);
var result = new FullResult(payload.RunToken, true, payload.SourceCommit, payload.ArchiveSha256, "13.6.1", "x86_64", SdkVersion, ExpectedTests, summary.Total, summary.Executed, summary.Passed, summary.Failed, summary.NotExecuted, summary.NativeTests, artifacts, 0, new string('e', 64), "", started, DateTimeOffset.UtcNow);
ValidateResult(result, payload);
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "13.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
foreach (var invalid in new[] { result with { Token = new string('f', 32) }, result with { Success = false }, result with { SourceCommit = new string('f', 40) }, result with { ArchiveSha256 = new string('f', 64) }, result with { NotExecuted = 5 }, result with { AudioCodeSignExit = 1 }, result with { NativeTests = RequiredNativeTests[..4] }, result with { NativeArtifacts = artifacts[..3] }, result with { NativeArtifacts = [artifacts[0] with { Architecture = "arm64" }, .. artifacts[1..]] }, result with { TrxSha256 = "" }, result with { SdkVersion = "10.0.100" }, result with { OsVersion = "12.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
Reject(() => ValidateResult(invalid, payload));
var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N"));
try
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Apple Recovery has Bash before any SDK is installed. Preserve the original
# daemon under its launchd label/PID while the unchanged read-only probe runs.
/bin/bash /Volumes/installstate/readiness.sh &
exec /usr/libexec/recoveryosd
+4
View File
@@ -67,6 +67,10 @@ printf '[firstboot] selected CLT: %s\n' "$label"
/usr/sbin/pkgutil --pkg-info=com.apple.pkg.CLTools_Executables || fail clt_receipt_failed
/usr/bin/xcrun --find swiftc || fail swiftc_missing
/usr/bin/xcrun swiftc --version || fail swiftc_version_failed
{
/usr/bin/xcrun --sdk macosx --show-sdk-version &&
/usr/bin/xcrun --sdk macosx --show-sdk-path
} > "$STATE_DIR/clt-sdk.log" 2>&1 || fail clt_sdk_identity_failed
printf 'import AppKit\nimport AVFoundation\nimport ScreenCaptureKit\nimport EventKit\nimport WebKit\nprint("native SDK ready")\n' > "$WORK/toolchain-smoke.swift"
/usr/bin/xcrun swiftc -target x86_64-apple-macos13.0 "$WORK/toolchain-smoke.swift" -o "$WORK/toolchain-smoke" || fail native_framework_compile_failed
"$WORK/toolchain-smoke" || fail native_framework_execution_failed
+72
View File
@@ -0,0 +1,72 @@
#!/bin/bash
# Full-only pre-.NET seam. Claim one persistent owned probe before forking;
# launchd then execs the original Apple daemon, including on a real failure.
set -u
PROOF_TOKEN="@@PROOF_TOKEN@@"
STATE_DIR="/Volumes/installstate"
MARKER="$STATE_DIR/probe.started"
bootstrap_failed() {
printf '[full-bootstrap] ERROR: %s\n' "$1" >&2
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || return 1
printf '{"token":"%s","phase":"bootstrap-failed","reason":"%s"}\n' "$PROOF_TOKEN" "$1" > "$STATE_DIR/guest-phase.bootstrap.$$.tmp" &&
/bin/mv -f "$STATE_DIR/guest-phase.bootstrap.$$.tmp" "$STATE_DIR/guest-phase.json"
}
wait_for_owned_state() {
local count=0
while :; do
if [ -e "$STATE_DIR/run.owner" ] || [ -L "$STATE_DIR/run.owner" ]; then
[ -f "$STATE_DIR/run.owner" ] && [ ! -L "$STATE_DIR/run.owner" ] &&
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || {
bootstrap_failed foreign_state_owner
return 1
}
return 0
fi
if (( count >= 120 )); then
bootstrap_failed state_share_mount_timeout
return 1
fi
/sbin/mount_9p installstate >/dev/null 2>&1 || :
count=$((count + 1))
sleep 1
done
}
owns_probe_marker() {
local record extra
[ -f "$MARKER" ] && [ ! -L "$MARKER" ] || return 1
{
IFS= read -r record || return 1
if IFS= read -r extra || [ -n "$extra" ]; then return 1; fi
} < "$MARKER"
[ "$record" = "$PROOF_TOKEN:$source_commit" ]
}
claim_probe() {
[[ "$PROOF_TOKEN" =~ ^[0-9a-f]{32}$ ]] || { bootstrap_failed invalid_probe_token; return 1; }
[ "$(cat "$STATE_DIR/run.owner" 2>/dev/null)" = "$PROOF_TOKEN" ] || { bootstrap_failed foreign_state_owner; return 1; }
source_commit=$(cat "$STATE_DIR/source.commit" 2>/dev/null) || { bootstrap_failed source_commit_missing; return 1; }
[[ "$source_commit" =~ ^[0-9a-f]{40}$ ]] || { bootstrap_failed source_commit_invalid; return 1; }
if [ -e "$MARKER" ] || [ -L "$MARKER" ]; then
owns_probe_marker || { bootstrap_failed foreign_or_invalid_probe_marker; return 1; }
return 2
fi
# Keep partial/failed markers: an incomplete first start is an error, no retry.
if ! ( set -o noclobber; printf '%s:%s\n' "$PROOF_TOKEN" "$source_commit" > "$MARKER" ); then
bootstrap_failed probe_marker_write_failed
return 1
fi
owns_probe_marker || { bootstrap_failed probe_marker_incomplete; return 1; }
return 0
}
if wait_for_owned_state && claim_probe; then
(
/bin/bash "$STATE_DIR/readiness.sh"
child_exit=$?
(( child_exit == 0 )) || bootstrap_failed first_probe_child_failed
) &
fi
exec /usr/libexec/recoveryosd
+7 -3
View File
@@ -112,7 +112,10 @@ cancel_probe() {
run_command() {
local name="$1"
shift
local process timer exit_code started waited
local process timer exit_code started waited command_limit=45
# Run 4161: even native uname/ps startup took 34-42s under TCG.
# Isolate only the failed UID gate; every other watchdog remains unchanged.
[[ "$name" != uid ]] || command_limit=180
LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
printf '\n[proof-command] %s:' "$name" >&3
printf ' %s' "$@" >&3
@@ -122,9 +125,10 @@ run_command() {
process=$!
ACTIVE_COMMAND="$process"
printf '[proof-start] %s child=%s shell=%s parent=%s seconds=%s\n' "$name" "$process" "$$" "$PPID" "$started" >&3
printf '[proof-limit] %s %ss\n' "$name" "$command_limit" >&3
(
trap 'exit 0' TERM INT
IFS= read -r -t 45 -u 9 unused || :
IFS= read -r -t "$command_limit" -u 9 unused || :
printf '[proof-timeout] %s child=%s elapsed=%ss signal=TERM\n' "$name" "$process" "$((SECONDS - started))" >&3
kill -TERM "$process" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
@@ -196,7 +200,7 @@ run_command version /usr/bin/sw_vers -productVersion
read_scalar || fail_probe product_version_invalid
os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded
# Bound readiness independently of the host's 40-minute overall deadline.