forked from Manuel/meeting-assistant
117 lines
11 KiB
C#
117 lines
11 KiB
C#
#:property PublishAot=false
|
|
using System.Diagnostics;
|
|
using System.Runtime.InteropServices;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
|
|
// Offline build/owned-child validation only. No guest, Docker or services.
|
|
if (!OperatingSystem.IsMacOS()) throw new InvalidOperationException("The disposable build driver uses the existing Apple SDK on this local host.");
|
|
var folder = Path.GetFullPath(args.Single());
|
|
var source = Path.Combine(folder, "NativeProcessProbe.c");
|
|
var output = Path.Combine(folder, "artifacts"); Directory.CreateDirectory(output);
|
|
var sourceHash = Hash(File.ReadAllBytes(source));
|
|
var sdk = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-path"], "sdk-path")).Stdout.Trim();
|
|
var sdkVersion = (await Run("/usr/bin/xcrun", ["--sdk", "macosx", "--show-sdk-version"], "sdk-version")).Stdout.Trim();
|
|
var compiler = await Run("/usr/bin/xcrun", ["--sdk", "macosx", "clang", "--version"], "compiler-version");
|
|
var binary = Path.Combine(output, "native-process-probe-x86_64");
|
|
string[] build = ["--sdk", "macosx", "clang", "-arch", "x86_64", "-mmacosx-version-min=14.0", "-std=c11", "-Os", "-Wall", "-Wextra", "-Werror", source, "-lproc", "-o", binary];
|
|
await Run("/usr/bin/xcrun", build, "build-x86_64");
|
|
await Run("/usr/bin/codesign", ["--force", "--sign", "-", binary], "adhoc-sign");
|
|
await Run("/usr/bin/codesign", ["--verify", "--strict", binary], "signature-verify");
|
|
var signature = await Run("/usr/bin/codesign", ["-d", "--verbose=4", "--entitlements", ":-", binary], "signature-details");
|
|
var architectures = await Run("/usr/bin/lipo", ["-archs", binary], "architectures");
|
|
var imports = await Run("/usr/bin/otool", ["-L", binary], "imports");
|
|
var loadCommands = await Run("/usr/bin/otool", ["-l", binary], "load-commands");
|
|
var symbols = await Run("/usr/bin/nm", ["-m", "-u", binary], "undefined-symbols");
|
|
if (architectures.Stdout.Trim() != "x86_64" || !Regex.IsMatch(loadCommands.Stdout, @"cmd LC_BUILD_VERSION\s+cmdsize [0-9]+\s+platform [0-9]+\s+minos 14\.0\b")) throw new Exception("Actual architecture/minimum OS differs.");
|
|
var importedLibraries = imports.Stdout.Split('\n').Skip(1).Where(line => line.Trim().Length != 0).Select(line => line.Trim().Split(' ')[0]).ToArray();
|
|
if (importedLibraries.Length != 1 || importedLibraries[0] != "/usr/lib/libSystem.B.dylib") throw new Exception("Probe imports another runtime/framework: " + string.Join(",", importedLibraries));
|
|
if (!Regex.IsMatch(symbols.Stdout, @"weak external _task_read_for_pid\b") || symbols.Stdout.Contains("_task_for_pid", StringComparison.Ordinal)
|
|
|| new[] { "_task_suspend", "_task_resume", "_thread_suspend", "_thread_resume", "_mach_vm_write" }.Any(symbols.Stdout.Contains)) throw new Exception("Read-only import contract failed.");
|
|
if (signature.Stdout.Contains("<dict>", StringComparison.Ordinal) || signature.Stderr.Contains("<dict>", StringComparison.Ordinal)) throw new Exception("Probe must not acquire entitlements.");
|
|
|
|
var cases = new List<object>(); bool x86Executed = false; string? executionUnavailable = null;
|
|
using var target = Process.Start(new ProcessStartInfo("/bin/sleep") { ArgumentList = { "30" }, UseShellExecute = false })!;
|
|
try
|
|
{
|
|
Result positive;
|
|
try { positive = await Run(binary, [target.Id.ToString(), Environment.ProcessId.ToString()], "owned-sleep", requireSuccess: false); x86Executed = true; }
|
|
catch (System.ComponentModel.Win32Exception exception) { executionUnavailable = exception.Message; positive = new(-1, "", ""); }
|
|
if (x86Executed)
|
|
{
|
|
var evidence = positive.Stdout + positive.Stderr;
|
|
if (positive.ExitCode != 0 || !evidence.Contains("[probe-entry]", StringComparison.Ordinal)
|
|
|| !Regex.IsMatch(evidence, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @" flags=0x[0-9a-f]+ nice=-?[0-9]+ status=[0-9]+")
|
|
|| !evidence.Contains("[role] selector=6", StringComparison.Ordinal) || !evidence.Contains("[probe-end] snapshot-only=true qualified-readiness=false", StringComparison.Ordinal)
|
|
|| !(Regex.IsMatch(evidence, @"\[task-read\] return=-?[0-9]+ errno=[0-9]+ port=0x[0-9a-f]+") || evidence.Contains("[task-read-unavailable] optional private symbol absent", StringComparison.Ordinal))) throw new Exception("Owned BSD snapshot/read-capability receipt failed: " + evidence);
|
|
var read = Regex.Match(evidence, @"\[task-read\] return=(-?[0-9]+) errno=([0-9]+) port=0x([0-9a-f]+)");
|
|
cases.Add(new { name = "owned-sleep", success = true, targetPid = target.Id, expectedParentPid = Environment.ProcessId, positive.ExitCode, outputBytes = Encoding.UTF8.GetByteCount(evidence), privateReadSymbolAvailable = read.Success, readReturn = read.Success ? int.Parse(read.Groups[1].Value) : (int?)null, readErrno = read.Success ? int.Parse(read.Groups[2].Value) : (int?)null, readPort = read.Success ? read.Groups[3].Value : null, targetIsApplePlatformBinary = true, targetArchitectureNotAsserted = true, recoveryPermissionProven = false });
|
|
var wrongParent = await Run(binary, [target.Id.ToString(), target.Id.ToString()], "owned-sleep-wrong-parent", requireSuccess: false);
|
|
var rejected = wrongParent.Stdout + wrongParent.Stderr;
|
|
if (wrongParent.ExitCode != 65 || !rejected.Contains("[ownership-rejected]", StringComparison.Ordinal)
|
|
|| !Regex.IsMatch(rejected, @"\[bsd\] pid=" + target.Id + " ppid=" + Environment.ProcessId + @"\b")
|
|
|| rejected.Contains("getpriority-role", StringComparison.Ordinal) || rejected.Contains("proc_pidinfo-task", StringComparison.Ordinal) || rejected.Contains("task_read_for_pid", StringComparison.Ordinal)) throw new Exception("Wrong parent reached role/task/Mach observation.");
|
|
cases.Add(new { name = "owned-sleep-wrong-parent", success = true, targetPid = target.Id, suppliedExpectedParentPid = target.Id, actualParentPid = Environment.ProcessId, wrongParent.ExitCode, furtherReadsReached = false });
|
|
var ownedPid = target.Id.ToString(); var parentPid = Environment.ProcessId.ToString();
|
|
string[][] invalid = [[], [ownedPid], ["", parentPid], ["0", parentPid], ["1", parentPid], ["-1", parentPid], ["+2", parentPid], ["2x", parentPid], [" 2", parentPid], ["999999999999999999999999", parentPid],
|
|
[ownedPid, ""], [ownedPid, "0"], [ownedPid, "1"], [ownedPid, "-1"], [ownedPid, "+2"], [ownedPid, "2x"], [ownedPid, " 2"], [ownedPid, "999999999999999999999999"], [ownedPid, parentPid, "extra"]];
|
|
for (var i = 0; i < invalid.Length; i++)
|
|
{
|
|
var result = await Run(binary, invalid[i], "invalid-pid-" + i, requireSuccess: false);
|
|
var text = result.Stdout + result.Stderr;
|
|
if (result.ExitCode != 64 || !text.Contains("[probe-entry]", StringComparison.Ordinal) || !text.Contains("[invalid-pid]", StringComparison.Ordinal) || text.Contains("proc_pidinfo", StringComparison.Ordinal)) throw new Exception("Invalid PID reached observation.");
|
|
cases.Add(new { name = "invalid-pid-" + i, success = true, result.ExitCode, nativeProcessObserved = false });
|
|
}
|
|
if (target.HasExited) throw new Exception("Own sleep exited unexpectedly during the snapshot.");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (!target.HasExited) target.Kill();
|
|
await target.WaitForExitAsync();
|
|
}
|
|
if (!target.HasExited) throw new Exception("Owned local child was not cleaned up.");
|
|
var manifest = new
|
|
{
|
|
success = x86Executed, buildVerified = true, selftestPassed = x86Executed, purpose = "Disposable offline native read-only process diagnostic; no CI runner requirement", sourcePath = source, sourceSha256 = sourceHash,
|
|
driverSha256 = Hash(File.ReadAllBytes(Path.Combine(folder, "ProbeDriver.cs"))), binaryPath = binary, binarySha256 = Hash(File.ReadAllBytes(binary)),
|
|
hostArchitecture = RuntimeInformation.OSArchitecture.ToString(), compiler = compiler.Stdout.Trim(), sdk, sdkVersion, minimumMacOS = "14.0", architecture = "x86_64", compilerArguments = build,
|
|
importedLibraries, signature = "ad-hoc; no entitlements", outputMaximumBytes = 32768, threadObservationMaximum = 32, frameWalkUsed = false, imageArrayReadUsed = false,
|
|
readOnlyTaskPortOnly = true, taskReadWeakImportVerified = true, taskReadReturnContract = "BSD int/errno", darwinRolePrioritySelector = 6, targetAndExpectedParentMandatory = true, bsdIdentityRequiredBeforeFurtherReads = true, snapshotIsReadiness = false, qualifiedReadiness = false,
|
|
x86Executed, executionUnavailable, localOwnedChildCleanedUp = target.HasExited, selftests = cases, guestExecuted = false, dockerExecuted = false, recoveryPermissionsProven = false,
|
|
primarySources = new[] { "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/kern/kern_resource.c#L691-L721", "https://github.com/apple-oss-distributions/xnu/blob/xnu-10063.141.1/bsd/sys/resource.h", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/xnu-10063.141.1/bsd/vm/vm_unix.c", "https://raw.githubusercontent.com/apple-oss-distributions/xnu/main/bsd/kern/syscalls.master" },
|
|
abiSourceIsExactGuestBinary = false, actualSdkExport = Path.Combine(sdk, "usr/lib/system/libsystem_kernel.tbd"), completedUtc = DateTimeOffset.UtcNow
|
|
};
|
|
File.WriteAllText(Path.Combine(output, "manifest.json"), JsonSerializer.Serialize(manifest, new JsonSerializerOptions { WriteIndented = true }));
|
|
Console.WriteLine(JsonSerializer.Serialize(manifest));
|
|
|
|
async Task<Result> Run(string executable, string[] arguments, string label, bool requireSuccess = true)
|
|
{
|
|
using var process = new Process { StartInfo = new ProcessStartInfo(executable) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false } };
|
|
foreach (var argument in arguments) process.StartInfo.ArgumentList.Add(argument);
|
|
process.Start();
|
|
async Task<string> Read(StreamReader reader)
|
|
{
|
|
var text = new StringBuilder(); var buffer = new char[2048];
|
|
while (await reader.ReadAsync(buffer) is var count && count != 0)
|
|
{
|
|
text.Append(buffer, 0, count);
|
|
if (Encoding.UTF8.GetByteCount(text.ToString()) > 32768) { if (!process.HasExited) process.Kill(); throw new Exception("Disposable probe/tool output exceeded32KiB."); }
|
|
}
|
|
return text.ToString();
|
|
}
|
|
var stdout = Read(process.StandardOutput); var stderr = Read(process.StandardError);
|
|
using var bound = new CancellationTokenSource(TimeSpan.FromSeconds(20));
|
|
try { await Task.WhenAll(stdout, stderr, process.WaitForExitAsync(bound.Token)); }
|
|
catch { if (!process.HasExited) process.Kill(); await process.WaitForExitAsync(); throw; }
|
|
var result = new Result(process.ExitCode, await stdout, await stderr);
|
|
File.WriteAllText(Path.Combine(output, label + ".stdout.log"), result.Stdout);
|
|
File.WriteAllText(Path.Combine(output, label + ".stderr.log"), result.Stderr);
|
|
if (requireSuccess && result.ExitCode != 0) throw new Exception(label + " failed: " + result.Stderr);
|
|
return result;
|
|
}
|
|
static string Hash(byte[] value) => Convert.ToHexStringLower(SHA256.HashData(value));
|
|
sealed record Result(int ExitCode, string Stdout, string Stderr);
|