forked from Manuel/meeting-assistant
Observe owned Recovery process without a guest SDK or task control rights
This commit is contained in:
@@ -30,6 +30,10 @@ static class NativeDiagnostic
|
||||
const string NativeVersionSource = "/System/Library/CoreServices/SystemVersion.plist";
|
||||
const string NativeVersionMethod = "guest-file/host-xml";
|
||||
const int MaximumDiskStackBytes = 512 * 1024;
|
||||
const string ProbeSourceHash = "38acf83b05694f9931c41ff1749e9b6b836f04c740b7f1a1c60e32332678cb1f";
|
||||
const string ProbeDriverHash = "d9d87415c12398f29b35697109f18d9b16f121dae969a4a05d0ec6a8cb874d25";
|
||||
const string ProbeBinaryHash = "b8d54e2945eeefb3e0c22468feb7aef7efa50813909058b1e4b40144dd7e3d3e";
|
||||
const string ProbeManifestHash = "9e71d39e2dd65ab83d0827a467e85893f410ef03e34fb72e08daa27e74861ba3";
|
||||
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";
|
||||
@@ -75,6 +79,7 @@ static class NativeDiagnostic
|
||||
ValidateContracts();
|
||||
ValidateBootProgress();
|
||||
ValidateDiskStackCapture(output);
|
||||
ValidateProbeAssetFixtures(output);
|
||||
await ValidateNativeSystemVersion(output);
|
||||
if (full) ValidateFullContracts();
|
||||
if (Option(args, "--source") is { } source)
|
||||
@@ -98,7 +103,12 @@ static class NativeDiagnostic
|
||||
nativeVersionFixtures = "native-system-version-fixtures.json", nativeProductVersionCommandRemoved = true,
|
||||
diskReadinessAttemptLimit = 1, diskCommandLimitSeconds = 600, diskCommandExtendedForDiagnosticObservation = true,
|
||||
diskObservationCommandLimitSeconds = 60, stackSamplingRequested = true, stackSamplingDurationSeconds = 1,
|
||||
stackSamplingIntervalMilliseconds = 100, stackSamplingMayDie = true, stackSamplingRuntimeSucceeded = false,
|
||||
stackSamplingIntervalMilliseconds = 100, stackSamplingMayDie = false, stackSamplingRuntimeSucceeded = false,
|
||||
nativeProcessProbeBeforeAndAfter = true, nativeProcessProbeObservationPauseSeconds = 180, sampleUsageControlLimitSeconds = 45,
|
||||
nativeProcessProbeSourceSha256 = ProbeSourceHash, nativeProcessProbeDriverSha256 = ProbeDriverHash,
|
||||
nativeProcessProbeBinarySha256 = ProbeBinaryHash, nativeProcessProbeManifestSha256 = ProbeManifestHash,
|
||||
nativeProcessProbeRecoveryPermissionProven = false, nativeProcessProbeAssetNegativeCases = 4,
|
||||
nativeProcessProbePrivateApiWeakImported = true, nativeProcessProbeStagingFixtureVerified = true,
|
||||
stackObservationIsQualifiedReadiness = false, diskStackMaximumCapturedBytes = MaximumDiskStackBytes,
|
||||
diskManagementDiagnosticLabel = "com.apple.storagekitd", diskStackCaptureFixtureCases = 4,
|
||||
resultNegativeCases = 6, containerNegativeCases = 11, recoveryPositiveCases = 4, recoveryNegativeCases = 12,
|
||||
@@ -285,9 +295,10 @@ static class NativeDiagnostic
|
||||
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, two explicit native SystemVersion getter blocks and one-attempt limit, baseline identity/service/disk gates and watchdogs must remain identical.");
|
||||
foreach (var required in new[] { "command_limit=600; fi", "run_command management_before /bin/launchctl print system/com.apple.storagekitd", "observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd", "observe_command diskutil-stack /usr/bin/sample \"$disk_process\" 1 100 -mayDie -file \"$stack_output\"", "stack_output=\"$STATE_DIR/diskutil-stack.txt\"", "read -r -t 60 -u 9", "\"$BASH_VERSION\"" })
|
||||
foreach (var required in new[] { "command_limit=600; fi", "run_command sample_usage /usr/bin/sample", "run_command management_before /bin/launchctl print system/com.apple.storagekitd", "observe_live_command storagekit-live /bin/launchctl print system/com.apple.storagekitd", "observe_command diskutil-stack /usr/bin/sample \"$disk_process\" 1 100 -file \"$stack_output\"", "observe_live_command diskutil-native-before \"$STATE_DIR/native-process-probe-x86_64\" \"$disk_process\" \"$$\"", "observe_live_command diskutil-native-after \"$STATE_DIR/native-process-probe-x86_64\" \"$disk_process\" \"$$\"", "read -r -t 180 -u 9", "stack_output=\"$STATE_DIR/diskutil-stack.txt\"", "read -r -t 60 -u 9", "\"$BASH_VERSION\"" })
|
||||
if (!readiness.Contains(required, StringComparison.Ordinal)) throw new InvalidOperationException("Owned optional disk observation contract changed: " + required);
|
||||
if (readiness.Contains("PENDING_OUTPUTS+=(\"$stack_output\")", StringComparison.Ordinal)) throw new InvalidOperationException("Stack reports must be bounded directly by the Linux host, without another guest copy.");
|
||||
ReadProbeAssets();
|
||||
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.");
|
||||
@@ -373,6 +384,10 @@ static class NativeDiagnostic
|
||||
static async Task PrepareSource(string source, string output, string token, bool writeSource, CancellationToken cancellation, bool full = false)
|
||||
{
|
||||
Directory.CreateDirectory(output);
|
||||
var probe = ReadProbeAssets();
|
||||
File.WriteAllBytes(Path.Combine(output, "native-process-probe-x86_64"), probe.Binary);
|
||||
File.WriteAllBytes(Path.Combine(output, "native-process-probe-manifest.json"), probe.Manifest);
|
||||
Save(Path.Combine(output, "native-process-probe-input-hashes.json"), new { sourceSha256 = ProbeSourceHash, driverSha256 = ProbeDriverHash, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, compilerExecutedInCI = false, qualifiedReadiness = false, recoveryPermissionProven = false });
|
||||
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.");
|
||||
@@ -413,9 +428,9 @@ static class NativeDiagnostic
|
||||
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, " 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");
|
||||
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\" ||\n ! cp -f \"$IMAGE_ASSETS/native-process-probe/native-process-probe-x86_64\" \"${script%/*}/native-process-probe-x86_64\" ||\n ! cp -f \"$IMAGE_ASSETS/native-process-probe/build-manifest.json\" \"${script%/*}/native-process-probe-manifest.json\"; then\n");
|
||||
image = ReplaceOnce(image, " chmod 0755 \"$script\"\n", " chmod 0755 \"$script\" \"${script%/*}/native-process-probe-x86_64\"\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 ! cmp -s \"$IMAGE_ASSETS/native-process-probe/native-process-probe-x86_64\" \"$state/native-process-probe-x86_64\" ||\n ! cmp -s \"$IMAGE_ASSETS/native-process-probe/build-manifest.json\" \"$state/native-process-probe-manifest.json\" ||\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)
|
||||
{
|
||||
@@ -434,6 +449,7 @@ static class NativeDiagnostic
|
||||
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);
|
||||
await Command("bash", ["-n", Path.Combine(output, "cpu.sh.patched")], output, "cpu-composition-syntax", cancellation);
|
||||
if (!writeSource) await ValidateProbeStaging(image, output, probe.Binary, probe.Manifest, cancellation);
|
||||
if (full && !writeSource) await ValidateFullBootstrap(wrapper, output, token, cancellation);
|
||||
if (!writeSource) return;
|
||||
File.WriteAllText(patchPath, patch, new UTF8Encoding(false));
|
||||
@@ -446,6 +462,72 @@ static class NativeDiagnostic
|
||||
File.WriteAllText(Path.Combine(source, "src/boot.sh"), boot, new UTF8Encoding(false));
|
||||
File.WriteAllText(cpuPath, cpu, new UTF8Encoding(false));
|
||||
File.WriteAllText(Path.Combine(source, "assets/ci-cpu-preflight.asm"), preflight, new UTF8Encoding(false));
|
||||
var probeAssets = Path.Combine(source, "assets/install/native-process-probe");
|
||||
Directory.CreateDirectory(probeAssets);
|
||||
File.WriteAllBytes(Path.Combine(probeAssets, "native-process-probe-x86_64"), probe.Binary);
|
||||
File.WriteAllBytes(Path.Combine(probeAssets, "build-manifest.json"), probe.Manifest);
|
||||
}
|
||||
|
||||
static (byte[] Binary, byte[] Manifest) ReadProbeAssets()
|
||||
{
|
||||
var folder = Path.Combine("tools", "ci", "native-process-probe");
|
||||
var source = File.ReadAllBytes(Path.Combine(folder, "NativeProcessProbe.c"));
|
||||
var driver = File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"));
|
||||
var binary = File.ReadAllBytes(Path.Combine(folder, "native-process-probe-x86_64"));
|
||||
var manifest = File.ReadAllBytes(Path.Combine(folder, "build-manifest.json"));
|
||||
ValidateProbeAssets(source, driver, binary, manifest);
|
||||
return (binary, manifest);
|
||||
}
|
||||
|
||||
static void ValidateProbeAssets(byte[] source, byte[] driver, byte[] binary, byte[] manifest)
|
||||
{
|
||||
if (Hash(source) != ProbeSourceHash || Hash(driver) != ProbeDriverHash || Hash(binary) != ProbeBinaryHash || Hash(manifest) != ProbeManifestHash)
|
||||
throw new InvalidOperationException("Disposable native diagnostic asset differs from its reviewed source/driver/binary/manifest pin.");
|
||||
using var document = JsonDocument.Parse(manifest); var value = document.RootElement;
|
||||
if (value.GetProperty("sourceSha256").GetString() != ProbeSourceHash || value.GetProperty("driverSha256").GetString() != ProbeDriverHash || value.GetProperty("binarySha256").GetString() != ProbeBinaryHash
|
||||
|| value.GetProperty("architecture").GetString() != "x86_64" || value.GetProperty("minimumMacOS").GetString() != "14.0" || value.GetProperty("sdkVersion").GetString() != "27.0"
|
||||
|| !value.GetProperty("importedLibraries").EnumerateArray().Select(item => item.GetString()).SequenceEqual(new[] { "/usr/lib/libSystem.B.dylib" })
|
||||
|| value.GetProperty("signature").GetString() != "ad-hoc" || value.GetProperty("entitlements").GetBoolean() || !value.GetProperty("offlineVerified").GetBoolean()
|
||||
|| !value.GetProperty("targetAndExpectedParentMandatory").GetBoolean() || !value.GetProperty("readOnlyTaskPortOnly").GetBoolean()
|
||||
|| !value.GetProperty("taskReadWeakImportVerified").GetBoolean()
|
||||
|| value.GetProperty("taskReadReturnContract").GetString() != "BSD int/errno" || value.GetProperty("outputMaximumBytes").GetInt32() != 32768
|
||||
|| value.GetProperty("recoveryPermissionsProven").GetBoolean() || value.GetProperty("qualifiedReadiness").GetBoolean())
|
||||
throw new InvalidOperationException("Disposable native diagnostic manifest claims another runtime, permissions or readiness.");
|
||||
}
|
||||
|
||||
static void ValidateProbeAssetFixtures(string output)
|
||||
{
|
||||
ReadProbeAssets(); var folder = Path.Combine("tools", "ci", "native-process-probe");
|
||||
var source = File.ReadAllBytes(Path.Combine(folder, "NativeProcessProbe.c")); var driver = File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"));
|
||||
var binary = File.ReadAllBytes(Path.Combine(folder, "native-process-probe-x86_64")); var manifest = File.ReadAllBytes(Path.Combine(folder, "build-manifest.json"));
|
||||
byte[] Changed(byte[] bytes) { var copy = bytes.ToArray(); copy[0] ^= 1; return copy; }
|
||||
var cases = new[] { ("wrong-source", Changed(source), driver, binary, manifest), ("wrong-driver", source, Changed(driver), binary, manifest), ("wrong-binary", source, driver, Changed(binary), manifest), ("wrong-manifest-field", source, driver, binary, Encoding.UTF8.GetBytes(Encoding.UTF8.GetString(manifest).Replace("\"recoveryPermissionsProven\": false", "\"recoveryPermissionsProven\": true", StringComparison.Ordinal))) };
|
||||
foreach (var test in cases)
|
||||
{
|
||||
try { ValidateProbeAssets(test.Item2, test.Item3, test.Item4, test.Item5); }
|
||||
catch (InvalidOperationException) { continue; }
|
||||
throw new InvalidOperationException("Disposable native asset validator accepted " + test.Item1);
|
||||
}
|
||||
Directory.CreateDirectory(output);
|
||||
Save(Path.Combine(output, "native-process-probe-assets-validation.json"), new { success = true, negativeCases = cases.Select(test => test.Item1), sourceSha256 = ProbeSourceHash, driverSha256 = ProbeDriverHash, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, nativeCompilerExecuted = false, nativeProbeExecuted = false, qualifiedReadiness = false });
|
||||
}
|
||||
|
||||
static async Task ValidateProbeStaging(string image, string output, byte[] binary, byte[] manifest, CancellationToken cancellation)
|
||||
{
|
||||
var fixture = Path.Combine(output, "validation-probe-staging");
|
||||
var tools = Path.Combine(fixture, "tools/recovery"); var assets = Path.Combine(fixture, "assets/install"); var state = Path.Combine(fixture, "state");
|
||||
Directory.CreateDirectory(tools); Directory.CreateDirectory(Path.Combine(assets, "native-process-probe")); Directory.CreateDirectory(state);
|
||||
File.WriteAllText(Path.Combine(tools, "launch.sh"), "# owned harmless wrapper fixture\n"); File.WriteAllText(Path.Combine(tools, "readiness.sh"), "# owned harmless readiness fixture\n");
|
||||
File.WriteAllBytes(Path.Combine(assets, "native-process-probe/native-process-probe-x86_64"), binary);
|
||||
File.WriteAllBytes(Path.Combine(assets, "native-process-probe/build-manifest.json"), manifest);
|
||||
var begin = image.IndexOf("createAutomatedInstallationFiles() {", StringComparison.Ordinal); var end = image.IndexOf("prepareInstallationState() {", begin, StringComparison.Ordinal);
|
||||
if (begin < 0 || end < 0) throw new InvalidOperationException("Actual staging producer missing.");
|
||||
var script = "IMAGE_TOOLS=\"$1/tools\"\nIMAGE_ASSETS=\"$1/assets/install\"\nerror() { printf '%s\\n' \"$*\" >&2; }\n" + image[begin..end] + "\ncreateAutomatedInstallationFiles \"$1/state/launch.sh\"\n";
|
||||
await Command("bash", ["-c", script, "diagnostic-staging-fixture", fixture], output, "native-probe-staging", cancellation);
|
||||
if (Hash(File.ReadAllBytes(Path.Combine(state, "native-process-probe-x86_64"))) != ProbeBinaryHash || Hash(File.ReadAllBytes(Path.Combine(state, "native-process-probe-manifest.json"))) != ProbeManifestHash
|
||||
|| Directory.GetFiles(state).Length != 5 || (File.GetUnixFileMode(Path.Combine(state, "native-process-probe-x86_64")) & UnixFileMode.UserExecute) == 0)
|
||||
throw new InvalidOperationException("Actual staging paths/bytes/executable mode differ; C/driver must not be staged.");
|
||||
Save(Path.Combine(fixture, "validation.json"), new { success = true, actualProducerSha256 = Hash(Encoding.UTF8.GetBytes(image[begin..end])), sourceAndDriverStaged = false, binarySha256 = ProbeBinaryHash, manifestSha256 = ProbeManifestHash, executable = true, nativeProbeExecuted = false, qualifiedReadiness = false });
|
||||
}
|
||||
|
||||
static void ValidateDaemon(string xml, string processType, bool patched)
|
||||
@@ -1538,16 +1620,38 @@ static class NativeDiagnostic
|
||||
snapshotDeadline.CancelAfter(TimeSpan.FromSeconds(20));
|
||||
const string snapshot = """
|
||||
printf '[snapshot UTC]\n'; date -u '+%Y-%m-%dT%H:%M:%SZ'
|
||||
for path in /proc/meminfo /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
|
||||
for path in /proc/meminfo /proc/loadavg /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io \
|
||||
/sys/fs/cgroup/cpu.max /sys/fs/cgroup/cpu.stat /sys/fs/cgroup/cpu.pressure \
|
||||
/sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.peak \
|
||||
/sys/fs/cgroup/memory.events /sys/fs/cgroup/memory.stat /sys/fs/cgroup/memory.pressure \
|
||||
/sys/fs/cgroup/memory.swap.current; do
|
||||
/sys/fs/cgroup/memory.swap.current /sys/fs/cgroup/io.stat /sys/fs/cgroup/io.pressure; do
|
||||
printf '\n[%s]\n' "$path"
|
||||
if [ -r "$path" ]; then cat "$path"; else printf 'unavailable\n'; fi
|
||||
done
|
||||
printf '\n[host paging counters]\n'
|
||||
awk '/^(pgmajfault|pswpin|pswpout) / {print}' /proc/vmstat
|
||||
printf '\n[owned QEMU process snapshot; counters are raw]\n'
|
||||
qemu_pid=$(head -c 16 /run/shm/qemu.pid 2>/dev/null || true)
|
||||
case "$qemu_pid" in ''|*[!0-9]*|0|1) printf 'owned QEMU PID unavailable\n' ;;
|
||||
*)
|
||||
qemu_exe=$(readlink "/proc/$qemu_pid/exe" 2>/dev/null || true)
|
||||
if [ "$qemu_exe" = /usr/bin/qemu-system-x86_64 ]; then
|
||||
printf 'pid=%s executable=%s\n' "$qemu_pid" "$qemu_exe"
|
||||
for file in cmdline stat status; do
|
||||
printf '\n[/proc/%s/%s]\n' "$qemu_pid" "$file"
|
||||
head -c 4096 "/proc/$qemu_pid/$file" 2>/dev/null | tr '\000' '\n' || true
|
||||
done
|
||||
task_count=0
|
||||
for file in /proc/"$qemu_pid"/task/*/stat; do
|
||||
[ -r "$file" ] || continue
|
||||
[ "$task_count" -lt 32 ] || { printf '[remaining own QEMU threads omitted]\n'; break; }
|
||||
printf '\n[%s]\n' "$file"; head -c 4096 "$file" 2>/dev/null || true
|
||||
task_count=$((task_count + 1))
|
||||
done
|
||||
else
|
||||
printf 'owned QEMU executable identity unavailable; no process files read\n'
|
||||
fi ;;
|
||||
esac
|
||||
""";
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user