forked from Manuel/meeting-assistant
Read Recovery version from current guest SystemVersion evidence
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user