1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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.
}
|