Read Recovery version from current guest SystemVersion evidence

This commit is contained in:
dh
2026-10-04 15:00:11 +02:00
parent 9735db1cdc
commit 6122be2cef
3 changed files with 327 additions and 42 deletions
+5 -1
View File
@@ -14,6 +14,8 @@ Run 4188 with Haswell recorded a boot loop; first-reset run 4189 retained repeat
Run 4190 used synchronous kernel serial output and QEMU interrupt/register tracing to preserve the failure context. Its kernel explicitly warned that synchronous output impacts performance. The current full candidate uses normal upstream boot arguments and only the existing iothread QEMU argument; it retains actual CPU/staging receipts independently of Docker log rotation. Its existing fresh-readiness gate precedes every installation permit. This allows one bounded full qualification to test boot performance and, only after readiness, installation/build/tests without duplicating the guest startup. No remote full result has qualified this candidate yet.
Full run 4191 at `9735db1` reached native Recovery: x86_64/root, Darwin 23.6.0 and successful launchd service queries. Both `sw_vers` attempts were stopped by the existing 45-second watchdog at about 50 seconds. Other successful commands took 24–47 seconds, and small log-copy batches took 85–181 seconds. Thus this run proves broad native startup latency and a probe-imposed abort, without proving a permanent `sw_vers` hang. Disk enumeration, installation and application tests were not reached. The next candidate obtains the version from the current guest's SystemVersion plist to reduce process launches; it does not claim that `sw_vers` has become functional.
## Entry points and dependencies
Orchestration remains the .NET 10 file-based app `tools/ci/MacOsNativeDiagnostic.cs`. Existing Bash/Python boot integration is necessary before a guest SDK exists. NASM assembles the CPU probe in the disposable image build, without host/runner installation. No new runner, device, capability, secret or service is used.
@@ -39,7 +41,9 @@ The locally assembled NASM 2.16.03 ROM is 65,536 bytes, SHA256 `c32746122cc68f3e
The original Apple recoveryosd runs under its existing job/PID beside the read-only probe. Exact known macOS 13/14 plist layouts and same-length replacements retain their allowlist. The patcher validates UDIF boundaries, updates changed mish/koly CRCs and reads back the image. Four raw/zlib positive and twelve rejection fixtures use an independent C# CRC32 reader. Apple chunklist authentication applies to the input, not the deliberately modified image.
Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service queries and exactly one writable whole 64-GiB disk. The complete successful sw_vers output must contain one valid ProductVersion field and EOF within 1,024 bytes. The actual native diskutil query remains mandatory.
Native readiness requires x86_64, UID 0, macOS 14+, successful launchd service queries and exactly one writable whole 64-GiB disk. The guest's existing Bash runtime reads its own `/System/Library/CoreServices/SystemVersion.plist` with a 4-KiB bound and mandatory EOF. Apple documents this path as the [system-version source](https://developer.apple.com/documentation/installer_js/system/1812284-version). The existing C# controller parses the captured XML with external resolution disabled, requires a flat string-valued dictionary with exactly one valid direct `ProductVersion` string and macOS 14+, and returns a token-bound answer to that guest. Missing, binary, oversized, ambiguous or malformed content fails. Before installation, the readiness receipt's version must match this current-file evidence. No configured `VERSION` or host OS value serves as proof. The actual native diskutil query remains mandatory.
The raw file, exact source path, length, SHA256 and parsing receipt are retained. The guest exchange uses its existing Bash before any SDK exists; the XML logic stays in C#/.NET. Its reply wait has a 180-second bound and requires the current token, bounded complete content and a valid version. This method establishes the current guest version, not successful execution of `sw_vers`.
Required commands retain 45 seconds, UID 180 seconds and the single disk query 120 seconds. The owned observer uses `/bin/ps -M -p <diskutil-child>` with a separate 60-second limit and two-second TERM/KILL grace. It avoids stack symbolication; thread waiting states do not identify an IPC endpoint. Observation failure passes no gate. Owned children are stopped on completion/cancellation; output remains 512 KiB per command and 4 MiB proof.
+271 -5
View File
@@ -6,6 +6,7 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Xml;
using System.Xml.Linq;
// .NET 10 file-based CI diagnostic. See docs/macos-native-diagnostic.md.
@@ -26,6 +27,8 @@ static class NativeDiagnostic
const string SdkVersion = "10.0.401";
const string SdkSha512 = "33401b4a2da8554e3306db6072ea8569d9fcc608509c271e0aa4b39e7cc432da3631f14e7e1e2445d67d72550d18ce44a8bbd2382a756867ad2edab6b1c963c0";
const string FullState = "/storage/14/ci-state";
const string NativeVersionSource = "/System/Library/CoreServices/SystemVersion.plist";
const string NativeVersionMethod = "guest-file/host-xml";
static readonly JsonSerializerOptions JsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, WriteIndented = true };
const string OriginalBootstrap = "[ ! -e /tmp/m ]&&{ /sbin/mount_9p installstate >/dev/null 2>&1;exec /Volumes/installstate/launch.sh;};: >/tmp/m\n";
const string MountOnlyBootstrap = "[ ! -e /tmp/m ]&& /sbin/mount_9p installstate >/dev/null 2>&1; : >/tmp/m\n";
@@ -70,6 +73,7 @@ static class NativeDiagnostic
{
ValidateContracts();
ValidateBootProgress();
await ValidateNativeSystemVersion(output);
if (full) ValidateFullContracts();
if (Option(args, "--source") is { } source)
{
@@ -84,7 +88,10 @@ static class NativeDiagnostic
helperSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "MacOsNativeDiagnostic.cs"))),
udifChecksumBindingSha256 = Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-udif-checksums.py"))),
baselineReadinessNormalized = true, readinessDiagnosticBlocksExcluded = 7,
productVersionParserBlockExcluded = true, productVersionSequenceRestored = true, productVersionMaximumBytes = 1024, nativeProductVersionCommandRemoved = true,
nativeVersionGetterBlocksExcluded = 2, nativeVersionGetterSequenceRestored = true,
nativeVersionMethod = NativeVersionMethod, nativeVersionSource = NativeVersionSource,
nativeVersionMaximumBytes = 4096, nativeVersionReplyLimitSeconds = 180,
nativeVersionFixtures = "native-system-version-fixtures.json", nativeProductVersionCommandRemoved = true,
diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 120, diskThreadObservationLimitSeconds = 60, stackSamplingUsed = false,
resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12,
independentFixtureCrc32Readback = true, resourceSnapshotRetention = true, cpuProfileSourceContractsVerified = true,
@@ -174,7 +181,7 @@ static class NativeDiagnostic
while (true)
{
deadline.Token.ThrowIfCancellationRequested();
await CaptureGuest(id, output, deadline.Token, full);
await CaptureGuest(id, output, deadline.Token, full, token: token);
if (!permitted) CheckRecoveryBootProgress(output);
if (full && phaseStarted.Elapsed > phaseBudget)
throw new InvalidOperationException("The bounded native " + phase + " phase exceeded " + phaseBudget.TotalMinutes + " minutes.");
@@ -189,6 +196,7 @@ static class NativeDiagnostic
{
var result = File.ReadAllText(resultPath);
ValidateResult(result, token);
ValidateNativeVersionBinding(output, token, result);
if (full)
{
await PermitInstallation(id, output, token, sourceCommit, result, deadline.Token);
@@ -268,7 +276,7 @@ static class NativeDiagnostic
var baseline = NormalizeReadinessDiagnostics(readiness);
baseline = ReplaceOnce(baseline, "while (( attempt < 1 && SECONDS - readiness_start < 600 )); do", "while (( SECONDS - readiness_start < 600 )); do");
if (Hash(Encoding.UTF8.GetBytes(baseline)) != "4d428f594dac14eff64ed87b172c81ecf85ac91da8c5460cd6ec4b1d310800c3")
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, the successful sw_vers version parser/sequence and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
throw new InvalidOperationException("Outside seven explicit diagnostic blocks, two explicit native SystemVersion getter blocks and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
if (Hash(File.ReadAllBytes(Path.Combine("tools", "ci", "macos-native-bootstrap.sh"))) != "94f069e116fdc7685a4d233cab6fa50df9f39274386bb82157674061e74fadb5")
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.");
@@ -311,11 +319,34 @@ static class NativeDiagnostic
}
if (blocks != 7 || source.Contains(end, StringComparison.Ordinal))
throw new InvalidOperationException("Readiness must contain exactly seven explicit disk IPC diagnostic blocks.");
source = RestoreVersionBlock(source, "successful sw_vers version parser", "");
source = RestoreVersionBlock(source, "successful sw_vers version extraction", "run_command version /usr/bin/sw_vers -productVersion\n(( LAST_EXIT == 0 )) || fail_probe product_version_failed\nread_scalar || fail_probe product_version_invalid\n");
source = RestoreVersionBlock(source, "native SystemVersion plist request helpers", "");
source = RestoreVersionBlock(source, "native SystemVersion plist getter", OriginalVersionGetter + "\n");
return source;
}
// Only this getter is restored for the baseline comparison. Native uname/id,
// launchd exits, writable-disk gates and every command watchdog stay intact.
const string OriginalVersionGetter = """
run_command platform /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
flush_outputs || finish false diagnostic_log_budget_exceeded
if (( platform_exit != 0 )); then
run_command system /bin/launchctl print system
system_exit="$LAST_EXIT"
run_command arbitration /bin/launchctl print system/com.apple.diskarbitrationd
arbitration_exit="$LAST_EXIT"
run_command recovery /bin/launchctl print system/com.apple.recoveryosd
recovery_exit="$LAST_EXIT"
printf '[proof-retry] sw_vers once after native service context; same 45-second deadline\n' >&3
run_command platform-warm /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
fi
(( platform_exit == 0 )) || fail_probe sw_vers_failed
run_command version /usr/bin/sw_vers -productVersion
(( LAST_EXIT == 0 )) || fail_probe product_version_failed
read_scalar || fail_probe product_version_invalid
""";
static string RestoreVersionBlock(string source, string name, string originalSequence)
{
var start = "# BEGIN " + name + "\n";
@@ -372,6 +403,7 @@ static class NativeDiagnostic
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, " chmod 0755 \"$script\"\n", " chmod 0755 \"$script\"\n printf '%s\\n' '" + token + "' > \"${script%/*}/run.owner\" || return 1\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");
if (full)
@@ -1123,6 +1155,7 @@ static class NativeDiagnostic
var stage = await Command("docker", ["exec", id, "cat", "/run/shm/native-stage.log"], output, "capture-native-stage", cancellation, requireSuccess: false);
ReportCpuPreflight(output, stage.ExitCode == 0 ? stage.Output : logs.Output);
await Command("docker", ["exec", id, "head", "-c", "4096", "/run/shm/kernel-handoffs.log"], output, "capture-kernel-handoffs", cancellation, requireSuccess: false, retainSuccessful: true);
if (token is not null) await CaptureNativeSystemVersion(id, output, token, full, cancellation);
var files = new List<(string, string)> { ("proof.log", "guest-proof.log"), ("result.json", "guest-result.json") };
if (full) files.AddRange([("guest-phase.json", "guest-phase.json"), ("full-result.json", "full-result.json"), ("firstboot.log", "firstboot.log"), ("unattended-firstboot.log", "unattended-firstboot.log"), ("install.log", "install.log"), ("apple.log", "apple.log"), ("disk-ownership-ioreg.log", "disk-ownership-ioreg.log"), ("installed-root.plist", "installed-root.plist"), ("apfs-containers.plist", "apfs-containers.plist"), ("physical-store.plist", "physical-store.plist"), ("clt-catalog.log", "clt-catalog.log"), ("clt-install.log", "clt-install.log"), ("clt-sdk.log", "clt-sdk.log")]);
foreach (var file in files)
@@ -1147,6 +1180,239 @@ static class NativeDiagnostic
await Command("docker", ["exec", id, "sh", "-c", "printf '[qemu]\n'; qemu-system-x86_64 --version | head -n 1; printf '[Recovery hash]\n'; test -f /storage/14/setup.dmg && sha256sum /storage/14/setup.dmg || exit 1; printf '[resources]\n'; df -Pk /storage; cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/cpu.max 2>/dev/null || true"], output, "guest-container-resources", cancellation, requireSuccess: false, retainSuccessful: true);
}
static int NativeVersionRequestLength(string request, string token)
{
var fields = request.Split('\n');
if (request.Length > 256 || request.Contains('\0') || fields.Length != 5 || fields[3] != "ready:" + token || fields[4] != ""
|| fields[0] != token || fields[1] != NativeVersionSource
|| !System.Text.RegularExpressions.Regex.IsMatch(fields[2], @"\A[0-9]{1,4}\z")
|| !int.TryParse(fields[2], out var length) || length is < 1 or > 4096)
throw new InvalidOperationException("Native SystemVersion request token/path/length/EOF is invalid.");
return length;
}
static string ParseNativeSystemVersion(byte[] raw, string request, string token)
{
if (raw.Length != NativeVersionRequestLength(request, token) || raw.Length is < 1 or > 4096 || raw.Contains((byte)0))
throw new InvalidOperationException("Native SystemVersion raw bytes are missing, binary, overbound or differ from the request.");
var xml = new UTF8Encoding(false, true).GetString(raw);
using var reader = XmlReader.Create(new StringReader(xml), new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, XmlResolver = null, MaxCharactersInDocument = 4096 });
var document = XDocument.Load(reader);
var root = document.Root;
if (root is null || root.Name != "plist" || root.Attribute("version")?.Value != "1.0" || root.Attributes().Count() != 1 || root.Elements().Count() != 1 || root.Elements().Single().Name != "dict" || root.Nodes().OfType<XText>().Any(text => !string.IsNullOrWhiteSpace(text.Value)))
throw new InvalidOperationException("Native SystemVersion requires one top-level plist/dict.");
var dict = root.Elements().Single();
var values = dict.Elements().ToArray();
if (dict.HasAttributes || dict.Nodes().OfType<XText>().Any(text => !string.IsNullOrWhiteSpace(text.Value)) || values.Length % 2 != 0 || dict.Descendants("key").Count(key => key.Value == "ProductVersion") != 1)
throw new InvalidOperationException("Native SystemVersion requires exactly one direct ProductVersion key.");
string? versionText = null;
for (var index = 0; index < values.Length; index += 2)
{
var key = values[index]; var value = values[index + 1];
if (key.Name != "key" || key.HasElements || key.HasAttributes) throw new InvalidOperationException("Native SystemVersion has an invalid dict key/value pair.");
if (value.Name != "string" || value.HasElements || value.HasAttributes) throw new InvalidOperationException("Native SystemVersion requires scalar string values.");
if (key.Value != "ProductVersion") continue;
versionText = value.Value;
}
if (versionText is null || !System.Text.RegularExpressions.Regex.IsMatch(versionText, @"\A[0-9]+\.[0-9]+(?:\.[0-9]+)?\z") || !Version.TryParse(versionText, out var version) || version.Major < 14)
throw new InvalidOperationException("Native ProductVersion is invalid or below macOS 14.");
return versionText;
}
static async Task CaptureNativeSystemVersion(string id, string output, string token, bool full, CancellationToken cancellation)
{
var evidencePath = Path.Combine(output, "native-system-version.json");
if (File.Exists(evidencePath)) return;
var state = full ? FullState : "/dev/shm/installstate";
var ready = await Command("docker", ["exec", id, "head", "-c", "257", state + "/native-system-version.request"], output, "capture-native-version-request", cancellation, requireSuccess: false);
if (ready.ExitCode != 0 || string.IsNullOrEmpty(ready.Output)) return;
// The built-in producer publishes the final marker after closing raw bytes.
// A still incomplete marker remains pending; its guest wait is bounded.
var requestFields = ready.Output.Split('\n');
if (ready.Output.Length <= 256 && (!ready.Output.EndsWith('\n') || requestFields.Length < 5 || !requestFields[^2].StartsWith("ready:", StringComparison.Ordinal))) return;
File.WriteAllText(Path.Combine(output, "native-system-version.request"), ready.Output, new UTF8Encoding(false));
var owner = await Command("docker", ["exec", id, "head", "-c", "81", state + "/run.owner"], output, "capture-native-version-owner", cancellation, requireSuccess: false);
if (owner.ExitCode != 0 || owner.Output != token + "\n") throw new InvalidOperationException("Native SystemVersion exchange has no current owned state.");
var started = Stopwatch.StartNew();
byte[] raw = []; string? version = null; string? error = null;
try
{
NativeVersionRequestLength(ready.Output, token);
// Linux coreutils transport preserves bytes; guest pre-SDK work uses only read/printf.
var capture = await Command("docker", ["exec", id, "bash", "-o", "pipefail", "-c", "head -c 4097 '" + state + "/native-system-version.plist' | base64 -w 0"], output, "capture-native-version-bytes", cancellation);
raw = Convert.FromBase64String(capture.Output);
File.WriteAllBytes(Path.Combine(output, "native-system-version.plist"), raw);
version = ParseNativeSystemVersion(raw, ready.Output, token);
}
catch (Exception exception) when (exception is InvalidOperationException or XmlException or DecoderFallbackException or FormatException)
{
error = exception.Message;
}
Save(evidencePath, new NativeVersionEvidence(token, NativeVersionSource, raw.Length, Hash(raw), NativeVersionMethod, version, error is null, started.Elapsed.TotalMilliseconds, DateTimeOffset.UtcNow, error));
var response = token + (error is null ? ":" + version : ":error:invalid_native_system_version") + "\n";
var reply = Path.Combine(output, "native-system-version.reply");
File.WriteAllText(reply, response, new UTF8Encoding(false));
await Command("docker", ["cp", reply, id + ":" + state + "/native-system-version.reply.tmp"], output, "stage-native-version-reply", cancellation);
await Command("docker", ["exec", id, "mv", state + "/native-system-version.reply.tmp", state + "/native-system-version.reply"], output, "publish-native-version-reply", cancellation);
Console.WriteLine(error is null ? "[native-version] method=" + NativeVersionMethod + " source=" + NativeVersionSource + " version=" + version : "[native-version] method=" + NativeVersionMethod + " source=" + NativeVersionSource + " failed: invalid native file/request; see raw evidence.");
}
static void ValidateNativeVersionBinding(string output, string token, string readiness)
{
using var evidence = JsonDocument.Parse(File.ReadAllText(Path.Combine(output, "native-system-version.json")));
using var result = JsonDocument.Parse(readiness);
var raw = File.ReadAllBytes(Path.Combine(output, "native-system-version.plist"));
var version = ParseNativeSystemVersion(raw, File.ReadAllText(Path.Combine(output, "native-system-version.request")), token);
var recorded = evidence.RootElement;
if (!recorded.GetProperty("success").GetBoolean() || recorded.GetProperty("token").GetString() != token
|| recorded.GetProperty("sourcePath").GetString() != NativeVersionSource || recorded.GetProperty("method").GetString() != NativeVersionMethod
|| recorded.GetProperty("byteLength").GetInt32() != raw.Length || recorded.GetProperty("sha256").GetString() != Hash(raw)
|| recorded.GetProperty("version").GetString() != version || result.RootElement.GetProperty("token").GetString() != token || result.RootElement.GetProperty("osVersion").GetString() != version)
throw new InvalidOperationException("Readiness version does not match the current token/path/length/SHA native guest-file evidence.");
}
sealed record NativeVersionEvidence(string Token, string SourcePath, int ByteLength, string Sha256, string Method, string? Version, bool Success, double ElapsedMilliseconds, DateTimeOffset CapturedUtc, string? Error);
static async Task ValidateNativeSystemVersion(string output)
{
Directory.CreateDirectory(output);
var fixture = Path.Combine(output, "validation-native-system-version");
Directory.CreateDirectory(fixture);
const string token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
var valid = Encoding.UTF8.GetBytes("<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\"><plist version=\"1.0\"><dict><key>ProductName</key><string>macOS</string><key>ProductVersion</key><string>14.6</string></dict></plist>\n");
string Request(byte[] raw) => token + "\n" + NativeVersionSource + "\n" + raw.Length + "\nready:" + token + "\n";
byte[] Changed(string from, string to) => Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(valid).Replace(from, to, StringComparison.Ordinal));
var duplicate = Changed("</dict>", "<key>ProductVersion</key><string>14.6</string></dict>");
var nested = Changed("<key>ProductVersion</key><string>14.6</string>", "<key>Nested</key><dict><key>ProductVersion</key><string>14.6</string></dict>");
var entity = Encoding.UTF8.GetBytes("<!DOCTYPE plist [<!ENTITY version SYSTEM 'file:///must-never-be-read'>]><plist version='1.0'><dict><key>ProductVersion</key><string>&version;</string></dict></plist>");
(string Name, byte[] Raw, string Request, string? Version)[] parserCases =
{
(Name: "valid14", Raw: valid, Request: Request(valid), Version: (string?)"14.6"),
("valid14patch", Changed("14.6", "14.6.1"), Request(Changed("14.6", "14.6.1")), (string?)"14.6.1"),
("duplicate", duplicate, Request(duplicate), (string?)null),
("nested", nested, Request(nested), (string?)null),
("binary-plist", "bplist00"u8.ToArray(), Request("bplist00"u8.ToArray()), (string?)null),
("nul", valid.Concat(new byte[] { 0 }).ToArray(), Request(valid.Concat(new byte[] { 0 }).ToArray()), (string?)null),
("invalid-utf8", new byte[] { 0xff }, Request(new byte[] { 0xff }), (string?)null),
("oversize", new byte[4097], Request(new byte[4097]), (string?)null),
("entity", entity, Request(entity), (string?)null),
("invalid-version", Changed("14.6", "14.6junk"), Request(Changed("14.6", "14.6junk")), (string?)null),
("below14", Changed("14.6", "13.6"), Request(Changed("14.6", "13.6")), (string?)null),
("bad-eof", valid, Request(valid).TrimEnd('\n'), (string?)null),
("stale-token", valid, Request(valid).Replace(token, new string('b', 32), StringComparison.Ordinal), (string?)null),
("wrong-path", valid, Request(valid).Replace(NativeVersionSource, "/metadata/VERSION", StringComparison.Ordinal), (string?)null),
("length-mismatch", valid, Request(valid).Replace("\n" + valid.Length + "\n", "\n" + (valid.Length + 1) + "\n", StringComparison.Ordinal), (string?)null),
("dict-garbage", Changed("</dict>", "garbage</dict>"), Request(Changed("</dict>", "garbage</dict>")), (string?)null),
("bogus-value", Changed("<string>macOS</string>", "<bogus/>"), Request(Changed("<string>macOS</string>", "<bogus/>")), (string?)null),
("key-in-value-position", Changed("<string>macOS</string>", "<key>macOS</key>"), Request(Changed("<string>macOS</string>", "<key>macOS</key>")), (string?)null)
};
var parserReceipts = new List<object>();
foreach (var test in parserCases)
{
string? actual = null;
try { actual = ParseNativeSystemVersion(test.Raw, test.Request, token); }
catch (Exception exception) when (exception is InvalidOperationException or XmlException or DecoderFallbackException) { }
File.WriteAllBytes(Path.Combine(fixture, test.Name + ".plist"), test.Raw);
parserReceipts.Add(new { test.Name, accepted = actual is not null, version = actual, expectedVersion = test.Version });
if (actual != test.Version) throw new InvalidOperationException("Native SystemVersion XML/request fixture failed: " + test.Name);
}
var bindingState = Path.Combine(fixture, "binding");
Directory.CreateDirectory(bindingState);
var goodEvidence = new NativeVersionEvidence(token, NativeVersionSource, valid.Length, Hash(valid), NativeVersionMethod, "14.6", true, 0, DateTimeOffset.UtcNow, null);
var goodResult = JsonSerializer.Serialize(new { token, osVersion = "14.6" });
File.WriteAllBytes(Path.Combine(bindingState, "native-system-version.plist"), valid);
File.WriteAllText(Path.Combine(bindingState, "native-system-version.request"), Request(valid));
Save(Path.Combine(bindingState, "native-system-version.json"), goodEvidence);
ValidateNativeVersionBinding(bindingState, token, goodResult);
var bindingCases = new[]
{
goodEvidence with { Token = new string('b', 32) }, goodEvidence with { SourcePath = "/metadata/VERSION" },
goodEvidence with { ByteLength = valid.Length + 1 }, goodEvidence with { Sha256 = new string('f', 64) },
goodEvidence with { Method = "environment" }, goodEvidence with { Version = "14.6.1" }, goodEvidence with { Success = false }
};
foreach (var invalid in bindingCases)
{
Save(Path.Combine(bindingState, "native-system-version.json"), invalid);
try { ValidateNativeVersionBinding(bindingState, token, goodResult); }
catch (InvalidOperationException) { continue; }
throw new InvalidOperationException("Native SystemVersion binding accepted changed evidence.");
}
Save(Path.Combine(bindingState, "native-system-version.json"), goodEvidence);
try { ValidateNativeVersionBinding(bindingState, token, goodResult.Replace("14.6", "14.6.1", StringComparison.Ordinal)); throw new Exception("Readiness version mismatch accepted."); }
catch (InvalidOperationException) { }
var readiness = File.ReadAllText(Path.Combine("tools", "ci", "macos-native-readiness.sh"));
const string begin = "# BEGIN native SystemVersion plist request helpers\n";
const string end = "# END native SystemVersion plist request helpers\n";
var from = readiness.IndexOf(begin, StringComparison.Ordinal) + begin.Length;
var to = readiness.IndexOf(end, from, StringComparison.Ordinal);
var producer = readiness[from..to];
var mapped = ReplaceOnce(ReplaceOnce(producer, "[ -f \"$source\" ]", "[ -f \"$SYSTEM_VERSION_FIXTURE\" ]"), "< \"$source\"", "< \"$SYSTEM_VERSION_FIXTURE\"");
(string Name, byte[]? Raw, string Owner, string Existing, string Reply, bool Success, bool Request)[] shellCases =
{
(Name: "valid", Raw: (byte[]?)valid, Owner: token, Existing: "", Reply: "valid", Success: true, Request: true),
("missing", (byte[]?)null, token, "", "none", false, false),
("nul", valid.Concat(new byte[] { 0 }).ToArray(), token, "", "none", false, false),
("control-binary", valid.Concat(new byte[] { 1 }).ToArray(), token, "", "none", false, false),
("oversize", Enumerable.Repeat((byte)'x', 4097).ToArray(), token, "", "none", false, false),
("foreign-owner", valid, new string('b', 32), "", "none", false, false),
("existing-request", valid, token, "request", "none", false, false),
("existing-reply", valid, token, "reply", "none", false, false),
("stale-reply", valid, token, "", "stale", false, true),
("reply-bad-eof", valid, token, "", "bad-eof", false, true),
("reply-overbound", valid, token, "", "overbound", false, true),
("reply-nul", valid, token, "", "nul", false, true),
("host-xml-error", "bplist00"u8.ToArray(), token, "", "valid", false, true),
("reply-timeout", valid, token, "", "none", false, true)
};
var shellReceipts = new List<object>();
foreach (var test in shellCases)
{
var state = Path.Combine(fixture, "producer-" + test.Name);
Directory.CreateDirectory(state);
var raw = Path.Combine(state, "source.plist");
if (test.Raw is not null) File.WriteAllBytes(raw, test.Raw);
File.WriteAllText(Path.Combine(state, "run.owner"), test.Owner + "\n");
if (test.Existing != "") File.WriteAllText(Path.Combine(state, "native-system-version." + test.Existing), "stale\n");
await Command("/usr/bin/mkfifo", ["-m", "600", Path.Combine(state, "wait.fifo")], output, "native-version-fifo-" + test.Name, CancellationToken.None);
var body = test.Name == "reply-timeout" ? ReplaceOnce(mapped, "SECONDS - started < 180", "SECONDS - started < 1") : mapped;
var script = "set -u\nSTATE_DIR=\"$1\"; SYSTEM_VERSION_FIXTURE=\"$2\"; PROOF_TOKEN=\"$3\"; SCALAR=\"\"\nexec 3> \"$STATE_DIR/proof.log\"\nexec 9<> \"$STATE_DIR/wait.fifo\"\n" + body + "\nif read_native_system_version; then printf 'disk-boundary\\n' > \"$STATE_DIR/disk-boundary\"; exit 0; else printf 'failed:%s\\n' \"$NATIVE_VERSION_ERROR\"; exit 1; fi\n";
File.WriteAllText(Path.Combine(state, "producer.sh"), script);
using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var child = Command("/bin/bash", ["-c", script, "native-version-producer-fixture", state, raw, token], output, "native-version-producer-" + test.Name, deadline.Token, requireSuccess: false);
var requestFile = Path.Combine(state, "native-system-version.request");
string? request = null;
while (!child.IsCompleted)
{
if (File.Exists(requestFile))
{
var current = File.ReadAllText(requestFile);
if (current.EndsWith("\nready:" + token + "\n", StringComparison.Ordinal)) { request = current; break; }
}
await Task.Delay(10, deadline.Token);
}
if (request is not null && test.Reply != "none")
{
string response;
try { response = token + ":" + ParseNativeSystemVersion(File.ReadAllBytes(Path.Combine(state, "native-system-version.plist")), request, token) + "\n"; }
catch (Exception exception) when (exception is InvalidOperationException or XmlException or DecoderFallbackException) { response = token + ":error:invalid_native_system_version\n"; }
response = test.Reply switch { "stale" => response.Replace(token, new string('b', 32), StringComparison.Ordinal), "bad-eof" => response.TrimEnd('\n'), "overbound" => response + new string('x', 81), "nul" => response + "\0", _ => response };
var reply = Path.Combine(state, "native-system-version.reply");
File.WriteAllText(reply + ".tmp", response);
File.Move(reply + ".tmp", reply);
}
var result = await child;
var published = request is not null;
var diskReached = File.Exists(Path.Combine(state, "disk-boundary"));
if ((result.ExitCode == 0) != test.Success || diskReached != test.Success || published != test.Request)
throw new InvalidOperationException("Native SystemVersion exact Bash producer/reply fixture failed: " + test.Name);
if (published && !File.ReadAllBytes(Path.Combine(state, "native-system-version.plist")).SequenceEqual(test.Raw!)) throw new InvalidOperationException("Native version producer did not preserve exact bytes.");
shellReceipts.Add(new { test.Name, result.ExitCode, requestPublished = published, diskReached, fixtureWaitSeconds = test.Name == "reply-timeout" ? 1 : 180 });
}
Save(Path.Combine(output, "native-system-version-fixtures.json"), new { success = true, parserCases = parserReceipts, bindingPositiveCases = 1, bindingNegativeCases = bindingCases.Length + 1, producerCases = shellReceipts, producerSha256 = Hash(Encoding.UTF8.GetBytes(producer)), sourcePathMappedOnlyForFixtureRead = true, actualBash = "/bin/bash", productionWaitSeconds = 180, timeoutFixtureWaitSeconds = 1, requestCommitLineRequired = true, dockerExecuted = false, guestExecuted = false });
}
static void ReportCpuPreflight(string output, string logs)
{
var receipt = Path.Combine(output, "cpu-preflight-runtime.json");
+51 -36
View File
@@ -99,26 +99,56 @@ read_scalar() {
SCALAR="$value"
}
# BEGIN successful sw_vers version parser
read_product_version() {
local value status line version="" fields=0
# Read the entire successful native output. EOF is mandatory; a NUL delimiter
# or reaching the 1025-byte sentinel must never hide a suffix.
LC_ALL=C IFS= read -r -n 1025 -d '' value < "$LAST_OUTPUT"; status=$?
(( status == 1 && ${#value} <= 1024 )) || return 1
while IFS= read -r line || [ -n "$line" ]; do
if [[ "$line" =~ ^[[:blank:]]*ProductVersion: ]]; then
fields=$((fields + 1))
(( fields == 1 )) || return 1
[[ "$line" =~ ^[[:blank:]]*ProductVersion:[[:blank:]]*([0-9]+\.[0-9]+(\.[0-9]+)?)[[:blank:]]*$ ]] || return 1
version="${BASH_REMATCH[1]}"
# BEGIN native SystemVersion plist request helpers
read_native_system_version() {
# Recovery has Bash 3.2 before .NET. Read bytes here; XML is parsed by the
# existing owned host controller, never by shell or VERSION metadata.
local LC_ALL=C source="/System/Library/CoreServices/SystemVersion.plist"
local value status owner reply started=$SECONDS invalid_bytes
local raw="$STATE_DIR/native-system-version.plist"
local ready="$STATE_DIR/native-system-version.request"
local response="$STATE_DIR/native-system-version.reply"
NATIVE_VERSION_ERROR=native_system_version_read_failed
printf '[native-version-start] method=guest-file/host-xml source=%s seconds=%s\n' "$source" "$started" >&3
IFS= read -r -n 81 -d '' owner < "$STATE_DIR/run.owner"; status=$?
(( status == 1 && ${#owner} <= 80 )) &&
[ "$owner" = "$PROOF_TOKEN"$'\n' ] || { NATIVE_VERSION_ERROR=native_system_version_foreign_owner; return 1; }
[ ! -e "$ready" ] && [ ! -L "$ready" ] &&
[ ! -e "$raw" ] && [ ! -L "$raw" ] &&
[ ! -e "$response" ] && [ ! -L "$response" ] || { NATIVE_VERSION_ERROR=native_system_version_stale_exchange; return 1; }
[ -f "$source" ] || return 1
IFS= read -r -n 4097 -d '' value < "$source"; status=$?
# EOF is mandatory. NUL stops read with status 0; 4097 is the overbound sentinel.
(( status == 1 && ${#value} > 0 && ${#value} <= 4096 )) || { NATIVE_VERSION_ERROR=native_system_version_invalid_bytes; return 1; }
invalid_bytes=${value//$'\t'/}
invalid_bytes=${invalid_bytes//$'\r'/}
invalid_bytes=${invalid_bytes//$'\n'/}
[[ "$invalid_bytes" =~ [[:cntrl:]] ]] && { NATIVE_VERSION_ERROR=native_system_version_binary; return 1; }
printf '%s' "$value" > "$raw" || return 1
# Publish this final marker only after the complete raw write has closed.
printf '%s\n%s\n%s\nready:%s\n' "$PROOF_TOKEN" "$source" "${#value}" "$PROOF_TOKEN" > "$ready" || return 1
printf '[native-version-request] method=guest-file/host-xml source=%s bytes=%s seconds=%s\n' "$source" "${#value}" "$SECONDS" >&3
while (( SECONDS - started < 180 )); do
if [ -e "$response" ] || [ -L "$response" ]; then
[ -f "$response" ] && [ ! -L "$response" ] || { NATIVE_VERSION_ERROR=native_system_version_invalid_reply; return 1; }
IFS= read -r -n 81 -d '' reply < "$response"; status=$?
(( status == 1 && ${#reply} <= 80 )) && [[ "$reply" = *$'\n' ]] || { NATIVE_VERSION_ERROR=native_system_version_invalid_reply; return 1; }
reply=${reply%$'\n'}
if [[ "$reply" =~ ^([0-9a-f]{32}):([0-9]+\.[0-9]+(\.[0-9]+)?)$ ]] && [ "${BASH_REMATCH[1]}" = "$PROOF_TOKEN" ]; then
SCALAR="${BASH_REMATCH[2]}"
NATIVE_VERSION_ERROR=""
printf '[native-version-result] method=guest-file/host-xml source=%s version=%s elapsed=%ss\n' "$source" "$SCALAR" "$((SECONDS - started))" >&3
return 0
fi
NATIVE_VERSION_ERROR=native_system_version_rejected_reply
return 1
fi
done <<< "$value"
(( fields == 1 )) || return 1
SCALAR="$version"
IFS= read -r -t 1 -u 9 unused || :
done
NATIVE_VERSION_ERROR=native_system_version_reply_timeout
return 1
}
# END successful sw_vers version parser
# END native SystemVersion plist request helpers
# BEGIN disk IPC diagnostic
# Optional observations have their own child/timer ownership. Thread state/time
# targets only this probe's diskutil and does not request stack symbolication.
@@ -283,24 +313,9 @@ run_command uid /usr/bin/id -u
read_scalar || fail_probe uid_output_invalid
uid="$SCALAR"
[ "$uid" = 0 ] || fail_probe recovery_account_not_root
run_command platform /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
flush_outputs || finish false diagnostic_log_budget_exceeded
if (( platform_exit != 0 )); then
run_command system /bin/launchctl print system
system_exit="$LAST_EXIT"
run_command arbitration /bin/launchctl print system/com.apple.diskarbitrationd
arbitration_exit="$LAST_EXIT"
run_command recovery /bin/launchctl print system/com.apple.recoveryosd
recovery_exit="$LAST_EXIT"
printf '[proof-retry] sw_vers once after native service context; same 45-second deadline\n' >&3
run_command platform-warm /usr/bin/sw_vers
platform_exit="$LAST_EXIT"
fi
(( platform_exit == 0 )) || fail_probe sw_vers_failed
# BEGIN successful sw_vers version extraction
read_product_version || fail_probe product_version_invalid
# END successful sw_vers version extraction
# BEGIN native SystemVersion plist getter
read_native_system_version || fail_probe "$NATIVE_VERSION_ERROR"
# END native SystemVersion plist getter
os_version="$SCALAR"
[[ "$os_version" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]] || fail_probe product_version_invalid
(( ${os_version%%.*} >= 14 )) || fail_probe unsupported_macos_version