Compare commits

..
14 changed files with 1354 additions and 689 deletions

No files matched your search

@@ -1,4 +1,4 @@
name: macOS 13 KVM Cryptex Recovery compatibility diagnostic name: macOS 14 TCG Recovery prerequisite diagnostic
on: on:
workflow_dispatch: workflow_dispatch:
@@ -6,7 +6,7 @@ on:
jobs: jobs:
macos-native-diagnostic: macos-native-diagnostic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 45 timeout-minutes: 95
env: env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1" DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1" DOTNET_NOLOGO: "1"
@@ -19,7 +19,7 @@ jobs:
with: with:
dotnet-version: "10.0.x" dotnet-version: "10.0.x"
- name: Probe macOS 13 Recovery with existing KVM and host CPU - name: Verify actual AVX2 emulation then probe macOS 14 Recovery
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources - name: Always clean up only this diagnostic's owned resources
+2 -2
View File
@@ -17,8 +17,8 @@ jobs:
uses: actions/setup-dotnet@v6 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: "10.0.x" dotnet-version: "10.0.x"
- name: Run owned macOS 13 KVM guest and all native tests - name: Run owned macOS 14 TCG guest and all native tests
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --recovery-format raw --output artifacts/native-macos-full run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full
- name: Always remove only this run's owned resources - name: Always remove only this run's owned resources
if: always() if: always()
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos-full run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos-full
+6 -4
View File
@@ -3,10 +3,12 @@ name: PR and Push Build/Test
on: on:
pull_request: pull_request:
push: push:
# The isolated NoAVX candidate uses the existing manual native workflow. # This temporary branch changes only the native prerequisite diagnostic.
# Its manual workflow provides that evidence; the PR branch still runs all jobs.
branches-ignore: branches-ignore:
- codex/macos-native-full-kvm-noavx - codex/macos-ci-kvm-compatibility
- codex/macos-native-full-kvm-noavx-raw - codex/macos-ci-tcg-supported
- codex/macos-native-full-tcg
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -149,7 +151,7 @@ jobs:
uses: actions/setup-dotnet@v6 uses: actions/setup-dotnet@v6
with: with:
dotnet-version: "10.0.x" dotnet-version: "10.0.x"
- name: Run owned macOS 13 KVM guest and all native tests - name: Run owned macOS 14 TCG guest and all native tests
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --output artifacts/native-macos-full
- name: Always remove only this run's owned resources - name: Always remove only this run's owned resources
if: always() if: always()
+3 -3
View File
@@ -169,13 +169,13 @@ 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. 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, using the runner's existing `/dev/kvm` device and Docker daemon. 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 prepared workflow requires a native macOS job on the same existing runner label and Docker daemon, using software CPU emulation without host devices or added capabilities.
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 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 native job runs an owned macOS 14+ x86_64 guest under TCG on the existing Ubuntu Docker runner. Before downloading Recovery, the actual pinned QEMU binary must execute an AVX/AVX2 instruction probe. The full flow 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 retained evidence and ownership-checked cleanup. Recovery, installation, toolchain operation and this CI path remain unqualified until actual remote guests produce every required receipt. Native tests can also 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. 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.
The separate manual [native Recovery diagnostic](docs/macos-native-diagnostic.md) probes macOS startup and disk readiness before qualifying the prepared Full flow. It neither installs macOS nor runs application tests. A green Recovery diagnostic alone does not verify macOS build/test CI support; each Full run repeats Recovery readiness for its own guest and disk. The separate manual [native Recovery diagnostic](docs/macos-native-diagnostic.md) can isolate macOS startup and disk readiness. It neither installs macOS nor runs application tests. A green Recovery diagnostic alone does not verify macOS build/test CI support; each Full run requires fresh Recovery readiness for its own guest and disk before permitting installation.
## Operations And Limitations ## Operations And Limitations
+43 -72
View File
@@ -1,115 +1,86 @@
# macOS 13 KVM/Cryptex/NoAVX diagnostic and prepared Full flow # macOS 14 TCG prerequisite diagnostic
Full RAW run 4219 at `d1594e90b3fa9c6f3bb52f12b754ae933105b8c8` verified RAW sector equality, targeted file-cache eviction, KVM, macOS 13.6/x86_64/root and successful cleanup, but all eight disk-list attempts timed out. Installation and application tests were not reached. The next host-only repair retains the completed Recovery hash once after the existing staging marker; it avoids rereading the immutable 711-MB DMG at every poll. Failed or missing-image captures are not retained as success and can retry. Guest code, assets, CPU, memory and deadlines are unchanged. This repair does not yet prove native readiness or CI success. This manual candidate uses the existing Ubuntu/x64 Docker runner. Before downloading Apple Recovery it tests actual AVX/AVX2 instruction execution in the pinned QEMU binary, then probes a fresh macOS 14+ Recovery guest. It does not install macOS, erase a disk, provision .NET/CLT or run Meeting Assistant tests. Readiness is only a prerequisite for full native CI.
In readiness mode, 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. ## Profile and evidence
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. The existing Intel Celeron 1037U has neither AVX nor AVX2. KVM run 4187 at `720a431` reached macOS 13.6/x86_64/root and visible whole writable 64-GiB media. Diskutil timed out after 122 seconds; the sampler produced no report after 61 seconds. The screen remained at the Apple boot progress bar. CPU throttling, memory-limit/OOM events and container swap were zero; memory peaked at 2.67 GB. Host paging occurred. No unsupported-instruction crash or particular IPC wait is proved.
The isolated Full NoAVX candidate starts at `a356b88ae4a0a26d68098460df86038f9831c080` on `codex/macos-native-full-kvm-noavx`. Its Compatibility code, archive/staging/configuration rejection checks, host-memory admission and captured-proof heartbeat are transferred from the offline-verified Recovery candidate `fef676c810cf78b39aabb872546a76e8ac2b29d5`. The additional NoAVX boot kext is the only guest change. Application/tests/specs, Apple bootstrap/installer, payload/SDK pins, owned-disk guards, TRX requirements, CPU/KVM/macOS 13 and guest/container limits are unchanged. Existing phase budgets remain Recovery 40, installation 80, toolchain 30 and tests 25 minutes within the 172-minute host/180-minute job limits. No successful native run is implied by preparation. [CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/kern_start.cpp) selects the installed/updated Rosetta Cryptex and patches APFS hash checking; it does not replace the running Recovery cache or emulate instructions. macOS 13 is outside the [.NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This candidate therefore uses macOS 14 and software CPU emulation without Cryptex. It changes the compatibility profile, not one isolated causal variable; actual success must be measured.
The further isolated branch `codex/macos-native-full-kvm-noavx-raw` starts from that completed Full candidate, `5f686c5c80b6bbb525745de173b57c6393f58bf0`. Its optional `--recovery-format raw` transfers the exact producer, backend selection and existing six producer/three backend fixtures from RAW repair `cae38b014f3278a5b939ff980b0138bff5d6b509`. The manual Full workflow selects RAW; the controller default remains DMG. Against this baseline, the only additional guest variable is the Recovery disk backend. KVM/host CPU, macOS 13, NoAVX/Cryptex, bootstrap/installer, application/tests and every resource/phase limit are retained. The run/validation comparison metadata names this Full baseline; the unchanged boot-assets receipt continues to describe the original NoAVX component comparison. Earlier TCG run 4159 observed guest AVX2 with the upstream-selected Skylake model. Runs 4161/4163 measured slow native startup and reached the 40-minute host limit before readiness. They predated the UDIF CRC repair at `94a70b2`, reuse of successful sw_vers output and capturing the large Recovery hash only once. They do not qualify this candidate. Host/workflow limits for the read-only mode are 90/95 minutes; a readiness pass does not establish that full installation/build/tests fit the pipeline.
In RAW mode the existing QEMU converts the same already-patched DMG, requires sector equality and unchanged DMG SHA256, then retains both images and their hash/equality receipt in owned storage. Before RAM admission, two existing GNU `dd` calls synchronize and request removal only of those verified files' caches (`oflag=nocache conv=nocreat,notrunc,fdatasync count=0`); either failure rejects preparation. This avoids the locally reproduced cgroup file-cache admission failure without a package, global cache clearing or larger memory limit. QEMU still attaches the same readonly virtio device and I/O thread with explicit `format=raw`. Offline Full/source validation and the generated real-QEMU fixture can validate preparation; they establish no guest boot, installation or native test result. Run 4188 with Haswell recorded a boot loop; first-reset run 4189 retained repeated supervisor instruction-fetch pagefaults at RIP/CR2 `0x24b0` before native readiness. Run 4190 at `3aaab45` restored `Skylake-Client-v4` and the upstream TCG `-spec-ctrl` mask. The actual AVX/AVX2 ROM passed (exit 33; AVX2-disabled control exit 0), and macOS 14's Darwin 23.6.0 kernel identified the Skylake CPU. It retained one kernel handoff, a running VM and later userspace execution without the earlier reset, but reached the 20-minute diagnostic deadline without the readiness hook. These observations do not identify Haswell as the original cause or qualify native tests.
Use `dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --full --recovery-format raw --output artifacts/native-macos-full`. Offline checks use `--validate --full --recovery-format raw --source <pristine-pinned-dockur-checkout> --cryptex-archive <verified-Cryptex-ZIP> --noavx-archive <verified-NoAVX-ZIP> --output <fresh-folder>`. The generated `raw-recovery-real-qemu-fixture.sh` accepts an already-patched disposable writable DMG copy and destination; it does not boot a guest. Cleanup remains the existing `--cleanup --output artifacts/native-macos-full` and removes only the run's owned resources. Run 4190 used synchronous kernel serial output and QEMU interrupt/register tracing to preserve the failure context. Its kernel explicitly warned that synchronous output impacts performance. The current full candidate uses normal upstream boot arguments and only the existing iothread QEMU argument; it retains actual CPU/staging receipts independently of Docker log rotation. Its existing fresh-readiness gate precedes every installation permit. This allows one bounded full qualification to test boot performance and, only after readiness, installation/build/tests without duplicating the guest startup. No remote full result has qualified this candidate yet.
The [upstream NoAVX AVXpel 12.6 ZIP](https://raw.githubusercontent.com/dortania/OpenCore-Legacy-Patcher/f40057a5292f4804b51bcfe78d5047c7302a6434/payloads/Kexts/Misc/NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip) is pinned to OCLP commit `f40057a5292f4804b51bcfe78d5047c7302a6434`, 98,356 bytes and locally verified SHA256 `b5d6319d0a1f335684a92ecf23369bc3deb776be19e92b0a40860021409d20df`. Its two expected bundle files, identity/version and `OSBundleRequired=Root` are verified. After existing Lilu/Cryptex, `Kernel.Add` enables `NoAVXFSCompressionTypeZlib-AVXpel.kext` with `Arch=x86_64`, executable `Contents/MacOS/NoAVXFSCompressionTypeZlib`, plist `Contents/Info.plist`, `MinKernel=22.0.0` and empty `MaxKernel`; both file copies enter the existing SHA256SUMS checks. This is a filesystem-decompression hypothesis, not proof of the current readiness hang's cause. Full run 4191 at `9735db1` reached native Recovery: x86_64/root, Darwin 23.6.0 and successful launchd service queries. Both `sw_vers` attempts were stopped by the existing 45-second watchdog at about 50 seconds. Other successful commands took 24–47 seconds, and small log-copy batches took 85–181 seconds. Thus this run proves broad native startup latency and a probe-imposed abort, without proving a permanent `sw_vers` hang. Disk enumeration, installation and application tests were not reached. The next candidate obtains the version from the current guest's SystemVersion plist to reduce process launches; it does not claim that `sw_vers` has become functional.
Memory admission requires the unchanged 4-GiB guest plus 512 MiB QEMU overhead. The copied four fixtures accept run 4204's captured 5,138,696 KiB and reject 4 GiB, missing or invalid availability. This reserves no memory against other host workloads. The existing one-minute heartbeat reads only retained `guest-proof.log`, printing at most its latest two start/completion/result/version markers, capped at 256 characters each; no new guest query or timer is added. Five existing offline fixtures exercise those bounds. Pushes on this candidate branch skip the PR/Push workflow; pull-request and manual triggers remain. Run 4192 at `6122be2` captured the actual 603-byte guest file and strictly parsed version 14.6.1. Its host reply was published successfully, but the guest's reply-existence check timed out before services or disk enumeration. Guest request/timeout UTC timestamps were not retained, so late delivery and 9p visibility cannot be distinguished. The current candidate removes this version reply: the guest publishes its raw file and a provisional version candidate; the host's full XML validation and exact result binding remain mandatory before any installation permit. The later installation-permit transport is still unqualified.
## Reasons and remaining gaps Run 4193 at `9164fe4` successfully derived the same current-file version in both guest and host. Its single `diskutil list physical` process was stopped at 124 seconds by the 120-second watchdog without output. The observer saw one runnable row and 3.18 seconds of accumulated CPU time, without a stack or proven IPC endpoint. `ioreg` was also stopped before producing output. The post-disk service gates, permit, installation and tests were not reached. A prior modified Recovery14 reference image has byte-identical SystemVersion contents but a different image hash; it contains StorageKit with the `com.apple.storagekitd` and `com.apple.storagekitd.dm` MachServices, not `diskmanagementd`. The next observation targets that actual service family and the own diskutil stack; this reference does not establish the live service state of run 4193.
The runtime-observation candidate compares against `96755c704e63884e9c45e8ebbb18b7b12e4bdd83` without changing VM parameters or native gates. After the existing KVM staging marker, the host samples only the owned container's QEMU `stat`, `status`, `io`, `wchan` and cgroup CPU/memory/I/O counters and pressure, at most once per 60 seconds and 173 attempts within the unchanged 172-minute outer budget. Ownership inspection and reads share a ten-second deadline; the exec also has a nine-second internal timeout. Missing files are optional and each successful UTC-stamped snapshot is limited to 16 KiB and appended to `runtime-resources.log`. Completion prints the complete history, bounded to 173 × 16 KiB, plus the existing resource limits bounded to 16 KiB, allowing counter deltas even when artifact retrieval fails. Command lines and environments are never read. Failure diagnostics additionally invoke `kmutil showloaded --list-only` under the existing 45-second watchdog; absent, empty or failed output does not establish whether the kexts loaded. The original gate failure is preserved. These observations provide performance evidence; they prove no bottleneck or fix by themselves. Local `--validate` covers marker retention, scheduling, PID/executable rejection, byte budgets, UTC retention and a real ten-second timeout without Docker or macOS. Run 4194 at `81a3b9c` stopped the same single query after 606 seconds without output. StorageKit exists but was not running and had never started in the before/live snapshots; the live snapshot covers approximately 103–160 seconds of the query, not its entire lifetime. `sample` hit its own watchdog without even its sampling-start message or report. This does not establish symbolication as the cause. The observed `1T` is a current Timeshare priority, not a thread count or proof of background policy. Neither installation nor tests ran.
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. Run 4195 at `4b7fd16` did not reach the new observer: the required `uname -m` timed out at its 45-second limit, was sent TERM at 51 seconds and exited 143 at 55 seconds without output. Later `id`, `sysctl` and targeted `ps` completed, but `uname -a` also timed out. No new QEMU fatal/reset, container OOM, swap or CPU throttling was recorded. The cause remains unknown. The next bounded run changes only the architecture command's limit to the existing UID command's 180 seconds; a successful native `uname -m` result is still mandatory.
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. ## Entry points and dependencies
[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. Orchestration remains the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Existing Bash/Python boot integration is necessary before a guest SDK exists. NASM assembles the CPU probe in the disposable image build, without host/runner installation. No new runner, device, capability, secret or service is used.
**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. The disposable native process diagnostic uses a small C boundary linked only to libSystem, so it can record entry and kernel observations before the guest has .NET or CLT. Its C# file-based build driver and retained source/compiler/SDK/minimum-OS/import/signature/hash receipt describe the one-time local build. The pipeline receives this diagnostic asset and does not invoke an Apple compiler or request a macOS runner. This asset is not one of the application's four freshly built helpers and cannot qualify a test or readiness gate.
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. The probe validates both the target PID and expected parent before reading role/task data. Public BSD observations include background flags, Nice, role and raw CPU/page-in counters. A read-only Mach port is attempted separately, with return and errno recorded before any DYLD/thread reads. The local Apple-sleep fixture returned EPERM; actual Recovery rights remain unknown. The probe has no control-port fallback, process suspension, remote writes, extra entitlements or SIP change. Unsuspended snapshots may be incomplete.
## Entry point and dependencies For local maintenance with an existing Apple SDK, run `dotnet run --file tools/ci/native-process-probe/ProbeDriver.cs -- tools/ci/native-process-probe`. It builds and signs into that folder's `artifacts/`, observes and cleans up its own temporary sleep child, and retains the build/self-test receipt there. Publishing a changed asset requires reviewing that receipt, refreshing the portable `build-manifest.json` and updating all four controller hash pins. CI verifies those pins before staging the binary and manifest into the owned Recovery state; it never runs the build driver.
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
~~~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
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 --noavx-archive /path/to/NoAVXFSCompressionTypeZlib-AVXpel-v12.6.zip --output /path/to/fresh/validation dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --output /path/to/fresh/validation
~~~
`--validate` checks result/container contracts without Docker. With `--source` it verifies the actual Cryptex ZIP/bundle, source seams, generated OpenCore configuration and staging/checksum contracts, checks Bash syntax, then exercises four raw/zlib Recovery fixtures and twelve rejection cases with independent C# CRC32 readback. It also checks preservation of a successful resource snapshot after a later failed capture, leaving the supplied source untouched. It does not download/extract the LongQT ISO, verify a complete Apple Recovery image or execute the active-Lilu runtime checks. The ISO checksum is enforced during the later Docker build; active Lilu and EFI-copy checks execute only during container boot. The optional local Cryptex ZIP must match the release size/hash; omitting it downloads only the public 69,703-byte release. Use a fresh output directory. Dependencies are .NET 10, Git, Bash and Python 3 with its standard library; manual execution also requires the existing Linux/x64 Docker daemon and its existing KVM device.
The optional `--noavx-archive` supplies the exact local NoAVX ZIP; omitting it downloads the pinned small archive. The unchanged NoAVX validation checks staged bytes and actual generated `Kernel.Add`, including four invalid archives, two staging failures and five configuration rejections. Use `--validate --full` with the same arguments to exercise the existing Full bootstrap, installer, disk and TRX contracts as well; `MacOsNativeGuest.cs --validate` checks the unchanged guest payload/tar/fresh-test-result contracts. These checks do not install an SDK or execute Apple frameworks.
The manual-only workflow keeps these owned run/cleanup entry points; validation invokes neither:
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
~~~ ```
## Exact bootasset contract The native diagnostic workflow is manual only. Temporary diagnostic branches are excluded from ordinary push jobs to avoid repeating unchanged Wine/portable jobs. Remove this routing when integrating qualified CI into the actual PR.
Dockur stays pinned to `16a5b470cdd601bae8b05b02d748d7edfb36c12e`. Original Recovery patcher/staging, Dockerfile, OpenCore script and active config hashes are verified before edits. Both existing QEMU image digests remain pinned; other existing upstream downloads are observed through image identity. `source-hashes.json` includes the generated Recovery patcher, both original/replacement daemon variants and `udif_checksums.py`, staged from `tools/ci/macos-native-udif-checksums.py`. This small Python module belongs to the existing Linux UDIF runtime; C# supplies orchestration, validation fixtures and an independent CRC32 implementation. ## Before Apple downloads
Run 4173 at `45d7bde71f9f5a1f7121585fe3ee9fc81f7c585f` failed before QEMU started: the full macOS 14 plist pattern was absent from the macOS 13 download. The original image's hash was not retained. An independently downloaded comparison for the same board `Mac-4B682C642B45593E` is macOS 13.6/22G120, Apple product 042-23155, 710,918,897 bytes, SHA256 `c19bd12f5cb1651b87b74d04f02a636da762ea46b81c7ebc9f205fa2a976d599`. Its Apple chunklist signature and chunks verified before any changes. It is comparison evidence, not the missing run-4173 image identity. The existing daemon must be Linux/x64 with two CPUs and 6 GiB memory; the runner must have 5 GiB available memory and the Docker filesystem 8 GiB free. These checks do not reconfigure resources. Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, both imported QEMU image digests and original source seams remain pinned.
The HFS+ catalog identifies `/System/Library/LaunchDaemons/com.apple.recoveryosd.plist` as file ID 57231, logical size 465 bytes and one 4,096-byte allocated block. Its exact XML SHA256 is `af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6`. This variant has `ProcessType=Interactive`; the previous macOS 14 variant has `App`. The patch accepts only these two exact layouts with exactly one daemon label and original `ProgramArguments=[/usr/libexec/recoveryosd]`. It preserves each variant's fields and process type, removes only the XML doctype to fit the wrapper arguments, and pads to the original file size. Unknown, duplicate, malformed or wrong-argument layouts fail before image writes. The early rc.cdrom hook remains mount-only; both the unchanged read-only wrapper and guarded Full wrapper exec the original Apple daemon. Actual `Skylake-Client-v4` CPU flags under TCG use `enforce=on` to reject unsupported requests. The CPU preflight uses that same composed flag list and QEMU binary before Recovery download/boot. `tools/ci/macos-tcg-cpu-preflight.asm` enables long mode/YMM state, executes AVX and AVX2 integer arithmetic, and checks an Int32 from the upper 128-bit lane. Only the correct result reaches [QEMU debug-exit](https://github.com/qemu/qemu/blob/v11.1.1/hw/misc/debugexit.c) code 33. No disks/network attach; failure/timeout fails preflight. This tests that instruction chain, not the complete ISA or macOS.
The checksum binding validates the original flattened UDIF boundaries and CRC32 values, stages every recompressed chunk before writing, then updates only the changed mish CRC32 and koly data-fork/master CRC32. [libdmg-hfsplus](https://github.com/planetbeing/libdmg-hfsplus/blob/master/dmg/dmglib.c) provides the checksum semantics; an independent C# reader matched all eight mish checksums on the unchanged comparison. Raw and inflated zlib bytes enter logical CRCs in run order; observed IGNORE runs are omitted. Unobserved ZERO runs, other compression/checksum types, overlaps and invalid boundaries are rejected. Base64 characters are replaced within the same metadata region, preserving its whitespace, length, partition tables and trailer offsets; the entire modified image is read again to verify CRCs. Apple chunklist authentication applies exclusively to the unchanged input, not the deliberately modified guest image. CRC integrity proves no Apple authenticity or native runtime gate. The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3ed642aa46c21b677f803c58f0d4ff665841723fcc5625f549`. Assembly/static review does not prove remote execution.
The [original LongQT v0.7 template](https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso), 15,884,288 bytes, is now Docker-ADD-checksummed to SHA256 `287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d`. Runtime copies actual `EFI_RELEASE/EFI/OC/Kexts`, including Lilu 1.7.1, even with official OpenCore DEBUG executables. Active Lilu: executable 526,984 bytes, SHA256 `0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52`; Info.plist SHA256 `fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a`. Staging checks both hashes and bundle version. Cryptex declares Lilu 1.4.7; [Lilu history](https://github.com/acidanthera/Lilu/blob/master/Changelog.md) includes Ventura/Sonoma installer/Recovery support before 1.7.1. Existing Lilu is kept. ## Native gates, bounds and cleanup
[CryptexFixup-1.0.5-RELEASE.zip](https://github.com/acidanthera/CryptexFixup/releases/download/1.0.5/CryptexFixup-1.0.5-RELEASE.zip), 69,703 bytes, SHA256 `25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30`, is checked in C#. Only expected Info.plist/executable files are accepted; identity/version/dependency and individual hashes are recorded. Runtime checks files before/after copying into fresh guest EFI. The original Apple recoveryosd runs under its existing job/PID beside the read-only probe. Exact known macOS 13/14 plist layouts and same-length replacements retain their allowlist. The patcher validates UDIF boundaries, updates changed mish/koly CRCs and reads back the image. Four raw/zlib positive and twelve rejection fixtures use an independent C# CRC32 reader. Apple chunklist authentication applies to the input, not the deliberately modified image.
Active `/assets/config.plist` receives exactly one enabled Cryptex immediately after enabled Lilu, preserving every other kext's order. Entry: `Arch=x86_64`, `BundlePath=CryptexFixup.kext`, `ExecutablePath=Contents/MacOS/CryptexFixup`, `PlistPath=Contents/Info.plist`, `MinKernel=22.0.0`, empty `MaxKernel`. [OpenCore Kernel.Add](https://github.com/acidanthera/OpenCorePkg/blob/1.0.7/Docs/Configuration.tex) requires dependencies first; bounds are Darwin versions. Runtime rechecks order/enabled/paths/architecture/bounds and rejects unverified `/custom.plist`. Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service queries and exactly one writable whole 64-GiB disk. The guest's existing Bash runtime reads its own `/System/Library/CoreServices/SystemVersion.plist` with a 4-KiB bound and mandatory EOF. Apple documents this path as the [system-version source](https://developer.apple.com/documentation/installer_js/system/1812284-version). Bash extracts only a provisional numeric version from the same bytes it publishes; subsequent guest probes remain read-only. The existing C# controller parses the full captured XML with external resolution disabled and requires a flat string-valued dictionary with exactly one valid direct `ProductVersion` string and macOS 14+. Missing, binary, oversized, ambiguous or malformed content fails. Before either readiness success or an installation permit, the result's version must exactly match this current-file evidence. No configured `VERSION` or host OS value serves as proof. The actual native diskutil query remains mandatory.
No new force/beta argument is needed for actual no-AVX2 CPUs. Baseline arguments remain. Validation rejects disabling arguments, `-crypt_allow_hash_validation` (disables the APFS patch) and unexpected Cryptex force/beta overrides. Manifest/profile enter the boot signature; this candidate always rebuilds `boot.img` and accepts no old cache as evidence. The raw file, exact source path, length, SHA256 and parsing receipt are retained and bound to the current token. This version evidence travels only from guest to host and requires no reply. The guest uses its existing Bash before any SDK exists; authoritative XML logic stays in C#/.NET. A Bash candidate alone cannot authorize installation or qualify readiness. This method establishes the current guest version, not successful execution of `sw_vers`.
## Gates, privileges and cleanup Ordinary commands retain 45 seconds; architecture and UID use 180 seconds. The single disk query retains its 600-second diagnostic window under the existing 90-minute Recovery deadline. An optional no-target `sample` CLI control precedes it. The owned observer takes an early native process snapshot, requests an ordinary one-second/100-ms `sample` without eager `-mayDie` symbol loading, and records live StorageKit state. After a cancelable 180-second builtin pause it takes a late snapshot of the same live disk child and expected parent. Each observation retains its own 60-second watchdog and two-second TERM/KILL grace. Missing tools, denied reads, failures and timed-out samples remain explicit missing evidence. Stack output is captured directly from the owned state with a 512-KiB bound, without another native copy command. Observation failure passes no gate. Owned children are stopped on query completion/cancellation; output remains 512 KiB per command and 4 MiB proof. No service is started or restarted by the observer.
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. The container retains 6 GiB memory/swap, two-CPU limit, 512 MiB shared memory and a 4-GiB/two-vCPU guest. One fresh anonymous /storage volume holds the sparse 64-GiB target. Inspection rejects devices, capabilities, binds, ports, host networking and privileged mode. KVM is disabled with no /dev/kvm mapping; guest networking stays slirp.
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. Evidence retains run/source/profile identity, CPU preflight, original/patched Recovery identity, container/QEMU state, native proof/result and cleanup. Sparse kernel-handoff lines are retained separately; two handoffs before readiness/installation permission fail early. After permission, normal installer reboots remain allowed. Optional bounded before/during/after pressure snapshots record host/cgroup counters. The /storage/14/setup.dmg hash is captured once after staging; successful evidence survives later capture failure. Screenshots/pressure observations pass no gate.
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. Both cleanup paths verify exact token/label/ID before removing only the owned container, anonymous volume and image. No pruning, host changes, original checkout changes or Meeting Assistant restart occurs. Artifacts remain seven days. Full CI remains unverified until an installed supported guest builds/signs fresh helpers and passes all 577 tests, including the five native macOS tests, with zero skips.
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. ## Prepared full build/test flow
Evidence retains run/profile identity, source/assets, EFI staging, container/resources, macOS 13 Recovery hash, native proof/result/outcome and cleanup. `[recovery-original]` logs the exact download's size/SHA256 before modifying it, including when patch failure later deletes the source. `guest-container-resources.last-success.stdout.log` and its timestamp/hash receipt preserve the last successful resource snapshot independently of a later failed stopped-container `docker exec`. Optional final Unix HMP capture includes `info kvm`, `info status` and a bounded PPM exported from `/tmp`; capture success passes no native gate. The separate manual `.gitea/workflows/macos-native-full.yaml` and the prepared required PR job invoke the same full mode:
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. ```sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --full --source /path/to/clean/pinned/dockur-clone --output /path/to/fresh/full-validation
## Experimental full guest flow
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/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 -- --run --full --output artifacts/native-macos-full
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --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 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. Full mode repeats CPU preflight and Recovery readiness for its own fresh guest. Before erasing the disposable target, the host verifies the owned container/anonymous volume, raw 64-GiB image, actual QEMU attachment/unique disk serial and state share. A token/source/disk-bound permit authorizes the guest. The guest independently checks whole/writable/size/unique serial before `diskutil eraseDisk`. It never erases a host disk or reuses an unrelated guest volume.
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 installer provisions the owned guest and returns into the prepared firstboot hook. Early firstboot logs and failures enter the state share even before account-package installation or test bootstrap. The installed root must be APFS backed by the exact owned physical store. Apple softwareupdate provisions CLT; a real Swift/SDK smoke build imports the required Apple frameworks. The source archive is bound to clean Git HEAD and SHA256; the pinned macOS x64 .NET SDK 10.0.401 is checked with SHA512. No prebuilt application/helper result counts as this run's evidence.
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. `tools/ci/MacOsNativeGuest.cs` restores/builds/tests the source inside the installed guest. Success requires four fresh x86_64 Mach-O helpers, a valid audio-app signature and a fresh source-bound TRX containing exactly 577 distinct passing tests, zero failures/skips and all five named native macOS tests. Archive, SDK, build, signature and TRX receipts are retained. The required PR job follows the existing Wine and portable jobs; all jobs still select `ubuntu-latest`.
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 workflow has 180 minutes; the controller reserves cleanup time with a shared 172-minute total deadline. Recovery, installation, CLT and test caps are 90/80/30/25 minutes under that same total, not additive promises. Actual supported-guest installation/performance and remote test success remain unqualified. The full run's own mandatory fresh-readiness and owned-disk gates prevent installation until that guest passes its prerequisites. CI does not deploy or restart the workstation application.
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.
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.
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, plus all four positive/twelve negative Recovery image cases and retained resource-snapshot checks. Its independent C# CRC32 reader validates fixture readback while the existing pinned Python UDIF binding performs the patch. 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.
File diff suppressed because it is too large. Load diff
+7 -7
View File
@@ -296,14 +296,14 @@ static class NativeGuest
static void ValidateResult(FullResult result, Payload payload) 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 < 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) 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)
throw new InvalidOperationException("Native guest receipt does not prove this source, toolchain, signed artifacts and all expected tests."); 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) static void RequirePlatform(string version, string architecture, string uid)
{ {
if (!Version.TryParse(version, out var parsed) || parsed.Major < 13 || architecture != "x86_64" || uid != "0") if (!Version.TryParse(version, out var parsed) || parsed.Major < 14 || architecture != "x86_64" || uid != "0")
throw new InvalidOperationException("Native build requires installed macOS 13+/x86_64 running as root."); throw new InvalidOperationException("Native build requires installed macOS 14+/x86_64 running as root.");
} }
static void RequireApfs(string xml) static void RequireApfs(string xml)
{ {
@@ -432,8 +432,8 @@ static class NativeGuest
Reject(() => ReadPayload(json.Replace("577", "572", StringComparison.Ordinal))); Reject(() => ReadPayload(json.Replace("577", "572", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal))); Reject(() => ReadPayload(json.Replace(payload.RunToken, "stale", StringComparison.Ordinal)));
Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal))); Reject(() => ReadPayload(json.Replace(payload.ArchiveSha256, "invalid", StringComparison.Ordinal)));
RequirePlatform("13.6.1", "x86_64", "0"); RequirePlatform("14.6.1", "x86_64", "0");
Reject(() => RequirePlatform("12.6.1", "x86_64", "0")); Reject(() => RequirePlatform("13.6.1", "x86_64", "0"));
Reject(() => RequirePlatform("14.6.1", "arm64", "0")); Reject(() => RequirePlatform("14.6.1", "arm64", "0"));
Reject(() => RequirePlatform("14.6.1", "x86_64", "501")); Reject(() => RequirePlatform("14.6.1", "x86_64", "501"));
RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>"); RequireApfs("<plist><dict><key>FilesystemType</key><string>apfs</string></dict></plist>");
@@ -486,9 +486,9 @@ static class NativeGuest
aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted"); aborted.Descendants(ns + "ResultSummary").Single().SetAttributeValue("outcome", "Aborted");
Reject(() => ValidateTrx(aborted.ToString(), ExpectedTests, started)); Reject(() => ValidateTrx(aborted.ToString(), ExpectedTests, started));
var artifacts = NativeNames.Select(name => new NativeArtifact(name, new string('d', 64), "x86_64")).ToArray(); 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, "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); 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);
ValidateResult(result, payload); 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 = "12.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 = "13.6.1" }, result with { StartedUtc = started.AddHours(-1) }, result with { StartedUtc = started.AddHours(-1), CompletedUtc = started.AddHours(-1).AddSeconds(1) } })
Reject(() => ValidateResult(invalid, payload)); Reject(() => ValidateResult(invalid, payload));
var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N")); var temporary = Path.Combine(OperatingSystem.IsMacOS() ? "/private/tmp" : Path.GetTempPath(), "meeting-assistant-guest-validation-" + Guid.NewGuid().ToString("N"));
try try
+8 -1
View File
@@ -16,9 +16,16 @@ phase() {
mv -f "$STATE_DIR/guest-phase.json.tmp" "$STATE_DIR/guest-phase.json" mv -f "$STATE_DIR/guest-phase.json.tmp" "$STATE_DIR/guest-phase.json"
} }
fail() { printf '[firstboot] ERROR: %s\n' "$1"; phase bootstrap-failed; exit 1; } fail() { printf '[firstboot] ERROR: %s\n' "$1"; phase bootstrap-failed; exit 1; }
require_installed_macos_version() {
# Numeric sw_vers product release with a major version of at least 14.
[[ "${1:-}" =~ ^(1[4-9]|[2-9][0-9]|[1-9][0-9]{2,})\.[0-9]+(\.[0-9]+)?$ ]]
}
phase toolchain-installing phase toolchain-installing
echo '[firstboot] verifying installed OS, APFS and owned physical store' echo '[firstboot] verifying installed OS, APFS and owned physical store'
sw_vers; uname -a; id uname -a; id
installed_version=$(/usr/bin/sw_vers -productVersion) || fail installed_os_version_failed
require_installed_macos_version "$installed_version" || fail installed_macos_14_or_newer_required
printf '[firstboot] installed macOS version=%s\n' "$installed_version"
[ "$(id -u)" = 0 ] && [ "$(uname -m)" = x86_64 ] || fail wrong_guest_platform [ "$(id -u)" = 0 ] && [ "$(uname -m)" = x86_64 ] || fail wrong_guest_platform
/usr/sbin/diskutil info -plist / > "$STATE_DIR/installed-root.plist" || fail root_diskutil_failed /usr/sbin/diskutil info -plist / > "$STATE_DIR/installed-root.plist" || fail root_diskutil_failed
[ "$(/usr/libexec/PlistBuddy -c 'Print :FilesystemType' "$STATE_DIR/installed-root.plist")" = apfs ] || fail root_is_not_installed_apfs [ "$(/usr/libexec/PlistBuddy -c 'Print :FilesystemType' "$STATE_DIR/installed-root.plist")" = apfs ] || fail root_is_not_installed_apfs
+164 -21
View File
@@ -14,6 +14,9 @@ TIMER_FIFO="/tmp/native-diagnostic-$PROOF_TOKEN-$$.fifo"
PENDING_OUTPUTS=() PENDING_OUTPUTS=()
ACTIVE_COMMAND="" ACTIVE_COMMAND=""
ACTIVE_TIMER="" ACTIVE_TIMER=""
# BEGIN disk IPC diagnostic
ACTIVE_OBSERVER=""
# END disk IPC diagnostic
os_version="" os_version=""
architecture="" architecture=""
uid=-1 uid=-1
@@ -96,8 +99,136 @@ read_scalar() {
SCALAR="$value" SCALAR="$value"
} }
# BEGIN native SystemVersion plist request helpers
read_native_system_version() {
# Recovery has Bash 3.2 before .NET. The numerical candidate is provisional;
# only the owned host's complete XML/evidence binding can qualify readiness.
local LC_ALL=C source="/System/Library/CoreServices/SystemVersion.plist"
local value status owner candidate remainder started=$SECONDS invalid_bytes
local raw="$STATE_DIR/native-system-version.plist"
local ready="$STATE_DIR/native-system-version.request"
NATIVE_VERSION_ERROR=native_system_version_read_failed
printf '[native-version-start] method=guest-file/host-xml source=%s seconds=%s\n' "$source" "$started" >&3
IFS= read -r -n 81 -d '' owner < "$STATE_DIR/run.owner"; status=$?
(( status == 1 && ${#owner} <= 80 )) &&
[ "$owner" = "$PROOF_TOKEN"$'\n' ] || { NATIVE_VERSION_ERROR=native_system_version_foreign_owner; return 1; }
[ ! -e "$ready" ] && [ ! -L "$ready" ] &&
[ ! -e "$raw" ] && [ ! -L "$raw" ] || { NATIVE_VERSION_ERROR=native_system_version_stale_exchange; return 1; }
[ -f "$source" ] || return 1
IFS= read -r -n 4097 -d '' value < "$source"; status=$?
# EOF is mandatory. NUL stops read with status 0; 4097 is the overbound sentinel.
(( status == 1 && ${#value} > 0 && ${#value} <= 4096 )) || { NATIVE_VERSION_ERROR=native_system_version_invalid_bytes; return 1; }
invalid_bytes=${value//$'\t'/}
invalid_bytes=${invalid_bytes//$'\r'/}
invalid_bytes=${invalid_bytes//$'\n'/}
[[ "$invalid_bytes" =~ [[:cntrl:]] ]] && { NATIVE_VERSION_ERROR=native_system_version_binary; return 1; }
printf '%s' "$value" > "$raw" || return 1
# Publish this final marker only after the complete raw write has closed.
printf '%s\n%s\n%s\nready:%s\n' "$PROOF_TOKEN" "$source" "${#value}" "$PROOF_TOKEN" > "$ready" || return 1
printf '[native-version-request] method=guest-file/host-xml source=%s bytes=%s seconds=%s\n' "$source" "${#value}" "$SECONDS" >&3
# This is a candidate from exactly these bytes, not an XML validity check.
NATIVE_VERSION_ERROR=native_system_version_candidate_invalid
[[ "$value" == *'<key>ProductVersion</key>'* ]] || return 1
remainder=${value#*'<key>ProductVersion</key>'}
remainder=${remainder#"${remainder%%[![:space:]]*}"}
[[ "$remainder" == '<string>'* ]] || return 1
remainder=${remainder#'<string>'}
candidate=${remainder%%'</string>'*}
[ "$candidate" != "$remainder" ] && [[ "$candidate" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || return 1
SCALAR="$candidate"
NATIVE_VERSION_ERROR=""
printf '[native-version-candidate] method=guest-file/host-xml source=%s candidate=%s qualified=false elapsed=%ss\n' "$source" "$SCALAR" "$((SECONDS - started))" >&3
return 0
}
# END native SystemVersion plist request helpers
# BEGIN disk IPC diagnostic
# Optional observations have their own child/timer ownership. Stack/thread
# observations target only this probe's live diskutil; none qualifies readiness.
observe_disk_query() {
local disk_process="$1" output="$2" observation_child="" observation_timer=""
local stack_output="$STATE_DIR/diskutil-stack.txt"
cancel_observation() {
trap '' TERM INT
if [ -n "$observation_child" ]; then
kill -TERM "$observation_child" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$observation_child" 2>/dev/null || :
wait "$observation_child" 2>/dev/null || :
fi
[ -z "$observation_timer" ] || { kill -TERM "$observation_timer" 2>/dev/null || :; wait "$observation_timer" 2>/dev/null || :; }
printf '[disk-observation] stopped after the owned disk query\n' >> "$output"
exit 143
}
observe_command() {
local name="$1" status started=$SECONDS
shift
printf '\n[disk-observation-command] %s:' "$name" >> "$output"
printf ' %s' "$@" >> "$output"
printf '\n' >> "$output"
"$@" >> "$output" 2>&1 &
observation_child=$!
(
trap 'exit 0' TERM INT
IFS= read -r -t 60 -u 9 unused || :
printf '[disk-observation-timeout] %s child=%s limit=60s\n' "$name" "$observation_child" >> "$output"
kill -TERM "$observation_child" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || :
kill -KILL "$observation_child" 2>/dev/null || :
) &
observation_timer=$!
wait "$observation_child"; status=$?
kill -TERM "$observation_timer" 2>/dev/null || :
wait "$observation_timer" 2>/dev/null || :
printf '[disk-observation-exit] %s status=%s elapsed=%ss\n' "$name" "$status" "$((SECONDS - started))" >> "$output"
OBSERVATION_EXIT="$status"
observation_child=""; observation_timer=""
}
observe_live_command() {
local name="$1"
shift
if ! kill -0 "$disk_process" 2>/dev/null; then
printf '[disk-observation-unavailable] %s: owned diskutil already exited\n' "$name" >> "$output"
elif [ ! -x "$1" ]; then
printf '[disk-observation-unavailable] %s: %s is unavailable\n' "$name" "$1" >> "$output"
else
observe_command "$name" "$@"
fi
}
trap cancel_observation TERM INT
printf '[disk-observation] owned-diskutil-child=%s parent-shell=%s\n' "$disk_process" "$$" >> "$output"
observe_live_command diskutil-native-before "$STATE_DIR/native-process-probe-x86_64" "$disk_process" "$$"
if [ ! -x /usr/bin/sample ]; then
printf '[disk-observation-unavailable] diskutil-stack: /usr/bin/sample is unavailable\n' >> "$output"
elif ! kill -0 "$disk_process" 2>/dev/null; then
printf '[disk-observation-unavailable] diskutil-stack: owned diskutil already exited\n' >> "$output"
elif [ -e "$stack_output" ] || [ -L "$stack_output" ]; then
printf '[disk-observation-unavailable] diskutil-stack: output already exists\n' >> "$output"
elif : > "$stack_output"; then
printf '[disk-stack-attempt] owned-diskutil-child=%s duration=1s interval=100ms limit=60s output=%s observation-only=true\n' "$disk_process" "$stack_output" >> "$output"
observe_command diskutil-stack /usr/bin/sample "$disk_process" 1 100 -file "$stack_output"
printf '[disk-stack-result] status=%s observation-only=true; raw report is captured by the Linux host\n' "$OBSERVATION_EXIT" >> "$output"
else
printf '[disk-observation-unavailable] diskutil-stack: output cannot be created\n' >> "$output"
fi
observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd
printf '[disk-observation-pause] limit=180s; canceled when owned query ends\n' >> "$output"
IFS= read -r -t 180 -u 9 unused || :
observe_live_command diskutil-native-after "$STATE_DIR/native-process-probe-x86_64" "$disk_process" "$$"
}
stop_disk_observation() {
[ -n "$ACTIVE_OBSERVER" ] || return 0
kill -TERM "$ACTIVE_OBSERVER" 2>/dev/null || :
wait "$ACTIVE_OBSERVER" 2>/dev/null || :
ACTIVE_OBSERVER=""
}
# END disk IPC diagnostic
cancel_probe() { cancel_probe() {
trap '' TERM INT trap '' TERM INT
# BEGIN disk IPC diagnostic
stop_disk_observation
# END disk IPC diagnostic
if [ -n "$ACTIVE_COMMAND" ]; then if [ -n "$ACTIVE_COMMAND" ]; then
kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || : kill -TERM "$ACTIVE_COMMAND" 2>/dev/null || :
IFS= read -r -t 2 -u 9 unused || : IFS= read -r -t 2 -u 9 unused || :
@@ -116,6 +247,12 @@ run_command() {
# Run 4161: even native uname/ps startup took 34-42s under TCG. # Run 4161: even native uname/ps startup took 34-42s under TCG.
# Isolate only the failed UID gate; every other watchdog remains unchanged. # Isolate only the failed UID gate; every other watchdog remains unchanged.
[[ "$name" != uid ]] || command_limit=180 [[ "$name" != uid ]] || command_limit=180
# BEGIN disk IPC diagnostic
# Run 4195: required uname -m exceeded 45s and returned 143 without output.
# Extend this architecture gate to the existing UID limit; ordinary commands stay 45s.
[[ "$name" != architecture ]] || command_limit=180
if [[ "$name" == disks && "${attempt:-0}" == 1 ]]; then command_limit=600; fi
# END disk IPC diagnostic
LAST_OUTPUT="/tmp/native-diagnostic-$name.out" LAST_OUTPUT="/tmp/native-diagnostic-$name.out"
printf '\n[proof-command] %s:' "$name" >&3 printf '\n[proof-command] %s:' "$name" >&3
printf ' %s' "$@" >&3 printf ' %s' "$@" >&3
@@ -136,6 +273,16 @@ run_command() {
) & ) &
timer=$! timer=$!
ACTIVE_TIMER="$timer" ACTIVE_TIMER="$timer"
# BEGIN disk IPC diagnostic
if [[ "$name" == disks && "$attempt" == 1 ]]; then
local observation_output="/tmp/native-diagnostic-disk-observation.out"
: > "$observation_output"
observe_disk_query "$process" "$observation_output" &
ACTIVE_OBSERVER=$!
printf '[disk-observation-start] observer=%s owned-diskutil-child=%s\n' "$ACTIVE_OBSERVER" "$process" >&3
PENDING_OUTPUTS+=("$observation_output")
fi
# END disk IPC diagnostic
wait "$process" wait "$process"
exit_code=$? exit_code=$?
waited=$SECONDS waited=$SECONDS
@@ -143,6 +290,9 @@ run_command() {
printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3 printf '[proof-native-wait] %s child=%s elapsed=%ss exit=%s\n' "$name" "$process" "$((waited - started))" "$exit_code" >&3
kill -TERM "$timer" 2>/dev/null || : kill -TERM "$timer" 2>/dev/null || :
wait "$timer" 2>/dev/null || : wait "$timer" 2>/dev/null || :
# BEGIN disk IPC diagnostic
stop_disk_observation
# END disk IPC diagnostic
ACTIVE_COMMAND=""; ACTIVE_TIMER="" ACTIVE_COMMAND=""; ACTIVE_TIMER=""
printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3 printf '[proof-cleanup] %s child=%s elapsed=%ss total=%ss\n' "$name" "$process" "$((SECONDS - waited))" "$((SECONDS - started))" >&3
printf '[proof-exit] %s\n' "$exit_code" >&3 printf '[proof-exit] %s\n' "$exit_code" >&3
@@ -157,7 +307,6 @@ diagnose_failure() {
run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features run_command context /usr/sbin/sysctl kern.bootargs machdep.cpu.brand_string machdep.cpu.features machdep.cpu.leaf7_features
run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm= run_command parent /bin/ps -p "$$" -p "$PPID" -o pid=,ppid=,comm=
run_command processes /bin/ps -axo pid,ppid,comm run_command processes /bin/ps -axo pid,ppid,comm
run_command loaded_kexts /usr/bin/kmutil showloaded --list-only
} }
fail_probe() { fail_probe() {
@@ -181,33 +330,27 @@ run_command uid /usr/bin/id -u
read_scalar || fail_probe uid_output_invalid read_scalar || fail_probe uid_output_invalid
uid="$SCALAR" uid="$SCALAR"
[ "$uid" = 0 ] || fail_probe recovery_account_not_root [ "$uid" = 0 ] || fail_probe recovery_account_not_root
run_command platform /usr/bin/sw_vers # BEGIN native SystemVersion plist getter
platform_exit="$LAST_EXIT" read_native_system_version || fail_probe "$NATIVE_VERSION_ERROR"
flush_outputs || finish false diagnostic_log_budget_exceeded # END native SystemVersion plist getter
if (( platform_exit != 0 )); then
run_command system /bin/launchctl print system
system_exit="$LAST_EXIT"
run_command arbitration /bin/launchctl print system/com.apple.diskarbitrationd
arbitration_exit="$LAST_EXIT"
run_command recovery /bin/launchctl print system/com.apple.recoveryosd
recovery_exit="$LAST_EXIT"
printf '[proof-retry] sw_vers once after native service context; same 45-second deadline\n' >&3
run_command platform-warm /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
fi
(( platform_exit == 0 )) || fail_probe sw_vers_failed
run_command version /usr/bin/sw_vers -productVersion
(( LAST_EXIT == 0 )) || fail_probe product_version_failed
read_scalar || fail_probe product_version_invalid
os_version="$SCALAR" os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid [[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version (( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded flush_outputs || finish false diagnostic_log_budget_exceeded
# BEGIN disk IPC diagnostic
printf '[disk-diagnostic-runtime] bash=%s stack-observation-only=true\n' "$BASH_VERSION" >&3
run_command sample_usage /usr/bin/sample
run_command arbitration_before /bin/launchctl print system/com.apple.diskarbitrationd
run_command management_before /bin/launchctl print system/com.apple.storagekitd
run_command media_before /usr/sbin/ioreg -r -c IOMedia -l -w 0
flush_outputs || finish false diagnostic_log_budget_exceeded
# END disk IPC diagnostic
# Bound readiness independently of the host's 40-minute overall deadline. # Bound readiness independently of the host's 40-minute overall deadline.
readiness_start=$SECONDS readiness_start=$SECONDS
attempt=0 attempt=0
while (( SECONDS - readiness_start < 600 )); do while (( attempt < 1 && SECONDS - readiness_start < 600 )); do
attempt=$((attempt + 1)) attempt=$((attempt + 1))
printf '\n[readiness-attempt] %s\n' "$attempt" >&3 printf '\n[readiness-attempt] %s\n' "$attempt" >&3
run_command disks /usr/sbin/diskutil list physical run_command disks /usr/sbin/diskutil list physical
+79
View File
@@ -0,0 +1,79 @@
; Bare 64-KiB BIOS for the existing Linux QEMU binary, not macOS firmware.
; Assemble: nasm -f bin -o ci-cpu-preflight.bin macos-tcg-cpu-preflight.asm
; No disks/network. isa-debug-exit returns 33 only after AVX + AVX2 execute
; and the upper 128-bit lane contains the expected arithmetic result.
; Unsupported instructions/triple faults cannot produce the success code.
BITS 16
ORG 0
start:
cli
cld
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x8000
; QEMU zeroes fresh RAM. Identity-map the first 2 MiB through three tables.
mov dword [0x1000], 0x2003
mov dword [0x2000], 0x3003
mov dword [0x3000], 0x0083
lgdt [cs:gdt_descriptor]
mov eax, 0x40620 ; PAE, OSFXSR, OSXMMEXCPT, OSXSAVE
mov cr4, eax
mov eax, 0x1000
mov cr3, eax
mov ecx, 0xc0000080 ; EFER.LME
rdmsr
or eax, 0x100
wrmsr
mov eax, cr0
and eax, ~0x0c ; clear EM and TS before vector instructions
or eax, 0x80000003 ; paging, protected mode, monitor coprocessor
mov cr0, eax
jmp dword 0x08:(0xf0000 + long_mode)
ALIGN 8
gdt:
dq 0
dq 0x00af9a000000ffff ; ring-0 long-mode code, base 0
dq 0x00cf92000000ffff ; ring-0 data, base 0
gdt_descriptor:
dw gdt_descriptor - gdt - 1
dd 0xf0000 + gdt
BITS 64
long_mode:
mov ax, 0x10
mov ds, ax
mov es, ax
mov ss, ax
mov rsp, 0x8000
xor ecx, ecx
mov eax, 7 ; XCR0 enables x87, SSE and AVX state
xor edx, edx
xsetbv
vxorps ymm0, ymm0, ymm0 ; AVX, including the upper YMM lane
vpcmpeqd ymm1, ymm1, ymm1 ; AVX2: all eight int32 lanes become -1
vpsrld ymm1, ymm1, 31 ; AVX2: all lanes become 1
vpaddd ymm2, ymm1, ymm1 ; AVX2: all lanes become 2
vextracti128 xmm3, ymm2, 1 ; AVX2: inspect the upper half, not only SSE
vmovd eax, xmm3
cmp eax, 2
jne fail
vzeroupper
mov eax, 0x10 ; QEMU debugexit computes (value << 1) | 1
jmp exit_qemu
fail:
mov eax, 0x11
exit_qemu:
mov dx, 0xf4
out dx, eax
hlt
jmp $
; CPU reset starts at the last 16 bytes; reload the real-mode CS base.
BITS 16
TIMES 0xfff0 - ($ - $$) db 0xff
jmp 0xf000:start
TIMES 0x10000 - ($ - $$) db 0xff
@@ -0,0 +1,106 @@
// Disposable pre-SDK observation boundary. No control task port or process writes.
#include <errno.h>
#include <limits.h>
#include <libproc.h>
#include <mach/mach.h>
#include <mach/mach_vm.h>
#include <mach/i386/thread_status.h>
#include <mach-o/dyld_images.h>
#include <stddef.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
#include <unistd.h>
// Private exported BSD API/selector, verified against XNU10063.141.1. Its
// return is BSD int + errno, not a Mach kern_return_t. See manifest sources.
extern int task_read_for_pid(mach_port_name_t, int, mach_port_name_t *) __attribute__((weak_import));
#define READ_ONLY_DARWIN_ROLE 6
static unsigned output_bytes;
static void emit(const char *format, ...) {
char line[256]; va_list args; va_start(args, format);
int length = vsnprintf(line, sizeof line, format, args); va_end(args);
if (length < 0 || length >= (int)sizeof line || output_bytes + (unsigned)length > 32768) exit(70);
output_bytes += (unsigned)length; fwrite(line, 1, (size_t)length, stderr);
}
static void phase(const char *name, const char *point) { emit("[phase] %s %s\n", name, point); }
int main(int argc, char **argv) {
setvbuf(stderr, NULL, _IONBF, 0);
emit("[probe-entry] self=%d architecture=%s snapshot-only=true\n", getpid(),
#if defined(__x86_64__)
"x86_64"
#else
"other"
#endif
);
if (argc != 3) { emit("[invalid-pid] require target and expected-parent decimal PIDs greater than1\n"); return 64; }
long parsed[2];
for (int argument = 1; argument <= 2; ++argument) {
if (!argv[argument][0]) { emit("[invalid-pid] empty PID\n"); return 64; }
for (const char *p = argv[argument]; *p; ++p) if (*p < '0' || *p > '9') { emit("[invalid-pid] decimal digits required\n"); return 64; }
errno = 0; char *end; parsed[argument - 1] = strtol(argv[argument], &end, 10);
if (errno || *end || parsed[argument - 1] <= 1 || parsed[argument - 1] > INT_MAX) { emit("[invalid-pid] PID outside permitted numeric range\n"); return 64; }
}
int pid = (int)parsed[0], expected_parent = (int)parsed[1]; struct proc_bsdinfo bsd = {0};
phase("proc_pidinfo", "before"); errno = 0;
int bytes = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsd, sizeof bsd); int bsd_errno = errno;
emit("[phase] proc_pidinfo after return=%d errno=%d\n", bytes, bsd_errno);
if (bytes != sizeof bsd) { emit("[bsd-unavailable] identity was not established\n"); return 66; }
emit("[bsd] pid=%u ppid=%u flags=0x%x nice=%d status=%u\n", bsd.pbi_pid, bsd.pbi_ppid, bsd.pbi_flags, bsd.pbi_nice, bsd.pbi_status);
if (bsd.pbi_pid != (unsigned)pid || bsd.pbi_ppid != (unsigned)expected_parent) { emit("[ownership-rejected] target=%d expected-parent=%d actual-pid=%u actual-parent=%u\n", pid, expected_parent, bsd.pbi_pid, bsd.pbi_ppid); return 65; }
phase("getpriority-role", "before"); errno = 0;
int role = getpriority(READ_ONLY_DARWIN_ROLE, (id_t)pid); int role_errno = errno;
emit("[role] selector=6 value=%d errno=%d\n", role, role_errno); phase("getpriority-role", "after");
struct proc_taskinfo taskinfo = {0}; phase("proc_pidinfo-task", "before"); errno = 0;
bytes = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskinfo, sizeof taskinfo); int taskinfo_errno = errno;
emit("[phase] proc_pidinfo-task after return=%d errno=%d\n", bytes, taskinfo_errno);
if (bytes == sizeof taskinfo) {
emit("[bsd-task] total-user-raw=%llu total-system-raw=%llu threads-user-raw=%llu threads-system-raw=%llu\n", taskinfo.pti_total_user, taskinfo.pti_total_system, taskinfo.pti_threads_user, taskinfo.pti_threads_system);
emit("[bsd-task] threads=%d running=%d policy=%d priority=%d faults=%d pageins=%d virtual-bytes=%llu resident-bytes=%llu\n", taskinfo.pti_threadnum, taskinfo.pti_numrunning, taskinfo.pti_policy, taskinfo.pti_priority, taskinfo.pti_faults, taskinfo.pti_pageins, taskinfo.pti_virtual_size, taskinfo.pti_resident_size);
}
if (!task_read_for_pid) {
emit("[task-read-unavailable] optional private symbol absent; public BSD snapshot remains observational\n");
emit("[probe-end] snapshot-only=true qualified-readiness=false\n"); return 0;
}
mach_port_t task = MACH_PORT_NULL; phase("task_read_for_pid", "before"); errno = 0;
int read_result = task_read_for_pid(mach_task_self(), pid, &task); int read_errno = errno;
emit("[task-read] return=%d errno=%d port=0x%x\n", read_result, read_errno, task); phase("task_read_for_pid", "after");
if (read_result != 0 || task == MACH_PORT_NULL) {
emit("[mach-unavailable] read-only capability denied; BSD snapshot remains observational\n");
if (task != MACH_PORT_NULL) mach_port_deallocate(mach_task_self(), task);
emit("[probe-end] snapshot-only=true qualified-readiness=false\n");
return 0;
}
emit("[mach-capability] read-only=true; unsuspended snapshots may be incomplete\n");
task_dyld_info_data_t dyld = {0}; mach_msg_type_number_t count = TASK_DYLD_INFO_COUNT;
phase("task_info-dyld", "before"); kern_return_t kr = task_info(task, TASK_DYLD_INFO, (task_info_t)&dyld, &count);
emit("[phase] task_info-dyld after kern=%d count=%u\n", kr, count);
size_t prefix = offsetof(struct dyld_all_image_infos, jitInfo);
if (kr == KERN_SUCCESS && count == TASK_DYLD_INFO_COUNT && dyld.all_image_info_format == TASK_DYLD_ALL_IMAGE_INFO_64 && dyld.all_image_info_size >= prefix) {
struct dyld_all_image_infos info = {0}; mach_vm_size_t received = 0; phase("dyld-prefix-read", "before");
kr = mach_vm_read_overwrite(task, dyld.all_image_info_addr, prefix, (mach_vm_address_t)(uintptr_t)&info, &received);
emit("[phase] dyld-prefix-read after kern=%d bytes=%llu\n", kr, (unsigned long long)received);
if (kr == KERN_SUCCESS && received == prefix) emit("[dyld] version=%u images=%u array=0x%llx libSystemInitialized=%d dyld=0x%llx array-null-is-pending=true\n", info.version, info.infoArrayCount, (unsigned long long)(uintptr_t)info.infoArray, info.version >= 2 ? info.libSystemInitialized : -1, info.version >= 2 ? (unsigned long long)(uintptr_t)info.dyldImageLoadAddress : 0);
}
thread_act_array_t threads = NULL; mach_msg_type_number_t thread_count = 0;
phase("task_threads", "before"); kr = task_threads(task, &threads, &thread_count);
emit("[phase] task_threads after kern=%d count=%u observed-limit=32\n", kr, thread_count);
if (kr == KERN_SUCCESS) {
for (unsigned i = 0; i < thread_count && i < 32; ++i) {
thread_basic_info_data_t basic = {0}; count = THREAD_BASIC_INFO_COUNT; phase("thread_info", "before");
kr = thread_info(threads[i], THREAD_BASIC_INFO, (thread_info_t)&basic, &count);
emit("[phase] thread_info after index=%u kern=%d count=%u\n", i, kr, count);
if (kr == KERN_SUCCESS && count == THREAD_BASIC_INFO_COUNT) emit("[thread-basic] index=%u run-state=%d policy=%d cpu=%d user=%d.%06d system=%d.%06d\n", i, basic.run_state, basic.policy, basic.cpu_usage, basic.user_time.seconds, basic.user_time.microseconds, basic.system_time.seconds, basic.system_time.microseconds);
x86_thread_state64_t registers = {0}; count = x86_THREAD_STATE64_COUNT; phase("thread_get_state-x86_64", "before");
kr = thread_get_state(threads[i], x86_THREAD_STATE64, (thread_state_t)&registers, &count);
emit("[phase] thread_get_state-x86_64 after index=%u kern=%d count=%u\n", i, kr, count);
if (kr == KERN_SUCCESS && count == x86_THREAD_STATE64_COUNT) emit("[thread-registers] index=%u rip=0x%llx rbp=0x%llx rsp=0x%llx\n", i, registers.__rip, registers.__rbp, registers.__rsp);
}
for (unsigned i = 0; i < thread_count; ++i) mach_port_deallocate(mach_task_self(), threads[i]);
vm_deallocate(mach_task_self(), (vm_address_t)threads, thread_count * sizeof *threads);
}
mach_port_deallocate(mach_task_self(), task);
emit("[probe-end] snapshot-only=true qualified-readiness=false\n"); return 0;
}
@@ -0,0 +1,116 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
// Offline build/owned-child validation only. No guest, Docker or services.
if (!OperatingSystem.IsMacOS()) throw new InvalidOperationException("The disposable build driver uses the existing Apple SDK on this local host.");
var folder = Path.GetFullPath(args.Single());
var source = Path.Combine(folder, "NativeProcessProbe.c");
var output = Path.Combine(folder, "artifacts"); Directory.CreateDirectory(output);
var sourceHash = Hash(File.ReadAllBytes(source));
var sdk = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "sdk-path")).Stdout.Trim();
var sdkVersion = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "sdk-version")).Stdout.Trim();
var compiler = await Run("/usr/bin/xcrun", ["--sdk", "macosx", "clang", "--version"], "compiler-version");
var binary = Path.Combine(output, "native-process-probe-x86_64");
string[] build = ["--sdk", "macosx", "clang", "-arch", "x86_64", "-mmacosx-version-min=14.0", "-std=c11", "-Os", "-Wall", "-Wextra", "-Werror", source, "-lproc", "-o", binary];
await Run("/usr/bin/xcrun", build, "build-x86_64");
await Run("/usr/bin/codesign", ["--force", "--sign", "-", binary], "adhoc-sign");
await Run("/usr/bin/codesign", ["--verify", "--strict", binary], "signature-verify");
var signature = await Run("/usr/bin/codesign", ["-d", "--verbose=4", "--entitlements", ":-", binary], "signature-details");
var architectures = await Run("/usr/bin/lipo", ["-archs", binary], "architectures");
var imports = await Run("/usr/bin/otool", ["-L", binary], "imports");
var loadCommands = await Run("/usr/bin/otool", ["-l", binary], "load-commands");
var symbols = await Run("/usr/bin/nm", ["-m", "-u", binary], "undefined-symbols");
if (architectures.Stdout.Trim() != "x86_64" || !Regex.IsMatch(loadCommands.Stdout, @"cmd LC_BUILD_VERSION\s+cmdsize [0-9]+\s+platform [0-9]+\s+minos 14\.0\b")) throw new Exception("Actual architecture/minimum OS differs.");
var importedLibraries = imports.Stdout.Split('\n').Skip(1).Where(line => line.Trim().Length != 0).Select(line => line.Trim().Split(' ')[0]).ToArray();
if (importedLibraries.Length != 1 || importedLibraries[0] != "/usr/lib/libSystem.B.dylib") throw new Exception("Probe imports another runtime/framework: " + string.Join(",", importedLibraries));
if (!Regex.IsMatch(symbols.Stdout, @"weak external _task_read_for_pid\b") || symbols.Stdout.Contains("_task_for_pid", StringComparison.Ordinal)
|| new[] { "_task_suspend", "_task_resume", "_thread_suspend", "_thread_resume", "_mach_vm_write" }.Any(symbols.Stdout.Contains)) throw new Exception("Read-only import contract failed.");
if (signature.Stdout.Contains("<dict>", StringComparison.Ordinal) || signature.Stderr.Contains("<dict>", StringComparison.Ordinal)) throw new Exception("Probe must not acquire entitlements.");
var cases = new List<object>(); bool x86Executed = false; string? executionUnavailable = null;
using var target = Process.Start(new ProcessStartInfo("/bin/sleep") { ArgumentList = { "30" }, UseShellExecute = false })!;
try
{
Result positive;
try { positive = await Run(binary, [target.Id.ToString(), Environment.ProcessId.ToString()], "owned-sleep", requireSuccess: false); x86Executed = true; }
catch (System.ComponentModel.Win32Exception exception) { executionUnavailable = exception.Message; positive = new(-1, "", ""); }
if (x86Executed)
{
var evidence = positive.Stdout + positive.Stderr;
if (positive.ExitCode != 0 || !evidence.Contains("[probe-entry]", StringComparison.Ordinal)
|| !Regex.IsMatch(evidence, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @" flags=0x[0-9a-f]+ nice=-?[0-9]+ status=[0-9]+")
|| !evidence.Contains("[role] selector=6", StringComparison.Ordinal) || !evidence.Contains("[probe-end] snapshot-only=true qualified-readiness=false", StringComparison.Ordinal)
|| !(Regex.IsMatch(evidence, @"\[task-read\] return=-?[0-9]+ errno=[0-9]+ port=0x[0-9a-f]+") || evidence.Contains("[task-read-unavailable] optional private symbol absent", StringComparison.Ordinal))) throw new Exception("Owned BSD snapshot/read-capability receipt failed: " + evidence);
var read = Regex.Match(evidence, @"\[task-read\] return=(-?[0-9]+) errno=([0-9]+) port=0x([0-9a-f]+)");
cases.Add(new { name = "owned-sleep", success = true, targetPid = target.Id, expectedParentPid = Environment.ProcessId, positive.ExitCode, outputBytes = Encoding.UTF8.GetByteCount(evidence), privateReadSymbolAvailable = read.Success, readReturn = read.Success ? int.Parse(read.Groups[1].Value) : (int?)null, readErrno = read.Success ? int.Parse(read.Groups[2].Value) : (int?)null, readPort = read.Success ? read.Groups[3].Value : null, targetIsApplePlatformBinary = true, targetArchitectureNotAsserted = true, recoveryPermissionProven = false });
var wrongParent = await Run(binary, [target.Id.ToString(), target.Id.ToString()], "owned-sleep-wrong-parent", requireSuccess: false);
var rejected = wrongParent.Stdout + wrongParent.Stderr;
if (wrongParent.ExitCode != 65 || !rejected.Contains("[ownership-rejected]", StringComparison.Ordinal)
|| !Regex.IsMatch(rejected, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @"\b")
|| rejected.Contains("getpriority-role", StringComparison.Ordinal) || rejected.Contains("proc_pidinfo-task", StringComparison.Ordinal) || rejected.Contains("task_read_for_pid", StringComparison.Ordinal)) throw new Exception("Wrong parent reached role/task/Mach observation.");
cases.Add(new { name = "owned-sleep-wrong-parent", success = true, targetPid = target.Id, suppliedExpectedParentPid = target.Id, actualParentPid = Environment.ProcessId, wrongParent.ExitCode, furtherReadsReached = false });
var ownedPid = target.Id.ToString(); var parentPid = Environment.ProcessId.ToString();
string[][] invalid = [[], [ownedPid], ["", parentPid], ["0", parentPid], ["1", parentPid], ["-1", parentPid], ["+2", parentPid], ["2x", parentPid], [" 2", parentPid], ["999999999999999999999999", parentPid],
[ownedPid, ""], [ownedPid, "0"], [ownedPid, "1"], [ownedPid, "-1"], [ownedPid, "+2"], [ownedPid, "2x"], [ownedPid, " 2"], [ownedPid, "999999999999999999999999"], [ownedPid, parentPid, "extra"]];
for (var i = 0; i < invalid.Length; i++)
{
var result = await Run(binary, invalid[i], "invalid-pid-" + i, requireSuccess: false);
var text = result.Stdout + result.Stderr;
if (result.ExitCode != 64 || !text.Contains("[probe-entry]", StringComparison.Ordinal) || !text.Contains("[invalid-pid]", StringComparison.Ordinal) || text.Contains("proc_pidinfo", StringComparison.Ordinal)) throw new Exception("Invalid PID reached observation.");
cases.Add(new { name = "invalid-pid-" + i, success = true, result.ExitCode, nativeProcessObserved = false });
}
if (target.HasExited) throw new Exception("Own sleep exited unexpectedly during the snapshot.");
}
}
finally
{
if (!target.HasExited) target.Kill();
await target.WaitForExitAsync();
}
if (!target.HasExited) throw new Exception("Owned local child was not cleaned up.");
var manifest = new
{
success = x86Executed, buildVerified = true, selftestPassed = x86Executed, purpose = "Disposable offline native read-only process diagnostic; no CI runner requirement", sourcePath = source, sourceSha256 = sourceHash,
driverSha256 = Hash(File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"))), binaryPath = binary, binarySha256 = Hash(File.ReadAllBytes(binary)),
hostArchitecture = RuntimeInformation.OSArchitecture.ToString(), compiler = compiler.Stdout.Trim(), sdk, sdkVersion, minimumMacOS = "14.0", architecture = "x86_64", compilerArguments = build,
importedLibraries, signature = "ad-hoc; no entitlements", outputMaximumBytes = 32768, threadObservationMaximum = 32, frameWalkUsed = false, imageArrayReadUsed = false,
readOnlyTaskPortOnly = true, taskReadWeakImportVerified = true, taskReadReturnContract = "BSD int/errno", darwinRolePrioritySelector = 6, targetAndExpectedParentMandatory = true, bsdIdentityRequiredBeforeFurtherReads = true, snapshotIsReadiness = false, qualifiedReadiness = false,
x86Executed, executionUnavailable, localOwnedChildCleanedUp = target.HasExited, selftests = cases, guestExecuted = false, dockerExecuted = false, recoveryPermissionsProven = false,
primarySources = new[] { "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/kern/kern_resource.c#L691-L721", "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/sys/resource.h", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/xnu-10063.141.1/bsd/vm/vm_unix.c", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/bsd/kern/syscalls.master" },
abiSourceIsExactGuestBinary = false, actualSdkExport = Path.Combine(sdk, "usr/lib/system/libsystem_kernel.tbd"), completedUtc = DateTimeOffset.UtcNow
};
File.WriteAllText(Path.Combine(output, "manifest.json"), JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
Console.WriteLine(JsonSerializer.Serialize(manifest));
async Task<Result> Run(string executable, string[] arguments, string label, bool requireSuccess = true)
{
using var process = new Process { StartInfo = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false } };
foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(argument);
process.Start();
async Task<string> Read(StreamReader reader)
{
var text = new StringBuilder(); var buffer = new char[2048];
while (await reader.ReadAsync(buffer) is var count && count != 0)
{
text.Append(buffer, 0, count);
if (Encoding.UTF8.GetByteCount(text.ToString()) > 32768) { if (!process.HasExited) process.Kill(); throw new Exception("Disposable probe/tool output exceeded32KiB."); }
}
return text.ToString();
}
var stdout = Read(process.StandardOutput); var stderr = Read(process.StandardError);
using var bound = new CancellationTokenSource(TimeSpan.FromSeconds(20));
try { await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(bound.Token)); }
catch { if (!process.HasExited) process.Kill(); await process.WaitForExitAsync(); throw; }
var result = new Result(process.ExitCode, await stdout, await stderr);
File.WriteAllText(Path.Combine(output, label + ".stdout.log"), result.Stdout);
File.WriteAllText(Path.Combine(output, label + ".stderr.log"), result.Stderr);
if (requireSuccess && result.ExitCode != 0) throw new Exception(label + " failed: " + result.Stderr);
return result;
}
static string Hash(byte[] value) => Convert.ToHexStringLower(SHA256.HashData(value));
sealed record Result(int ExitCode, string Stdout, string Stderr);
@@ -0,0 +1,26 @@
{
"purpose": "Disposable prebuilt diagnostic; no macOS compiler or runner is required by CI",
"sourceSha256": "38acf83b05694f9931c41ff1749e9b6b836f04c740b7f1a1c60e32332678cb1f",
"driverSha256": "d9d87415c12398f29b35697109f18d9b16f121dae969a4a05d0ec6a8cb874d25",
"binarySha256": "b8d54e2945eeefb3e0c22468feb7aef7efa50813909058b1e4b40144dd7e3d3e",
"compiler": "Apple clang 21.0.0 (clang-2100.3.34.2)",
"sdkVersion": "27.0",
"minimumMacOS": "14.0",
"architecture": "x86_64",
"importedLibraries": ["/usr/lib/libSystem.B.dylib"],
"signature": "ad-hoc",
"entitlements": false,
"offlineVerified": true,
"targetAndExpectedParentMandatory": true,
"readOnlyTaskPortOnly": true,
"taskReadWeakImportVerified": true,
"taskReadReturnContract": "BSD int/errno",
"darwinRolePrioritySelector": 6,
"outputMaximumBytes": 32768,
"threadObservationMaximum": 32,
"frameWalkUsed": false,
"imageArrayReadUsed": false,
"localPlatformReadPortDenied": true,
"recoveryPermissionsProven": false,
"qualifiedReadiness": false
}
Binary file not shown.