using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; // Private control instrument, not a general-purpose process-tree/security boundary. // All termination authority comes from CreateProcessW or this invocation's unnamed job. public sealed class D2NativeLauncher { public sealed class Identity { public uint pid { get; set; } public string created { get; set; } public string exe { get; set; } } public sealed class Subject { public uint pid { get; set; } public string created { get; set; } public string exe { get; set; } public uint? native_exit { get; set; } public string state { get; set; } = "NOT_STARTED"; } public sealed class JobState { public bool assigned { get; set; } public bool membership_read { get; set; } public bool kill_on_close_read { get; set; } public uint? limit_flags { get; set; } public uint? active_processes { get; set; } } public sealed class Record { public int version { get; set; } public string label { get; set; } public string scope { get; set; } public Identity launcher { get; set; } public Subject subject { get; set; } public string completion_reason { get; set; } public string coverage { get; set; } public string termination { get; set; } public JobState job { get; set; } public string started_utc { get; set; } public string ended_utc { get; set; } public string error { get; set; } } public sealed class MembershipRow { public uint pid { get; set; } public string created { get; set; } public string exe { get; set; } public bool anchored { get; set; } public bool? in_job { get; set; } public bool query_ok { get; set; } public int? win32_error { get; set; } public string error { get; set; } } [StructLayout(LayoutKind.Sequential)] struct IO_COUNTERS { public ulong ReadOperationCount, WriteOperationCount, OtherOperationCount; public ulong ReadTransferCount, WriteTransferCount, OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] struct BASIC_LIMITS { public long PerProcessUserTimeLimit, PerJobUserTimeLimit; public uint LimitFlags; public UIntPtr MinimumWorkingSetSize, MaximumWorkingSetSize; public uint ActiveProcessLimit; public UIntPtr Affinity; public uint PriorityClass, SchedulingClass; } [StructLayout(LayoutKind.Sequential)] struct EXTENDED_LIMITS { public BASIC_LIMITS BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit, JobMemoryLimit, PeakProcessMemoryUsed, PeakJobMemoryUsed; } [StructLayout(LayoutKind.Sequential)] struct ACCOUNTING { public long TotalUserTime, TotalKernelTime, ThisPeriodTotalUserTime, ThisPeriodTotalKernelTime; public uint TotalPageFaultCount, TotalProcesses, ActiveProcesses, TotalTerminatedProcesses; } [StructLayout(LayoutKind.Sequential)] struct SECURITY_ATTRIBUTES { public uint nLength; public IntPtr lpSecurityDescriptor; [MarshalAs(UnmanagedType.Bool)] public bool bInheritHandle; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] struct STARTUPINFO { public uint cb; public string lpReserved, lpDesktop, lpTitle; public uint dwX, dwY, dwXSize, dwYSize, dwXCountChars, dwYCountChars, dwFillAttribute, dwFlags; public ushort wShowWindow, cbReserved2; public IntPtr lpReserved2, hStdInput, hStdOutput, hStdError; } [StructLayout(LayoutKind.Sequential)] struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public IntPtr lpAttributeList; } [StructLayout(LayoutKind.Sequential)] struct PROCESS_INFORMATION { public IntPtr hProcess, hThread; public uint dwProcessId, dwThreadId; } [StructLayout(LayoutKind.Sequential)] struct FILETIME { public uint Low, High; public long Value { get { return ((long)High << 32) | Low; } } } [StructLayout(LayoutKind.Sequential)] struct PROCESS_BASIC_INFORMATION { public IntPtr Reserved1, PebBaseAddress, Reserved2a, Reserved2b; public UIntPtr UniqueProcessId, InheritedFromUniqueProcessId; } [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr CreateJobObjectW(IntPtr attributes, string name); [DllImport("kernel32.dll", SetLastError = true)] static extern bool SetInformationJobObject(IntPtr job, int infoClass, ref EXTENDED_LIMITS info, uint length); [DllImport("kernel32.dll", EntryPoint = "QueryInformationJobObject", SetLastError = true)] static extern bool QueryLimits(IntPtr job, int infoClass, out EXTENDED_LIMITS info, uint length, IntPtr returnedLength); [DllImport("kernel32.dll", EntryPoint = "QueryInformationJobObject", SetLastError = true)] static extern bool QueryAccounting(IntPtr job, int infoClass, out ACCOUNTING info, uint length, IntPtr returnedLength); [DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); [DllImport("kernel32.dll", SetLastError = true)] static extern bool IsProcessInJob(IntPtr process, IntPtr job, out bool member); [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateJobObject(IntPtr job, uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] static extern bool TerminateProcess(IntPtr process, uint exitCode); [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr handle); [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetHandleInformation(IntPtr handle, out uint flags); [DllImport("kernel32.dll", SetLastError = true)] static extern uint GetProcessId(IntPtr process); [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetProcessTimes(IntPtr process, out FILETIME created, out FILETIME exited, out FILETIME kernel, out FILETIME user); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool QueryFullProcessImageNameW(IntPtr process, uint flags, StringBuilder image, ref uint length); [DllImport("kernel32.dll", SetLastError = true)] static extern bool GetExitCodeProcess(IntPtr process, out uint code); [DllImport("kernel32.dll", SetLastError = true)] static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds); [DllImport("kernel32.dll", SetLastError = true)] static extern uint ResumeThread(IntPtr thread); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern IntPtr CreateFileW(string path, uint access, uint share, ref SECURITY_ATTRIBUTES attributes, uint creation, uint flags, IntPtr template); [DllImport("kernel32.dll", SetLastError = true)] static extern bool InitializeProcThreadAttributeList(IntPtr list, int count, uint flags, ref UIntPtr size); [DllImport("kernel32.dll", SetLastError = true)] static extern bool UpdateProcThreadAttribute(IntPtr list, uint flags, UIntPtr attribute, IntPtr value, UIntPtr size, IntPtr previous, IntPtr returned); [DllImport("kernel32.dll")] static extern void DeleteProcThreadAttributeList(IntPtr list); [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool CreateProcessW(string application, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, IntPtr environment, string directory, ref STARTUPINFOEX startup, out PROCESS_INFORMATION process); [DllImport("kernel32.dll", SetLastError = true)] static extern IntPtr OpenProcess(uint access, bool inheritHandle, uint pid); [DllImport("ntdll.dll")] static extern int NtQueryInformationProcess(IntPtr process, int infoClass, out PROCESS_BASIC_INFORMATION info, uint size, out uint returned); [DllImport("kernel32.dll")] static extern void ExitProcess(uint code); readonly Record record; readonly Stopwatch clock; readonly long totalMs, workMs; readonly string recordFile, stopFile, fault; readonly FileStream events; readonly UTF8Encoding utf8 = new UTF8Encoding(false); IntPtr job, process, thread, input, output, errorOutput, queriedSupervisor, queriedBrain; bool resumed, directExited, cleanupFailed, dispositionConfirmed, membershipPublished; string ownedHome, lastMembershipRequestId, lastMembershipRequestText; D2NativeLauncher(int seconds, string path, string stop, string control, Stopwatch elapsed, FileStream journal, string initial) { clock = elapsed; totalMs = (long)seconds * 1000; // Reserve part of the single bound for termination + reference release + accounting. workMs = totalMs - Math.Min(3000L, Math.Max(250L, totalMs / 5)); recordFile = path; stopFile = stop; fault = control; events = journal; record = JsonSerializer.Deserialize(initial); if (record == null || record.subject == null || record.job == null) throw new InvalidDataException("INVALID_INITIAL_RECORD"); } public static int Run(string label, int seconds, string recordFile, string argsFile, string exe, string outFile, string errFile, string admission, string scope, string stopFile, string environmentFile, string fault, Stopwatch clock, FileStream events, string initialJson) { var owner = new D2NativeLauncher(seconds, recordFile, stopFile, fault, clock, events, initialJson); return owner.Execute(argsFile, exe, outFile, errFile, admission, scope, environmentFile); } static Win32Exception NativeError(string operation) { int code = Marshal.GetLastWin32Error(); return new Win32Exception(code, operation + " failed; win32_error=" + code.ToString(CultureInfo.InvariantCulture)); } void AddError(Exception error) { record.error = record.error == null ? error.ToString() : record.error + "\n" + error; } void RequireWorkTime() { if (clock.ElapsedMilliseconds >= workMs) throw new TimeoutException("SETUP_EXHAUSTED_OPERATION_BOUND"); } uint WaitSlice() { return (uint)Math.Max(0L, Math.Min(25L, totalMs - clock.ElapsedMilliseconds)); } // MSVC/.NET Windows argument encoding. Always quote, doubling backslashes only // before a quote or the closing quote. No shell expansion or newline splitting. static void AppendArgument(StringBuilder target, string arg) { if (arg == null || arg.IndexOf('\0') >= 0) throw new InvalidDataException("NUL_OR_NULL_ARGUMENT"); target.Append('"'); int slashes = 0; foreach (char ch in arg) { if (ch == '\\') { slashes++; continue; } if (ch == '"') target.Append('\\', slashes * 2 + 1); else target.Append('\\', slashes); slashes = 0; target.Append(ch); } target.Append('\\', slashes * 2); target.Append('"'); } static string ReadText(string path) { // Atomic publishers need delete sharing; never hold a read reference between polls. using (var file = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) using (var reader = new StreamReader(file, new UTF8Encoding(false, true), true)) return reader.ReadToEnd(); } ProcessStartInfo ReadStartInfo(string exe, string argsFile, string environmentFile) { if (!Path.IsPathFullyQualified(exe) || exe.IndexOf('\0') >= 0 || exe.IndexOf('"') >= 0) throw new InvalidDataException("ABSOLUTE_EXECUTABLE_REQUIRED"); var psi = new ProcessStartInfo(exe) { UseShellExecute = false }; using (var document = JsonDocument.Parse(ReadText(argsFile))) { if (document.RootElement.ValueKind != JsonValueKind.Array) throw new InvalidDataException("ARGV_MUST_BE_JSON_STRING_ARRAY"); foreach (var arg in document.RootElement.EnumerateArray()) { if (arg.ValueKind != JsonValueKind.String) throw new InvalidDataException("ARGV_ELEMENT_MUST_BE_STRING"); string value = arg.GetString(); if (value.IndexOf('\0') >= 0) throw new InvalidDataException("NUL_ARGUMENT_NOT_REPRESENTABLE"); psi.ArgumentList.Add(value); } } if (!String.IsNullOrEmpty(environmentFile)) { using (var document = JsonDocument.Parse(ReadText(environmentFile))) { if (document.RootElement.ValueKind != JsonValueKind.Object) throw new InvalidDataException("ENVIRONMENT_MUST_BE_JSON_OBJECT"); var names = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var property in document.RootElement.EnumerateObject()) { string name = property.Name; if (!names.Add(name) || name.Length == 0 || name.IndexOf('=') >= 0 || name.IndexOf('\0') >= 0 || property.Value.ValueKind != JsonValueKind.String) throw new InvalidDataException("INVALID_ENVIRONMENT_ENTRY: " + name); string value = property.Value.GetString(); if (value.IndexOf('\0') >= 0) throw new InvalidDataException("NUL_ENVIRONMENT_VALUE: " + name); if (String.Equals(name, "OWL_SESSION_ID", StringComparison.OrdinalIgnoreCase) || String.Equals(name, "SPT_AGENT_ID", StringComparison.OrdinalIgnoreCase) || String.Equals(name, "SPT_ENDPOINT_ID", StringComparison.OrdinalIgnoreCase) || String.Equals(name, "SPT_SESSION_ID", StringComparison.OrdinalIgnoreCase)) throw new InvalidDataException("SESSION_GUARD_OVERRIDE_REFUSED: " + name); psi.Environment[name] = value; if (String.Equals(name, "SPT_HOME", StringComparison.OrdinalIgnoreCase)) { if (!Path.IsPathFullyQualified(value)) throw new InvalidDataException("SPT_HOME_MUST_BE_ABSOLUTE"); ownedHome = Path.GetFullPath(value).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); } } } } return psi; } static IntPtr EnvironmentBlock(ProcessStartInfo psi) { var names = new List(psi.Environment.Keys); names.Sort(StringComparer.OrdinalIgnoreCase); var block = new StringBuilder(); foreach (string name in names) { string value = psi.Environment[name]; if (value == null || name.IndexOf('\0') >= 0 || value.IndexOf('\0') >= 0) throw new InvalidDataException("INVALID_INHERITED_ENVIRONMENT"); block.Append(name).Append('=').Append(value).Append('\0'); } block.Append('\0'); // StringToHGlobalUni adds the second NUL even for an empty inherited environment. return Marshal.StringToHGlobalUni(block.ToString()); } void CreateJob() { // Real C# null: PowerShell string marshaling of $null is not native NULL. job = CreateJobObjectW(IntPtr.Zero, null); if (job == IntPtr.Zero) throw NativeError("CreateJobObjectW"); uint handleFlags; if (!GetHandleInformation(job, out handleFlags)) throw NativeError("GetHandleInformation(job)"); if ((handleFlags & 1) != 0) throw new InvalidOperationException("JOB_HANDLE_IS_INHERITABLE"); var basic = new BASIC_LIMITS { LimitFlags = 0x2000 }; var requested = new EXTENDED_LIMITS { BasicLimitInformation = basic }; if (!SetInformationJobObject(job, 9, ref requested, (uint)Marshal.SizeOf())) throw NativeError("SetInformationJobObject"); EXTENDED_LIMITS actual; if (!QueryLimits(job, 9, out actual, (uint)Marshal.SizeOf(), IntPtr.Zero)) throw NativeError("QueryInformationJobObject(limits)"); record.job.limit_flags = actual.BasicLimitInformation.LimitFlags; record.job.kill_on_close_read = record.job.limit_flags == 0x2000; Publish("limits_read"); if (!record.job.kill_on_close_read) throw new InvalidOperationException("JOB_LIMIT_READBACK_NOT_EXACTLY_0x2000"); } static IntPtr Stdio(string path, bool read) { var attributes = new SECURITY_ATTRIBUTES { nLength = (uint)Marshal.SizeOf(), bInheritHandle = true }; IntPtr handle = CreateFileW(path, read ? 0x80000000u : 0x40000000u, read ? 3u : 1u, ref attributes, read ? 3u : 1u, 0x80, IntPtr.Zero); if (handle == new IntPtr(-1)) throw NativeError("CreateFileW(" + path + ")"); return handle; } void CreateSubject(ProcessStartInfo psi, string outFile, string errFile) { input = Stdio("NUL", true); output = Stdio(outFile, false); errorOutput = Stdio(errFile, false); IntPtr attributeList = IntPtr.Zero, handleList = IntPtr.Zero, environment = IntPtr.Zero; bool initialized = false; try { UIntPtr size = UIntPtr.Zero; bool first = InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref size); int firstError = Marshal.GetLastWin32Error(); if (first || firstError != 122 || size == UIntPtr.Zero) throw new Win32Exception(firstError, "InitializeProcThreadAttributeList(size) did not return required buffer size"); attributeList = Marshal.AllocHGlobal(checked((int)size.ToUInt64())); if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref size)) throw NativeError("InitializeProcThreadAttributeList"); initialized = true; handleList = Marshal.AllocHGlobal(IntPtr.Size * 3); Marshal.WriteIntPtr(handleList, 0, input); Marshal.WriteIntPtr(handleList, IntPtr.Size, output); Marshal.WriteIntPtr(handleList, IntPtr.Size * 2, errorOutput); if (!UpdateProcThreadAttribute(attributeList, 0, new UIntPtr(0x00020002u), handleList, new UIntPtr((uint)(IntPtr.Size * 3)), IntPtr.Zero, IntPtr.Zero)) throw NativeError("UpdateProcThreadAttribute(HANDLE_LIST)"); var startup = new STARTUPINFOEX(); var basic = new STARTUPINFO { cb = (uint)Marshal.SizeOf(), dwFlags = 0x100, hStdInput = input, hStdOutput = output, hStdError = errorOutput }; startup.StartupInfo = basic; startup.lpAttributeList = attributeList; var command = new StringBuilder(); AppendArgument(command, psi.FileName); foreach (string arg in psi.ArgumentList) { command.Append(' '); AppendArgument(command, arg); } if (command.Length >= 32767) throw new InvalidDataException("NATIVE_COMMAND_LINE_TOO_LONG"); environment = EnvironmentBlock(psi); RequireWorkTime(); PROCESS_INFORMATION created; if (!CreateProcessW(psi.FileName, command, IntPtr.Zero, IntPtr.Zero, true, 0x00000004u | 0x00000400u | 0x00080000u | 0x08000000u, environment, null, ref startup, out created)) throw NativeError("CreateProcessW(CREATE_SUSPENDED)"); // Take ownership before any fallible identity or evidence operation. process = created.hProcess; thread = created.hThread; record.subject.pid = created.dwProcessId; record.subject.state = "SUSPENDED"; Identity identity = ReadIdentity(process); if (identity.pid != created.dwProcessId) throw new InvalidOperationException("CREATED_HANDLE_PID_MISMATCH"); record.subject.created = identity.created; record.subject.exe = identity.exe; Publish("created_suspended"); } finally { if (environment != IntPtr.Zero) Marshal.FreeHGlobal(environment); if (initialized) DeleteProcThreadAttributeList(attributeList); if (attributeList != IntPtr.Zero) Marshal.FreeHGlobal(attributeList); if (handleList != IntPtr.Zero) Marshal.FreeHGlobal(handleList); } } static Identity ReadIdentity(IntPtr handle) { uint pid = GetProcessId(handle); if (pid == 0) throw NativeError("GetProcessId"); FILETIME created, exited, kernel, user; if (!GetProcessTimes(handle, out created, out exited, out kernel, out user)) throw NativeError("GetProcessTimes"); var image = new StringBuilder(32768); uint length = (uint)image.Capacity; if (!QueryFullProcessImageNameW(handle, 0, image, ref length)) throw NativeError("QueryFullProcessImageNameW"); return new Identity { pid = pid, created = DateTime.FromFileTimeUtc(created.Value).ToString("o"), exe = image.ToString() }; } bool ObserveExit(uint waitMilliseconds, bool terminated) { if (directExited) return true; uint result = WaitForSingleObject(process, waitMilliseconds); if (result == 258) return false; if (result == 0xffffffff) throw NativeError("WaitForSingleObject(subject)"); if (result != 0) throw new InvalidOperationException("UNEXPECTED_SUBJECT_WAIT: " + result); uint code; if (!GetExitCodeProcess(process, out code)) throw NativeError("GetExitCodeProcess"); // Wait, not STILL_ACTIVE (259), proves exit. 259 is also a legal native exit. record.subject.native_exit = code; record.subject.state = terminated ? "TERMINATED" : "EXITED"; directExited = true; Publish(terminated ? "subject_terminated" : "subject_exited"); return true; } void CloseOwned(ref IntPtr handle, string name) { if (handle == IntPtr.Zero) return; if (!CloseHandle(handle)) throw NativeError("CloseHandle(" + name + ")"); handle = IntPtr.Zero; } void SafeClose(ref IntPtr handle, string name) { try { CloseOwned(ref handle, name); } catch (Exception error) { cleanupFailed = true; AddError(error); } } void Evidence(string name) { try { Publish(name); } catch (Exception error) { cleanupFailed = true; AddError(error); } } void Shutdown() { if (process == IntPtr.Zero && record.subject.pid == 0) return; bool terminationRequested = false; try { if (!directExited) ObserveExit(0, false); } catch (Exception error) { cleanupFailed = true; AddError(error); } try { uint exitCode = record.completion_reason == "setup_refused" ? 127u : 124u; if (record.job.assigned) { if (!TerminateJobObject(job, exitCode)) throw NativeError("TerminateJobObject"); terminationRequested = true; } else if (!directExited) { if (!TerminateProcess(process, exitCode)) throw NativeError("TerminateProcess(owned suspended subject)"); terminationRequested = true; } Evidence("termination_requested"); } catch (Exception error) { cleanupFailed = true; AddError(error); } try { while (!directExited) { if (ObserveExit(WaitSlice(), terminationRequested)) break; if (clock.ElapsedMilliseconds >= totalMs) break; } if (!directExited) throw new TimeoutException("DIRECT_SUBJECT_EXIT_UNCONFIRMED_WITHIN_BOUND"); } catch (Exception error) { cleanupFailed = true; record.subject.state = "UNREADABLE"; AddError(error); } // Outstanding process references can keep ActiveProcesses nonzero. Observe, // close all subject references, THEN ask the kernel for whole-job accounting. SafeClose(ref queriedBrain, "read-only-brain"); SafeClose(ref queriedSupervisor, "read-only-supervisor"); SafeClose(ref thread, "primary-thread"); SafeClose(ref process, "subject"); SafeClose(ref input, "stdin"); SafeClose(ref output, "stdout"); SafeClose(ref errorOutput, "stderr"); Evidence(process == IntPtr.Zero && thread == IntPtr.Zero && queriedBrain == IntPtr.Zero && queriedSupervisor == IntPtr.Zero ? "subject_released" : "subject_release_failed"); if (record.job.assigned) { try { while (true) { ACCOUNTING accounting; // -2 is not a job (NULL would query the caller's enclosing job). IntPtr queriedJob = fault == "accounting" ? new IntPtr(-2) : job; if (!QueryAccounting(queriedJob, 1, out accounting, (uint)Marshal.SizeOf(), IntPtr.Zero)) throw NativeError("QueryInformationJobObject(accounting)"); if (fault == "accounting") throw new InvalidOperationException("FAULT_HANDLE_UNEXPECTEDLY_ACCEPTED"); record.job.active_processes = accounting.ActiveProcesses; Evidence("accounting_read"); if (accounting.ActiveProcesses == 0) break; if (clock.ElapsedMilliseconds >= totalMs) throw new TimeoutException("JOB_ACTIVE_PROCESSES_NOT_ZERO_WITHIN_BOUND"); Thread.Sleep((int)WaitSlice()); } } catch (Exception error) { cleanupFailed = true; record.job.active_processes = null; AddError(error); Evidence("accounting_failed"); } } dispositionConfirmed = !cleanupFailed && directExited && (!record.job.assigned || record.job.active_processes == 0); } int Execute(string argsFile, string exe, string outFile, string errFile, string admission, string scope, string environmentFile) { try { RequireWorkTime(); ProcessStartInfo psi = ReadStartInfo(exe, argsFile, environmentFile); CreateJob(); RequireWorkTime(); CreateSubject(psi, outFile, errFile); RequireWorkTime(); if (!AssignProcessToJobObject(fault == "assignment" ? new IntPtr(-2) : job, process)) throw NativeError("AssignProcessToJobObject"); if (fault == "assignment") throw new InvalidOperationException("FAULT_HANDLE_UNEXPECTEDLY_ACCEPTED"); record.job.assigned = true; record.coverage = "PARTIAL"; Publish("assigned"); bool member; if (!IsProcessInJob(process, job, out member)) throw NativeError("IsProcessInJob(subject)"); record.job.membership_read = member; if (!member) throw new InvalidOperationException("ASSIGNED_SUBJECT_NOT_IN_JOB"); record.coverage = admission == "process-tree" ? "COMPLETE" : "PARTIAL"; Publish("membership_read"); RequireWorkTime(); uint previous = ResumeThread(thread); if (previous == 0xffffffff) throw NativeError("ResumeThread"); resumed = true; record.subject.state = "RUNNING"; if (previous != 1) throw new InvalidOperationException("UNEXPECTED_PRIMARY_SUSPEND_COUNT: " + previous); Publish("resumed"); if (fault == "crash-after-resume") ExitProcess(198); CloseOwned(ref thread, "primary-thread"); CloseOwned(ref input, "stdin"); CloseOwned(ref output, "stdout"); CloseOwned(ref errorOutput, "stderr"); while (true) { bool exited = directExited || ObserveExit(0, false); if (scope == "step" && exited) { record.completion_reason = "subject_exit"; break; } if (scope == "run") { PollMembership(); if (File.Exists(stopFile)) { record.completion_reason = "stop_requested"; break; } } if (clock.ElapsedMilliseconds >= workMs) { record.completion_reason = "deadline_expired"; break; } Thread.Sleep((int)Math.Min(25L, Math.Max(1L, workMs - clock.ElapsedMilliseconds))); } } catch (TimeoutException error) { record.completion_reason = "deadline_expired"; AddError(error); } catch (Exception error) { record.completion_reason = resumed ? "confirmation_failed" : "setup_refused"; if (resumed) cleanupFailed = true; AddError(error); } finally { try { Shutdown(); } catch (Exception error) { cleanupFailed = true; AddError(error); } // Each close is independent: one error must not bypass another custody handle. SafeClose(ref queriedBrain, "read-only-brain-final"); SafeClose(ref queriedSupervisor, "read-only-supervisor-final"); SafeClose(ref thread, "primary-thread-final"); SafeClose(ref process, "subject-final"); SafeClose(ref input, "stdin-final"); SafeClose(ref output, "stdout-final"); SafeClose(ref errorOutput, "stderr-final"); SafeClose(ref job, "job"); Evidence(job == IntPtr.Zero ? "job_closed" : "job_close_failed"); } if (cleanupFailed) { record.termination = "UNREADABLE"; record.completion_reason = "confirmation_failed"; } else if (dispositionConfirmed) record.termination = "CONFIRMED_GONE"; record.ended_utc = DateTime.UtcNow.ToString("o"); try { Publish("final"); } catch (Exception error) { Console.Error.WriteLine("FINAL_STATUS_WRITE_FAILED: " + error); return 126; } if (record.error != null) Console.Error.WriteLine(record.error); if (record.completion_reason == "confirmation_failed") return 126; if (record.completion_reason == "setup_refused") return 127; if (record.completion_reason == "deadline_expired") return 124; if (record.completion_reason == "stop_requested") return record.termination == "CONFIRMED_GONE" ? 0 : 126; if (record.completion_reason == "subject_exit" && record.subject.native_exit.HasValue) return unchecked((int)record.subject.native_exit.Value); return 126; } void AtomicJson(string path, object value, bool replace) { string temporary = path + ".tmp." + Guid.NewGuid().ToString("N"); byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(value); using (var file = new FileStream(temporary, FileMode.CreateNew, FileAccess.Write, FileShare.Read)) { file.Write(bytes, 0, bytes.Length); file.Flush(true); } while (true) { try { if (replace) File.Replace(temporary, path, null); else File.Move(temporary, path); break; } catch (IOException error) { int native = error.HResult & 0xffff; if ((native != 32 && native != 33) || clock.ElapsedMilliseconds >= totalMs) throw; Thread.Sleep((int)WaitSlice()); } } // Failed unpublished temps are preserved, never mistaken for current status. } void Publish(string name) { // Journal first: a crash between publication steps leaves evidence ahead of status, // never a claimed state without its chronology. Append snapshots, not mutable refs. byte[] bytes = utf8.GetBytes(JsonSerializer.Serialize(new { @event = name, elapsed_ms = clock.ElapsedMilliseconds, record = record }) + "\n"); events.Write(bytes, 0, bytes.Length); events.Flush(true); AtomicJson(recordFile, record, true); } static bool SameAnchor(string expected, string actual) { DateTime a = DateTime.Parse(expected, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind).ToUniversalTime(); DateTime b = DateTime.Parse(actual, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind).ToUniversalTime(); return Math.Abs((a - b).Ticks) <= 10; } MembershipRow Membership(IntPtr handle, Identity expected, Identity parent) { var row = new MembershipRow { pid = expected.pid, created = expected.created }; try { Identity actual = ReadIdentity(handle); row.created = actual.created; row.exe = actual.exe; row.anchored = actual.pid == expected.pid && SameAnchor(expected.created, actual.created); if (!row.anchored) throw new InvalidOperationException("IDENTITY_ANCHOR_MISMATCH"); if (!String.Equals(actual.exe, record.subject.exe, StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("DAEMON_EXECUTABLE_MISMATCH"); DateTime born = DateTime.Parse(actual.created, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); DateTime earliest = DateTime.Parse(parent == null ? record.subject.created : parent.created, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind); if (born < earliest) throw new InvalidOperationException("MEMBERSHIP_PROCESS_PREDATES_CUSTODY"); if (parent != null) { PROCESS_BASIC_INFORMATION basic; uint returned; int status = NtQueryInformationProcess(handle, 0, out basic, (uint)Marshal.SizeOf(), out returned); if (status != 0) throw new InvalidOperationException("NtQueryInformationProcess failed; ntstatus=0x" + status.ToString("X8")); if (basic.InheritedFromUniqueProcessId.ToUInt64() != parent.pid) throw new InvalidOperationException("BRAIN_PARENT_NOT_ANCHORED_SUPERVISOR"); } uint wait = WaitForSingleObject(handle, 0); if (wait == 0xffffffff) throw NativeError("WaitForSingleObject(membership)"); if (wait != 258) throw new InvalidOperationException("MEMBERSHIP_SUBJECT_NOT_LIVE"); bool member; bool ok = IsProcessInJob(handle, job, out member); int nativeError = ok ? 0 : Marshal.GetLastWin32Error(); row.query_ok = ok; row.win32_error = nativeError; if (!ok) throw new Win32Exception(nativeError, "IsProcessInJob(query) failed"); row.in_job = member; } catch (Exception error) { row.error = error.ToString(); var native = error as Win32Exception; if (native != null) row.win32_error = native.NativeErrorCode; } return row; } static Identity RequestIdentity(JsonElement element) { if (element.ValueKind != JsonValueKind.Object) throw new InvalidDataException("MEMBERSHIP_IDENTITY_REQUIRED"); uint pid = element.GetProperty("pid").GetUInt32(); string created = element.GetProperty("created").GetString(); if (pid == 0 || String.IsNullOrEmpty(created)) throw new InvalidDataException("MEMBERSHIP_ANCHOR_REQUIRED"); return new Identity { pid = pid, created = created }; } MembershipRow OpenMembership(ref IntPtr handle, Identity expected, Identity parent) { // QUERY_INFORMATION + SYNCHRONIZE only: no termination, duplication, VM, // assignment, or other authority over a PID supplied by a query. handle = OpenProcess(0x00000400u | 0x00100000u, false, expected.pid); if (handle != IntPtr.Zero) return Membership(handle, expected, parent); var failure = NativeError("OpenProcess(membership readback)"); return new MembershipRow { pid = expected.pid, created = expected.created, win32_error = failure.NativeErrorCode, error = failure.ToString() }; } void PollMembership() { string requestPath = recordFile + ".membership-request.json"; if (!File.Exists(requestPath)) return; string text = ReadText(requestPath); if (text == lastMembershipRequestText) return; lastMembershipRequestText = text; MembershipRow supervisor = null, brain = null; string queryError = null, requestId = null; bool valid = false; try { using (var document = JsonDocument.Parse(text)) { var request = document.RootElement; requestId = request.GetProperty("request_id").GetString(); if (String.IsNullOrWhiteSpace(requestId)) throw new InvalidDataException("MEMBERSHIP_REQUEST_ID_REQUIRED"); if (requestId == lastMembershipRequestId) return; lastMembershipRequestId = requestId; if (ownedHome == null) throw new InvalidOperationException("MEMBERSHIP_REQUIRES_EXPLICIT_ENVIRONMENTFILE_SPT_HOME"); string requestedHome = request.GetProperty("home").GetString(); if (!Path.IsPathFullyQualified(requestedHome) || !String.Equals(ownedHome, Path.GetFullPath(requestedHome).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException("MEMBERSHIP_HOME_NOT_OWNED_ENVIRONMENT"); Identity expectedSupervisor = RequestIdentity(request.GetProperty("supervisor")); if (uint.Parse(ReadText(Path.Combine(ownedHome, "daemon.pid")).Trim(), CultureInfo.InvariantCulture) != expectedSupervisor.pid) throw new InvalidOperationException("HOME_DAEMON_PID_NOT_SUPERVISOR"); supervisor = OpenMembership(ref queriedSupervisor, expectedSupervisor, null); if (supervisor.error != null || supervisor.in_job != true) throw new InvalidOperationException("SUPERVISOR_MEMBERSHIP_UNPROVEN"); JsonElement brainElement; if (request.TryGetProperty("brain", out brainElement) && brainElement.ValueKind != JsonValueKind.Null) { Identity expectedBrain = RequestIdentity(brainElement); using (var ready = JsonDocument.Parse(ReadText(Path.Combine(ownedHome, "brain.ready")))) { if (ready.RootElement.GetProperty("pid").GetUInt32() != expectedBrain.pid) throw new InvalidOperationException("HOME_BRAIN_READY_PID_MISMATCH"); } var actualSupervisor = new Identity { pid = supervisor.pid, created = supervisor.created, exe = supervisor.exe }; brain = OpenMembership(ref queriedBrain, expectedBrain, actualSupervisor); if (brain.error != null || brain.in_job != true) record.coverage = "PARTIAL"; // Re-read supervisor liveness while both identity references are held. uint supervisorWait = WaitForSingleObject(queriedSupervisor, 0); if (supervisorWait == 0xffffffff) throw NativeError("WaitForSingleObject(supervisor recheck)"); if (supervisorWait != 258) throw new InvalidOperationException("SUPERVISOR_EXITED_DURING_READBACK"); valid = brain.error == null && brain.in_job == true && brain.anchored; } } } catch (Exception error) { queryError = error.ToString(); record.coverage = "PARTIAL"; } finally { // Keep any failed-close reference in the owner for final cleanup, never lose it. SafeClose(ref queriedBrain, "read-only-brain"); SafeClose(ref queriedSupervisor, "read-only-supervisor"); } if (cleanupFailed) { valid = false; queryError = queryError ?? "MEMBERSHIP_HANDLE_RELEASE_FAILED"; } AtomicJson(recordFile + ".membership.json", new { version = 1, request_id = requestId, home = ownedHome, measured_utc = DateTime.UtcNow.ToString("o"), valid = valid, supervisor = supervisor, brain = brain, error = queryError }, membershipPublished); membershipPublished = true; Publish("membership_query"); if (cleanupFailed) throw new InvalidOperationException("MEMBERSHIP_HANDLE_RELEASE_FAILED"); } }