Compare commits

..
Author SHA1 Message Date
dh 147c544496 ci: observe transcript append failure and Windows reader sharing
PR and Push Build/Test / build-and-test (push) Successful in 18m50s
PR and Push Build/Test / portable-build-and-test (push) Successful in 8m15s
2026-10-03 22:44:31 +02:00
dh 94a70b2005 ci: patch known Recovery variants and preserve UDIF checksums
PR and Push Build/Test / build-and-test (push) Failing after 18m28s
PR and Push Build/Test / portable-build-and-test (push) Successful in 6m35s
2026-10-03 21:53:38 +02:00
dh 45d7bde71f ci: probe macOS Ventura on existing KVM hardware
PR and Push Build/Test / build-and-test (push) Canceled after 0s
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
2026-10-03 20:58:12 +02:00
dh 4606de0696 ci: preserve Apple Recovery daemon during read-only readiness probe
PR and Push Build/Test / build-and-test (push) Canceled after 0s
PR and Push Build/Test / portable-build-and-test (push) Canceled after 0s
2026-10-03 20:17:56 +02:00
13 changed files with 896 additions and 382 deletions
@@ -1,36 +0,0 @@
name: Existing Docker-host KVM diagnostic
on:
workflow_dispatch:
jobs:
existing-kvm-diagnostic:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "1"
DOTNET_NOLOGO: "1"
steps:
- name: Checkout diagnostic source
uses: actions/checkout@v7
- name: Setup .NET for the diagnostic helper
uses: actions/setup-dotnet@v6
with:
dotnet-version: "10.0.x"
- name: Test only the existing Docker-host KVM device
run: dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --run --output artifacts/existing-kvm
- name: Always retry cleanup of this diagnostic's owned container
if: always()
run: dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --cleanup --output artifacts/existing-kvm
- name: Preserve KVM diagnostic evidence
if: always()
uses: actions/upload-artifact@v3
with:
name: existing-docker-host-kvm-diagnostic
path: artifacts/existing-kvm/
if-no-files-found: error
retention-days: 7
@@ -1,4 +1,4 @@
name: Native macOS Recovery diagnostic on Ubuntu
name: macOS 13 KVM Cryptex Recovery compatibility diagnostic
on:
workflow_dispatch:
@@ -19,7 +19,7 @@ jobs:
with:
dotnet-version: "10.0.x"
- name: Probe native macOS Recovery with existing Docker resources
- name: Probe macOS 13 Recovery with existing KVM and host CPU
run: dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
- name: Always clean up only this diagnostic's owned resources
@@ -112,6 +112,15 @@ jobs:
--nologo
test -s MeetingAssistant/bin/Release/net10.0-windows10.0.19041.0/win-x64/MeetingAssistant.dll
- name: Measure transcript reader sharing on the Windows Wine host
run: |
mkdir -p artifacts/tests
transcript_probe_exit=0
"${WINE_BIN}" "${WIN_DOTNET_DIR}/dotnet.exe" run \
--file tools/ci/TranscriptFileShareProbe.cs > artifacts/tests/transcript-file-sharing.json || transcript_probe_exit=$?
cat artifacts/tests/transcript-file-sharing.json
exit "${transcript_probe_exit}"
- name: Run tests via Wine (Windows dotnet host)
run: |
rm -f artifacts/tests/wine.trx
@@ -191,18 +191,21 @@ public sealed class RecordingCoordinatorTests
NullLogger<MarkdownMeetingNoteStore>.Instance);
var artifactStore = new MarkdownMeetingArtifactStore(
NullLogger<MarkdownMeetingArtifactStore>.Instance);
var writeProbe = new TranscriptWriteDiagnostic();
var transcriptStore = new DiagnosticTranscriptStore(
new VaultTranscriptStore(Options.Create(options), NullLogger<VaultTranscriptStore>.Instance),
writeProbe);
var coordinator = new MeetingRecordingCoordinator(
audioSource,
new TestSpeechRecognitionPipelineFactory(
new FixedSegmentStreamingTranscriptionProvider(
new TranscriptionSegment(
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(5),
"Guest-1",
"Azure returned ***** here."))),
new VaultTranscriptStore(
Options.Create(options),
NullLogger<VaultTranscriptStore>.Instance),
new DiagnosticTranscriptionProvider(
new FixedSegmentStreamingTranscriptionProvider(
new TranscriptionSegment(
TimeSpan.FromSeconds(4),
TimeSpan.FromSeconds(5),
"Guest-1",
"Azure returned ***** here.")), writeProbe)),
transcriptStore,
noteStore,
new CapturingMeetingNoteOpener(),
artifactStore,
@@ -218,9 +221,45 @@ public sealed class RecordingCoordinatorTests
NullLogger<MeetingWorkflowEngine>.Instance));
var started = await coordinator.StartAsync(CancellationToken.None);
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "Azure returned"));
await coordinator.StopAsync(CancellationToken.None);
Exception? waitFailure = null;
Exception? stopFailure = null;
try
{
await audioSource.WriteAsync(new AudioChunk([1, 0], 16000, 1), CancellationToken.None);
writeProbe.Record("test-audio-enqueued");
await WaitUntilAsync(() => FileContainsText(started.TranscriptPath!, "Azure returned"));
}
catch (Exception exception)
{
waitFailure = exception;
writeProbe.Record("test-wait-failed", exception);
}
finally
{
try
{
await coordinator.StopAsync(CancellationToken.None);
writeProbe.Record("test-stop-completed");
}
catch (Exception exception)
{
stopFailure = exception;
writeProbe.Record("test-stop-failed", exception);
}
}
if (waitFailure is TimeoutException)
{
throw new TimeoutException($"{waitFailure.Message} {writeProbe.Describe()}", waitFailure);
}
if (waitFailure is not null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(waitFailure).Throw();
}
if (stopFailure is not null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(stopFailure).Throw();
}
var content = await File.ReadAllTextAsync(started.TranscriptPath!);
Assert.Contains("[00:00:04] Guest-1: Azure returned [redacted] here.", content);
@@ -5212,6 +5251,94 @@ public sealed class RecordingCoordinatorTests
}
}
// Temporary diagnosis of Run4174. Observes public provider/store boundaries only.
private sealed class TranscriptWriteDiagnostic
{
private readonly System.Diagnostics.Stopwatch elapsed = System.Diagnostics.Stopwatch.StartNew();
private readonly ConcurrentQueue<string> events = new();
public void Record(string name, Exception? error = null)
{
events.Enqueue($"{elapsed.ElapsedMilliseconds}ms:{name}" + (error is null ? "" :
$":{error.GetType().FullName}:HResult=0x{error.HResult:X8}:{error.Message}"));
}
public string Describe() => "[DEBUG-transcript-write-4174] " + string.Join(" | ", events);
}
private sealed class DiagnosticTranscriptionProvider(
IStreamingTranscriptionProvider inner,
TranscriptWriteDiagnostic probe) : IStreamingTranscriptionProvider
{
public async IAsyncEnumerable<TranscriptionSegment> TranscribeAsync(
IAsyncEnumerable<AudioChunk> audio,
SpeechRecognitionPipelineOptions options,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var segment in inner.TranscribeAsync(ObserveAudioAsync(audio, cancellationToken), options, cancellationToken))
{
probe.Record("fake-segment-yielded");
yield return segment;
}
}
private async IAsyncEnumerable<AudioChunk> ObserveAudioAsync(
IAsyncEnumerable<AudioChunk> audio,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
await foreach (var chunk in audio.WithCancellation(cancellationToken))
{
probe.Record("fake-audio-consumed");
yield return chunk;
}
}
}
private sealed class DiagnosticTranscriptStore(
ITranscriptStore inner,
TranscriptWriteDiagnostic probe) : ITranscriptStore
{
public Task<TranscriptSession> CreateSessionAsync(CancellationToken cancellationToken) =>
inner.CreateSessionAsync(cancellationToken);
public Task<TranscriptSession> CreateSessionAsync(MeetingAssistantOptions options, DateTimeOffset startedAt, CancellationToken cancellationToken) =>
inner.CreateSessionAsync(options, startedAt, cancellationToken);
public Task ReplaceLinesAsync(TranscriptSession session, IReadOnlyList<string> replacementLines, CancellationToken cancellationToken) =>
inner.ReplaceLinesAsync(session, replacementLines, cancellationToken);
public Task UpdateMetadataAsync(TranscriptSession session, MeetingSessionArtifacts artifacts, MeetingNote meetingNote, CancellationToken cancellationToken) =>
inner.UpdateMetadataAsync(session, artifacts, meetingNote, cancellationToken);
public async Task<TranscriptLineReference> AppendLineAsync(TranscriptSession session, string line, CancellationToken cancellationToken)
{
probe.Record("append-entered");
try
{
var reference = await inner.AppendLineAsync(session, line, cancellationToken);
probe.Record("append-completed");
return reference;
}
catch (Exception exception)
{
probe.Record("append-failed", exception);
throw;
}
}
public async Task ReplaceLineAsync(TranscriptSession session, TranscriptLineReference lineReference, string replacementLine, CancellationToken cancellationToken)
{
probe.Record("rewrite-entered");
try
{
await inner.ReplaceLineAsync(session, lineReference, replacementLine, cancellationToken);
probe.Record("rewrite-completed");
}
catch (Exception exception)
{
probe.Record("rewrite-failed", exception);
throw;
}
}
}
private sealed class TestSpeechRecognitionPipelineFactory : ISpeechRecognitionPipelineFactory
{
private readonly IStreamingTranscriptionProvider provider;
-28
View File
@@ -1,28 +0,0 @@
# Existing Docker-host KVM diagnostic
This manual-only diagnostic checks whether the existing Ubuntu runner's Docker daemon can expose its already-existing `/dev/kvm` and successfully initialize QEMU's KVM accelerator. It does not install or load host modules, change the host, or request infrastructure. A prior container configured with `KVM=N` and no device mappings cannot answer this question.
The entry point is the .NET 10 file-based app `tools/ci/ExistingKvmDiagnostic.cs`. From the repository root:
```sh
dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --help
dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --run --output artifacts/existing-kvm
dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --cleanup --output artifacts/existing-kvm
dotnet run --file tools/ci/ExistingKvmDiagnostic.cs -- --validate-evidence /path/to/downloaded-kvm-artifact
```
`--run` requires an empty output directory, the existing Docker CLI/daemon and Git. It records the source commit/helper SHA-256, Docker context/server identity, exact image metadata, commands, raw stdout/stderr, exit/state evidence, result and cleanup receipts. The workflow `.gitea/workflows/macos-kvm-diagnostic.yaml` is dispatched manually and always uploads these files. `--help` invokes no Docker command. `--validate-evidence` reads `qemu-monitor.stdout.log`, `qemu-monitor.result.json` and `container-exited.stdout.log` through the same success parser as `--run`; it invokes no Docker command and writes no files. This mode checks only saved HMP protocol and clean exit interpretation, without requalifying source, ownership or the container boundary.
The image is pinned to `qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df`; if absent it may be pulled into the existing daemon's cache. One random-name/label container invokes `/usr/bin/qemu-system-x86_64` directly with KVM only, `-cpu host`, `-S`, no default devices, no display and HMP on stdin. It attaches no OS, disk or persistent volume and never continues the paused CPU. A read-only 4-KiB tmpfs at `/storage` (`ro,nosuid,nodev,noexec,size=4096,mode=0555`) replaces the image's inherited `VOLUME /storage`. Inspect must prove `Mounts=[]` and precisely that sole tmpfs entry; otherwise a specific mount/tmpfs error is retained before QEMU starts. The only host device mapping is `/dev/kvm:/dev/kvm:rw`. The filesystem is read-only, network is `none`, all Linux capabilities are dropped, and no-new-privileges is set. Limits are 0.5 CPU, 256 MiB container RAM/no additional swap, 32 PIDs, and 64 MiB paused guest RAM. There are no binds, ports, privileged mode, added capabilities or host networking.
The helper has a 95-second operation budget and a separate 20-second cleanup budget, plus at most two seconds to drain killed command output. The workflow permits five minutes including SDK setup, compilation and upload. Cleanup checks the saved random name/label and exact full container ID before stopping or removing that container. An interrupted create can recover its ID only from the saved random name with the exact ownership label. `docker rm --volumes` also removes any anonymous volume attached to that exact owned container if creation did not match the expected tmpfs boundary. Cleanup does not remove images, prune resources, or touch another container.
Actual run 4165 failed the original zero-mount guard because this pinned image declared `/storage` as a volume and Docker created an anonymous writable volume. QEMU never started: the saved container state was `created`, PID zero and `StartedAt` zero. Its exact container ID was `1753f95ef334244e7a1b393a839f218ea885363de7d5335eec53132d64627010`, owner token `43b7f4676c514f2a95c63c02577ac36e`, and anonymous volume `ef7daa62ef89a2ffb8aae50a9b7803f1d9b3075ee509aa3183f3e170f69ce595`. The original cleanup proved container removal; it did not prove volume removal.
Run 4167's temporary, frozen-target cleanup verified the original daemon ID `528941c8-73ac-49ff-8eb7-69113eb4a2a1`, absence of the old container ID/name, the exact local volume and absence of container references. Removal without force exited zero; a subsequent inspect returned `no such volume`, and the receipt recorded `outcome=removed` at `2026-10-03T18:03:06.5767095Z`. The one-time cleanup mode and workflow step have therefore been removed. No generalized orphan cleanup is provided. The original source evidence is run 4165 artifact ZIP SHA-256 `6745d90e8b81c867740405c99b4364cc165c47ebb165455052314459d5cd547b` and created-container inspect SHA-256 `7afdfc6c30c933bee2ef1d6c18ed011c8b2f709d1a5e88531928f9f40471c055`.
`kvm_usable` requires QEMU to report the complete line `kvm support: enabled` and exactly `VM status: paused` or `VM status: paused (prelaunch)`, followed by clean monitor/container exit after `quit`. The monitor command must exit zero without timeout or error, and the container must be stopped with exit zero and no OOM. A device path alone is insufficient. Other receipts distinguish a Docker-reported missing daemon-host device, observed access denial, an unavailable QEMU KVM backend, an initialization error, and inconclusive evidence. These categories describe the observed output; they do not diagnose BIOS, nested virtualization, policy or hardware causes. All failures remain failed workflow runs with retained raw evidence. Even a usable result proves only this blank paused KVM initialization, not macOS boot, installation, native build or tests.
Actual run 4167 successfully initialized KVM and reported `VM status: paused (prelaunch)` before clean exit. Its original workflow still failed because the parser accepted only `paused`. The read-only CLI evidence mode reproduced that behavioral failure against the actual artifact (exit one, `usable=false`); after the narrow state-parser correction, the identical artifact passed (exit zero, `usable=true`). This reinterprets retained evidence and does not claim that the original workflow result changed or that another VM was run.
The CLI and monitor behavior follow the primary [QEMU command-line reference](https://www.qemu.org/docs/master/system/qemu-manpage.html) and [QEMU monitor reference](https://www.qemu.org/docs/master/system/monitor.html). Device/container options follow the [Docker create reference](https://docs.docker.com/reference/cli/docker/container/create/) and [Docker tmpfs reference](https://docs.docker.com/engine/storage/tmpfs/). Moby 28.3.3's [volume creation](https://raw.githubusercontent.com/moby/moby/v28.3.3/daemon/create_unix.go) and [mount detection](https://raw.githubusercontent.com/moby/moby/v28.3.3/container/container_unix.go) explicitly skip an inherited anonymous volume when that destination already has the tmpfs entry.
+45 -22
View File
@@ -1,45 +1,68 @@
# Native macOS Recovery diagnostic on the existing Ubuntu runner
# macOS 13 KVM/Cryptex compatibility diagnostic
This manual diagnostic tests the unresolved Recovery startup boundary before adding a native macOS application test job. It does not install macOS, erase a guest disk, install .NET or Apple CLT, or run Meeting Assistant tests. A green diagnostic means only that a real macOS 14+ x86_64 Recovery guest has a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk.
This separate manual candidate probes Recovery readiness on the existing Ubuntu Docker daemon with KVM, the real Intel host CPU and macOS 13. It does not install macOS, erase a disk, install .NET or Apple CLT, or run Meeting Assistant. Passing proves only a fresh macOS 13+ x86_64 Recovery guest with root identity, a working launchd system domain, DiskArbitration and exactly one writable 64-GiB guest disk.
The workflow `.gitea/workflows/macos-native-diagnostic.yaml` has only `workflow_dispatch`; it does not run on ordinary pushes or pull requests. It uses the same `ubuntu-latest` label and existing Docker daemon as the current builds. There are no runner changes, extra host devices, privileged containers, added capabilities, published ports, host networking or new secrets. It fails clearly if the existing Docker daemon cannot fit its bounded resource budget.
Baseline: bootstrap commit `4606de069678e8f95dfe3c7dad1bf5ce5384d30c`; separate branch `codex/macos-ci-kvm-compatibility`. KVM, CPU passthrough, Recovery major version and guest Cryptex staging change together. This is a compatibility experiment, not a causal single-variable A/B test. The TCG/bootstrap experiment remains separate.
## Helper entry point and invocation
## Reasons and remaining gaps
The orchestration is a .NET 10 file-based C# app at `tools/ci/MacOsNativeDiagnostic.cs`:
The existing daemon's Intel Celeron 1037U lacks AVX/AVX2; a separate diagnostic proved KVM enabled/paused state and clean exit. `CPU_MODEL=host` preserves actual instruction availability rather than advertising AVX2 through emulated Skylake. This candidate refuses a TCG or CPU-model fallback.
```sh
All four Swift helpers target `x86_64-apple-macos13.0`; the macOS 14 EventKit call has an existing macOS 13 fallback. Inspected native Mach-O files in pinned .NET SDK 10.0.401 x64 declare `minos 12.0`. These source/binary minima are not runtime qualification or vendor support: macOS 13 is outside [Microsoft's current .NET 10 supported-OS policy](https://github.com/dotnet/core/blob/main/release-notes/10.0/supported-os.md). This probe does not install that SDK, compile helpers or test calendar/audio permissions.
[Official CryptexFixup 1.0.5](https://github.com/acidanthera/CryptexFixup/blob/1.0.5/CryptexFixup/kern_start.cpp) activates without AVX2 and registers for normal, installer/Recovery and safe-mode boots. It redirects installer/updater ramrod to Apple Silicon's Rosetta Cryptex and bypasses APFS root-hash authentication on Ventura and newer. It does not emulate missing instructions. This kernel patch affects only the owned guest, never a host module.
**Recovery cache gap:** CryptexFixup does not replace an already running Recovery BaseSystem shared cache. Its installer/update selector targets the installed Cryptex, but this readiness-only run invokes no installer. Staging or loading it therefore proves no Recovery userland compatibility. Actual CPU/kernel behavior, guest injection, all native gates and any later installed-Cryptex/build/test behavior remain unqualified until observed.
Apple Recovery uses the pinned public InternetRecovery protocol with board ID and session/asset tokens, without Apple ID or workstation credentials. The macOS 13 selection, downloaded hash and actual guest version are retained; the hook downloads no full installer or SDK.
## Entry point and dependencies
Orchestration/validation remain the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Bash/Python stay only in the existing pinned Linux/macOS boot integration.
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --help
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/pinned/dockur-clone --output artifacts/native-validation
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --validate --source /path/to/clean/pinned/dockur-clone --cryptex-archive /path/to/CryptexFixup-1.0.5-RELEASE.zip --output /path/to/fresh/validation
~~~
`--validate` checks result/container contracts without Docker. With `--source` it verifies the actual Cryptex ZIP/bundle, source seams, generated OpenCore configuration and staging/checksum contracts, checks Bash syntax, then exercises four raw/zlib Recovery fixtures and twelve rejection cases with independent C# CRC32 readback. It also checks preservation of a successful resource snapshot after a later failed capture, leaving the supplied source untouched. It does not download/extract the LongQT ISO, verify a complete Apple Recovery image or execute the active-Lilu runtime checks. The ISO checksum is enforced during the later Docker build; active Lilu and EFI-copy checks execute only during container boot. The optional local Cryptex ZIP must match the release size/hash; omitting it downloads only the public 69,703-byte release. Use a fresh output directory. Dependencies are .NET 10, Git, Bash and Python 3 with its standard library; manual execution also requires the existing Linux/x64 Docker daemon and its existing KVM device.
The manual-only workflow keeps these owned run/cleanup entry points; validation invokes neither:
~~~sh
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run --output artifacts/native-macos
dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --cleanup --output artifacts/native-macos
```
~~~
Dependencies are the existing Linux/x64 runner, .NET 10 SDK, Git, Bash and Docker CLI/socket. The actual execution downloads public Dockur source, upstream build assets, Docker images and Apple Recovery; it does not use workstation credentials. The existing upstream Python UDIF patcher and the Bash hook are retained because they run inside the pinned Linux/macOS boot integration. Independent orchestration and validation remain C#.
## Exact bootasset contract
The helper clones Dockur commit `16a5b470cdd601bae8b05b02d748d7edfb36c12e`, verifies its exact Recovery patcher hash, and makes three narrowly verified source edits. The early `rc.cdrom.sh` hook only mounts the existing state share and returns. A same-length XML replacement makes the existing `com.apple.recoveryosd` LaunchDaemon execute `/bin/bash /Volumes/installstate/launch.sh` after boot tasks. The staged `launch.sh` is replaced entirely by the checked-in read-only readiness probe. All replacement counts are exact; an upstream mismatch fails. The two imported QEMU image digests are pinned and the final image/source/Recovery hashes are retained. Other upstream Dockerfile downloads are observed through the resulting image identity rather than asserted to be immutable.
Dockur stays pinned to `16a5b470cdd601bae8b05b02d748d7edfb36c12e`. Original Recovery patcher/staging, Dockerfile, OpenCore script and active config hashes are verified before edits. Both existing QEMU image digests remain pinned; other existing upstream downloads are observed through image identity. `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.
The VM uses TCG (`KVM=N`), slirp networking, a 4-GiB guest, two virtual CPUs and a sparse 64-GiB data disk. Its container has a 6-GiB memory/swap ceiling and a two-CPU limit. The existing Docker daemon must report at least two CPUs and 6 GiB total memory, the runner must have at least 5 GiB available memory, and the Docker filesystem must have at least 8 GiB free before Recovery downloads or boot. Native commands have 45-second watchdogs, except the single UID gate's targeted 180-second timing experiment. The ten-minute disk-readiness phase, 40-minute host deadline and 45-minute workflow limit remain unchanged.
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.
Actual remote run 4155 stopped at the first `sw_vers` with exit 143. Run 4159 then proved native Darwin/x86_64, root identity and guest AVX2, but reached the host deadline before `sw_vers` or the service/disk gates. Its logged command durations included timer cleanup and output copying, so they did not isolate native execution time.
The HFS+ catalog identifies `/System/Library/LaunchDaemons/com.apple.recoveryosd.plist` as file ID 57231, logical size 465 bytes and one 4,096-byte allocated block. Its exact XML SHA256 is `af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6`. This variant has `ProcessType=Interactive`; the previous macOS 14 variant has `App`. The patch accepts only these two exact layouts with exactly one daemon label and original `ProgramArguments=[/usr/libexec/recoveryosd]`. It preserves each variant's fields and process type, removes only the XML doctype to fit the wrapper arguments, and pads to the original file size. Unknown, duplicate, malformed or wrong-argument layouts fail before image writes. The early rc.cdrom hook remains mount-only; the unchanged wrapper runs the Apple daemon.
The next probe runs mandatory architecture, root identity and platform gates before optional process/CPU diagnostics. It keeps the proof log open, uses Bash 3.2's timed FIFO reads instead of starting a separate sleep process for every watchdog, and groups output copying and byte-limit checks. Separate markers record fork/exec/wait, timer cleanup and output flush durations. Raw output still fails above 512 KiB per command, proof above 4 MiB fails, and scalar gates reject hidden suffixes or multiline values. A local harmless-command harness verifies all 16 timeout, cancellation, output and scalar cases; this does not qualify macOS Recovery.
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.
After an initial platform failure the hook collects native launchd context and repeats the identical `sw_vers` command once, with the same 45-second limit. Native product version and all original identity/service/disk gates remain required. Optional process and CPU diagnostics run only after a gate fails. The upstream AVX2 warning reads host flags; run 4159 observed AVX2 in the actual guest. No host or guest CPU settings change.
The [original LongQT v0.7 template](https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso), 15,884,288 bytes, is now Docker-ADD-checksummed to SHA256 `287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d`. Runtime copies actual `EFI_RELEASE/EFI/OC/Kexts`, including Lilu 1.7.1, even with official OpenCore DEBUG executables. Active Lilu: executable 526,984 bytes, SHA256 `0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52`; Info.plist SHA256 `fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a`. Staging checks both hashes and bundle version. Cryptex declares Lilu 1.4.7; [Lilu history](https://github.com/acidanthera/Lilu/blob/master/Changelog.md) includes Ventura/Sonoma installer/Recovery support before 1.7.1. Existing Lilu is kept.
Actual run 4161 separated native wait from timer cleanup: architecture passed after 39 seconds, but the UID gate was terminated by its 45-second watchdog (51-second fork/exec/wait duration). Native ps commands passed after 34-42 seconds; output flushes took 289 and 76 seconds. The next diagnostic changes only the UID gate's watchdog to 180 seconds while retaining exit-zero/exact-root checks. This tests whether the measured short limit caused that failure; it does not establish a guest startup or service cause, and it does not qualify native CI. The earlier local 16-case harness qualified the previous 45-second timer/cancellation/output behavior, not this new timing experiment or the actual emulated guest.
[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.
## Evidence and cleanup
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`.
Evidence is written under the requested output directory: run identity and candidate commit, Docker/runner resources, exact source patch artifacts and hashes, image/container inspection, Recovery hash, native platform/process/launchctl/diskutil logs, machine-readable guest result, outcome and cleanup receipt. The workflow retains these as a seven-day artifact. Phase names and up to 512 KiB of the final native proof also appear in CI stdout, on success or failure, with the run token replaced; no environment or credential dump is printed. A Docker start/build exit zero is not a successful native result. A missing, stale, unsupported-platform, read-only or wrong-size guest receipt fails.
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.
While Recovery readiness is pending, a minute heartbeat reports elapsed guest time and the container's running state. Before final cleanup, an optional ten-second capture rechecks the saved container ID/ownership label and uses the pinned image's existing Unix HMP socket, `nc.openbsd` and a five-second `timeout` to collect only [`info status` and `screendump`](https://www.qemu.org/docs/master/system/monitor.html), retaining the command transcript, exit codes and fresh bounded PPM screenshot. Capture failure is visible and never changes native readiness success.
## Gates, privileges and cleanup
Run 4159 generated a 6,220,817-byte screenshot file under `/dev/shm`, but `docker cp` could not retrieve it. Screenshots now use the regular container path `/tmp/native-diagnostic-screen-<runToken>.ppm`, avoiding Docker's documented [`/dev`/tmpfs copy limitation](https://docs.docker.com/reference/cli/docker/container/cp/#corner-cases).
The Apple wrapper is byte-identical to baseline: background `/Volumes/installstate/readiness.sh` then `exec /usr/libexec/recoveryosd` under the same launchd job/PID. Source evidence does not prove Apple's executable ran.
Every container/image has a random run token in its ownership label. `finally` cleanup and the workflow's `always()` step inspect that exact label before removing the matching container and its anonymous storage volume, then the matching image. They never remove an unrelated name or volume, prune Docker, modify host settings or restart Meeting Assistant. Temporary source files are deleted only when their local marker matches the same token. Evidence remains available after cleanup.
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.
The earlier background-only local bootstrap never obtained DiskManagement readiness. This separate LaunchDaemon probe is still an experiment until the actual remote run produces the required native evidence. Full macOS CI support remains unverified until an installed guest subsequently compiles/signs the native helpers and passes all application tests, including all five native tests without skips.
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.
Remote run 4152 passed Docker access and resource checks but failed before VM startup: the runner's BuildKit could not checksum a dangling `/etc/alternatives/awk.1.gz` link while copying the entire QEMU filesystem. The candidate now derives directly from the same pinned QEMU filesystem image and overwrites its QEMU executable as before. Inspection of that exact digest reports an empty image `Config`, so it adds no inherited environment, user, command or healthcheck. Actual run 4155 built that image and started QEMU/XNU successfully, then failed the first native `sw_vers` after its 45-second watchdog. It did not prove native readiness.
Only device mapping: exactly `/dev/kvm:/dev/kvm:rw`. Inspection rejects other devices/permissions, added capabilities, device requests/rules, binds, tmpfs overrides, published ports, host networking, privileged mode, wrong limits, unexpected persistent mounts or changed CPU/OS profile. No host modules, infrastructure, secrets, SSH or app lifecycle actions are involved. Guest slirp networking remains.
Evidence retains run/profile identity, source/assets, EFI staging, container/resources, macOS 13 Recovery hash, native proof/result/outcome and cleanup. `[recovery-original]` logs the exact download's size/SHA256 before modifying it, including when patch failure later deletes the source. `guest-container-resources.last-success.stdout.log` and its timestamp/hash receipt preserve the last successful resource snapshot independently of a later failed stopped-container `docker exec`. Optional final Unix HMP capture includes `info kvm`, `info status` and a bounded PPM exported from `/tmp`; capture success passes no native gate.
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.
-256
View File
@@ -1,256 +0,0 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.RegularExpressions;
return await ExistingKvmDiagnostic.Run(args);
static class ExistingKvmDiagnostic
{
const string Image = "qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df";
const string Label = "cloud.schweigert.meeting-assistant.existing-kvm-probe";
const string StorageTmpfsOptions = "ro,nosuid,nodev,noexec,size=4096,mode=0555";
static readonly string[] QemuArguments = ["-machine", "pc", "-accel", "kvm", "-cpu", "host", "-m", "64", "-smp", "1", "-S", "-nodefaults",
"-display", "none", "-monitor", "stdio", "-serial", "none", "-parallel", "none", "-nic", "none"];
static readonly JsonSerializerOptions Json = new() { WriteIndented = true };
public static async Task<int> Run(string[] args)
{
if (args.SequenceEqual(new[] { "--help" }))
{
Console.WriteLine("ExistingKvmDiagnostic.cs --run|--cleanup --output DIRECTORY\nExistingKvmDiagnostic.cs --validate-evidence DIRECTORY\nRequires .NET 10; --run/--cleanup also require the existing Docker CLI/daemon. --help never calls Docker.\n--run tests only a paused, diskless QEMU with existing /dev/kvm; retains evidence and cleans up its own container. --cleanup retries that saved cleanup.\n--validate-evidence reads saved monitor/result/container evidence through the run's success parser; no Docker commands or file writes.");
return 0;
}
if (args.Length == 2 && args[0] == "--validate-evidence") return ValidateEvidence(Path.GetFullPath(args[1]));
if (args.Length != 3 || args[0] is not ("--run" or "--cleanup") || args[1] != "--output")
throw new ArgumentException("Use --help, --validate-evidence DIRECTORY or --run|--cleanup --output DIRECTORY.");
var output = Path.GetFullPath(args[2]);
Directory.CreateDirectory(output);
if (args[0] == "--cleanup") return await Cleanup(output) ? 0 : 1;
if (Directory.EnumerateFileSystemEntries(output).Any()) throw new InvalidOperationException("Run output must be empty; existing receipts cannot be reused.");
var started = DateTimeOffset.UtcNow;
var token = Guid.NewGuid().ToString("N");
var owner = new Owner(token, "meeting-assistant-kvm-" + token);
Save(output, "owner.json", owner);
Save(output, "source.json", new { image = Image, helperSha256 = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes("tools/ci/ExistingKvmDiagnostic.cs"))).ToLowerInvariant() });
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(95));
var status = "inconclusive";
string? detail = null;
try
{
await Require(Command("git", ["rev-parse", "HEAD"], output, "source-commit", budget.Token));
await Require(Command("docker", ["context", "show"], output, "docker-context", budget.Token));
await Require(Command("docker", ["version", "--format", "{{json .}}"], output, "docker-version", budget.Token));
await Require(Command("docker", ["info", "--format", "{\"ID\":{{json .ID}},\"Name\":{{json .Name}},\"ServerVersion\":{{json .ServerVersion}},\"KernelVersion\":{{json .KernelVersion}},\"OperatingSystem\":{{json .OperatingSystem}},\"OSType\":{{json .OSType}},\"Architecture\":{{json .Architecture}}}"], output, "docker-daemon", budget.Token));
var image = await Command("docker", ["image", "inspect", Image], output, "image-before", budget.Token);
if (image.ExitCode != 0) await Require(Command("docker", ["pull", "--platform", "linux/amd64", Image], output, "image-pull", budget.Token));
await Require(Command("docker", ["image", "inspect", Image], output, "image-exact", budget.Token));
using var imageDocument = JsonDocument.Parse(Read(output, "image-exact", "stdout"));
var imageId = imageDocument.RootElement[0].GetProperty("Id").GetString();
var create = await Command("docker", ["create", "--platform", "linux/amd64", "--pull", "never", "--name", owner.Name,
"--label", Label + "=" + token, "--cidfile", Path.Combine(output, "container.id"), "--interactive", "--read-only",
"--network", "none", "--cap-drop", "ALL", "--security-opt", "no-new-privileges", "--cpus", "0.5",
"--memory", "256m", "--memory-swap", "256m", "--pids-limit", "32", "--no-healthcheck",
"--tmpfs", "/storage:" + StorageTmpfsOptions,
"--device", "/dev/kvm:/dev/kvm:rw", "--entrypoint", "/usr/bin/qemu-system-x86_64", Image, .. QemuArguments], output, "container-create", budget.Token);
if (create.ExitCode != 0)
{
status = ClassifyFailure(Read(output, "container-create", "stderr"));
detail = "Docker did not successfully create the device-mapped container; see container-create logs.";
}
else
{
var created = await InspectOwned(output, owner, "container-created", budget.Token);
ValidateBoundary(created, imageId);
var id = created.GetProperty("Id").GetString()!;
var monitor = await Command("docker", ["start", "--attach", "--interactive", id], output, "qemu-monitor", budget.Token, "info version\ninfo kvm\ninfo status\nquit\n");
var exited = await InspectOwned(output, owner, "container-exited", budget.Token);
var state = exited.GetProperty("State");
var text = Read(output, "qemu-monitor", "stdout");
status = KvmUsable(text, monitor, state) ? "kvm_usable" : ClassifyFailure(Read(output, "qemu-monitor", "stderr") + "\n" + state.GetProperty("Error").GetString());
detail = status == "kvm_usable" ? "QEMU initialized KVM, reported enabled and paused, and exited successfully after quit. No guest CPU or OS was run."
: "KVM initialization or its enabled/paused/clean-exit proof did not pass; inspect raw monitor output and container state.";
}
}
catch (Exception error) { detail = error.Message; }
var cleaned = await Cleanup(output);
Save(output, "result.json", new { started, finished = DateTimeOffset.UtcNow, status, usable = status == "kvm_usable", cleaned, detail, image = Image, token });
Console.WriteLine(JsonSerializer.Serialize(new { status, cleaned, detail }));
return status == "kvm_usable" && cleaned ? 0 : 1;
}
static bool KvmUsable(string text, CommandResult monitor, JsonElement state)
{
var cleanExit = monitor.ExitCode == 0 && !monitor.TimedOut && monitor.Error is null && !state.GetProperty("Running").GetBoolean()
&& state.GetProperty("ExitCode").GetInt32() == 0 && !state.GetProperty("OOMKilled").GetBoolean();
var enabled = Regex.IsMatch(text, @"(?m)^kvm support: enabled\r?$", RegexOptions.CultureInvariant);
var paused = Regex.IsMatch(text, @"(?m)^VM status: paused(?: \(prelaunch\))?\r?$", RegexOptions.CultureInvariant);
return cleanExit && enabled && paused;
}
static int ValidateEvidence(string evidence)
{
try
{
using var monitorDocument = JsonDocument.Parse(File.ReadAllText(Path.Combine(evidence, "qemu-monitor.result.json")));
var result = monitorDocument.RootElement;
var monitor = new CommandResult(result.GetProperty("ExitCode").GetInt32(), result.GetProperty("TimedOut").GetBoolean(), result.GetProperty("Error").GetString());
using var exited = JsonDocument.Parse(Read(evidence, "container-exited", "stdout"));
if (exited.RootElement.GetArrayLength() != 1) throw new InvalidOperationException("Expected exactly one saved exited container.");
var usable = KvmUsable(Read(evidence, "qemu-monitor", "stdout"), monitor, exited.RootElement[0].GetProperty("State"));
Console.WriteLine(JsonSerializer.Serialize(new { usable, scope = "Saved HMP protocol and clean exit interpretation only; source, ownership and container boundary are not requalified." }));
return usable ? 0 : 1;
}
catch (Exception error) { Console.Error.WriteLine("Evidence interpretation failed: " + error.Message); return 1; }
}
static string ClassifyFailure(string text)
{
if (text.Contains("/dev/kvm", StringComparison.Ordinal) && text.Contains("error gathering device information", StringComparison.OrdinalIgnoreCase)
&& text.Contains("no such file or directory", StringComparison.OrdinalIgnoreCase)) return "daemon_device_missing";
if (text.Contains("Permission denied", StringComparison.OrdinalIgnoreCase) || text.Contains("Operation not permitted", StringComparison.OrdinalIgnoreCase)) return "access_denied_observed";
if (text.Contains("invalid accelerator kvm", StringComparison.OrdinalIgnoreCase)) return "qemu_kvm_backend_unavailable";
if (text.Contains("failed to initialize kvm", StringComparison.OrdinalIgnoreCase)) return "kvm_initialization_failed";
return "inconclusive";
}
static void ValidateBoundary(JsonElement container, string? imageId)
{
var host = container.GetProperty("HostConfig");
var devices = host.GetProperty("Devices");
if (container.GetProperty("Mounts").GetArrayLength() != 0)
throw new InvalidOperationException("Container mount boundary failed: persistent volumes or binds were created; expected Mounts=[] with only the read-only /storage tmpfs.");
if (!host.TryGetProperty("Tmpfs", out var tmpfs) || tmpfs.ValueKind != JsonValueKind.Object || tmpfs.EnumerateObject().Count() != 1
|| !tmpfs.TryGetProperty("/storage", out var options) || options.GetString() != StorageTmpfsOptions)
throw new InvalidOperationException("Container tmpfs boundary failed: expected only /storage:" + StorageTmpfsOptions + ".");
if (imageId == null || container.GetProperty("Image").GetString() != imageId || container.GetProperty("Config").GetProperty("Image").GetString() != Image
|| container.GetProperty("Path").GetString() != "/usr/bin/qemu-system-x86_64" || !container.GetProperty("Args").EnumerateArray().Select(x => x.GetString()).SequenceEqual(QemuArguments)
|| host.GetProperty("Privileged").GetBoolean() || !host.GetProperty("ReadonlyRootfs").GetBoolean()
|| host.GetProperty("NetworkMode").GetString() != "none"
|| devices.GetArrayLength() != 1 || devices[0].GetProperty("PathOnHost").GetString() != "/dev/kvm"
|| devices[0].GetProperty("PathInContainer").GetString() != "/dev/kvm" || devices[0].GetProperty("CgroupPermissions").GetString() != "rw"
|| !host.GetProperty("CapDrop").EnumerateArray().Any(x => x.GetString() == "ALL")
|| !Empty(host.GetProperty("CapAdd")) || !Empty(host.GetProperty("Binds")) || !Empty(host.GetProperty("PortBindings"))
|| !Empty(host.GetProperty("DeviceCgroupRules"))
|| !host.GetProperty("SecurityOpt").EnumerateArray().Any(x => x.GetString() == "no-new-privileges")
|| host.GetProperty("Memory").GetInt64() != 268435456 || host.GetProperty("MemorySwap").GetInt64() != 268435456
|| host.GetProperty("NanoCpus").GetInt64() != 500000000 || host.GetProperty("PidsLimit").GetInt64() != 32)
throw new InvalidOperationException("Created container does not match the diagnostic's restricted resource boundary.");
}
static bool Empty(JsonElement value) => value.ValueKind == JsonValueKind.Null
|| value.ValueKind == JsonValueKind.Array && value.GetArrayLength() == 0
|| value.ValueKind == JsonValueKind.Object && !value.EnumerateObject().Any();
static async Task<JsonElement> InspectOwned(string output, Owner owner, string step, CancellationToken cancellation)
{
var idPath = Path.Combine(output, "container.id");
var savedId = File.Exists(idPath) ? File.ReadAllText(idPath).Trim() : null;
if (savedId != null && !Regex.IsMatch(savedId, "^[a-f0-9]{64}$")) throw new InvalidOperationException("Saved container ID is invalid.");
await Require(Command("docker", ["inspect", "--type", "container", savedId ?? owner.Name], output, step, cancellation));
using var document = JsonDocument.Parse(Read(output, step, "stdout"));
var values = document.RootElement;
if (values.GetArrayLength() != 1) throw new InvalidOperationException("Container inspection did not return exactly one object.");
var value = values[0];
var id = value.GetProperty("Id").GetString()!;
if (!Regex.IsMatch(id, "^[a-f0-9]{64}$") || (savedId != null && id != savedId) || value.GetProperty("Name").GetString() != "/" + owner.Name
|| !value.GetProperty("Config").GetProperty("Labels").TryGetProperty(Label, out var label) || label.GetString() != owner.Token)
throw new InvalidOperationException("Container ownership ID/name/label mismatch; refusing resource operations.");
// Recover an interrupted create receipt by the saved random name and exact label, then use only its full ID.
if (savedId == null) File.WriteAllText(idPath, id + "\n");
return value.Clone();
}
static async Task<bool> Cleanup(string output)
{
using var budget = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var prefix = "cleanup-" + Guid.NewGuid().ToString("N");
try
{
if (!File.Exists(Path.Combine(output, "owner.json"))) { Save(output, "cleanup.json", new { cleaned = true, reason = "No owned resource receipt exists." }); return true; }
var owner = JsonSerializer.Deserialize<Owner>(File.ReadAllText(Path.Combine(output, "owner.json")))!;
if (!Regex.IsMatch(owner.Token, "^[a-f0-9]{32}$") || owner.Name != "meeting-assistant-kvm-" + owner.Token)
throw new InvalidOperationException("Invalid saved ownership receipt.");
var container = await InspectOwned(output, owner, prefix + "-inspect", budget.Token);
var id = container.GetProperty("Id").GetString()!;
if (container.GetProperty("State").GetProperty("Running").GetBoolean())
{
await Command("docker", ["stop", "--time", "1", id], output, prefix + "-stop", budget.Token);
await InspectOwned(output, owner, prefix + "-reinspect", budget.Token);
}
await Require(Command("docker", ["rm", "--force", "--volumes", id], output, prefix + "-remove", budget.Token));
var receipt = new { cleaned = true, id, finished = DateTimeOffset.UtcNow };
Save(output, prefix + ".receipt.json", receipt);
Save(output, "cleanup.json", receipt);
return true;
}
catch (Exception error)
{
var path = Path.Combine(output, prefix + "-inspect.stderr.log");
var absent = File.Exists(path) && new FileInfo(path).Length <= 1024 * 1024
&& File.ReadAllText(path).Contains("No such container", StringComparison.OrdinalIgnoreCase);
var receipt = new { cleaned = absent, reason = error.Message, finished = DateTimeOffset.UtcNow };
Save(output, prefix + ".receipt.json", receipt);
Save(output, "cleanup.json", receipt);
return absent;
}
}
static async Task<CommandResult> Command(string program, string[] arguments, string output, string step, CancellationToken cancellation, string? input = null)
{
var started = DateTimeOffset.UtcNow;
Save(output, step + ".command.json", new { program, arguments, input, started });
var start = new ProcessStartInfo(program) { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, RedirectStandardInput = input != null };
foreach (var argument in arguments) start.ArgumentList.Add(argument);
using var process = new Process { StartInfo = start };
await using var stdout = File.Create(Path.Combine(output, step + ".stdout.log"));
await using var stderr = File.Create(Path.Combine(output, step + ".stderr.log"));
int? exit = null;
var timedOut = false;
string? error = null;
try
{
cancellation.ThrowIfCancellationRequested();
process.Start();
var copies = Task.WhenAll(process.StandardOutput.BaseStream.CopyToAsync(stdout), process.StandardError.BaseStream.CopyToAsync(stderr));
try
{
if (input != null)
{
try { await process.StandardInput.WriteAsync(input.AsMemory(), cancellation); await process.StandardInput.FlushAsync(cancellation); }
catch (IOException) { /* QEMU can reject KVM before accepting stdin; preserve its stderr and state. */ }
process.StandardInput.Close();
}
await process.WaitForExitAsync(cancellation);
}
catch (OperationCanceledException) { timedOut = true; if (!process.HasExited) process.Kill(entireProcessTree: true); }
await copies.WaitAsync(TimeSpan.FromSeconds(2));
if (process.HasExited) exit = process.ExitCode;
}
catch (Exception exception)
{
error = exception.Message;
try { if (!process.HasExited) process.Kill(entireProcessTree: true); } catch (InvalidOperationException) { /* Process never started or already exited. */ }
}
var result = new CommandResult(exit, timedOut, error);
Save(output, step + ".result.json", new { result.ExitCode, result.TimedOut, result.Error, started, finished = DateTimeOffset.UtcNow });
return result;
}
static async Task Require(Task<CommandResult> command)
{
var result = await command;
if (result.ExitCode != 0 || result.TimedOut || result.Error != null) throw new InvalidOperationException($"Command did not succeed: exit={result.ExitCode}, timedOut={result.TimedOut}, error={result.Error}");
}
static string Read(string output, string step, string stream)
{
var path = Path.Combine(output, step + "." + stream + ".log");
if (new FileInfo(path).Length > 1024 * 1024) throw new InvalidOperationException("Diagnostic output exceeds the one-MiB interpretation limit; inspect the retained raw log.");
return File.ReadAllText(path);
}
static void Save(string output, string name, object value) => File.WriteAllText(Path.Combine(output, name), JsonSerializer.Serialize(value, Json) + "\n");
sealed record Owner(string Token, string Name);
sealed record CommandResult(int? ExitCode, bool TimedOut, string? Error);
}
+401 -25
View File
@@ -1,5 +1,7 @@
#:property PublishAot=false
using System.Diagnostics;
using System.Buffers.Binary;
using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -12,6 +14,10 @@ return await NativeDiagnostic.Execute(args);
static class NativeDiagnostic
{
const string DockurCommit = "16a5b470cdd601bae8b05b02d748d7edfb36c12e";
const string CryptexUrl = "https://github.com/acidanthera/CryptexFixup/releases/download/1.0.5/CryptexFixup-1.0.5-RELEASE.zip";
const string CryptexHash = "25041d94a0fe9a0261caf0ba89b36dfcb21682bf3c697a34bcaddc839576ab30";
const string OpenCoreTemplateHash = "287328995d4198f1b05166f087d85bf7ef66bedafe150d17ad112ac8de60051d";
const string UdifChecksumBindingHash = "6109d04619e800c483fdac363d593cd1cd69f34131d2521417334e11d41c8bfa";
const string OwnerLabel = "org.meeting-assistant.native-diagnostic";
const long GuestDiskBytes = 64L * 1024 * 1024 * 1024;
const long ContainerMemoryBytes = 6L * 1024 * 1024 * 1024;
@@ -42,12 +48,16 @@ static class NativeDiagnostic
static readonly string DiagnosticDaemon = (OriginalDaemon + "\n")
.Replace("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n", "", StringComparison.Ordinal)
.Replace("\t\t<string>/usr/libexec/recoveryosd</string>", "\t\t<string>/bin/bash</string>\n\t\t<string>/Volumes/installstate/launch.sh</string>", StringComparison.Ordinal);
// Exact XML framing read from the Apple 13 comparison download, not an assertion
// about the unretained bytes downloaded by run 4173.
static readonly string OriginalDaemon13 = ReplaceOnce(OriginalDaemon + "\n", "<string>App</string>", "<string>Interactive</string>");
static readonly string DiagnosticDaemon13 = ReplaceOnce(DiagnosticDaemon, "<string>App</string>", "<string>Interactive</string>");
public static async Task<int> Execute(string[] args)
{
if (args.Length == 0 || args.Contains("--help"))
{
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone]");
Console.WriteLine("dotnet run --file tools/ci/MacOsNativeDiagnostic.cs -- --run|--cleanup|--validate [--output artifacts/native-macos] [--source existing-dockur-clone] [--cryptex-archive verified-release.zip]");
return 0;
}
var output = Path.GetFullPath(Option(args, "--output") ?? "artifacts/native-macos");
@@ -55,7 +65,12 @@ static class NativeDiagnostic
{
ValidateContracts();
if (Option(args, "--source") is { } source)
await PrepareSource(Path.GetFullPath(source), output, "validation", false, CancellationToken.None);
{
await PrepareSource(Path.GetFullPath(source), output, "validation", false, Option(args, "--cryptex-archive"), CancellationToken.None);
await ValidateResourceRetention(output);
await ValidateRecoveryPatch(output);
Save(Path.Combine(output, "validation.json"), new { success = true, profile = "kvm-host-ventura-cryptex", helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))), udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))), baselineReadinessNormalized = true, resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12, independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cryptexArchiveVerified = true, configurationAndStagingContractsVerified = true, templateIsoDownloaded = false, activeLiluRuntimeChecked = false, sourceModified = false, dockerExecuted = false, guestExecuted = false, completedUtc = DateTimeOffset.UtcNow });
}
Console.WriteLine("Source patch contracts and diagnostic result validation passed; no Docker or guest execution occurred.");
return 0;
}
@@ -83,7 +98,7 @@ static class NativeDiagnostic
throw new InvalidOperationException("This diagnostic runs on the existing Linux/x64 runner only.");
ValidateContracts();
var sourceCommit = (await Command("git", ["rev-parse", "HEAD"], output, "candidate-commit", deadline.Token)).Output.Trim();
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
Save(Path.Combine(output, "run-metadata.json"), new { token, startedUtc = DateTimeOffset.UtcNow, sourceCommit, dockurCommit = DockurCommit, profile = "kvm-host-ventura-cryptex", causalSingleVariableTest = false, kvm = true, cpuModel = "host", recoveryMajor = 13, cryptexVersion = "1.0.5", liluVersion = "1.7.1", runId = Environment.GetEnvironmentVariable("GITHUB_RUN_ID"), server = Environment.GetEnvironmentVariable("GITHUB_SERVER_URL"), architecture = RuntimeInformation.ProcessArchitecture.ToString(), deadlineMinutes = 40 });
var info = await Command("docker", ["info", "--format", "{{json .}}"], output, "docker-info", deadline.Token);
using (var document = JsonDocument.Parse(info.Output))
{
@@ -102,13 +117,13 @@ static class NativeDiagnostic
await Command("git", ["-C", source, "checkout", "--detach", DockurCommit], output, "dockur-checkout", deadline.Token);
var actualCommit = (await Command("git", ["-C", source, "rev-parse", "HEAD"], output, "dockur-commit", deadline.Token)).Output.Trim();
if (actualCommit != DockurCommit) throw new InvalidOperationException("Dockur source pin mismatch.");
await PrepareSource(source, output, token, true, deadline.Token);
await PrepareSource(source, output, token, true, Option(args, "--cryptex-archive"), deadline.Token);
await Command("docker", ["build", "--platform", "linux/amd64", "--label", OwnerLabel + "=" + token, "--tag", state.ImageTag, source], output, "docker-build", deadline.Token, echo: true);
var imageInspect = await Command("docker", ["image", "inspect", state.ImageTag], output, "image-inspect", deadline.Token);
using (var image = JsonDocument.Parse(imageInspect.Output))
state = state with { ImageId = image.RootElement[0].GetProperty("Id").GetString() };
Save(statePath, state);
var create = await Command("docker", ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--env", "KVM=N", "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=14", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2", state.ImageTag], output, "docker-create", deadline.Token);
var create = await Command("docker", ["create", "--name", state.ContainerName, "--label", OwnerLabel + "=" + token, "--memory", "6g", "--memory-swap", "6g", "--cpus", "2", "--shm-size", "512m", "--log-opt", "max-size=8m", "--log-opt", "max-file=1", "--device", "/dev/kvm:/dev/kvm:rw", "--env", "KVM=Y", "--env", "CPU_MODEL=host", "--env", "NETWORK=slirp", "--env", "DISPLAY=web", "--env", "MANUAL=N", "--env", "VERSION=13", "--env", "RAM_SIZE=4G", "--env", "CPU_CORES=2", "--env", "DISK_SIZE=64G", "--env", "DISK_TYPE=sata", "--env", "ARGUMENTS=-object iothread,id=io2", state.ImageTag], output, "docker-create", deadline.Token);
var id = create.Output.Trim();
if (!System.Text.RegularExpressions.Regex.IsMatch(id, "^[0-9a-f]{64}$")) throw new InvalidOperationException("Docker did not return a container identity.");
state = state with { ContainerId = id };
@@ -116,7 +131,7 @@ static class NativeDiagnostic
await Command("docker", ["inspect", id], output, "container-created", deadline.Token);
AssertContainer(File.ReadAllText(Path.Combine(output, "container-created.stdout.log")), token);
await Command("docker", ["start", id], output, "docker-start", deadline.Token);
Console.WriteLine("The owned unprivileged TCG guest is starting. Success requires native macOS 14+/x86_64 and a writable 64-GiB disk; no installer will run.");
Console.WriteLine("The owned restricted KVM/host-CPU macOS 13 compatibility guest is starting. Success requires native macOS 13+/x86_64 and a writable 64-GiB disk; no installer will run. This is not a single-variable causal test.");
var recoveryStarted = Stopwatch.StartNew();
var heartbeat = Stopwatch.StartNew();
while (true)
@@ -168,49 +183,223 @@ static class NativeDiagnostic
static void ValidateContracts()
{
XDocument.Parse(DiagnosticDaemon);
if (Encoding.UTF8.GetByteCount(DiagnosticDaemon) > Encoding.UTF8.GetByteCount(OriginalDaemon + "\n")) throw new InvalidOperationException("Daemon replacement exceeds original file.");
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "14.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
var baseline = ReplaceOnce(readiness, "(( ${os_version%%.*} >= 13 ))", "(( ${os_version%%.*} >= 14 ))");
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
throw new InvalidOperationException("Compatibility readiness may change only the baseline's macOS minimum to 13; identity, services, disk and limits must remain identical.");
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
throw new InvalidOperationException("Compatibility profile must preserve the baseline Apple recoveryosd wrapper.");
if (Hash(Encoding.UTF8.GetBytes(OriginalDaemon13)) != "af9d7f6c1948079bd4384d27b6882678d6fb4e338fcf6a8be8f84fceef174ad6") throw new InvalidOperationException("macOS 13 allowlist bytes differ from the independently read comparison plist.");
foreach (var variant in new[] { (OriginalDaemon + "\n", DiagnosticDaemon, "App"), (OriginalDaemon13, DiagnosticDaemon13, "Interactive") })
{
ValidateDaemon(variant.Item1, variant.Item3, false);
ValidateDaemon(variant.Item2, variant.Item3, true);
if (Encoding.UTF8.GetByteCount(variant.Item2) > Encoding.UTF8.GetByteCount(variant.Item1)) throw new InvalidOperationException("Daemon replacement exceeds original file.");
}
var good = JsonSerializer.Serialize(new { token = "validation", success = true, osVersion = "13.6.1", architecture = "x86_64", uid = 0, disk = "/dev/disk1", diskBytes = GuestDiskBytes, readOnly = false, systemExit = 0, diskArbitrationExit = 0, recoveryExit = 0, diskListExit = 0 });
ValidateResult(good, "validation");
foreach (var invalid in new[] { good.Replace("14.6.1", "13.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
foreach (var invalid in new[] { good.Replace("13.6.1", "12.6.1"), good.Replace("x86_64", "arm64"), good.Replace("\"readOnly\":false", "\"readOnly\":true"), good.Replace("\"success\":true", "\"success\":false"), good.Replace("68719476736", "17179869184"), good.Replace("validation", "stale") })
{
try { ValidateResult(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an invalid/stale result.");
}
var boundary = """
[{"Config":{"Labels":{"org.meeting-assistant.native-diagnostic":"validation"},"Env":["KVM=Y","CPU_MODEL=host","VERSION=13"]},"HostConfig":{"Privileged":false,"NetworkMode":"default","Memory":6442450944,"MemorySwap":6442450944,"NanoCpus":2000000000,"ShmSize":536870912,"CapAdd":null,"DeviceRequests":null,"Binds":null,"PortBindings":{},"DeviceCgroupRules":null,"Tmpfs":null,"Devices":[{"PathOnHost":"/dev/kvm","PathInContainer":"/dev/kvm","CgroupPermissions":"rw"}]},"Mounts":[{"Type":"volume","Destination":"/storage","RW":true}]}]
""";
AssertContainer(boundary, "validation");
foreach (var invalid in new[] { boundary.Replace("\"Privileged\":false", "\"Privileged\":true"), boundary.Replace("\"CgroupPermissions\":\"rw\"", "\"CgroupPermissions\":\"rwm\""), boundary.Replace("/dev/kvm", "/dev/other"), boundary.Replace("KVM=Y", "KVM=N"), boundary.Replace("CPU_MODEL=host", "CPU_MODEL=Skylake-Client-v4"), boundary.Replace("VERSION=13", "VERSION=14"), boundary.Replace("6442450944", "8589934592"), boundary.Replace("\"NetworkMode\":\"default\"", "\"NetworkMode\":\"host\""), boundary.Replace("\"CapAdd\":null", "\"CapAdd\":[\"NET_ADMIN\"]"), boundary.Replace("\"Type\":\"volume\"", "\"Type\":\"bind\""), boundary.Replace("/storage", "/host") })
{
try { AssertContainer(invalid, "validation"); } catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Diagnostic validator accepted an excessive/wrong-profile container boundary.");
}
}
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation)
static async Task PrepareSource(string source, string output, string token, bool writeSource, string? cryptexArchive, CancellationToken cancellation)
{
Directory.CreateDirectory(output);
var patchPath = Path.Combine(source, "src/install/recovery/patch.py");
var originalPatch = File.ReadAllText(patchPath);
if (Hash(Encoding.UTF8.GetBytes(originalPatch)) != "84f13db88c02edbf5ce21a39571fe58f12bebf5b0886c2d012f16ddbaed45323") throw new InvalidOperationException("Pinned Recovery patcher hash mismatch.");
var patch = ReplaceOnce(originalPatch, OriginalBootstrap, MountOnlyBootstrap);
var oldConstants = "RECOVERY_ORIGINAL = b\"/usr/libexec/recoveryosd\"\nRECOVERY_REPLACEMENT = b\"/private/etc/rc.cdrom.sh\"";
var daemon = OriginalDaemon + "\n";
var constants = "RECOVERY_ORIGINAL = b'''" + daemon + "'''\nRECOVERY_REPLACEMENT = b'''" + DiagnosticDaemon + "'''.ljust(len(RECOVERY_ORIGINAL), b\" \")";
patch = ReplaceOnce(patch, oldConstants, constants);
var patch = PrepareRecoveryPatch(originalPatch);
var checksumBinding = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"));
if (Hash(Encoding.UTF8.GetBytes(checksumBinding)) != UdifChecksumBindingHash) throw new InvalidOperationException("Recovery UDIF checksum binding hash mismatch.");
File.WriteAllText(Path.Combine(output, "udif_checksums.py"), checksumBinding, new UTF8Encoding(false));
var dockerPath = Path.Combine(source, "Dockerfile");
if (Hash(File.ReadAllBytes(dockerPath)) != "a0e804235967400eb70e755d63eff8a33a7761922ddd6e9723faa8e828fd8aa3") throw new InvalidOperationException("Pinned Dockerfile hash mismatch.");
// The existing runner's BuildKit cannot checksum dangling manpage links during COPY /.
// This pinned filesystem image has an empty Config; FROM preserves the same runtime defaults.
var dockerfile = ReplaceOnce(File.ReadAllText(dockerPath), "FROM scratch AS base\nCOPY --from=qemux/qemu:7.50 --exclude=usr/bin/qemu-system-x86_64 / /\n", "FROM qemux/qemu:7.50@sha256:e7f6fda52503a546fd649670ba46e4bc23dc6dcef275bc3fac48877fbbc430df AS base\n");
dockerfile = ReplaceAllExact(dockerfile, "--from=qemux/qemu-macos:latest ", "--from=qemux/qemu-macos:latest@sha256:af64297171228f27d5f616249e18f6ad5e2fbc79c1cc517252521e8bcd8eadaa ", 2);
dockerfile = ReplaceOnce(dockerfile, "ADD $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso", "ADD --checksum=sha256:" + OpenCoreTemplateHash + " $REPO_KVM_OPENCORE/releases/download/v$VERSION_KVM_OPENCORE/LongQT-OpenCore-v$VERSION_KVM_OPENCORE.iso /opencore.iso");
var compatibility = await PrepareCompatibility(source, output, cryptexArchive, cancellation);
var entryPath = Path.Combine(source, "src/entry.sh");
var entry = ReplaceOnce(File.ReadAllText(entryPath), "set -Eeuo pipefail\n", "set -Eeuo pipefail\n\n# Diagnostic budget: inspect existing Docker storage before Recovery download/boot.\ndf -Pk /storage\nfree_kib=$(df -Pk /storage | awk 'NR==2 {print $4}')\n[[ \"$free_kib\" =~ ^[0-9]+$ ]] && (( free_kib >= 8 * 1024 * 1024 )) || { echo 'Existing Docker storage has less than the 8-GiB diagnostic budget.' >&2; exit 1; }\n");
entry = ReplaceOnce(entry, ". init.sh # Initialize system\n", ". init.sh # Initialize system\n# Fail before Apple downloads if the existing daemon cannot retain this profile.\nenabled \"$KVM\" && [[ \"$CPU_MODEL\" == host && \"$VERSION\" == 13 ]] && grep -Eq '^vendor_id[[:space:]]*:[[:space:]]*GenuineIntel$' /proc/cpuinfo || { error 'Compatibility probe requires existing Intel KVM and the exact host/13 profile.'; exit 1; }\n");
entry = ReplaceOnce(entry, "trap - ERR\n", "[[ \"$KVM_OPTS\" == *'accel=kvm'* || \"$KVM_OPTS\" == *'-accel kvm'* ]] && [[ \"$KVM_OPTS\" != *tcg* && \"$CPU_MODEL\" == host ]] || { error 'Compatibility profile refuses a TCG/CPU fallback.'; exit 1; }\ninfo '[compatibility-profile] accelerator=kvm cpu=host recovery=13; actual guest gates still pending'\n\ntrap - ERR\n");
var hookPath = Path.Combine("tools", "ci", "macos-native-readiness.sh");
var hook = ReplaceOnce(File.ReadAllText(hookPath), "@@PROOF_TOKEN@@", token);
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", hook), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("early-bootstrap.sh", MountOnlyBootstrap) })
var wrapper = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"));
var imagePath = Path.Combine(source, "src", "image.sh");
var originalImage = File.ReadAllText(imagePath);
if (Hash(Encoding.UTF8.GetBytes(originalImage)) != "c08bf9436fb8b72ea82fdf0e677641ab2fc42a0a59e2cf0309c00df519884c5c") throw new InvalidOperationException("Pinned Recovery staging script hash mismatch.");
var image = ReplaceOnce(originalImage, " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\"; then\n", " if ! cp -f \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cp -f \"$IMAGE_TOOLS/recovery/readiness.sh\" \"${script%/*}/readiness.sh\"; then\n");
image = ReplaceOnce(image, " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n", " if ! cmp -s \"$IMAGE_TOOLS/recovery/launch.sh\" \"$script\" ||\n ! cmp -s \"$IMAGE_TOOLS/recovery/readiness.sh\" \"$state/readiness.sh\" ||\n");
image = ReplaceOnce(image, " if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n", " info \"[recovery-original] bytes=$(stat -c%s -- \"$image\") sha256=$(sha256sum \"$image\" | awk '{print $1}')\"\n if ! result=$(python3 \"$IMAGE_TOOLS/recovery/patch.py\" \"$image\"); then\n");
foreach (var pair in new[] { ("recovery-patch.py", patch), ("Dockerfile.patched", dockerfile), ("container-entry.sh", entry), ("guest-launch.sh", wrapper), ("guest-readiness.sh", hook), ("image.sh.patched", image), ("recoveryosd-original.plist", daemon), ("recoveryosd-diagnostic.plist", DiagnosticDaemon), ("recoveryosd-13-original.plist", OriginalDaemon13), ("recoveryosd-13-diagnostic.plist", DiagnosticDaemon13), ("early-bootstrap.sh", MountOnlyBootstrap), ("boot.sh.patched", compatibility.Boot), ("opencore-config.plist", compatibility.Config) })
File.WriteAllText(Path.Combine(output, pair.Item1), pair.Item2, new UTF8Encoding(false));
Save(Path.Combine(output, "source-hashes.json"), Directory.GetFiles(output).Where(path => Path.GetFileName(path) is "recovery-patch.py" or "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "early-bootstrap.sh").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
Save(Path.Combine(output, "source-hashes.json"), Directory.GetFiles(output).Where(path => Path.GetFileName(path) is "recovery-patch.py" or "udif_checksums.py" or "Dockerfile.patched" or "container-entry.sh" or "guest-launch.sh" or "guest-readiness.sh" or "image.sh.patched" or "recoveryosd-original.plist" or "recoveryosd-diagnostic.plist" or "recoveryosd-13-original.plist" or "recoveryosd-13-diagnostic.plist" or "early-bootstrap.sh" or "boot.sh.patched" or "opencore-config.plist" or "compatibility-boot-assets.json").ToDictionary(path => Path.GetFileName(path)!, path => Hash(File.ReadAllBytes(path))));
await Command("bash", ["-n", Path.Combine(output, "guest-launch.sh")], output, "guest-hook-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "guest-readiness.sh")], output, "guest-readiness-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "image.sh.patched")], output, "guest-staging-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "container-entry.sh")], output, "entry-syntax", cancellation);
await Command("bash", ["-n", Path.Combine(output, "boot.sh.patched")], output, "boot-staging-syntax", cancellation);
if (!writeSource) return;
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/udif_checksums.py"), checksumBinding, new UTF8Encoding(false));
File.WriteAllText(dockerPath, dockerfile, new UTF8Encoding(false));
File.WriteAllText(entryPath, entry, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/launch.sh"), hook, new UTF8Encoding(false));
File.WriteAllText(imagePath, image, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/launch.sh"), wrapper, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/install/recovery/readiness.sh"), hook, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "src/boot.sh"), compatibility.Boot, new UTF8Encoding(false));
File.WriteAllText(Path.Combine(source, "assets/config.plist"), compatibility.Config, new UTF8Encoding(false));
var target = Path.Combine(source, "assets", "native-compatibility");
if (Directory.Exists(target)) throw new InvalidOperationException("Refusing an existing compatibility asset overlay.");
foreach (var file in Directory.GetFiles(compatibility.Assets, "*", SearchOption.AllDirectories))
{
var destination = Path.Combine(target, Path.GetRelativePath(compatibility.Assets, file));
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
File.Copy(file, destination, false);
}
}
static void ValidateDaemon(string xml, string processType, bool patched)
{
var pairs = XDocument.Parse(xml).Root!.Element("dict")!.Elements().ToArray();
if (pairs.Length != 10 || !pairs.Where((_, index) => index % 2 == 0).Select(element => element.Value).SequenceEqual(new[] { "Label", "OnDemand", "ProcessType", "EnablePressuredExit", "ProgramArguments" })) throw new InvalidOperationException("Recovery daemon fields changed.");
if (pairs[1].Value != "com.apple.recoveryosd" || pairs[3].Name != "false" || pairs[5].Value != processType || pairs[7].Name != "false" || pairs[9].Name != "array" || !pairs[9].Elements().Select(element => element.Value).SequenceEqual(patched ? new[] { "/bin/bash", "/Volumes/installstate/launch.sh" } : new[] { "/usr/libexec/recoveryosd" })) throw new InvalidOperationException("Recovery daemon identity/arguments changed.");
}
static string PrepareRecoveryPatch(string original)
{
var patch = ReplaceOnce(original, OriginalBootstrap, MountOnlyBootstrap);
patch = ReplaceOnce(patch, "import zlib\n", "import zlib\nfrom udif_checksums import ChecksumPlan\n");
var constants = "RECOVERY_13_ORIGINAL = b'''" + OriginalDaemon13 + "'''\nRECOVERY_13_REPLACEMENT = b'''" + DiagnosticDaemon13 + "'''.ljust(len(RECOVERY_13_ORIGINAL), b\" \")\nRECOVERY_14_ORIGINAL = b'''" + OriginalDaemon + "\n'''\nRECOVERY_14_REPLACEMENT = b'''" + DiagnosticDaemon + "'''.ljust(len(RECOVERY_14_ORIGINAL), b\" \")\nRECOVERY_LABEL = b'<string>com.apple.recoveryosd</string>'";
patch = ReplaceOnce(patch, "RECOVERY_ORIGINAL = b\"/usr/libexec/recoveryosd\"\nRECOVERY_REPLACEMENT = b\"/private/etc/rc.cdrom.sh\"", constants);
patch = ReplaceOnce(patch, " if len(RECOVERY_REPLACEMENT) != len(RECOVERY_ORIGINAL):\n raise RuntimeError(\"recoveryosd launch-path replacement length mismatch\")", " if len(RECOVERY_13_REPLACEMENT) != len(RECOVERY_13_ORIGINAL) or len(RECOVERY_14_REPLACEMENT) != len(RECOVERY_14_ORIGINAL):\n raise RuntimeError(\"recoveryosd launch-path replacement length mismatch\")");
patch = ReplaceOnce(patch, " (\"recoveryosd launch path\", RECOVERY_ORIGINAL, RECOVERY_REPLACEMENT),", " (\"recoveryosd macOS 13 launch path\", RECOVERY_13_ORIGINAL, RECOVERY_13_REPLACEMENT),\n (\"recoveryosd macOS 14 launch path\", RECOVERY_14_ORIGINAL, RECOVERY_14_REPLACEMENT),");
patch = ReplaceOnce(patch, " chunks = {}\n", " chunks = {}\n recovery_label_count = 0\n");
patch = ReplaceOnce(patch, " plist = plistlib.loads(image.read(xml_length))\n", " plist = plistlib.loads(image.read(xml_length))\n checksums = ChecksumPlan(image, koly, plist, xml_offset, xml_length, size)\n");
patch = ReplaceOnce(patch, " key = (blkx_index, run_index)\n", " recovery_label_count += decoded.count(RECOVERY_LABEL)\n key = (blkx_index, run_index)\n");
var variantValidation = """
if len(matches[patches[0][0]]) != 1:
raise RuntimeError("Expected exactly one rc.cdrom.sh bootstrap")
variants = [item for item in patches[1:] if matches[item[0]]]
if recovery_label_count != 1 or len(variants) != 1 or len(matches[variants[0][0]]) != 1:
raise RuntimeError("Expected exactly one known recoveryosd plist and launch path")
patches = (patches[0], variants[0])
print("[recovery-daemon] " + variants[0][0])
""";
patch = ReplaceOnce(patch, " for name, _, _ in patches:\n count = len(matches[name])\n if count != 1:\n raise RuntimeError(f\"Expected exactly one {name}, found {count}\")", IndentPython(variantValidation, 8));
// Plan all recompressed chunks before the first image write. A later
// compression failure must not leave an earlier chunk patched.
patch = ReplaceOnce(patch, " for key, chunk in chunks.items():\n patched = bytearray", " planned = []\n for key, chunk in chunks.items():\n patched = bytearray");
patch = ReplaceOnce(patch, " image.seek(chunk[\"physical_offset\"])\n image.write(stored)", " planned.append((chunk[\"physical_offset\"], stored))\n\n checksum_xml, checksum_koly = checksums.prepare(planned)\n for physical_offset, stored in planned:\n image.seek(physical_offset)\n image.write(stored)\n image.seek(xml_offset)\n image.write(checksum_xml)\n image.seek(size - 512)\n image.write(checksum_koly)");
patch = ReplaceOnce(patch, " image.flush()\n", " image.flush()\n checksums.verify()\n");
return patch;
}
static string IndentPython(string text, int spaces)
{
var lines = text.Split('\n');
var common = lines.Where(line => line.Length > 0).Min(line => line.TakeWhile(character => character == ' ').Count());
return string.Join("\n", lines.Select(line => new string(' ', spaces) + (line.Length > 0 ? line[common..] : "")));
}
static async Task<(string Boot, string Config, string Assets)> PrepareCompatibility(string source, string output, string? archivePath, CancellationToken cancellation)
{
var boot = File.ReadAllText(Path.Combine(source, "src", "boot.sh"));
var config = File.ReadAllText(Path.Combine(source, "assets", "config.plist"));
if (Hash(Encoding.UTF8.GetBytes(boot)) != "82b56525707a8f586e040f56108b5034c02e7fecfea071f1857e596cba10cbed" || Hash(Encoding.UTF8.GetBytes(config)) != "3b0ec58b693cfa0fadf3e3f952486e87af8c27d504f9545d1e90ae2dc3777096") throw new InvalidOperationException("Pinned OpenCore staging/config hashes mismatch.");
byte[] bytes;
if (archivePath is not null) bytes = await File.ReadAllBytesAsync(archivePath, cancellation);
else
{
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30), MaxResponseContentBufferSize = 2 * 1024 * 1024 };
bytes = await client.GetByteArrayAsync(CryptexUrl, cancellation);
}
if (bytes.Length != 69703 || Hash(bytes) != CryptexHash) throw new InvalidOperationException("Official CryptexFixup release size/hash mismatch.");
File.WriteAllBytes(Path.Combine(output, "CryptexFixup-1.0.5-RELEASE.zip"), bytes);
var assets = Path.Combine(output, "compatibility-assets");
if (Directory.Exists(assets)) throw new InvalidOperationException("Compatibility validation requires a fresh output directory.");
Directory.CreateDirectory(assets);
using var archive = new ZipArchive(new MemoryStream(bytes), ZipArchiveMode.Read);
var required = new[] { "CryptexFixup.kext/Contents/Info.plist", "CryptexFixup.kext/Contents/MacOS/CryptexFixup" };
var entries = archive.Entries.Where(entry => entry.FullName.StartsWith("CryptexFixup.kext/", StringComparison.Ordinal) && !entry.FullName.EndsWith('/')).ToArray();
if (entries.Length != 2 || required.Any(name => entries.Count(entry => entry.FullName == name) != 1)) throw new InvalidOperationException("Cryptex bundle has an unexpected file layout.");
foreach (var entry in entries)
{
if (entry.Length <= 0 || entry.Length > 1024 * 1024) throw new InvalidOperationException("Cryptex bundle file exceeded the staging bound.");
var destination = Path.Combine(assets, entry.FullName);
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
entry.ExtractToFile(destination, false);
}
var info = XDocument.Load(Path.Combine(assets, required[0])).Root!.Element("dict")!;
if (PlistValue(info, "CFBundleIdentifier").Value != "com.khronokernel.CryptexFixup" || PlistValue(info, "CFBundleVersion").Value != "1.0.5" || PlistValue(info, "CFBundleExecutable").Value != "CryptexFixup" || PlistValue(PlistValue(info, "OSBundleLibraries"), "as.vit9696.Lilu").Value != "1.4.7") throw new InvalidOperationException("Cryptex bundle identity/version/Lilu dependency mismatch.");
var fileHashes = required.ToDictionary(name => name, name => Hash(File.ReadAllBytes(Path.Combine(assets, name))));
File.WriteAllText(Path.Combine(assets, "SHA256SUMS"), string.Concat(fileHashes.Select(pair => pair.Value + " " + pair.Key + "\n")), new UTF8Encoding(false));
Save(Path.Combine(output, "compatibility-boot-assets.json"), new { cryptexUrl = CryptexUrl, cryptexSha256 = CryptexHash, cryptexBytes = bytes.Length, cryptexFiles = fileHashes, templateUrl = "https://github.com/LongQT-sea/OpenCore-ISO/releases/download/v0.7/LongQT-OpenCore-v0.7.iso", templateSha256 = OpenCoreTemplateHash, templateBytes = 15884288, liluVersion = "1.7.1", liluBinarySha256 = "0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52", liluBinaryBytes = 526984, liluInfoSha256 = "fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a", causalSingleVariableTest = false });
var document = XDocument.Parse(config, LoadOptions.PreserveWhitespace);
var add = PlistValue(PlistValue(document.Root!.Element("dict")!, "Kernel"), "Add");
var expected = new[] { "Lilu.kext", "VMHide.kext", "VirtualSMC.kext", "WhateverGreen.kext", "VoodooPS2Controller.kext", "VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext", "AppleMCEReporterDisabler.kext" };
if (!add.Elements("dict").Select(dict => PlistValue(dict, "BundlePath").Value).SequenceEqual(expected) || add.Elements("dict").Any(dict => PlistValue(dict, "Enabled").Name != "true")) throw new InvalidOperationException("Pinned Kernel.Add order/enabled contract mismatch.");
var cryptex = XElement.Parse("<dict><key>Arch</key><string>x86_64</string><key>BundlePath</key><string>CryptexFixup.kext</string><key>Comment</key><string>Official CryptexFixup 1.0.5; owned compatibility guest only</string><key>Enabled</key><true/><key>ExecutablePath</key><string>Contents/MacOS/CryptexFixup</string><key>MaxKernel</key><string></string><key>MinKernel</key><string>22.0.0</string><key>PlistPath</key><string>Contents/Info.plist</string></dict>");
add.Elements("dict").First().AddAfterSelf(cryptex);
var bootArguments = PlistValue(PlistValue(PlistValue(PlistValue(document.Root.Element("dict")!, "NVRAM"), "Add"), "7C436110-AB2A-4BBB-A880-FE41995C9F82"), "boot-args").Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (bootArguments.Intersect(new[] { "-cryptoff", "-liluoff", "-crypt_allow_hash_validation", "-crypt_force_avx", "-cryptbeta", "-lilubetaall" }).Any()) throw new InvalidOperationException("Unexpected Cryptex/Lilu disabling or forcing boot argument.");
boot = ReplaceOnce(boot, " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n", " cp -a \"$template/OC/Resources\" \"$EFI_DIR/OC/\"\n" + CompatibilityStaging + "\n");
boot = ReplaceOnce(boot, " PLIST=\"/assets/config.plist\"\n", " [ ! -e /custom.plist ] || { error 'Compatibility profile refuses an unverified custom OpenCore config!'; exit 12; }\n PLIST=\"/assets/config.plist\"\n");
boot = ReplaceOnce(boot, " checkOpenCoreConfig\n addVmHideKext\n", " checkOpenCoreConfig\n" + CompatibilityConfigCheck + "\n addVmHideKext\n");
boot = ReplaceOnce(boot, " if [ -s \"$target\" ] && [ \"$previous\" = \"$current\" ]; then\n IMG=\"$target\"\n return 0\n fi\n", " # This owned compatibility probe always rebuilds; never trust a cached boot.img.\n");
boot = ReplaceOnce(boot, " echo \"VMHIDE=$vmhide\"\n", " echo \"VMHIDE=$vmhide\"\n echo \"COMPATIBILITY=kvm-host-ventura-cryptex\"\n sha256sum /assets/native-compatibility/SHA256SUMS\n");
return (boot, document.ToString(), assets);
}
static XElement PlistValue(XElement dictionary, string key)
{
var keys = dictionary.Elements("key").Where(element => element.Value == key).ToArray();
if (keys.Length != 1 || keys[0].ElementsAfterSelf().FirstOrDefault() is not { } value) throw new InvalidOperationException("Missing/duplicate plist key: " + key);
return value;
}
const string CompatibilityStaging = """
# Only the freshly extracted, owned guest EFI is changed; never the host.
local lilu="$EFI_DIR/OC/Kexts/Lilu.kext/Contents"
printf '%s %s\n' \
fc885f3319f326e3af60e7965a5216b671772d39d40993ec695758bb43d6ea3a "$lilu/Info.plist" \
0c016d93cfe40c7fa3965813175c1b991a76f3d295efd5be66ae712b4a3ffb52 "$lilu/MacOS/Lilu" | sha256sum -c - || { error "Pinned active Lilu files mismatch!"; exit 12; }
[ "$(xmlstarlet sel -T -t -v '/plist/dict/key[.="CFBundleVersion"]/following-sibling::string[1]' "$lilu/Info.plist")" = 1.7.1 ] || { error "Active Lilu version mismatch!"; exit 12; }
[ ! -e "$EFI_DIR/OC/Kexts/CryptexFixup.kext" ] || { error "Unexpected pre-existing Cryptex kext!"; exit 12; }
(cd /assets/native-compatibility && sha256sum -c SHA256SUMS) || { error "Pinned Cryptex staging files mismatch!"; exit 12; }
cp -a /assets/native-compatibility/CryptexFixup.kext "$EFI_DIR/OC/Kexts/"
(cd "$EFI_DIR/OC/Kexts" && sha256sum -c /assets/native-compatibility/SHA256SUMS) || { error "Active Cryptex copy mismatch!"; exit 12; }
info "[compatibility-boot] Lilu=1.7.1 CryptexFixup=1.0.5 files=verified; guest injection and Recovery readiness remain unproved"
""";
const string CompatibilityConfigCheck = """
local kernel='/plist/dict/key[.="Kernel"]/following-sibling::dict[1]/key[.="Add"]/following-sibling::array[1]'
local actual expected
actual=$(xmlstarlet sel -T -t -m "$kernel/dict" -v 'key[.="BundlePath"]/following-sibling::string[1]' -n "$CFG") || exit 12
expected=$(printf '%s\n' Lilu.kext CryptexFixup.kext VMHide.kext VirtualSMC.kext WhateverGreen.kext VoodooPS2Controller.kext VoodooPS2Controller.kext/Contents/PlugIns/VoodooPS2Keyboard.kext AppleMCEReporterDisabler.kext)
[ "$actual" = "$expected" ] || { error "Active Kernel.Add order mismatch!"; exit 12; }
[ "$(xmlstarlet sel -T -t -v "name($kernel/dict[1]/key[.='Enabled']/following-sibling::*[1])" -v "name($kernel/dict[2]/key[.='Enabled']/following-sibling::*[1])" "$CFG")" = truetrue ] || { error "Active Lilu/Cryptex must both be enabled!"; exit 12; }
actual=$(xmlstarlet sel -T -t -m "$kernel/dict[2]" -v 'key[.="Arch"]/following-sibling::string[1]' -n -v 'key[.="ExecutablePath"]/following-sibling::string[1]' -n -v 'key[.="PlistPath"]/following-sibling::string[1]' -n -v 'key[.="MinKernel"]/following-sibling::string[1]' -n -v 'key[.="MaxKernel"]/following-sibling::string[1]' "$CFG") || exit 12
expected=$(printf '%s\n' x86_64 Contents/MacOS/CryptexFixup Contents/Info.plist 22.0.0 '')
[ "$actual" = "$expected" ] || { error "Active Cryptex Kernel.Add paths/architecture/Darwin bounds mismatch!"; exit 12; }
info "[compatibility-config] Kernel.Add=Lilu,CryptexFixup before remaining baseline kexts; MinKernel=22.0.0 MaxKernel=empty"
""";
static string ReplaceOnce(string text, string oldValue, string newValue) => ReplaceAllExact(text, oldValue, newValue, 1);
static string ReplaceAllExact(string text, string oldValue, string newValue, int expected)
{
@@ -223,8 +412,8 @@ static class NativeDiagnostic
{
using var document = JsonDocument.Parse(json);
var result = document.RootElement;
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 14 || result.GetProperty("architecture").GetString() != "x86_64" || result.GetProperty("uid").GetInt32() != 0 || !System.Text.RegularExpressions.Regex.IsMatch(result.GetProperty("disk").GetString() ?? "", "^/dev/disk[0-9]+$") || result.GetProperty("diskBytes").GetInt64() != GuestDiskBytes || result.GetProperty("readOnly").GetBoolean() || new[] { "systemExit", "diskArbitrationExit", "recoveryExit", "diskListExit" }.Any(key => result.GetProperty(key).GetInt32() != 0))
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 14+/x86_64, service readiness and the writable 64-GiB disk.");
if (result.GetProperty("token").GetString() != token || !result.GetProperty("success").GetBoolean() || !Version.TryParse(result.GetProperty("osVersion").GetString(), out var version) || version.Major < 13 || result.GetProperty("architecture").GetString() != "x86_64" || result.GetProperty("uid").GetInt32() != 0 || !System.Text.RegularExpressions.Regex.IsMatch(result.GetProperty("disk").GetString() ?? "", "^/dev/disk[0-9]+$") || result.GetProperty("diskBytes").GetInt64() != GuestDiskBytes || result.GetProperty("readOnly").GetBoolean() || new[] { "systemExit", "diskArbitrationExit", "recoveryExit", "diskListExit" }.Any(key => result.GetProperty(key).GetInt32() != 0))
throw new InvalidOperationException("The fresh guest receipt did not prove native macOS 13+/x86_64, service readiness and the writable 64-GiB disk.");
}
static void AssertContainer(string json, string token)
@@ -232,8 +421,31 @@ static class NativeDiagnostic
using var document = JsonDocument.Parse(json);
var container = document.RootElement[0];
var config = container.GetProperty("HostConfig");
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token || config.GetProperty("Privileged").GetBoolean() || config.GetProperty("NetworkMode").GetString() != "default" && config.GetProperty("NetworkMode").GetString() != "bridge" || config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes || new[] { "CapAdd", "Devices", "DeviceRequests", "Binds", "PortBindings" }.Any(key => config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null && (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any())))
throw new InvalidOperationException("Created container exceeds the owned/unprivileged diagnostic boundary.");
var devices = config.GetProperty("Devices");
var mounts = container.GetProperty("Mounts");
var environment = container.GetProperty("Config").GetProperty("Env").EnumerateArray().Select(value => value.GetString()).ToArray();
if (container.GetProperty("Config").GetProperty("Labels").GetProperty(OwnerLabel).GetString() != token
|| config.GetProperty("Privileged").GetBoolean()
|| config.GetProperty("NetworkMode").GetString() is not ("default" or "bridge")
|| config.GetProperty("Memory").GetInt64() != ContainerMemoryBytes
|| config.GetProperty("MemorySwap").GetInt64() != ContainerMemoryBytes
|| config.GetProperty("NanoCpus").GetInt64() != 2000000000
|| config.GetProperty("ShmSize").GetInt64() != 536870912
|| new[] { "CapAdd", "DeviceRequests", "Binds", "PortBindings", "DeviceCgroupRules", "Tmpfs" }.Any(key =>
config.TryGetProperty(key, out var value) && value.ValueKind != JsonValueKind.Null
&& (value.ValueKind == JsonValueKind.Array ? value.GetArrayLength() != 0 : value.EnumerateObject().Any()))
|| devices.GetArrayLength() != 1
|| devices[0].GetProperty("PathOnHost").GetString() != "/dev/kvm"
|| devices[0].GetProperty("PathInContainer").GetString() != "/dev/kvm"
|| devices[0].GetProperty("CgroupPermissions").GetString() != "rw"
|| mounts.GetArrayLength() != 1
|| mounts[0].GetProperty("Type").GetString() != "volume"
|| mounts[0].GetProperty("Destination").GetString() != "/storage"
|| !mounts[0].GetProperty("RW").GetBoolean()
|| new[] { "KVM=Y", "CPU_MODEL=host", "VERSION=13" }.Any(expected =>
environment.Count(value => value is not null && value.StartsWith(expected.Split('=')[0] + "=", StringComparison.Ordinal)) != 1
|| !environment.Contains(expected)))
throw new InvalidOperationException("Created container exceeds the owned restricted KVM/host-CPU compatibility boundary.");
}
static async Task CaptureGuest(string id, string output, CancellationToken cancellation, bool final = false, string? token = null)
@@ -245,7 +457,7 @@ static class NativeDiagnostic
var result = await Command("docker", ["exec", id, "cat", "/dev/shm/installstate/" + file.Item1], output, "capture-" + file.Item1, cancellation, requireSuccess: false);
if (result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output)) File.WriteAllText(Path.Combine(output, file.Item2), result.Output);
}
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test ! -f /storage/14/setup.dmg || sha256sum /storage/14/setup.dmg; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false);
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test ! -f /storage/13/setup.dmg || sha256sum /storage/13/setup.dmg; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false, retainSuccessful: true);
}
static async Task CaptureMonitor(string id, string output, string token, CancellationToken cancellation)
@@ -265,7 +477,7 @@ static class NativeDiagnostic
var monitor = await Command("docker", ["exec", id, "sh", "-c", """
test -S /run/shm/monitor.sock || exit 1
rm -f -- "$1" || exit 1
printf 'info status\nscreendump %s\n' "$1" | /usr/bin/timeout -s KILL 5 /usr/bin/nc.openbsd -q 1 -w 2 -U /run/shm/monitor.sock
printf 'info kvm\ninfo status\nscreendump %s\n' "$1" | /usr/bin/timeout -s KILL 5 /usr/bin/nc.openbsd -q 1 -w 2 -U /run/shm/monitor.sock
monitor_exit=$?
printf '\n[monitor-exit] %s\n' "$monitor_exit"
[ "$monitor_exit" -eq 0 ] || exit "$monitor_exit"
@@ -321,7 +533,166 @@ static class NativeDiagnostic
}
}
static async Task<CommandResult> Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false)
static async Task ValidateRecoveryPatch(string output)
{
if (Crc32(Encoding.ASCII.GetBytes("123456789")) != 0xcbf43926) throw new InvalidOperationException("Independent C# CRC32 known vector failed.");
var fixture = Path.Combine(output, "validation-recovery-patch");
Directory.CreateDirectory(fixture);
var patch = File.ReadAllText(Path.Combine(output, "recovery-patch.py"));
const string marker = "SCRIPT_ORIGINAL = b'''";
var start = patch.IndexOf(marker, StringComparison.Ordinal) + marker.Length;
var end = patch.IndexOf("'''", start, StringComparison.Ordinal);
var bootstrap = patch[start..end];
var cases = new[] {
("13-zlib", OriginalDaemon13, true, true), ("14-zlib", OriginalDaemon + "\n", true, true),
("13-raw", OriginalDaemon13, false, true), ("14-raw", OriginalDaemon + "\n", false, true),
("unknown", OriginalDaemon13.Replace("Interactive", "Unknown"), true, false),
("duplicate", OriginalDaemon13 + OriginalDaemon + "\n", true, false),
("duplicate-unknown", OriginalDaemon13 + OriginalDaemon13.Replace("Interactive", "Unknown"), true, false),
("malformed", OriginalDaemon13.Replace("</array>", "</broken>"), true, false),
("wrong-arguments", OriginalDaemon13.Replace("/usr/libexec/recoveryosd", "/usr/libexec/wrongdaemon"), true, false),
("duplicate-arguments", OriginalDaemon13.Replace("</array>", "<string>/usr/libexec/recoveryosd</string></array>"), true, false),
("corrupt-data-crc", OriginalDaemon13, true, false), ("unsupported-crc", OriginalDaemon13, true, false),
("xml-boundary", OriginalDaemon13, true, false), ("physical-boundary", OriginalDaemon13, true, false),
("unknown-zero-run", OriginalDaemon13, true, false), ("logical-boundary", OriginalDaemon13, false, false)
};
foreach (var item in cases)
{
var path = Path.Combine(fixture, item.Item1 + ".dmg");
CreateRecoveryFixture(path, bootstrap, item.Item2, item.Item3);
if (item.Item1 is "corrupt-data-crc" or "unsupported-crc" or "xml-boundary" or "physical-boundary" or "unknown-zero-run" or "logical-boundary")
{
var corrupt = File.ReadAllBytes(path);
var trailer = corrupt.Length - 512;
if (item.Item1 == "corrupt-data-crc") corrupt[trailer + 88] ^= 1;
else if (item.Item1 == "unsupported-crc") BinaryPrimitives.WriteUInt32BigEndian(corrupt.AsSpan(trailer + 80), 3);
else if (item.Item1 == "xml-boundary") BinaryPrimitives.WriteUInt64BigEndian(corrupt.AsSpan(trailer + 224), (ulong)corrupt.Length);
else if (item.Item1 == "logical-boundary") BinaryPrimitives.WriteUInt64BigEndian(corrupt.AsSpan(trailer + 492), 1);
else
{
var xmlOffset = checked((int)BinaryPrimitives.ReadUInt64BigEndian(corrupt.AsSpan(trailer + 216)));
var xmlLength = checked((int)BinaryPrimitives.ReadUInt64BigEndian(corrupt.AsSpan(trailer + 224)));
var xmlText = Encoding.UTF8.GetString(corrupt, xmlOffset, xmlLength);
var data = XDocument.Parse(xmlText).Descendants("data").Single().Value;
var mish = Convert.FromBase64String(data);
if (item.Item1 == "physical-boundary") BinaryPrimitives.WriteUInt64BigEndian(mish.AsSpan(236), (ulong)corrupt.Length);
else BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(204), 0);
var replacement = Encoding.UTF8.GetBytes(ReplaceOnce(xmlText, data, Convert.ToBase64String(mish)));
replacement.CopyTo(corrupt, xmlOffset);
}
File.WriteAllBytes(path, corrupt);
}
var before = File.ReadAllBytes(path);
var result = await Command("python3", ["-B", Path.Combine(output, "recovery-patch.py"), path], fixture, item.Item1, CancellationToken.None, requireSuccess: false);
var after = File.ReadAllBytes(path);
if ((result.ExitCode == 0) != item.Item4) throw new InvalidOperationException("Recovery fixture result mismatch: " + item.Item1 + ": " + result.Error);
if (!item.Item4 && !before.SequenceEqual(after)) throw new InvalidOperationException("Rejected Recovery fixture was modified: " + item.Item1);
if (item.Item4)
{
var decoded = DecodeRecoveryFixture(after, item.Item3);
var original = Encoding.UTF8.GetBytes(item.Item2);
var expectedText = item.Item1.StartsWith("13", StringComparison.Ordinal) ? DiagnosticDaemon13 : DiagnosticDaemon;
var expected = Encoding.UTF8.GetBytes(expectedText.PadRight(item.Item2.Length, ' '));
if (before.Length != after.Length || !decoded.AsSpan(4096, original.Length).SequenceEqual(expected)) throw new InvalidOperationException("Recovery fixture changed byte extent or daemon fields: " + item.Item1);
ValidateDaemon(expectedText, item.Item1.StartsWith("13", StringComparison.Ordinal) ? "Interactive" : "App", true);
VerifyRecoveryFixtureChecksums(after, decoded);
}
}
Save(Path.Combine(fixture, "receipt.json"), new { success = true, positiveCases = 4, negativeCases = 12, rejectedImagesUnmodified = true, knownDaemonFieldsPreserved = true, readBackCrc32IndependentlyVerified = true, syntheticUdifFixtures = true, guestExecuted = false });
}
static uint Crc32(ReadOnlySpan<byte> bytes)
{
var crc = uint.MaxValue;
foreach (var value in bytes)
{
crc ^= value;
for (var bit = 0; bit < 8; bit++) crc = (crc >> 1) ^ ((crc & 1) != 0 ? 0xedb88320u : 0);
}
return ~crc;
}
static void CreateRecoveryFixture(string path, string bootstrap, string daemon, bool compressed)
{
var decoded = new byte[16384];
Encoding.UTF8.GetBytes(bootstrap).CopyTo(decoded, 64);
Encoding.UTF8.GetBytes(daemon).CopyTo(decoded, 4096);
byte[] stored;
if (compressed)
{
using var memory = new MemoryStream();
using (var zipper = new ZLibStream(memory, CompressionLevel.Fastest, true)) zipper.Write(decoded);
stored = memory.ToArray();
}
else stored = decoded;
var mish = new byte[284];
Encoding.ASCII.GetBytes("mish").CopyTo(mish, 0);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(4), 1);
BinaryPrimitives.WriteUInt64BigEndian(mish.AsSpan(16), 32);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(64), 2);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(68), 32);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(72), Crc32(decoded));
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(200), 2);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(204), compressed ? 0x80000005u : 1u);
BinaryPrimitives.WriteUInt64BigEndian(mish.AsSpan(220), 32);
BinaryPrimitives.WriteUInt64BigEndian(mish.AsSpan(236), (ulong)stored.Length);
BinaryPrimitives.WriteUInt32BigEndian(mish.AsSpan(244), 0xffffffff);
var xml = Encoding.UTF8.GetBytes("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<plist version=\"1.0\"><dict><key>resource-fork</key><dict><key>blkx</key><array><dict><key>Data</key><data>" + Convert.ToBase64String(mish) + "</data></dict></array></dict></dict></plist>\n");
var koly = new byte[512];
Encoding.ASCII.GetBytes("koly").CopyTo(koly, 0);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(4), 4);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(8), 512);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(12), 1);
BinaryPrimitives.WriteUInt64BigEndian(koly.AsSpan(32), (ulong)stored.Length);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(80), 2);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(84), 32);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(88), Crc32(stored));
BinaryPrimitives.WriteUInt64BigEndian(koly.AsSpan(216), (ulong)stored.Length);
BinaryPrimitives.WriteUInt64BigEndian(koly.AsSpan(224), (ulong)xml.Length);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(352), 2);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(356), 32);
BinaryPrimitives.WriteUInt32BigEndian(koly.AsSpan(360), Crc32(mish.AsSpan(72, 4)));
BinaryPrimitives.WriteUInt64BigEndian(koly.AsSpan(492), 32);
File.WriteAllBytes(path, stored.Concat(xml).Concat(koly).ToArray());
}
static byte[] DecodeRecoveryFixture(byte[] image, bool compressed)
{
var length = checked((int)BinaryPrimitives.ReadUInt64BigEndian(image.AsSpan(image.Length - 512 + 32)));
if (!compressed) return image[..length];
using var input = new MemoryStream(image, 0, length);
using var decoder = new ZLibStream(input, CompressionMode.Decompress);
using var output = new MemoryStream();
decoder.CopyTo(output);
return output.ToArray();
}
static void VerifyRecoveryFixtureChecksums(byte[] image, byte[] decoded)
{
var trailer = image.Length - 512;
var length = checked((int)BinaryPrimitives.ReadUInt64BigEndian(image.AsSpan(trailer + 32)));
var xmlLength = checked((int)BinaryPrimitives.ReadUInt64BigEndian(image.AsSpan(trailer + 224)));
var xml = XDocument.Parse(Encoding.UTF8.GetString(image, length, xmlLength));
var mish = Convert.FromBase64String(xml.Descendants("data").Single().Value);
if (Crc32(image.AsSpan(0, length)) != BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(trailer + 88)) || Crc32(decoded) != BinaryPrimitives.ReadUInt32BigEndian(mish.AsSpan(72)) || Crc32(mish.AsSpan(72, 4)) != BinaryPrimitives.ReadUInt32BigEndian(image.AsSpan(trailer + 360))) throw new InvalidOperationException("Independent C# fixture CRC32 readback failed.");
}
static async Task ValidateResourceRetention(string output)
{
var fixture = Path.Combine(output, "validation-resource-retention");
Directory.CreateDirectory(fixture);
const string label = "capture-resource-fixture";
const string successful = "Successful snapshot before stopped-container capture.\n";
await Command("bash", ["-c", "printf '%s\\n' 'Successful snapshot before stopped-container capture.'"], fixture, label, CancellationToken.None, retainSuccessful: true);
await Command("bash", ["-c", "printf '%s\\n' 'Container is not running.' >&2; exit 1"], fixture, label, CancellationToken.None, requireSuccess: false, retainSuccessful: true);
var retained = Path.Combine(fixture, label + ".last-success.stdout.log");
if (!File.Exists(retained) || File.ReadAllText(retained) != successful || File.ReadAllText(Path.Combine(fixture, label + ".stdout.log")) != "" || !File.ReadAllText(Path.Combine(fixture, label + ".stderr.log")).Contains("Container is not running."))
throw new InvalidOperationException("A failed final capture lost the last successful resource snapshot.");
using var receipt = JsonDocument.Parse(File.ReadAllText(Path.Combine(fixture, label + ".last-success.json")));
if (receipt.RootElement.GetProperty("stdoutSha256").GetString() != Hash(Encoding.UTF8.GetBytes(successful)) || receipt.RootElement.GetProperty("exitCode").GetInt32() != 0) throw new InvalidOperationException("Last successful snapshot receipt does not identify the retained bytes.");
}
static async Task<CommandResult> Command(string executable, string[] arguments, string output, string label, CancellationToken cancellation, bool requireSuccess = true, bool echo = false, bool retainSuccessful = false)
{
if (!label.StartsWith("capture-", StringComparison.Ordinal) && label is not "container" and not "container-running" and not "guest-container-resources")
Console.WriteLine("[native-diagnostic] " + label);
@@ -358,6 +729,11 @@ static class NativeDiagnostic
{
await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(commandToken));
var result = new CommandResult(process.ExitCode, await stdout, await stderr);
if (retainSuccessful && result.ExitCode == 0 && !string.IsNullOrWhiteSpace(result.Output))
{
File.WriteAllText(Path.Combine(output, label + ".last-success.stdout.log"), result.Output, new UTF8Encoding(false));
Save(Path.Combine(output, label + ".last-success.json"), new { exitCode = result.ExitCode, stdoutSha256 = Hash(Encoding.UTF8.GetBytes(result.Output)), capturedUtc = DateTimeOffset.UtcNow });
}
if (requireSuccess && result.ExitCode != 0) throw new InvalidOperationException($"{label} exited {result.ExitCode}: {result.Error[..Math.Min(result.Error.Length, 1500)]}");
return result;
}
+88
View File
@@ -0,0 +1,88 @@
#:property PublishAot=false
// Local diagnostic only. See TranscriptFileShareProbe.md for the contract and limits.
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
const string original = "---\ntitle: transcript probe\n---\n\n# Meeting Transcript\n";
const string written = original + "[00:00:04] Guest-1: Azure returned ***** here.\n";
var root = Path.Combine(Path.GetTempPath(), "meeting-assistant-file-share-probe", Guid.NewGuid().ToString("N"));
if (Directory.Exists(root))
throw new IOException("Probe directory already exists; refusing to reuse it.");
Directory.CreateDirectory(root);
var cases = new List<WriteObservation>();
var cleanupCompleted = false;
try
{
var path = Path.Combine(root, "original-reader.md");
await File.WriteAllTextAsync(path, original);
using (var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
{
if (reader.ReadToEnd() != original)
throw new InvalidDataException("Original reader did not read the initial fixture.");
cases.Add(await ObserveWriteAsync("held-original-reader", path, written));
}
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
cases.Add(await ObserveWriteAsync("original-reader-released", path, written));
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
path = Path.Combine(root, "compatible-reader.md");
await File.WriteAllTextAsync(path, original);
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
using (var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true))
{
if (reader.ReadToEnd() != original)
throw new InvalidDataException("Compatible reader did not read the initial fixture.");
cases.Add(await ObserveWriteAsync("held-compatible-reader", path, written));
}
cases[^1] = cases[^1] with { ContentAfterReaderClosed = await File.ReadAllTextAsync(path) };
}
finally
{
Directory.Delete(root, recursive: true);
cleanupCompleted = !Directory.Exists(root);
}
var windowsContractMatched = OperatingSystem.IsWindows()
? !cases[0].Completed && cases[0].Error is { Type: "System.IO.IOException", NativeCode: 32 }
&& cases[0].ContentAfterReaderClosed == original
: (bool?)null;
var compatibleAndReleasedWritesCompleted = cases.Skip(1).All(result =>
result.Completed && result.Error is null && result.ContentAfterReaderClosed == written);
Console.WriteLine(JsonSerializer.Serialize(new
{
Schema = "meeting-assistant-file-share-probe/v1",
Runtime = RuntimeInformation.FrameworkDescription,
OS = RuntimeInformation.OSDescription,
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
ProbeDirectory = root,
CleanupCompleted = cleanupCompleted,
WindowsContractMatched = windowsContractMatched,
CompatibleAndReleasedWritesCompleted = compatibleAndReleasedWritesCompleted,
Cases = cases,
EvidenceLimit = "Held-reader access-mode probe; it does not reproduce the timing or establish the cause of Run4174."
}, new JsonSerializerOptions { WriteIndented = true }));
return cleanupCompleted && compatibleAndReleasedWritesCompleted && windowsContractMatched != false ? 0 : 1;
static async Task<WriteObservation> ObserveWriteAsync(string name, string path, string content)
{
var elapsed = Stopwatch.StartNew();
Task? write = null;
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
write = File.WriteAllTextAsync(path, content, deadline.Token);
await write;
return new(name, true, write.Status.ToString(), elapsed.ElapsedMilliseconds, null, null);
}
catch (Exception exception)
{
return new(name, false, write?.Status.ToString() ?? "not-returned", elapsed.ElapsedMilliseconds,
new(exception.GetType().FullName!, $"0x{exception.HResult:X8}", exception.HResult & 0xffff, exception.Message), null);
}
}
sealed record WriteObservation(string Name, bool Completed, string TaskStatus, long ElapsedMilliseconds,
WriteError? Error, string? ContentAfterReaderClosed);
sealed record WriteError(string Type, string HResult, int NativeCode, string Message);
+25
View File
@@ -0,0 +1,25 @@
# Transcript file sharing diagnostic
Purpose: distinguish a writer error from a completed write when a reader with the original test's access mode remains open. This temporary diagnostic does not change the app, its tests, or their 577-case count.
Entry point: `tools/ci/TranscriptFileShareProbe.cs`, a .NET 10 file-based app with BCL-only dependencies. From this clone:
```sh
/Users/dh/.dotnet/dotnet run --file tools/ci/TranscriptFileShareProbe.cs
```
On another machine use its .NET 10 SDK executable. The file-based app requires an SDK supporting file-based apps; the prepared local run uses SDK 10.0.401. Native AOT is disabled for this diagnostic so its JSON report can use the normal reflection serializer. It prints JSON with runtime/OS, write completion, exception type/HResult/native error code, content after reader disposal, and cleanup status. It creates a unique directory beneath the system temporary directory, writes two small fixture files, and deletes only that directory in `finally`. It starts no Meeting Assistant app, service, network client, container, or VM. The SDK can create its normal file-based build cache. For a fresh CLI profile set `DOTNET_GENERATE_ASPNET_CERTIFICATE=false` and `DOTNET_CLI_TELEMETRY_OPTOUT=1` to disable unrelated certificate/telemetry initialization.
The optional instrumentation in the existing recording-coordinator test observes public provider and transcript-store boundaries: audio consumed, fake segment yielded, append/rewrite entered, completed, or failed. Timeout output uses `[DEBUG-transcript-write-4174]` and includes elapsed milliseconds, exception type, HResult, and message. The reader, 15-second wait and final redaction assertions are unchanged. `StopAsync` always runs in `finally`; a stop failure does not mask the original timeout. Timestamps distinguish events before and after the failed wait. Instrumentation can affect race timing; a passing run alone does not explain the original failure. This temporary instrumentation should be removed after the actual Wine incident is explained.
The original-reader case uses the same path-taking `StreamReader` constructor as `File.ReadAllText`. It deliberately holds the reader after reading the fixture so that the overlap is deterministic. The original test normally disposes that reader immediately after `ReadToEnd`; therefore this probe checks compatible access modes, not the historical race's timing. The second write happens after disposing that reader. The compatible case holds `FileAccess.Read` with `FileShare.ReadWrite | FileShare.Delete`. No reader fix is applied to the actual test.
The existing Wine job runs this probe after its actual Windows SDK build and before the unchanged full test cohort. It writes `artifacts/tests/transcript-file-sharing.json` and prints that report into the CI log. Invocation there uses the already installed Windows SDK through the existing `WINE_BIN`; no runner or infrastructure capability is added. The measured Wine result is pending until this exact workflow executes.
## Primary-source contract
In [.NET runtime v10.0.12 File.cs](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L572), `ReadAllText` constructs a path-taking `StreamReader`; [`StreamReader.cs`](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/StreamReader.cs#L203) opens read access sharing only further readers. `WriteAllTextAsync` delegates to a create-mode write; [its writer](https://github.com/dotnet/runtime/blob/v10.0.12/src/libraries/System.Private.CoreLib/src/System/IO/File.cs#L1416) opens write access with reader sharing.
[Windows CreateFileW documentation](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew#parameters) requires existing access and sharing modes to remain compatible until handle closure. A held reader that does not permit writes therefore prevents that writer from opening; the Windows contract expects an `IOException` with sharing-violation native code 32. Allowing read/write sharing removes that incompatibility. Delete sharing is included for the comparison but this probe does not rename or delete an open file.
On Windows the CLI asserts that the original held-reader write fails with code 32 and leaves the original bytes, and that both subsequent writes complete with the expected content. On other platforms it reports the original-reader observation without asserting Windows behavior (`WindowsContractMatched: null`), and still checks completed compatible/released writes and cleanup. The Unix/macOS implementation can differ. A local macOS success is not evidence of Wine behavior or the cause of Run4174's timeout.
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# Apple Recovery has Bash before any SDK is installed. Preserve the original
# daemon under its launchd label/PID while the unchanged read-only probe runs.
/bin/bash /Volumes/installstate/readiness.sh &
exec /usr/libexec/recoveryosd
+1 -1
View File
@@ -200,7 +200,7 @@ run_command version /usr/bin/sw_vers -productVersion
read_scalar || fail_probe product_version_invalid
os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version
(( ${os_version%%.*} >= 13 )) || fail_probe unsupported_macos_version
flush_outputs || finish false diagnostic_log_budget_exceeded
# Bound readiness independently of the host's 40-minute overall deadline.
+181
View File
@@ -0,0 +1,181 @@
"""Checksum binding for the pinned Linux Recovery UDIF patcher, not a new CLI.
Source semantics: planetbeing/libdmg-hfsplus dmg/dmglib.c and dmg/blkx.c.
Only flattened, single-segment XML UDIF with CRC32 and raw/zlib data is accepted.
The caller plans same-length chunk writes; XML formatting and all offsets remain.
"""
import base64
import json
import plistlib
import re
import struct
import zlib
def u32(data, offset):
return struct.unpack_from(">I", data, offset)[0]
def u64(data, offset):
return struct.unpack_from(">Q", data, offset)[0]
def crc_contract(data, offset):
if u32(data, offset) != 2 or u32(data, offset + 4) != 32 or any(data[offset + 12:offset + 136]):
raise RuntimeError("Unsupported UDIF checksum type/size/padding")
return u32(data, offset + 8)
class ChecksumPlan:
def __init__(self, image, koly, plist, xml_offset, xml_length, size):
self.image, self.koly, self.plist = image, koly, plist
self.xml_offset, self.xml_length, self.size = xml_offset, xml_length, size
self.data_offset, self.data_length = u64(koly, 24), u64(koly, 32)
if (u32(koly, 4) != 4 or u32(koly, 8) != 512 or u32(koly, 12) != 1
or self.data_offset != 0 or u64(koly, 40) or u64(koly, 48)
or u32(koly, 60) not in (0, 1) or self.data_length != xml_offset
or xml_offset + xml_length > size - 512 or xml_length > 8 * 1024 * 1024):
raise RuntimeError("Unsupported or out-of-bounds flattened UDIF layout")
crc_contract(koly, 80)
crc_contract(koly, 352)
image.seek(xml_offset)
self.xml = image.read(xml_length)
if len(self.xml) != xml_length or not self.xml.lstrip().startswith(b"<?xml"):
raise RuntimeError("Unsupported UDIF metadata framing")
self.blocks = plist["resource-fork"]["blkx"]
self.physical_runs = {}
intervals = []
for block in self.blocks:
mish = block["Data"]
if len(mish) < 244 or mish[:4] != b"mish" or (len(mish) - 204) % 40 or u32(mish, 200) != (len(mish) - 204) // 40:
raise RuntimeError("Malformed UDIF block table")
crc_contract(mish, 64)
if u64(mish, 8) + u64(mish, 16) > u64(koly, 492):
raise RuntimeError("UDIF partition exceeds logical disk boundary")
count = u32(mish, 200)
if u32(mish, 204 + (count - 1) * 40) != 0xffffffff:
raise RuntimeError("UDIF block table has no final terminator")
for kind, offset, length, sectors in self.runs(mish):
if kind in (2, 0x7ffffffe, 0xffffffff):
if length:
raise RuntimeError("Non-data UDIF run has stored bytes")
continue
if kind not in (1, 0x80000005) or not sectors or length <= 0 or sectors * 512 > 32 * 1024 * 1024:
raise RuntimeError("Unsupported UDIF compression/run boundary")
if offset < self.data_offset or offset + length > self.data_offset + self.data_length:
raise RuntimeError("UDIF data run exceeds data-fork boundary")
intervals.append((offset, offset + length))
self.physical_runs[offset] = length
intervals.sort()
if any(left[1] > right[0] for left, right in zip(intervals, intervals[1:])):
raise RuntimeError("Overlapping UDIF physical data runs")
def runs(self, mish):
for entry in range(204, len(mish), 40):
kind = u32(mish, entry)
sector, sectors = u64(mish, entry + 8), u64(mish, entry + 16)
if sector + sectors > u64(mish, 16):
raise RuntimeError("UDIF run exceeds its partition boundary")
offset = self.data_offset + u64(mish, 24) + u64(mish, entry + 24)
yield kind, offset, u64(mish, entry + 32), sectors
def read(self, offset, length):
self.image.seek(offset)
result = self.image.read(length)
if len(result) != length:
raise RuntimeError("Short UDIF checksum read")
return result
def logical_crcs(self, mish, replacements):
original_crc = patched_crc = 0
for kind, offset, length, sectors in self.runs(mish):
# IGNORE runs are excluded by the independently verified Apple 13
# baseline. Unknown ZERO/compression types are rejected above.
if kind not in (1, 0x80000005):
continue
old = self.read(offset, length)
new = replacements.get(offset, old)
old_decoded = old if kind == 1 else zlib.decompress(old)
new_decoded = new if kind == 1 else zlib.decompress(new)
if len(old_decoded) != sectors * 512 or len(new_decoded) != sectors * 512 or len(old) != len(new):
raise RuntimeError("UDIF checksum run changed physical/logical extent")
original_crc = zlib.crc32(old_decoded, original_crc)
patched_crc = zlib.crc32(new_decoded, patched_crc)
return original_crc, patched_crc
def data_crcs(self, replacements):
old_crc = new_crc = 0
cursor = self.data_offset
def same_until(stop):
nonlocal cursor, old_crc, new_crc
while cursor < stop:
data = self.read(cursor, min(1024 * 1024, stop - cursor))
old_crc, new_crc = zlib.crc32(data, old_crc), zlib.crc32(data, new_crc)
cursor += len(data)
for offset, new in sorted(replacements.items()):
same_until(offset)
old = self.read(offset, len(new))
old_crc, new_crc = zlib.crc32(old, old_crc), zlib.crc32(new, new_crc)
cursor += len(new)
same_until(self.data_offset + self.data_length)
return old_crc, new_crc
def prepare(self, planned):
replacements = dict(planned)
if len(replacements) != len(planned):
raise RuntimeError("Duplicate planned UDIF physical writes")
if any(self.physical_runs.get(offset) != len(data) for offset, data in replacements.items()):
raise RuntimeError("Planned UDIF write does not preserve an existing data-run boundary")
old_master = bytearray()
new_master = bytearray()
changed = []
for block in self.blocks:
mish = block["Data"]
old_crc, new_crc = self.logical_crcs(mish, replacements)
if old_crc != crc_contract(mish, 64):
raise RuntimeError("Original UDIF logical CRC32 mismatch")
old_master.extend(struct.pack(">I", old_crc))
new_master.extend(struct.pack(">I", new_crc))
if new_crc != old_crc:
new_mish = bytearray(mish)
struct.pack_into(">I", new_mish, 72, new_crc)
changed.append((mish, bytes(new_mish)))
old_data_crc, new_data_crc = self.data_crcs(replacements)
if old_data_crc != crc_contract(self.koly, 80) or zlib.crc32(old_master) != crc_contract(self.koly, 352):
raise RuntimeError("Original UDIF data-fork/master CRC32 mismatch")
xml = self.xml
for old_mish, new_mish in changed:
matches = [match for match in re.finditer(rb"<data>([\sA-Za-z0-9+/=]*)</data>", xml)
if base64.b64decode(match.group(1)) == old_mish]
if len(matches) != 1:
raise RuntimeError("UDIF block checksum XML identity is ambiguous")
match = matches[0]
encoded = iter(base64.b64encode(new_mish))
text = bytes(value if chr(value).isspace() else next(encoded) for value in match.group(1))
xml = xml[:match.start(1)] + text + xml[match.end(1):]
if len(xml) != self.xml_length:
raise RuntimeError("UDIF checksum update changed XML region length")
new_plist = plistlib.loads(xml)
expected = dict(self.plist)
expected["resource-fork"] = dict(self.plist["resource-fork"])
expected["resource-fork"]["blkx"] = [dict(block, Data=dict(changed).get(block["Data"], block["Data"])) for block in self.blocks]
if new_plist != expected:
raise RuntimeError("UDIF checksum update changed unrelated metadata")
koly = bytearray(self.koly)
struct.pack_into(">I", koly, 88, new_data_crc)
struct.pack_into(">I", koly, 360, zlib.crc32(new_master))
self.receipt = dict(originalDataCrc32=f"{old_data_crc:08x}", patchedDataCrc32=f"{new_data_crc:08x}",
originalMasterCrc32=f"{zlib.crc32(old_master):08x}", patchedMasterCrc32=f"{zlib.crc32(new_master):08x}",
changedBlockChecksums=len(changed), imageBytes=self.size, xmlOffset=self.xml_offset,
xmlBytes=self.xml_length, physicalAndLogicalExtentsPreserved=True)
return xml, bytes(koly)
def verify(self):
koly = self.read(self.size - 512, 512)
xml = self.read(self.xml_offset, self.xml_length)
verifier = ChecksumPlan(self.image, koly, plistlib.loads(xml), self.xml_offset, self.xml_length, self.size)
verifier.prepare([])
self.image.seek(0, 2)
if self.image.tell() != self.size:
raise RuntimeError("Patched UDIF image length changed")
print("[recovery-udif] " + json.dumps(dict(self.receipt, readBackChecksumsVerified=True), sort_keys=True))