summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-13 23:20:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-13 23:20:20 +0900
commite07183ee6d3e57da325213e898cdcd6687320f53 (patch)
treea05a71ec3ed419d54f450a0105ec22973c66a66b
parent9337ac3e5edd42bb17f5e2ade6c2de02c7ef576d (diff)
test(security): close on-demand elevation acceptance gaps
Add an actual-process acceptance harness for the administrative Named Pipe boundary. Exercise production pipe creation and peer PID inspection, remote-form client rejection, Task and protocol identity mismatch, duplicate network application, oversized frames, and an intentional helper-crash recovery point without adding test-only operations to the privileged protocol. Add a reusable PowerShell attack-matrix runner that preserves the helper parent-image check and drives the self-contained published helper through real UAC. Refactor the client pipe setup into internal diagnostic hooks while leaving the production DACL, PIPE_REJECT_REMOTE_CLIENTS, PID comparison, and helper lifetime behavior unchanged. Record the completed UAC rejection, HTTP MCP server-continuity, helper crash, startup Recovery rejection/success, loopback restoration, WFP residue, and regression evidence. Promote the three on-demand elevation traceability rows from PARTIAL to PASS and capture the 80-test Windows Security/Recovery result and final Bundle checksum. Verified: Release build warning/error 0; Core 105 PASS; Planner 10 PASS; administrative boundary 4 PASS; Windows Security/Recovery 80 PASS with skip 0; actual IPC attack matrix PASS; actual Task-time and Recovery-time UAC rejection PASS; Journal/backup/main/helper residue 0; fresh self-contained Bundle show PASS.
-rw-r--r--AgentSandboxMcp.slnx1
-rw-r--r--doc/AuditReport.md4
-rw-r--r--plans/RequirementTraceability.md11
-rw-r--r--scripts/Verify-AdminIpcAttackMatrix.ps132
-rw-r--r--src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs35
-rw-r--r--tests/Agent.Admin.AcceptanceHarness/Agent.Admin.AcceptanceHarness.csproj12
-rw-r--r--tests/Agent.Admin.AcceptanceHarness/Program.cs183
7 files changed, 262 insertions, 16 deletions
diff --git a/AgentSandboxMcp.slnx b/AgentSandboxMcp.slnx
index 33c59f0..276acf6 100644
--- a/AgentSandboxMcp.slnx
+++ b/AgentSandboxMcp.slnx
@@ -9,6 +9,7 @@
<Project Path="src/Agent.Windows/Agent.Windows.csproj" />
</Folder>
<Folder Name="/tests/">
+ <Project Path="tests/Agent.Admin.AcceptanceHarness/Agent.Admin.AcceptanceHarness.csproj" />
<Project Path="tests/Agent.Mcp.TestHost/Agent.Mcp.TestHost.csproj" />
<Project Path="tests/Agent.Core.Tests/Agent.Core.Tests.csproj" />
<Project Path="tests/Agent.Planner.Tests/Agent.Planner.Tests.csproj" />
diff --git a/doc/AuditReport.md b/doc/AuditReport.md
index 2155236..d90c1d0 100644
--- a/doc/AuditReport.md
+++ b/doc/AuditReport.md
@@ -2,7 +2,9 @@
> 2026-08-13オンデマンド昇格改定: 以下の管理者環境証跡は常時管理者方式の過去commitに対する履歴である。`asInvoker` main、Task単位helper、Named Pipe、dynamic WFP、persistent loopback Recoveryは最新commitで再検証するまでPASS根拠としない。
-2026-08-13 working treeでmainを`asInvoker`、昇格hostを`requireAdministrator/uiAccess=false`とし、WFP/loopback mutationを固定`admin/` hostへ分離した。通常tokenのHTTP/stdio MCPから実Taskを実行し、初回`process_run`時のUAC、logon SID DACL Named Pipe、LPAC Process Tree、dynamic WFP、ACL/WFP cleanupを確認した。Windows suiteは昇格子Processだけで79件PASS、Core 105件、Planner 10件、administrative boundary 4件がfail/skip 0。実UAC拒否、hostile peer実Process matrix、実helper crashからのstartup Recoveryは未再検証のため、TraceabilityではPARTIALを維持する。
+2026-08-13 working treeでmainを`asInvoker`、昇格hostを`requireAdministrator/uiAccess=false`とし、WFP/loopback mutationを固定`admin/` hostへ分離した。通常tokenのHTTP/stdio MCPから実Taskを実行し、初回`process_run`時のUAC、logon SID DACL Named Pipe、LPAC Process Tree、dynamic WFP、ACL/WFP cleanupを確認した。Windows suiteは昇格子Processだけで79件PASS、Core 105件、Planner 10件、administrative boundary 4件がfail/skip 0。
+
+残っていた異常系も同日のactual-Process受入で再検証した。Task時UACを実際に拒否するとHTTP MCP Taskだけが`failed/sandbox.elevation_cancelled`となり、同一Server PIDで後続`tools/list`が成功した。固定Pipeではlocal peer PID取得、remote-form接続拒否、別Task ID、別protocol version、二重apply、過大message拒否を実helperで確認した。network適用後のhelper強制終了ではApplied Journalを保持し、startup RecoveryのUAC拒否でbind前exit 2、次回承認で`recovery.end=clean`となった。Windows標準`CheckNetIsolation`と昇格`netsh wfp show state`でloopback exemption、WFP filter/sublayer、Journal、helper Processの残留0を確認した。
> 2026-08-13 schema version 2追補: YAMLを唯一のSandbox desired stateとし、個別のExecutable Approval/Sandbox Resolution操作を廃止した。CLIはcommand省略のServer起動、`apply`、`show`に限定し、確認結果はconfig別Applied Configuration Storeへ一括保存する。本追補より前の管理者・publish証跡は旧schemaの履歴であり、schema version 2のPASS根拠ではない。最終検証はRelease build warning/error 0、Core 104件、Planner 10件、Windows Security/Recovery 56件、README operational 19件がfail/skip 0。publish HTTP MCPも`deepseek-v4-flash:cloud`でcompleted 3/3となり、cmd→cmd/curl/venv Python、Network none、終了後残留0を確認した。
diff --git a/plans/RequirementTraceability.md b/plans/RequirementTraceability.md
index 241866a..f0f47f2 100644
--- a/plans/RequirementTraceability.md
+++ b/plans/RequirementTraceability.md
@@ -4,9 +4,9 @@
| Requirement | 実装 | 検証 | 状態 |
| --- | --- | --- | --- |
-| 非昇格Server / Task時昇格 | main/admin manifest、`WindowsPlatformCheck`、遅延`process_run`初期化 | embedded manifest、通常token stdio/HTTP、実UAC、file-only無昇格、terminal error unit | PARTIAL — 実UAC拒否操作は未再検証 |
-| WFP / loopback Process境界 | `Agent.Admin.Host`、固定Named Pipe protocol | static call graph、strict schema、PID検証、none実HTTP、Windows suite | PARTIAL — hostile peer/remote pipeの実Process matrixは未再検証 |
-| Recovery | 既存`TaskResourceJournal`と通常/昇格Recovery振分け | Windows crash matrix、legacy `wfp.dynamic`、loopback helper unit | PARTIAL — 実helper crashと実UAC startup Recoveryは未再検証 |
+| 非昇格Server / Task時昇格 | main/admin manifest、`WindowsPlatformCheck`、遅延`process_run`初期化 | embedded manifest、通常token stdio/HTTP、実UAC、file-only無昇格、terminal error unit、2026-08-13実UAC拒否HTTP MCP | PASS |
+| WFP / loopback Process境界 | `Agent.Admin.Host`、固定Named Pipe protocol | static call graph、strict schema、PID検証、none実HTTP、Windows suite、actual-Process IPC attack matrix | PASS |
+| Recovery | 既存`TaskResourceJournal`と通常/昇格Recovery振分け | Windows crash matrix、legacy `wfp.dynamic`、実helper強制終了、startup Recovery拒否/成功、WFP/loopback残留0 | PASS |
Sandbox Configuration schema version 2後の現行実装を対象とする。2026-08-13以前の結果は`plans/progress/`と本書末尾に履歴として保持するが、schema version 2の現行PASS判定には使用しない。
@@ -72,6 +72,11 @@ Internal Tool testは件数を仕様としてassertしない。`Design.md`のToo
| published HTTP MCP on-demand elevation | 2026-08-13 / main standard-user、helper Task時UAC / `deepseek-v4-flash:cloud` | completed 3/3。cmd→cmd/curl/Python 3.14、Network none、昇格request/success、WFP/ACL cleanup、helper残留0 |
| published stdio Operational E2E | 2026-08-13 / main standard-user、helper Task時UAC / `deepseek-v4-flash:cloud` | completed 4/4。複雑Python実装、9 unittest、Python subprocess、cleanup PASS |
| on-demand elevation manifests / Bundle | 2026-08-13 / fresh extraction | main `asInvoker`、helper `requireAdministrator/uiAccess=false`、固定`admin/`配置、local資産保持を確認。ZIP SHA-256 `59c6d110c919cb616812272cb3189b03e5a0cb1b42178942cc23748a57d3d0e8` |
+| actual UAC rejection / Server continuity | 2026-08-13 / main standard-user、UAC拒否 | HTTP MCP `agent_run` Task `0530fea1-90dd-40f1-bf7d-4deac7546ae8`が`failed/sandbox.elevation_cancelled`。同一Server PIDで直後の`tools/list`成功 |
+| administrative IPC actual-Process matrix | 2026-08-13 / standard-user+Task単位helper UAC | local peer PID取得、remote-form pipe拒否、別Task ID、別protocol version、二重apply、64 KiB超messageを全件拒否 |
+| helper crash / startup Recovery | 2026-08-13 / standard-user+Recovery helper UAC | network適用後にhelperを強制終了。Recovery UAC拒否はexit 2・bind前fatal・Journal保持、次回承認で`recovery.end=clean`、Journal/loopback/WFP filter/sublayer/helper残留0 |
+| post-adversarial Windows Security/Recovery suite | 2026-08-13 / Windows Security childのみadministrator | 80 PASS、fail/skip 0。共有Application Profile/WFP状態を直列実行 |
+| post-adversarial self-contained Bundle | 2026-08-13 / fresh extraction | `show` PASS、deploymentのlocal設定とApplied Configuration保持、ZIP SHA-256 `e473bb52cb384857012971ed545db9f7c9cc92284105930b7181e5823567944e` |
| schema v2 Release build | 2026-08-13 / non-elevated | warning 0、error 0 |
| schema v2 Agent.Core.Tests / Planner.Tests | 2026-08-13 / non-elevated | Core 104 PASS、Planner 10 PASS、fail/skip 0 |
| schema v2 Windows Security/Recovery + README operational | 2026-08-13 / administrator | 環境非依存56件と実環境19件の計75 PASS、fail/skip 0。LPAC/ACL/Job/WFP、PowerShell/cmd/venv Python、Network、ACL monkeyを含む |
diff --git a/scripts/Verify-AdminIpcAttackMatrix.ps1 b/scripts/Verify-AdminIpcAttackMatrix.ps1
new file mode 100644
index 0000000..c0703f0
--- /dev/null
+++ b/scripts/Verify-AdminIpcAttackMatrix.ps1
@@ -0,0 +1,32 @@
+param(
+ [string]$Configuration = 'Release',
+ [string]$BundlePath = 'build\Release\publish\agent-sandbox-mcp-1.0.0-win-x64'
+)
+
+$ErrorActionPreference = 'Stop'
+$repository = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
+$harnessDirectory = Join-Path $repository "build\$Configuration\Agent.Admin.AcceptanceHarness\net10.0-windows10.0.26100.0"
+$sourceAppHost = Join-Path $harnessDirectory 'Agent.Admin.AcceptanceHarness.exe'
+$testAppHost = Join-Path $harnessDirectory 'agent-mcp.exe'
+$helper = Join-Path ([IO.Path]::GetFullPath((Join-Path $repository $BundlePath))) 'admin\agent-mcp-admin.exe'
+
+if (-not (Test-Path -LiteralPath $sourceAppHost) -or -not (Test-Path -LiteralPath $helper)) {
+ throw 'Build the solution and publish the Release Bundle before running this acceptance matrix.'
+}
+
+# The helper independently requires the parent image name agent-mcp.exe. Renaming only
+# the test apphost preserves its embedded managed DLL target while exercising that check.
+Copy-Item -LiteralPath $sourceAppHost -Destination $testAppHost -Force
+
+& $testAppHost local-peer
+if ($LASTEXITCODE -ne 0) { throw 'Local peer PID authentication failed.' }
+& $testAppHost remote-peer
+if ($LASTEXITCODE -ne 0) { throw 'Remote-form Named Pipe rejection failed.' }
+
+Write-Host 'Four UAC prompts follow. Approve each prompt to test the real elevated helper.' -ForegroundColor Yellow
+foreach ($scenario in 'wrong-task', 'wrong-version', 'double-apply', 'oversized') {
+ & $testAppHost $scenario $helper
+ if ($LASTEXITCODE -ne 0) { throw "Administrative IPC scenario '$scenario' failed." }
+}
+
+Write-Output 'Administrative IPC actual-Process attack matrix PASS.'
diff --git a/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs b/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs
index 57339dc..6caa29b 100644
--- a/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs
+++ b/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs
@@ -41,17 +41,7 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I
throw new AdminOperationException("sandbox.helper_incompatible", $"Elevated helper does not exist at the fixed Bundle path '{canonicalHelper}'.");
string pipeName = $"AgentSandboxMcp-{Environment.ProcessId}-{Guid.NewGuid():N}";
- using WindowsIdentity identity = WindowsIdentity.GetCurrent();
- SecurityIdentifier logon = GetLogonSid(identity);
- var security = new PipeSecurity();
- security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
- security.AddAccessRule(new PipeAccessRule(logon, PipeAccessRights.ReadWrite, AccessControlType.Allow));
- security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
- PipeAccessRights.ReadWrite, AccessControlType.Allow));
- security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null),
- PipeAccessRights.FullControl, AccessControlType.Deny));
-
- NamedPipeServerStream pipe = CreateLocalPipe(pipeName, security);
+ NamedPipeServerStream pipe = CreateLocalPipeForCurrentLogon(pipeName);
Process? helper = null;
try
{
@@ -86,7 +76,7 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I
await connection.ConfigureAwait(false);
linked.Cancel();
- if (!Native.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out uint clientPid) || clientPid != helper.Id || helper.HasExited)
+ if (!TryGetClientProcessId(pipe, out uint clientPid) || clientPid != helper.Id || helper.HasExited)
throw new AdminOperationException("sandbox.ipc_rejected", "The Named Pipe peer does not match the elevated Process started for this Task.");
await AdminProtocol.WriteAsync(pipe, new AdminRequest(AdminProtocol.Version, "hello", taskId), cancellationToken).ConfigureAwait(false);
AdminResponse hello = await AdminProtocol.ReadAsync<AdminResponse>(pipe, cancellationToken).ConfigureAwait(false);
@@ -101,6 +91,24 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I
}
}
+ internal static NamedPipeServerStream CreateLocalPipeForCurrentLogon(string pipeName)
+ {
+ using WindowsIdentity identity = WindowsIdentity.GetCurrent();
+ SecurityIdentifier logon = GetLogonSid(identity);
+ var security = new PipeSecurity();
+ security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
+ security.AddAccessRule(new PipeAccessRule(logon, PipeAccessRights.ReadWrite, AccessControlType.Allow));
+ security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null),
+ PipeAccessRights.ReadWrite, AccessControlType.Allow));
+ security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null),
+ PipeAccessRights.FullControl, AccessControlType.Deny));
+
+ return CreateLocalPipe(pipeName, security);
+ }
+
+ internal static bool TryGetClientProcessId(NamedPipeServerStream pipe, out uint clientPid) =>
+ Native.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out clientPid);
+
private static string Quote(string value) => '"' + value.Replace("\"", "\\\"") + '"';
private static SecurityIdentifier GetLogonSid(WindowsIdentity identity)
@@ -186,6 +194,9 @@ internal sealed class ElevatedNetworkSession(NamedPipeServerStream pipe, Process
{
private bool _cleaned;
+ internal NamedPipeServerStream Pipe => pipe;
+ internal Process HelperProcess => helper;
+
public async Task<bool> InspectLoopbackAsync(string applicationSid, CancellationToken cancellationToken)
{
AdminResponse response = await SendAsync(new(AdminProtocol.Version, "inspect-loopback", taskId, applicationSid), cancellationToken).ConfigureAwait(false);
diff --git a/tests/Agent.Admin.AcceptanceHarness/Agent.Admin.AcceptanceHarness.csproj b/tests/Agent.Admin.AcceptanceHarness/Agent.Admin.AcceptanceHarness.csproj
new file mode 100644
index 0000000..cbca477
--- /dev/null
+++ b/tests/Agent.Admin.AcceptanceHarness/Agent.Admin.AcceptanceHarness.csproj
@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <AssemblyName>Agent.Admin.AcceptanceHarness</AssemblyName>
+ <RootNamespace>Agent.Admin.AcceptanceHarness</RootNamespace>
+ <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
+ </PropertyGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\src\Agent.Admin\Agent.Admin.Client\Agent.Admin.Client.csproj" />
+ <ProjectReference Include="..\..\src\Agent.Windows\Agent.Windows.csproj" />
+ </ItemGroup>
+</Project>
diff --git a/tests/Agent.Admin.AcceptanceHarness/Program.cs b/tests/Agent.Admin.AcceptanceHarness/Program.cs
new file mode 100644
index 0000000..046d741
--- /dev/null
+++ b/tests/Agent.Admin.AcceptanceHarness/Program.cs
@@ -0,0 +1,183 @@
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.IO.Pipes;
+using System.Net;
+using System.Reflection;
+using Agent.Admin.Client;
+using Agent.Windows.Network;
+using Agent.Windows.Recovery;
+using Agent.Windows.Security;
+
+if (args.Length == 0) return Usage();
+try
+{
+ switch (args[0])
+ {
+ case "local-peer":
+ await VerifyLocalPeerAsync();
+ break;
+ case "remote-peer":
+ await VerifyRemotePeerAsync();
+ break;
+ case "connect":
+ return await ConnectAsync(args);
+ case "wrong-task":
+ case "wrong-version":
+ case "double-apply":
+ case "oversized":
+ Require(args, 2);
+ await VerifyHostRejectsAsync(args[0], Path.GetFullPath(args[1]));
+ break;
+ case "crash-local":
+ Require(args, 4);
+ await LeaveCrashJournalAsync(Path.GetFullPath(args[1]), Path.GetFullPath(args[2]), Path.GetFullPath(args[3]));
+ break;
+ default:
+ return Usage();
+ }
+ Console.WriteLine($"PASS {args[0]}");
+ return 0;
+}
+catch (Exception ex)
+{
+ Console.Error.WriteLine(ex);
+ return 1;
+}
+
+static int Usage()
+{
+ Console.Error.WriteLine("Internal acceptance harness. See scripts/Verify-AdminIpcAttackMatrix.ps1.");
+ return 2;
+}
+
+static void Require(string[] values, int count)
+{
+ if (values.Length != count) throw new ArgumentException("Invalid acceptance harness arguments.");
+}
+
+static async Task VerifyLocalPeerAsync()
+{
+ string pipeName = $"AgentSandboxMcp-Acceptance-{Guid.NewGuid():N}";
+ await using NamedPipeServerStream pipe = CreateAcceptancePipe(pipeName);
+ using Process child = StartSelf("connect", ".", pipeName, "success");
+ await pipe.WaitForConnectionAsync().WaitAsync(TimeSpan.FromSeconds(10));
+ uint clientPid = GetClientProcessId(pipe);
+ if (clientPid != child.Id || clientPid == Environment.ProcessId)
+ throw new InvalidOperationException("The actual local Named Pipe peer PID was not authenticated.");
+ await child.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
+ if (child.ExitCode != 0) throw new InvalidOperationException($"Local peer exited with {child.ExitCode}.");
+}
+
+static async Task VerifyRemotePeerAsync()
+{
+ string pipeName = $"AgentSandboxMcp-Acceptance-{Guid.NewGuid():N}";
+ await using NamedPipeServerStream pipe = CreateAcceptancePipe(pipeName);
+ using Process child = StartSelf("connect", Environment.MachineName, pipeName, "rejected");
+ await child.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
+ if (child.ExitCode != 0) throw new InvalidOperationException($"Remote-form peer was not rejected (exit {child.ExitCode}).");
+ if (pipe.IsConnected) throw new InvalidOperationException("PIPE_REJECT_REMOTE_CLIENTS accepted a remote-form connection.");
+}
+
+static async Task<int> ConnectAsync(string[] values)
+{
+ Require(values, 4);
+ bool expectSuccess = values[3] == "success";
+ try
+ {
+ await using var pipe = new NamedPipeClientStream(values[1], values[2], PipeDirection.InOut, PipeOptions.Asynchronous);
+ await pipe.ConnectAsync(2_000);
+ return expectSuccess ? 0 : 1;
+ }
+ catch (Exception) when (!expectSuccess)
+ {
+ return 0;
+ }
+}
+
+static Process StartSelf(params string[] arguments)
+{
+ var start = new ProcessStartInfo { FileName = Environment.ProcessPath!, UseShellExecute = false };
+ foreach (string argument in arguments) start.ArgumentList.Add(argument);
+ return Process.Start(start) ?? throw new InvalidOperationException("Acceptance child did not start.");
+}
+
+static NamedPipeServerStream CreateAcceptancePipe(string pipeName) =>
+ (NamedPipeServerStream)(typeof(ElevatedNetworkSessionFactory).GetMethod("CreateLocalPipeForCurrentLogon",
+ BindingFlags.Static | BindingFlags.NonPublic)?.Invoke(null, [pipeName])
+ ?? throw new MissingMethodException("Production pipe factory test hook was not found."));
+
+static uint GetClientProcessId(NamedPipeServerStream pipe)
+{
+ object?[] arguments = [pipe, 0u];
+ bool success = (bool)(typeof(ElevatedNetworkSessionFactory).GetMethod("TryGetClientProcessId",
+ BindingFlags.Static | BindingFlags.NonPublic)?.Invoke(null, arguments) ?? false);
+ if (!success) throw new InvalidOperationException("GetNamedPipeClientProcessId failed.");
+ return (uint)arguments[1]!;
+}
+
+static NamedPipeServerStream SessionPipe(IElevatedNetworkSession session) =>
+ (NamedPipeServerStream)(session.GetType().GetProperty("Pipe", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(session)
+ ?? throw new MissingMemberException("Elevated session pipe test hook was not found."));
+
+static Process SessionHelper(IElevatedNetworkSession session) =>
+ (Process)(session.GetType().GetProperty("HelperProcess", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(session)
+ ?? throw new MissingMemberException("Elevated session Process test hook was not found."));
+
+static async Task VerifyHostRejectsAsync(string mode, string helperPath)
+{
+ Guid taskId = Guid.NewGuid();
+ var factory = new ElevatedNetworkSessionFactory(helperPath);
+ await using IElevatedNetworkSession abstraction = await factory.StartAsync(taskId, CancellationToken.None);
+ IElevatedNetworkSession session = abstraction;
+ NamedPipeServerStream pipe = SessionPipe(session);
+ Process helper = SessionHelper(session);
+ string sid = AppContainerProfile.EnsureFixed().Sid.Value;
+ if (mode == "double-apply")
+ {
+ await session.ApplyAsync(sid, "none", [], false, CancellationToken.None);
+ await AssertRejectedAsync(pipe, new(AdminProtocol.Version, "apply-network", taskId, sid, "none", [], false));
+ return;
+ }
+ if (mode == "oversized")
+ {
+ byte[] header = new byte[sizeof(int)];
+ BinaryPrimitives.WriteInt32LittleEndian(header, AdminProtocol.MaxMessageBytes + 1);
+ await pipe.WriteAsync(header);
+ await pipe.FlushAsync();
+ await helper.WaitForExitAsync().WaitAsync(TimeSpan.FromSeconds(10));
+ if (helper.ExitCode != 2) throw new InvalidOperationException("Oversized message did not terminate the helper with its rejection exit code.");
+ return;
+ }
+ AdminRequest request = mode == "wrong-task"
+ ? new(AdminProtocol.Version, "inspect-loopback", Guid.NewGuid(), sid)
+ : new(AdminProtocol.Version + 1, "inspect-loopback", taskId, sid);
+ await AssertRejectedAsync(pipe, request);
+}
+
+static async Task AssertRejectedAsync(NamedPipeServerStream pipe, AdminRequest request)
+{
+ await AdminProtocol.WriteAsync(pipe, request, CancellationToken.None);
+ AdminResponse response = await AdminProtocol.ReadAsync<AdminResponse>(pipe, CancellationToken.None);
+ if (response.Success || response.ErrorCode != "sandbox.ipc_rejected")
+ throw new InvalidOperationException("The hostile administrative request was not rejected canonically.");
+}
+
+static async Task LeaveCrashJournalAsync(string helperPath, string journalRoot, string readyPath)
+{
+ Guid taskId = Guid.NewGuid();
+ var factory = new ElevatedNetworkSessionFactory(helperPath);
+ IElevatedNetworkSession abstraction = await factory.StartAsync(taskId, CancellationToken.None);
+ IElevatedNetworkSession session = abstraction;
+ Process helper = SessionHelper(session);
+ string sid = AppContainerProfile.EnsureFixed().Sid.Value;
+ bool existing = await session.InspectLoopbackAsync(sid, CancellationToken.None);
+ if (existing) throw new InvalidOperationException("The fixed Application SID already has a loopback exemption; recover it before this scenario.");
+ TaskResourceJournal journal = TaskResourceJournal.Create(journalRoot, taskId);
+ _ = await TaskNetworkPolicy.ApplyAsync(session, journal, AppContainerProfile.EnsureFixed().Sid, "local",
+ [new NetworkEndpoint(IPAddress.Loopback, 11434), new NetworkEndpoint(IPAddress.IPv6Loopback, 11434)],
+ new MemorySecurityAuditSink(), null, CancellationToken.None);
+ await File.WriteAllTextAsync(readyPath, $"task_id={taskId:D}\nhelper_pid={helper.Id}\n");
+ await helper.WaitForExitAsync();
+ // Intentionally do not dispose the Task Network Policy. The killed helper owns the
+ // dynamic WFP session; the persistent loopback PREPARE/APPLIED record must survive.
+}