mcp-pacemaker 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -1
- package/CONTRIBUTING.md +49 -13
- package/README.md +361 -60
- package/THIRD_PARTY_NOTICES.txt +1 -28
- package/autostart/windows/account-task.ps1 +167 -0
- package/autostart/windows/inspect-task.ps1 +42 -0
- package/autostart/windows/inspect-worker-process-sessions.ps1 +155 -0
- package/autostart/windows/legacy-task.ps1 +301 -0
- package/autostart/windows/register-task.ps1 +11 -2
- package/bin/account-task-adapter.mjs +47 -0
- package/bin/account-upgrade-controller.mjs +198 -0
- package/bin/account-upgrade-worker.mjs +134 -0
- package/bin/account-worker-session.mjs +215 -0
- package/bin/cli-dispatch.mjs +154 -0
- package/bin/cli-main.mjs +1007 -0
- package/bin/cli-selection.mjs +43 -0
- package/bin/cli.mjs +7 -876
- package/bin/legacy-1.3.json +32 -0
- package/bin/legacy-installation.mjs +217 -0
- package/bin/legacy-task.mjs +52 -0
- package/bin/legacy-upgrade.mjs +471 -0
- package/bin/managed-runtime.mjs +170 -0
- package/bin/managed-state.mjs +257 -0
- package/bin/managed-upgrade.mjs +491 -0
- package/bin/mcp-bridge.mjs +132 -4
- package/bin/pooling-batches.mjs +13 -0
- package/bin/pooling-writer.mjs +4 -0
- package/bin/prewarm.mjs +33 -21
- package/bin/runtime-context.mjs +100 -0
- package/bin/service-control.mjs +27 -4
- package/bin/stable-launcher.mjs +54 -0
- package/bin/task-only-controller.mjs +226 -0
- package/bin/task-scope.mjs +65 -0
- package/bin/task-transaction-protocol.mjs +173 -0
- package/bin/top.mjs +22 -10
- package/bin/update-check.mjs +13 -2
- package/bin/upgrade-autostart.mjs +47 -0
- package/bin/upgrade-capability.json +8 -0
- package/bin/upgrade-dependencies.mjs +122 -0
- package/bin/upgrade-package.mjs +203 -0
- package/bin/windows-legacy/LegacyProcessBroker.build.json +42 -0
- package/bin/windows-legacy/LegacyProcessBroker.exe +0 -0
- package/bin/windows-legacy/src/AssemblyInfo.cs +8 -0
- package/bin/windows-legacy/src/LegacyNative.cs +783 -0
- package/bin/windows-legacy/src/LegacyProcessBroker.cs +841 -0
- package/bin/windows-legacy-process.mjs +241 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.build.json +38 -0
- package/bin/windows-lifetime/ProcessLifetimeHelper.exe +0 -0
- package/bin/windows-lifetime/src/AssemblyInfo.cs +8 -0
- package/bin/windows-lifetime/src/ProcessLifetimeHelper.cs +468 -0
- package/bin/windows-process-lifetime.mjs +218 -0
- package/bin/windows-task-channel/TaskChannelGuard.build.json +45 -0
- package/bin/windows-task-channel/TaskChannelGuard.exe +0 -0
- package/bin/windows-task-channel/src/AssemblyInfo.cs +8 -0
- package/bin/windows-task-channel/src/ChannelCliContext.cs +210 -0
- package/bin/windows-task-channel/src/ChannelFiles.cs +231 -0
- package/bin/windows-task-channel/src/ChannelNative.cs +477 -0
- package/bin/windows-task-channel/src/ChannelSession.cs +203 -0
- package/bin/windows-task-channel/src/TaskChannelGuard.cs +686 -0
- package/bin/windows-task-channel.mjs +335 -0
- package/docs/account-upgrades.md +205 -0
- package/docs/execution-context.md +113 -0
- package/package.json +12 -5
- package/supervisor/supervise.mjs +226 -40
- package/tools/account-upgrade/qualify-two-account.ps1 +66 -0
- package/tools/windows-legacy/README.md +225 -0
- package/tools/windows-legacy/build.ps1 +122 -0
- package/tools/windows-legacy/inventory.mjs +90 -0
- package/tools/windows-process-lifetime/README.md +119 -0
- package/tools/windows-process-lifetime/build.ps1 +122 -0
- package/tools/windows-process-lifetime/inventory.mjs +86 -0
- package/tools/windows-task-channel/README.md +329 -0
- package/tools/windows-task-channel/build.ps1 +107 -0
- package/tools/windows-task-channel/inventory.mjs +117 -0
- package/ui/dist/THIRD_PARTY_NOTICES.txt +1 -28
- package/ui/dist/assets/{index-CgPruWYi.js → index-CcwMo2sI.js} +1 -1
- package/ui/dist/assets/index-PVcuDNrN.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/third-party-manifest.json +37 -25
- package/ui/dist/assets/index-CaLPg0c-.css +0 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.ComponentModel;
|
|
4
|
+
using System.Diagnostics;
|
|
5
|
+
using System.Globalization;
|
|
6
|
+
using System.IO;
|
|
7
|
+
using System.IO.Pipes;
|
|
8
|
+
using System.Linq;
|
|
9
|
+
using System.Security.Principal;
|
|
10
|
+
using System.Text;
|
|
11
|
+
using System.Threading;
|
|
12
|
+
using System.Threading.Tasks;
|
|
13
|
+
using System.Web.Script.Serialization;
|
|
14
|
+
|
|
15
|
+
internal sealed class TaskChannelGuard
|
|
16
|
+
{
|
|
17
|
+
private readonly ChannelNative native = new ChannelNative();
|
|
18
|
+
private readonly JavaScriptSerializer json = new JavaScriptSerializer { MaxJsonLength = 65536, RecursionLimit = 12 };
|
|
19
|
+
private readonly UTF8Encoding utf8 = new UTF8Encoding(false, true);
|
|
20
|
+
private readonly Stopwatch clock = Stopwatch.StartNew();
|
|
21
|
+
private Stream input;
|
|
22
|
+
private Stream output;
|
|
23
|
+
private ProcessHandle parent;
|
|
24
|
+
private Identity controller;
|
|
25
|
+
private Identity guardIdentity;
|
|
26
|
+
private ProcessHandle heldPeer;
|
|
27
|
+
private NamedPipeServerStream heldPipe;
|
|
28
|
+
private Identity heldPeerIdentity;
|
|
29
|
+
private SessionFact targetSession;
|
|
30
|
+
private int limit = 13000;
|
|
31
|
+
private long partialDeadline;
|
|
32
|
+
private long handshakeDeadline;
|
|
33
|
+
private Task<string> controlRead;
|
|
34
|
+
private Task controlPump;
|
|
35
|
+
private Task<Dictionary<string, object>> peerRead;
|
|
36
|
+
private readonly Queue<Command> commands = new Queue<Command>();
|
|
37
|
+
private readonly object commandLock = new object();
|
|
38
|
+
private bool controlClosing;
|
|
39
|
+
private bool readyPublished;
|
|
40
|
+
private bool monitorStopped;
|
|
41
|
+
|
|
42
|
+
private static int Main(string[] arguments)
|
|
43
|
+
{
|
|
44
|
+
if (arguments.Length != 0) return 2;
|
|
45
|
+
return new TaskChannelGuard().Run();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private int Run()
|
|
49
|
+
{
|
|
50
|
+
input = Console.OpenStandardInput();
|
|
51
|
+
output = Console.OpenStandardOutput();
|
|
52
|
+
var monitor = new Thread(Monitor) { IsBackground = true };
|
|
53
|
+
monitor.Start();
|
|
54
|
+
try
|
|
55
|
+
{
|
|
56
|
+
try
|
|
57
|
+
{
|
|
58
|
+
int selfPid = Process.GetCurrentProcess().Id;
|
|
59
|
+
using (var self = native.Open(selfPid))
|
|
60
|
+
{
|
|
61
|
+
guardIdentity = native.Identity(selfPid, self);
|
|
62
|
+
Emit(new { type = "started" });
|
|
63
|
+
var initial = NextControl();
|
|
64
|
+
Wait(initial, 5000);
|
|
65
|
+
var request = Parse<Request>(initial.GetAwaiter().GetResult());
|
|
66
|
+
bool cliContext = request.action == "cli-context";
|
|
67
|
+
bool sessionContext = request.action == "current-session";
|
|
68
|
+
var context = new ChannelCliContext(native, CheckQueryProgress);
|
|
69
|
+
int parentPid;
|
|
70
|
+
if (cliContext ||
|
|
71
|
+
sessionContext)
|
|
72
|
+
{
|
|
73
|
+
// A blocking read on this synchronous pipe would block its server-PID query.
|
|
74
|
+
parentPid = context.Parent(self, selfPid);
|
|
75
|
+
StartControlReader();
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
{
|
|
79
|
+
StartControlReader();
|
|
80
|
+
parentPid = native.Parent(selfPid, self);
|
|
81
|
+
}
|
|
82
|
+
bool captureParentThreads = cliContext ||
|
|
83
|
+
sessionContext ||
|
|
84
|
+
request.action == "caller" ||
|
|
85
|
+
request.action == "serve";
|
|
86
|
+
parent = captureParentThreads ? context.OpenParent(parentPid) : native.Open(parentPid);
|
|
87
|
+
if (!native.Alive(parent) ||
|
|
88
|
+
native.Birth(parent) > native.Birth(self))
|
|
89
|
+
throw new InvalidOperationException("PARENT_GENERATION_INVALID");
|
|
90
|
+
controller = native.Identity(parentPid, parent);
|
|
91
|
+
using (parent)
|
|
92
|
+
using (var parentToken = native.Token(parent))
|
|
93
|
+
{
|
|
94
|
+
if (sessionContext)
|
|
95
|
+
{
|
|
96
|
+
var session = new ChannelSession().Observe(
|
|
97
|
+
this.controller.ownerSid, this.controller.sessionId, null);
|
|
98
|
+
this.Emit(new
|
|
99
|
+
{
|
|
100
|
+
type = "result",
|
|
101
|
+
identity = this.controller,
|
|
102
|
+
proofScope = "current-user-session-snapshot",
|
|
103
|
+
session,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else if (cliContext)
|
|
107
|
+
{
|
|
108
|
+
var observed = context.Observe(parent, controller);
|
|
109
|
+
Emit(new { type = "result", identity = controller, observed.proofScope,
|
|
110
|
+
observed.ordinaryEligible, observed.reason, observed.actorFacts, observed.observation });
|
|
111
|
+
}
|
|
112
|
+
else if (request.action == "caller")
|
|
113
|
+
{
|
|
114
|
+
Emit(new { type = "result", identity = controller, actorFacts = Facts() });
|
|
115
|
+
}
|
|
116
|
+
else if (request.action == "inspect-root")
|
|
117
|
+
{
|
|
118
|
+
using (var files = new ChannelFiles(controller.ownerSid, CheckQueryProgress))
|
|
119
|
+
{
|
|
120
|
+
var proof = files.InspectRoot(request.root);
|
|
121
|
+
Emit(new { type = "result", identity = controller, actorFacts = Facts(false), proof });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (request.action == "read-manifest") ReadManifest(request);
|
|
125
|
+
else if (request.action == "serve") Serve(request);
|
|
126
|
+
else throw new InvalidOperationException("UNKNOWN_ROLE");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
catch (Exception error)
|
|
132
|
+
{
|
|
133
|
+
try { Emit(new { type = "failed", reason = Reason(error), errorType = error.GetType().Name,
|
|
134
|
+
nativeError = error is Win32Exception ? ((Win32Exception)error).NativeErrorCode : 0 }); }
|
|
135
|
+
catch (IOException) { }
|
|
136
|
+
return 3;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
finally { Volatile.Write(ref monitorStopped, true); }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private Task<string> NextControl()
|
|
143
|
+
{
|
|
144
|
+
var next = Task.Run(() => Read(input, 65536));
|
|
145
|
+
Volatile.Write(ref controlRead, next);
|
|
146
|
+
return next;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private void StartControlReader()
|
|
150
|
+
{
|
|
151
|
+
controlPump = Task.Run(() =>
|
|
152
|
+
{
|
|
153
|
+
var parser = new JavaScriptSerializer { MaxJsonLength = 65536, RecursionLimit = 12 };
|
|
154
|
+
int lastId = 0;
|
|
155
|
+
while (true)
|
|
156
|
+
{
|
|
157
|
+
var value = parser.DeserializeObject(Read(input, 65536));
|
|
158
|
+
if (!(value is Dictionary<string, object>)) throw new InvalidOperationException("OBJECT_FRAME_REQUIRED");
|
|
159
|
+
var command = parser.ConvertToType<Command>(value);
|
|
160
|
+
if (command.action == "close")
|
|
161
|
+
{
|
|
162
|
+
Volatile.Write(ref controlClosing, true);
|
|
163
|
+
lock (commandLock)
|
|
164
|
+
{
|
|
165
|
+
if (commands.Count < 32) commands.Enqueue(command);
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (command.action != "authorize" &&
|
|
170
|
+
command.action != "send") throw new InvalidOperationException("UNKNOWN_COMMAND");
|
|
171
|
+
if (lastId == int.MaxValue ||
|
|
172
|
+
command.id != lastId + 1) throw new InvalidOperationException("COMMAND_SEQUENCE_INVALID");
|
|
173
|
+
lastId = command.id;
|
|
174
|
+
lock (commandLock)
|
|
175
|
+
{
|
|
176
|
+
if (commands.Count >= 32) throw new InvalidOperationException("COMMAND_QUEUE_BOUNDS");
|
|
177
|
+
commands.Enqueue(command);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private bool TryControl(out Command command)
|
|
184
|
+
{
|
|
185
|
+
lock (commandLock)
|
|
186
|
+
{
|
|
187
|
+
command = commands.Count == 0 ? null : commands.Dequeue();
|
|
188
|
+
return command != null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private void NextPeerRead()
|
|
193
|
+
{
|
|
194
|
+
peerRead = Task.Run(ReadPeerFrame);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private Dictionary<string, object> ReadPeerFrame()
|
|
198
|
+
{
|
|
199
|
+
var parser = new JavaScriptSerializer { MaxJsonLength = 16384, RecursionLimit = 12 };
|
|
200
|
+
var value = parser.DeserializeObject(Read(heldPipe, 16384, true));
|
|
201
|
+
var frame = value as Dictionary<string, object>;
|
|
202
|
+
if (frame == null) throw new InvalidOperationException("OBJECT_FRAME_REQUIRED");
|
|
203
|
+
return frame;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private void CheckPeerRead()
|
|
207
|
+
{
|
|
208
|
+
if (peerRead != null &&
|
|
209
|
+
peerRead.IsCompleted) peerRead.GetAwaiter().GetResult();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private bool CloseRequested(Task<string> task)
|
|
213
|
+
{
|
|
214
|
+
if (task == null ||
|
|
215
|
+
task.Status != TaskStatus.RanToCompletion) return false;
|
|
216
|
+
try
|
|
217
|
+
{
|
|
218
|
+
var parser = new JavaScriptSerializer { MaxJsonLength = 65536, RecursionLimit = 12 };
|
|
219
|
+
var command = parser.Deserialize<Dictionary<string, object>>(task.Result);
|
|
220
|
+
object action;
|
|
221
|
+
return command != null && command.TryGetValue("action", out action) && (action as string) == "close";
|
|
222
|
+
}
|
|
223
|
+
catch (ArgumentException) { return false; }
|
|
224
|
+
catch (InvalidOperationException) { return false; }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private int Deadline()
|
|
228
|
+
{
|
|
229
|
+
return Volatile.Read(ref readyPublished) ? Volatile.Read(ref limit) : Math.Min(13000, Volatile.Read(ref limit));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private void Monitor()
|
|
233
|
+
{
|
|
234
|
+
long cancellationAt = -1;
|
|
235
|
+
Task<string> inspected = null;
|
|
236
|
+
bool close = false;
|
|
237
|
+
while (!Volatile.Read(ref monitorStopped))
|
|
238
|
+
{
|
|
239
|
+
try
|
|
240
|
+
{
|
|
241
|
+
var heldParent = Volatile.Read(ref parent);
|
|
242
|
+
if (heldParent != null &&
|
|
243
|
+
!heldParent.IsClosed &&
|
|
244
|
+
!native.Alive(heldParent)) Environment.Exit(4);
|
|
245
|
+
if (clock.ElapsedMilliseconds > Deadline()) Environment.Exit(4);
|
|
246
|
+
var control = Volatile.Read(ref controlRead);
|
|
247
|
+
if (control != inspected &&
|
|
248
|
+
control != null &&
|
|
249
|
+
control.IsCompleted)
|
|
250
|
+
{
|
|
251
|
+
close = CloseRequested(control);
|
|
252
|
+
inspected = control;
|
|
253
|
+
}
|
|
254
|
+
var pump = Volatile.Read(ref controlPump);
|
|
255
|
+
bool cancelled = Volatile.Read(ref controlClosing) ||
|
|
256
|
+
(control != null && (control.IsFaulted || control.IsCanceled || close)) ||
|
|
257
|
+
(pump != null && (pump.IsFaulted || pump.IsCanceled));
|
|
258
|
+
if (cancelled &&
|
|
259
|
+
cancellationAt < 0) cancellationAt = clock.ElapsedMilliseconds;
|
|
260
|
+
if (cancellationAt >= 0 &&
|
|
261
|
+
clock.ElapsedMilliseconds - cancellationAt >= 500) Environment.Exit(4);
|
|
262
|
+
}
|
|
263
|
+
catch (ObjectDisposedException) { return; }
|
|
264
|
+
catch (Exception) { Environment.Exit(4); }
|
|
265
|
+
Thread.Sleep(20);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private string Reason(Exception error)
|
|
270
|
+
{
|
|
271
|
+
if (error is InvalidOperationException &&
|
|
272
|
+
error.Message.Length < 100 &&
|
|
273
|
+
error.Message.All(character => char.IsUpper(character) || char.IsDigit(character) || character == '_'))
|
|
274
|
+
return error.Message;
|
|
275
|
+
return "QUERY_OR_PROTOCOL_REFUSED";
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private ActorFacts Facts(bool observeThreads = true)
|
|
279
|
+
{
|
|
280
|
+
CheckParent();
|
|
281
|
+
var facts = native.Facts(parent, observeThreads, CheckQueryProgress);
|
|
282
|
+
if (facts.ownerSid != controller.ownerSid ||
|
|
283
|
+
facts.sessionId != controller.sessionId) throw new InvalidOperationException("PARENT_TOKEN_CHANGED");
|
|
284
|
+
CheckParent();
|
|
285
|
+
return facts;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private void RequireAdmin(ActorFacts facts)
|
|
289
|
+
{
|
|
290
|
+
bool authorized = facts.elevated &&
|
|
291
|
+
facts.enabledAdministrator &&
|
|
292
|
+
!facts.restricted &&
|
|
293
|
+
!facts.appContainer &&
|
|
294
|
+
!facts.guardThreadImpersonating &&
|
|
295
|
+
facts.parentThreadImpersonation == "observed-none";
|
|
296
|
+
if (!authorized) throw new InvalidOperationException("ACTUAL_ADMIN_TOKEN_REQUIRED");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private void CheckParent()
|
|
300
|
+
{
|
|
301
|
+
if (parent != null &&
|
|
302
|
+
!native.Alive(parent)) throw new InvalidOperationException("PARENT_EXITED");
|
|
303
|
+
var control = Volatile.Read(ref controlRead);
|
|
304
|
+
if (control != null &&
|
|
305
|
+
control.IsFaulted) throw new InvalidOperationException("CONTROL_DISCONNECTED");
|
|
306
|
+
var pump = Volatile.Read(ref controlPump);
|
|
307
|
+
if (pump != null &&
|
|
308
|
+
pump.IsCompleted) pump.GetAwaiter().GetResult();
|
|
309
|
+
if (clock.ElapsedMilliseconds > Deadline()) throw new InvalidOperationException("OPERATION_EXPIRED");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private void CheckQueryProgress()
|
|
313
|
+
{
|
|
314
|
+
CheckParent();
|
|
315
|
+
if (Volatile.Read(ref controlClosing) ||
|
|
316
|
+
CloseRequested(Volatile.Read(ref controlRead))) throw new InvalidOperationException("CONTROL_CLOSED");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private void Wait(Task task, int timeout)
|
|
320
|
+
{
|
|
321
|
+
long deadline = clock.ElapsedMilliseconds + timeout;
|
|
322
|
+
while (!task.IsCompleted)
|
|
323
|
+
{
|
|
324
|
+
CheckParent();
|
|
325
|
+
if (clock.ElapsedMilliseconds >= deadline) throw new InvalidOperationException("CHANNEL_DEADLINE");
|
|
326
|
+
Thread.Sleep(20);
|
|
327
|
+
}
|
|
328
|
+
task.GetAwaiter().GetResult();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private T Parse<T>(string text)
|
|
332
|
+
{
|
|
333
|
+
if (string.IsNullOrEmpty(text)) throw new InvalidOperationException("EMPTY_FRAME");
|
|
334
|
+
var value = json.DeserializeObject(text);
|
|
335
|
+
if (!(value is Dictionary<string, object>)) throw new InvalidOperationException("OBJECT_FRAME_REQUIRED");
|
|
336
|
+
return json.ConvertToType<T>(value);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private string Read(Stream stream, int maximum, bool peer = false)
|
|
340
|
+
{
|
|
341
|
+
using (var buffer = new MemoryStream())
|
|
342
|
+
{
|
|
343
|
+
while (true)
|
|
344
|
+
{
|
|
345
|
+
int value = stream.ReadByte();
|
|
346
|
+
if (value < 0) throw new InvalidOperationException("CHANNEL_DISCONNECTED");
|
|
347
|
+
if (value == 10)
|
|
348
|
+
{
|
|
349
|
+
if (buffer.Length + 1 > maximum) throw new InvalidOperationException("FRAME_BOUNDS");
|
|
350
|
+
if (peer) Interlocked.Exchange(ref partialDeadline, 0);
|
|
351
|
+
return utf8.GetString(buffer.ToArray());
|
|
352
|
+
}
|
|
353
|
+
if (peer &&
|
|
354
|
+
buffer.Length == 0) Interlocked.Exchange(ref partialDeadline, clock.ElapsedMilliseconds + 5000);
|
|
355
|
+
if (value == 13 ||
|
|
356
|
+
buffer.Length >= maximum) throw new InvalidOperationException("FRAME_BOUNDS_OR_FRAMING");
|
|
357
|
+
buffer.WriteByte((byte)value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private byte[] Bytes(object value, int maximum)
|
|
363
|
+
{
|
|
364
|
+
var bytes = utf8.GetBytes(json.Serialize(value) + "\n");
|
|
365
|
+
if (bytes.Length > maximum) throw new InvalidOperationException("FRAME_BOUNDS");
|
|
366
|
+
return bytes;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private void Emit(object value)
|
|
370
|
+
{
|
|
371
|
+
var envelope = json.Deserialize<Dictionary<string, object>>(json.Serialize(value));
|
|
372
|
+
envelope.Add("guardIdentity", guardIdentity);
|
|
373
|
+
var bytes = Bytes(envelope, 65536);
|
|
374
|
+
output.Write(bytes, 0, bytes.Length);
|
|
375
|
+
output.Flush();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private void Validate(Request request)
|
|
379
|
+
{
|
|
380
|
+
Guid operation;
|
|
381
|
+
if (!Guid.TryParseExact(request.operationId, "D", out operation) ||
|
|
382
|
+
operation.ToString("D") != request.operationId) throw new InvalidOperationException("OPERATION_ID_INVALID");
|
|
383
|
+
if (new SecurityIdentifier(request.targetSid).Value != request.targetSid ||
|
|
384
|
+
request.sessionId < 0) throw new InvalidOperationException("TARGET_INVALID");
|
|
385
|
+
if (request.handshakeTimeoutMs < 2000 ||
|
|
386
|
+
request.handshakeTimeoutMs > 30000 ||
|
|
387
|
+
request.lifetimeMs < 2000 ||
|
|
388
|
+
request.lifetimeMs > 900000) throw new InvalidOperationException("DEADLINE_BOUNDS");
|
|
389
|
+
if (request.bootstrap == null ||
|
|
390
|
+
request.document == null) throw new InvalidOperationException("BOOTSTRAP_REQUIRED");
|
|
391
|
+
if (Bytes(request.document, 49152).Length < 3) throw new InvalidOperationException("DOCUMENT_REQUIRED");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private void BindBootstrap(ChannelFiles files, Bootstrap bootstrap, string manifestPath, string parentPath, string operationId, bool cross)
|
|
395
|
+
{
|
|
396
|
+
var node = files.HoldFile(bootstrap.nodePath, cross);
|
|
397
|
+
var script = files.HoldFile(bootstrap.scriptPath, cross);
|
|
398
|
+
if (files.Hash(node) != bootstrap.nodeSha256 ||
|
|
399
|
+
files.Hash(script) != bootstrap.scriptSha256) throw new InvalidOperationException("BOOTSTRAP_HASH_MISMATCH");
|
|
400
|
+
bootstrap.nodePath = files.Canonical(node);
|
|
401
|
+
bootstrap.scriptPath = files.Canonical(script);
|
|
402
|
+
var expected = new[] { bootstrap.scriptPath, "--manifest", manifestPath, "--operation", operationId,
|
|
403
|
+
"--controller-root", parentPath };
|
|
404
|
+
if (bootstrap.argv != null &&
|
|
405
|
+
!bootstrap.argv.SequenceEqual(expected, StringComparer.Ordinal))
|
|
406
|
+
throw new InvalidOperationException("BOOTSTRAP_ARGV_MISMATCH");
|
|
407
|
+
bootstrap.argv = expected;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private Identity BindPeer(ChannelFiles files, int pid, ProcessHandle process, Bootstrap bootstrap,
|
|
411
|
+
string targetSid, int sessionId, string expectedCreationTime)
|
|
412
|
+
{
|
|
413
|
+
var identity = native.Identity(pid, process);
|
|
414
|
+
CheckPeer(identity, targetSid, sessionId, expectedCreationTime);
|
|
415
|
+
using (var image = new FileStream(identity.imagePath, FileMode.Open, FileAccess.Read, FileShare.Read))
|
|
416
|
+
{
|
|
417
|
+
if (!string.Equals(files.Canonical(image), bootstrap.nodePath, StringComparison.OrdinalIgnoreCase) ||
|
|
418
|
+
files.Hash(image) != bootstrap.nodeSha256) throw new InvalidOperationException("PEER_IMAGE_MISMATCH");
|
|
419
|
+
}
|
|
420
|
+
var arguments = native.Arguments(pid, process);
|
|
421
|
+
if (arguments.Length != bootstrap.argv.Length + 1 ||
|
|
422
|
+
!arguments.Skip(1).SequenceEqual(bootstrap.argv, StringComparer.Ordinal))
|
|
423
|
+
throw new InvalidOperationException("PEER_ARGV_MISMATCH");
|
|
424
|
+
identity.imageSha256 = bootstrap.nodeSha256;
|
|
425
|
+
identity.argvSha256 = files.Digest(utf8.GetBytes(json.Serialize(arguments.Skip(1).ToArray())));
|
|
426
|
+
if (!native.Alive(process)) throw new InvalidOperationException("PEER_EXITED");
|
|
427
|
+
return identity;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
private void CheckPeer(Identity identity, string targetSid, int sessionId, string expectedCreationTime)
|
|
431
|
+
{
|
|
432
|
+
if (identity.ownerSid != targetSid ||
|
|
433
|
+
identity.sessionId != sessionId ||
|
|
434
|
+
(expectedCreationTime != null && identity.creationTime != expectedCreationTime))
|
|
435
|
+
throw new InvalidOperationException("PEER_IDENTITY_MISMATCH");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
private void Serve(Request request)
|
|
439
|
+
{
|
|
440
|
+
Validate(request);
|
|
441
|
+
limit = request.lifetimeMs;
|
|
442
|
+
var facts = Facts();
|
|
443
|
+
bool cross = request.targetSid != controller.ownerSid;
|
|
444
|
+
if (cross) RequireAdmin(facts);
|
|
445
|
+
targetSession = Session(request.targetSid, request.sessionId, null);
|
|
446
|
+
using (var files = new ChannelFiles(controller.ownerSid))
|
|
447
|
+
{
|
|
448
|
+
var manifestParent = files.PathValue(request.manifestParent);
|
|
449
|
+
files.Chain(manifestParent, true);
|
|
450
|
+
var operationDirectory = Path.Combine(manifestParent, request.operationId);
|
|
451
|
+
var manifestPath = Path.Combine(operationDirectory, "manifest.json");
|
|
452
|
+
BindBootstrap(files, request.bootstrap, manifestPath, manifestParent, request.operationId, cross);
|
|
453
|
+
if (cross)
|
|
454
|
+
{
|
|
455
|
+
files.HoldFile(controller.imagePath, true);
|
|
456
|
+
files.HoldFile(Process.GetCurrentProcess().MainModule.FileName, true);
|
|
457
|
+
}
|
|
458
|
+
var endpoint = @"\\.\pipe\mcp-pacemaker-task-" + Guid.NewGuid().ToString("N");
|
|
459
|
+
var pipeHandle = files.WithDescriptor(request.targetSid, false, security =>
|
|
460
|
+
ChannelApi.CreateNamedPipeW(endpoint, 0x40080003, 8, 1, 16384, 16384, 0, ref security), true);
|
|
461
|
+
if (pipeHandle.IsInvalid)
|
|
462
|
+
{
|
|
463
|
+
pipeHandle.Dispose();
|
|
464
|
+
throw new InvalidOperationException("EXCLUSIVE_PIPE_CREATE_FAILED");
|
|
465
|
+
}
|
|
466
|
+
using (var pipe = new NamedPipeServerStream(PipeDirection.InOut, true, false, pipeHandle))
|
|
467
|
+
{
|
|
468
|
+
files.CreateDirectory(operationDirectory, request.targetSid);
|
|
469
|
+
var manifest = new Manifest
|
|
470
|
+
{
|
|
471
|
+
protocol = 1, operationId = request.operationId, targetSid = request.targetSid,
|
|
472
|
+
sessionId = request.sessionId, endpoint = endpoint, controllerIdentity = controller,
|
|
473
|
+
actorFacts = facts, bootstrap = request.bootstrap, document = request.document, targetSession = targetSession
|
|
474
|
+
};
|
|
475
|
+
files.CreateManifest(manifestPath, request.targetSid, Bytes(manifest, 65536));
|
|
476
|
+
var connected = pipe.WaitForConnectionAsync();
|
|
477
|
+
handshakeDeadline = clock.ElapsedMilliseconds + request.handshakeTimeoutMs;
|
|
478
|
+
Emit(new { type = "ready", endpoint, manifestPath, actorFacts = facts, controllerIdentity = controller, targetSession });
|
|
479
|
+
Volatile.Write(ref readyPublished, true);
|
|
480
|
+
while (!connected.IsCompleted)
|
|
481
|
+
{
|
|
482
|
+
CheckParent();
|
|
483
|
+
Command early;
|
|
484
|
+
if (TryControl(out early))
|
|
485
|
+
{
|
|
486
|
+
if (early.action == "close") { Emit(new { type = "closed", reason = "controller-close" }); return; }
|
|
487
|
+
if (early.action != "authorize") throw new InvalidOperationException("PEER_NOT_CONNECTED");
|
|
488
|
+
Authorize(early.id, cross);
|
|
489
|
+
}
|
|
490
|
+
if (clock.ElapsedMilliseconds >= handshakeDeadline) throw new InvalidOperationException("HANDSHAKE_TIMEOUT");
|
|
491
|
+
Thread.Sleep(20);
|
|
492
|
+
}
|
|
493
|
+
connected.GetAwaiter().GetResult();
|
|
494
|
+
uint pid, session;
|
|
495
|
+
native.Require(ChannelApi.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out pid));
|
|
496
|
+
native.Require(ChannelApi.GetNamedPipeClientSessionId(pipe.SafePipeHandle, out session));
|
|
497
|
+
using (var peerProcess = native.Open(checked((int)pid)))
|
|
498
|
+
using (var peerToken = native.Token(peerProcess))
|
|
499
|
+
{
|
|
500
|
+
if (session != request.sessionId) throw new InvalidOperationException("PIPE_SESSION_MISMATCH");
|
|
501
|
+
var peer = BindPeer(files, checked((int)pid), peerProcess, request.bootstrap,
|
|
502
|
+
request.targetSid, request.sessionId, request.expectedCreationTime);
|
|
503
|
+
uint confirmed;
|
|
504
|
+
native.Require(ChannelApi.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out confirmed));
|
|
505
|
+
if (pid != confirmed) throw new InvalidOperationException("PIPE_CLIENT_CHANGED");
|
|
506
|
+
CheckConnection();
|
|
507
|
+
heldPeer = peerProcess;
|
|
508
|
+
heldPipe = pipe;
|
|
509
|
+
heldPeerIdentity = peer;
|
|
510
|
+
targetSession = Session(targetSession.ownerSid, targetSession.sessionId, targetSession.logonTime);
|
|
511
|
+
CheckConnection();
|
|
512
|
+
handshakeDeadline = 0;
|
|
513
|
+
Emit(new { type = "worker", peer, targetSession });
|
|
514
|
+
NextPeerRead();
|
|
515
|
+
while (true)
|
|
516
|
+
{
|
|
517
|
+
CheckParent();
|
|
518
|
+
if (!native.Alive(peerProcess)) throw new InvalidOperationException("PEER_EXITED");
|
|
519
|
+
long partial = Interlocked.Read(ref partialDeadline);
|
|
520
|
+
if (partial != 0 &&
|
|
521
|
+
clock.ElapsedMilliseconds > partial) throw new InvalidOperationException("FRAME_TIMEOUT");
|
|
522
|
+
Command message;
|
|
523
|
+
if (TryControl(out message))
|
|
524
|
+
{
|
|
525
|
+
if (message.action == "close") { Emit(new { type = "closed", reason = "controller-close" }); return; }
|
|
526
|
+
if (message.action == "authorize")
|
|
527
|
+
{
|
|
528
|
+
Authorize(message.id, cross);
|
|
529
|
+
}
|
|
530
|
+
else if (message.action == "send")
|
|
531
|
+
{
|
|
532
|
+
if (message.frame == null) throw new InvalidOperationException("OBJECT_FRAME_REQUIRED");
|
|
533
|
+
var bytes = Bytes(message.frame, 16384);
|
|
534
|
+
Wait(pipe.WriteAsync(bytes, 0, bytes.Length), 1000);
|
|
535
|
+
CheckConnection();
|
|
536
|
+
Emit(new { type = "sent", id = message.id });
|
|
537
|
+
}
|
|
538
|
+
else throw new InvalidOperationException("UNKNOWN_COMMAND");
|
|
539
|
+
}
|
|
540
|
+
if (peerRead.IsCompleted)
|
|
541
|
+
{
|
|
542
|
+
var frame = peerRead.GetAwaiter().GetResult();
|
|
543
|
+
Emit(new { type = "frame", peer, frame });
|
|
544
|
+
NextPeerRead();
|
|
545
|
+
}
|
|
546
|
+
Thread.Sleep(20);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
private void Authorize(int id, bool cross)
|
|
554
|
+
{
|
|
555
|
+
var current = Facts();
|
|
556
|
+
if (cross) RequireAdmin(current);
|
|
557
|
+
targetSession = Session(targetSession.ownerSid, targetSession.sessionId, targetSession.logonTime);
|
|
558
|
+
CheckConnection();
|
|
559
|
+
Emit(new { type = "authorized", id, actorFacts = current, controllerIdentity = controller, targetSession });
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
private SessionFact Session(string sid, int sessionId, string logonTime)
|
|
563
|
+
{
|
|
564
|
+
var observation = Task.Run(() => new ChannelSession().Observe(sid, sessionId, logonTime));
|
|
565
|
+
Wait(observation, 5000);
|
|
566
|
+
return observation.GetAwaiter().GetResult();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private void CheckConnection()
|
|
570
|
+
{
|
|
571
|
+
CheckQueryProgress();
|
|
572
|
+
if (handshakeDeadline != 0 &&
|
|
573
|
+
clock.ElapsedMilliseconds >= handshakeDeadline) throw new InvalidOperationException("HANDSHAKE_TIMEOUT");
|
|
574
|
+
long partial = Interlocked.Read(ref partialDeadline);
|
|
575
|
+
if (partial != 0 &&
|
|
576
|
+
clock.ElapsedMilliseconds > partial) throw new InvalidOperationException("FRAME_TIMEOUT");
|
|
577
|
+
if (heldPeer == null) return;
|
|
578
|
+
if (!native.Alive(heldPeer)) throw new InvalidOperationException("PEER_EXITED");
|
|
579
|
+
using (var token = native.Token(heldPeer))
|
|
580
|
+
{
|
|
581
|
+
if (native.Sid(token) != heldPeerIdentity.ownerSid ||
|
|
582
|
+
native.Number(token, 12) != heldPeerIdentity.sessionId)
|
|
583
|
+
throw new InvalidOperationException("PEER_TOKEN_CHANGED");
|
|
584
|
+
}
|
|
585
|
+
uint available;
|
|
586
|
+
if (!ChannelApi.PeekNamedPipe(heldPipe.SafePipeHandle, IntPtr.Zero, 0, IntPtr.Zero, out available, IntPtr.Zero))
|
|
587
|
+
throw new InvalidOperationException("CHANNEL_DISCONNECTED");
|
|
588
|
+
if (!native.Alive(heldPeer)) throw new InvalidOperationException("PEER_EXITED");
|
|
589
|
+
CheckPeerRead();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private void ReadManifest(Request request)
|
|
593
|
+
{
|
|
594
|
+
Guid operation;
|
|
595
|
+
if (!Guid.TryParseExact(request.operationId, "D", out operation)) throw new InvalidOperationException("OPERATION_ID_INVALID");
|
|
596
|
+
Manifest manifest;
|
|
597
|
+
using (var preliminary = new ChannelFiles(controller.ownerSid))
|
|
598
|
+
{
|
|
599
|
+
var parentPath = preliminary.PathValue(request.manifestParent);
|
|
600
|
+
var path = preliminary.PathValue(request.path);
|
|
601
|
+
var expected = Path.Combine(parentPath, operation.ToString("D"), "manifest.json");
|
|
602
|
+
if (!string.Equals(path, expected, StringComparison.OrdinalIgnoreCase))
|
|
603
|
+
throw new InvalidOperationException("MANIFEST_PATH_MISMATCH");
|
|
604
|
+
using (var stream = preliminary.HoldFile(path, false))
|
|
605
|
+
{
|
|
606
|
+
if (stream.Length > 65536) throw new InvalidOperationException("MANIFEST_BOUNDS");
|
|
607
|
+
var bytes = new byte[checked((int)stream.Length)];
|
|
608
|
+
int count = stream.Read(bytes, 0, bytes.Length);
|
|
609
|
+
if (count != bytes.Length) throw new InvalidOperationException("MANIFEST_READ_INCOMPLETE");
|
|
610
|
+
manifest = Parse<Manifest>(utf8.GetString(bytes));
|
|
611
|
+
}
|
|
612
|
+
if (manifest.protocol != 1 ||
|
|
613
|
+
manifest.operationId != request.operationId ||
|
|
614
|
+
manifest.targetSid != controller.ownerSid ||
|
|
615
|
+
manifest.sessionId != controller.sessionId ||
|
|
616
|
+
manifest.controllerIdentity == null ||
|
|
617
|
+
manifest.targetSession == null ||
|
|
618
|
+
manifest.targetSession.ownerSid != manifest.targetSid ||
|
|
619
|
+
manifest.targetSession.sessionId != manifest.sessionId ||
|
|
620
|
+
string.IsNullOrEmpty(manifest.targetSession.logonTime))
|
|
621
|
+
throw new InvalidOperationException("MANIFEST_BINDING_MISMATCH");
|
|
622
|
+
var actor = manifest.controllerIdentity;
|
|
623
|
+
using (var protectedFiles = new ChannelFiles(actor.ownerSid))
|
|
624
|
+
{
|
|
625
|
+
protectedFiles.Chain(path, true);
|
|
626
|
+
var owner = protectedFiles.Owner(path);
|
|
627
|
+
if (owner != actor.ownerSid) throw new InvalidOperationException("MANIFEST_OWNER_MISMATCH");
|
|
628
|
+
bool cross = actor.ownerSid != controller.ownerSid;
|
|
629
|
+
BindBootstrap(protectedFiles, manifest.bootstrap, path, parentPath, request.operationId, cross);
|
|
630
|
+
var reader = BindPeer(protectedFiles, controller.pid, parent, manifest.bootstrap,
|
|
631
|
+
manifest.targetSid, manifest.sessionId, controller.creationTime);
|
|
632
|
+
var session = Session(manifest.targetSid, manifest.sessionId, manifest.targetSession.logonTime);
|
|
633
|
+
Emit(new { type = "result", document = manifest, readerIdentity = reader, targetSession = session,
|
|
634
|
+
protection = new { actorOwnerSid = owner, actorProcessQueried = false,
|
|
635
|
+
scope = cross ? "other-user-read-only" : "same-user-not-self-tamper-proof",
|
|
636
|
+
loadedCodeAttestation = false } });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
internal sealed class Request
|
|
643
|
+
{
|
|
644
|
+
public string action { get; set; }
|
|
645
|
+
public string operationId { get; set; }
|
|
646
|
+
public string targetSid { get; set; }
|
|
647
|
+
public int sessionId { get; set; }
|
|
648
|
+
public Bootstrap bootstrap { get; set; }
|
|
649
|
+
public string manifestParent { get; set; }
|
|
650
|
+
public Dictionary<string, object> document { get; set; }
|
|
651
|
+
public int handshakeTimeoutMs { get; set; }
|
|
652
|
+
public int lifetimeMs { get; set; }
|
|
653
|
+
public string expectedCreationTime { get; set; }
|
|
654
|
+
public string path { get; set; }
|
|
655
|
+
public string root { get; set; }
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
internal sealed class Bootstrap
|
|
659
|
+
{
|
|
660
|
+
public string nodePath { get; set; }
|
|
661
|
+
public string scriptPath { get; set; }
|
|
662
|
+
public string nodeSha256 { get; set; }
|
|
663
|
+
public string scriptSha256 { get; set; }
|
|
664
|
+
public string[] argv { get; set; }
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
internal sealed class Command
|
|
668
|
+
{
|
|
669
|
+
public string action { get; set; }
|
|
670
|
+
public int id { get; set; }
|
|
671
|
+
public Dictionary<string, object> frame { get; set; }
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
internal sealed class Manifest
|
|
675
|
+
{
|
|
676
|
+
public int protocol { get; set; }
|
|
677
|
+
public string operationId { get; set; }
|
|
678
|
+
public string endpoint { get; set; }
|
|
679
|
+
public string targetSid { get; set; }
|
|
680
|
+
public int sessionId { get; set; }
|
|
681
|
+
public Identity controllerIdentity { get; set; }
|
|
682
|
+
public ActorFacts actorFacts { get; set; }
|
|
683
|
+
public SessionFact targetSession { get; set; }
|
|
684
|
+
public Bootstrap bootstrap { get; set; }
|
|
685
|
+
public Dictionary<string, object> document { get; set; }
|
|
686
|
+
}
|