@holoscript/holosystem 0.2.2 → 0.2.4

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.
@@ -0,0 +1,667 @@
1
+ using System;
2
+ using System.Collections.Generic;
3
+ using System.IO;
4
+ using System.Runtime.InteropServices;
5
+ using System.Text;
6
+ using System.Threading.Tasks;
7
+
8
+ internal static class Program
9
+ {
10
+ private const string Protocol = "holoscript.holosystem.windows-sandbox-launch.v1";
11
+ private const string LowIntegritySid = "S-1-16-4096";
12
+ private const int OutputLimit = 1024 * 1024;
13
+
14
+ private const uint TokenAssignPrimary = 0x0001;
15
+ private const uint TokenDuplicate = 0x0002;
16
+ private const uint TokenQuery = 0x0008;
17
+ private const uint TokenAdjustDefault = 0x0080;
18
+ private const uint TokenAdjustSessionId = 0x0100;
19
+ private const uint DisableMaxPrivilege = 0x1;
20
+ private const uint SeGroupIntegrity = 0x20;
21
+ private const uint SePrivilegeEnabled = 0x2;
22
+ private const int TokenPrivileges = 3;
23
+ private const int TokenIntegrityLevel = 25;
24
+
25
+ private const uint CreateSuspended = 0x00000004;
26
+ private const uint CreateNoWindow = 0x08000000;
27
+ private const uint CreateUnicodeEnvironment = 0x00000400;
28
+ private const uint ExtendedStartupInfoPresent = 0x00080000;
29
+ private const uint StartfUseStdHandles = 0x00000100;
30
+ private const uint HandleFlagInherit = 0x00000001;
31
+ private const uint GenericRead = 0x80000000;
32
+ private const uint FileShareRead = 0x00000001;
33
+ private const uint FileShareWrite = 0x00000002;
34
+ private const uint OpenExisting = 3;
35
+ private const uint FileAttributeNormal = 0x00000080;
36
+ private const uint WaitObject0 = 0;
37
+ private const uint WaitTimeout = 258;
38
+
39
+ private const uint JobObjectLimitActiveProcess = 0x00000008;
40
+ private const uint JobObjectLimitProcessMemory = 0x00000100;
41
+ private const uint JobObjectLimitKillOnJobClose = 0x00002000;
42
+ private const uint JobObjectUiLimitHandles = 0x00000001;
43
+ private const uint JobObjectUiLimitReadClipboard = 0x00000002;
44
+ private const uint JobObjectUiLimitWriteClipboard = 0x00000004;
45
+ private const uint JobObjectUiLimitSystemParameters = 0x00000008;
46
+ private const uint JobObjectUiLimitDisplaySettings = 0x00000010;
47
+ private const uint JobObjectUiLimitGlobalAtoms = 0x00000020;
48
+ private const uint JobObjectUiLimitDesktop = 0x00000040;
49
+ private const uint JobObjectUiLimitExitWindows = 0x00000080;
50
+ private const int JobObjectBasicUiRestrictions = 4;
51
+ private const int JobObjectExtendedLimitInformation = 9;
52
+
53
+ private sealed class Arguments
54
+ {
55
+ public string Executable;
56
+ public string WorkingDirectory;
57
+ public string SandboxRoot;
58
+ public string WritableTemp;
59
+ public int TimeoutMilliseconds;
60
+ public UIntPtr ProcessMemoryBytes;
61
+ public string[] ChildArguments;
62
+ }
63
+
64
+ private sealed class LaunchResult
65
+ {
66
+ public bool Launched;
67
+ public bool TimedOut;
68
+ public int? ExitCode;
69
+ public bool FilteredToken;
70
+ public bool DisableMaxPrivilege;
71
+ public int EnabledPrivilegeCount;
72
+ public bool PrivilegesBounded;
73
+ public bool LowIntegrity;
74
+ public bool AssignedBeforeResume;
75
+ public bool HandleAllowlist;
76
+ public bool KillOnClose;
77
+ public bool ActiveProcessLimit;
78
+ public bool ProcessMemoryLimit;
79
+ public bool UiRestrictions;
80
+ public bool WritableTempLowIntegrity;
81
+ public byte[] StandardOutput = new byte[0];
82
+ public byte[] StandardError = new byte[0];
83
+ public string ErrorStage;
84
+ public int ErrorCode;
85
+ }
86
+
87
+ public static int Main(string[] args)
88
+ {
89
+ LaunchResult result = new LaunchResult();
90
+ try
91
+ {
92
+ Arguments parsed = ParseArguments(args);
93
+ result = Launch(parsed);
94
+ }
95
+ catch (Exception error)
96
+ {
97
+ result.ErrorStage = "launcher-exception";
98
+ result.ErrorCode = Marshal.GetLastWin32Error();
99
+ result.StandardError = Encoding.UTF8.GetBytes(error.GetType().Name + ": " + error.Message);
100
+ }
101
+
102
+ Console.OutputEncoding = new UTF8Encoding(false);
103
+ Console.WriteLine(ToJson(result));
104
+ return 0;
105
+ }
106
+
107
+ private static Arguments ParseArguments(string[] args)
108
+ {
109
+ Dictionary<string, string> values = new Dictionary<string, string>(StringComparer.Ordinal);
110
+ int separator = Array.IndexOf(args, "--");
111
+ if (separator < 0 || separator % 2 != 0)
112
+ {
113
+ throw new ArgumentException("A closed key/value launcher prefix followed by -- is required.");
114
+ }
115
+ for (int index = 0; index < separator; index += 2)
116
+ {
117
+ string key = args[index];
118
+ if (!key.StartsWith("--", StringComparison.Ordinal) || values.ContainsKey(key))
119
+ {
120
+ throw new ArgumentException("Launcher fields must be unique closed-vocabulary names.");
121
+ }
122
+ values.Add(key, args[index + 1]);
123
+ }
124
+
125
+ string[] required = {
126
+ "--executable", "--working-directory", "--sandbox-root", "--writable-temp",
127
+ "--timeout-ms", "--process-memory-bytes"
128
+ };
129
+ if (values.Count != required.Length)
130
+ {
131
+ throw new ArgumentException("Unknown or missing launcher field.");
132
+ }
133
+ foreach (string key in required)
134
+ {
135
+ if (!values.ContainsKey(key)) throw new ArgumentException("Missing launcher field " + key + ".");
136
+ }
137
+
138
+ int timeout;
139
+ ulong memory;
140
+ if (!Int32.TryParse(values["--timeout-ms"], out timeout) || timeout < 1000 || timeout > 120000)
141
+ {
142
+ throw new ArgumentException("timeout-ms is outside the fixed launcher bound.");
143
+ }
144
+ if (!UInt64.TryParse(values["--process-memory-bytes"], out memory) || memory < 134217728 || memory > 1073741824)
145
+ {
146
+ throw new ArgumentException("process-memory-bytes is outside the fixed launcher bound.");
147
+ }
148
+
149
+ string executable = Path.GetFullPath(values["--executable"]);
150
+ string working = Path.GetFullPath(values["--working-directory"]);
151
+ string root = Path.GetFullPath(values["--sandbox-root"]);
152
+ string writableTemp = Path.GetFullPath(values["--writable-temp"]);
153
+ string rootPrefix = root.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal)
154
+ ? root : root + Path.DirectorySeparatorChar;
155
+ if (!IsInsideRoot(executable, root, rootPrefix) ||
156
+ !IsInsideRoot(working, root, rootPrefix) ||
157
+ !IsInsideRoot(writableTemp, root, rootPrefix))
158
+ {
159
+ throw new ArgumentException("Executable, working directory, and writable temp must stay inside sandbox-root.");
160
+ }
161
+
162
+ string[] child = new string[args.Length - separator - 1];
163
+ Array.Copy(args, separator + 1, child, 0, child.Length);
164
+ return new Arguments {
165
+ Executable = executable,
166
+ WorkingDirectory = working,
167
+ SandboxRoot = root,
168
+ WritableTemp = writableTemp,
169
+ TimeoutMilliseconds = timeout,
170
+ ProcessMemoryBytes = new UIntPtr(memory),
171
+ ChildArguments = child,
172
+ };
173
+ }
174
+
175
+ private static bool IsInsideRoot(string value, string root, string rootPrefix)
176
+ {
177
+ return value.Equals(root, StringComparison.OrdinalIgnoreCase) ||
178
+ value.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase);
179
+ }
180
+
181
+ private static LaunchResult Launch(Arguments args)
182
+ {
183
+ LaunchResult result = new LaunchResult();
184
+ IntPtr processToken = IntPtr.Zero;
185
+ IntPtr restrictedToken = IntPtr.Zero;
186
+ IntPtr lowSid = IntPtr.Zero;
187
+ IntPtr job = IntPtr.Zero;
188
+ IntPtr stdoutRead = IntPtr.Zero;
189
+ IntPtr stdoutWrite = IntPtr.Zero;
190
+ IntPtr stderrRead = IntPtr.Zero;
191
+ IntPtr stderrWrite = IntPtr.Zero;
192
+ IntPtr nullInput = IntPtr.Zero;
193
+ IntPtr attributeList = IntPtr.Zero;
194
+ IntPtr handleList = IntPtr.Zero;
195
+ PROCESS_INFORMATION process = new PROCESS_INFORMATION();
196
+
197
+ try
198
+ {
199
+ ApplyLowIntegrityLabel(args.WritableTemp);
200
+ result.WritableTempLowIntegrity = true;
201
+
202
+ uint tokenAccess = TokenAssignPrimary | TokenDuplicate | TokenQuery | TokenAdjustDefault | TokenAdjustSessionId;
203
+ Check(OpenProcessToken(GetCurrentProcess(), tokenAccess, out processToken), "open-process-token");
204
+ Check(CreateRestrictedToken(processToken, DisableMaxPrivilege, 0, null, 0, IntPtr.Zero, 0, null, out restrictedToken), "create-restricted-token");
205
+ result.FilteredToken = true;
206
+ result.DisableMaxPrivilege = true;
207
+
208
+ Check(ConvertStringSidToSid(LowIntegritySid, out lowSid), "create-low-integrity-sid");
209
+ SetTokenIntegrity(restrictedToken, lowSid);
210
+ result.LowIntegrity = HasLowIntegrity(restrictedToken);
211
+ result.EnabledPrivilegeCount = EnabledPrivilegeCount(restrictedToken);
212
+ result.PrivilegesBounded = result.EnabledPrivilegeCount <= 1;
213
+ if (!result.LowIntegrity || !result.PrivilegesBounded)
214
+ {
215
+ throw new InvalidOperationException("Restricted-token evidence did not verify before launch.");
216
+ }
217
+
218
+ job = CreateJobObject(IntPtr.Zero, null);
219
+ Check(job != IntPtr.Zero, "create-job-object");
220
+ ConfigureJob(job, args.ProcessMemoryBytes);
221
+ result.KillOnClose = true;
222
+ result.ActiveProcessLimit = true;
223
+ result.ProcessMemoryLimit = true;
224
+ result.UiRestrictions = true;
225
+
226
+ SECURITY_ATTRIBUTES pipeSecurity = new SECURITY_ATTRIBUTES();
227
+ pipeSecurity.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
228
+ pipeSecurity.bInheritHandle = true;
229
+ Check(CreatePipe(out stdoutRead, out stdoutWrite, ref pipeSecurity, 0), "create-stdout-pipe");
230
+ Check(SetHandleInformation(stdoutRead, HandleFlagInherit, 0), "protect-stdout-reader");
231
+ Check(CreatePipe(out stderrRead, out stderrWrite, ref pipeSecurity, 0), "create-stderr-pipe");
232
+ Check(SetHandleInformation(stderrRead, HandleFlagInherit, 0), "protect-stderr-reader");
233
+
234
+ nullInput = CreateFile("NUL", GenericRead, FileShareRead | FileShareWrite, ref pipeSecurity, OpenExisting, FileAttributeNormal, IntPtr.Zero);
235
+ Check(nullInput != new IntPtr(-1), "open-null-input");
236
+
237
+ UIntPtr attributeBytes = UIntPtr.Zero;
238
+ InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref attributeBytes);
239
+ attributeList = Marshal.AllocHGlobal((int)attributeBytes.ToUInt64());
240
+ Check(InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeBytes), "initialize-handle-allowlist");
241
+ handleList = Marshal.AllocHGlobal(IntPtr.Size * 3);
242
+ Marshal.WriteIntPtr(handleList, 0, nullInput);
243
+ Marshal.WriteIntPtr(handleList, IntPtr.Size, stdoutWrite);
244
+ Marshal.WriteIntPtr(handleList, IntPtr.Size * 2, stderrWrite);
245
+ Check(UpdateProcThreadAttribute(
246
+ attributeList,
247
+ 0,
248
+ new IntPtr(0x00020002),
249
+ handleList,
250
+ new UIntPtr((uint)(IntPtr.Size * 3)),
251
+ IntPtr.Zero,
252
+ IntPtr.Zero
253
+ ), "set-handle-allowlist");
254
+ result.HandleAllowlist = true;
255
+
256
+ STARTUPINFOEX startup = new STARTUPINFOEX();
257
+ startup.StartupInfo.cb = Marshal.SizeOf(typeof(STARTUPINFOEX));
258
+ startup.StartupInfo.dwFlags = StartfUseStdHandles;
259
+ startup.StartupInfo.hStdInput = nullInput;
260
+ startup.StartupInfo.hStdOutput = stdoutWrite;
261
+ startup.StartupInfo.hStdError = stderrWrite;
262
+ startup.lpAttributeList = attributeList;
263
+
264
+ StringBuilder commandLine = new StringBuilder(BuildCommandLine(args.Executable, args.ChildArguments));
265
+ uint flags = CreateSuspended | CreateNoWindow | CreateUnicodeEnvironment | ExtendedStartupInfoPresent;
266
+ bool created = CreateProcessAsUser(
267
+ restrictedToken,
268
+ args.Executable,
269
+ commandLine,
270
+ IntPtr.Zero,
271
+ IntPtr.Zero,
272
+ true,
273
+ flags,
274
+ IntPtr.Zero,
275
+ args.WorkingDirectory,
276
+ ref startup,
277
+ out process
278
+ );
279
+ Check(created, "create-restricted-process");
280
+ result.Launched = true;
281
+
282
+ CloseHandle(stdoutWrite);
283
+ stdoutWrite = IntPtr.Zero;
284
+ CloseHandle(stderrWrite);
285
+ stderrWrite = IntPtr.Zero;
286
+
287
+ Check(AssignProcessToJobObject(job, process.hProcess), "assign-process-to-job");
288
+ result.AssignedBeforeResume = true;
289
+
290
+ Task<byte[]> stdoutTask = ReadBoundedAsync(stdoutRead);
291
+ Task<byte[]> stderrTask = ReadBoundedAsync(stderrRead);
292
+ uint resumed = ResumeThread(process.hThread);
293
+ if (resumed == UInt32.MaxValue) ThrowWin32("resume-restricted-process");
294
+
295
+ uint wait = WaitForSingleObject(process.hProcess, (uint)args.TimeoutMilliseconds);
296
+ if (wait == WaitTimeout)
297
+ {
298
+ result.TimedOut = true;
299
+ TerminateJobObject(job, 0x7f);
300
+ WaitForSingleObject(process.hProcess, 5000);
301
+ }
302
+ else if (wait != WaitObject0)
303
+ {
304
+ ThrowWin32("wait-restricted-process");
305
+ }
306
+
307
+ uint exitCode;
308
+ Check(GetExitCodeProcess(process.hProcess, out exitCode), "read-process-exit");
309
+ result.ExitCode = unchecked((int)exitCode);
310
+ Task.WaitAll(new Task[] { stdoutTask, stderrTask }, 5000);
311
+ result.StandardOutput = stdoutTask.IsCompleted ? stdoutTask.Result : new byte[0];
312
+ result.StandardError = stderrTask.IsCompleted ? stderrTask.Result : new byte[0];
313
+ }
314
+ catch (LauncherException error)
315
+ {
316
+ result.ErrorStage = error.Stage;
317
+ result.ErrorCode = error.NativeError;
318
+ if (process.hProcess != IntPtr.Zero) TerminateJobObject(job, 0x7e);
319
+ }
320
+ finally
321
+ {
322
+ CloseIfValid(process.hThread);
323
+ CloseIfValid(process.hProcess);
324
+ CloseIfValid(stdoutRead);
325
+ CloseIfValid(stdoutWrite);
326
+ CloseIfValid(stderrRead);
327
+ CloseIfValid(stderrWrite);
328
+ CloseIfValid(nullInput);
329
+ if (attributeList != IntPtr.Zero) DeleteProcThreadAttributeList(attributeList);
330
+ if (handleList != IntPtr.Zero) Marshal.FreeHGlobal(handleList);
331
+ if (attributeList != IntPtr.Zero) Marshal.FreeHGlobal(attributeList);
332
+ CloseIfValid(job);
333
+ CloseIfValid(restrictedToken);
334
+ CloseIfValid(processToken);
335
+ if (lowSid != IntPtr.Zero) LocalFree(lowSid);
336
+ }
337
+ return result;
338
+ }
339
+
340
+ private static void ApplyLowIntegrityLabel(string path)
341
+ {
342
+ IntPtr descriptor;
343
+ uint size;
344
+ Check(ConvertStringSecurityDescriptorToSecurityDescriptor("S:(ML;OICI;NW;;;LW)", 1, out descriptor, out size), "create-low-integrity-acl");
345
+ try
346
+ {
347
+ Check(SetFileSecurity(path, 0x00000010, descriptor), "apply-low-integrity-acl");
348
+ }
349
+ finally
350
+ {
351
+ LocalFree(descriptor);
352
+ }
353
+ }
354
+
355
+ private static void SetTokenIntegrity(IntPtr token, IntPtr sid)
356
+ {
357
+ int sidLength = (int)GetLengthSid(sid);
358
+ int structLength = Marshal.SizeOf(typeof(TOKEN_MANDATORY_LABEL));
359
+ IntPtr buffer = Marshal.AllocHGlobal(structLength + sidLength);
360
+ try
361
+ {
362
+ IntPtr copiedSid = IntPtr.Add(buffer, structLength);
363
+ Check(CopySid((uint)sidLength, copiedSid, sid), "copy-low-integrity-sid");
364
+ TOKEN_MANDATORY_LABEL label = new TOKEN_MANDATORY_LABEL();
365
+ label.Label.Sid = copiedSid;
366
+ label.Label.Attributes = SeGroupIntegrity;
367
+ Marshal.StructureToPtr(label, buffer, false);
368
+ Check(SetTokenInformation(token, TokenIntegrityLevel, buffer, structLength + sidLength), "set-low-integrity-token");
369
+ }
370
+ finally
371
+ {
372
+ Marshal.FreeHGlobal(buffer);
373
+ }
374
+ }
375
+
376
+ private static bool HasLowIntegrity(IntPtr token)
377
+ {
378
+ IntPtr buffer = QueryTokenInformation(token, TokenIntegrityLevel);
379
+ try
380
+ {
381
+ TOKEN_MANDATORY_LABEL label = (TOKEN_MANDATORY_LABEL)Marshal.PtrToStructure(buffer, typeof(TOKEN_MANDATORY_LABEL));
382
+ IntPtr countPointer = GetSidSubAuthorityCount(label.Label.Sid);
383
+ byte count = Marshal.ReadByte(countPointer);
384
+ if (count == 0) return false;
385
+ int level = Marshal.ReadInt32(GetSidSubAuthority(label.Label.Sid, (uint)(count - 1)));
386
+ return level <= 4096;
387
+ }
388
+ finally
389
+ {
390
+ Marshal.FreeHGlobal(buffer);
391
+ }
392
+ }
393
+
394
+ private static int EnabledPrivilegeCount(IntPtr token)
395
+ {
396
+ IntPtr buffer = QueryTokenInformation(token, TokenPrivileges);
397
+ try
398
+ {
399
+ uint count = unchecked((uint)Marshal.ReadInt32(buffer));
400
+ int offset = 4;
401
+ int size = Marshal.SizeOf(typeof(LUID_AND_ATTRIBUTES));
402
+ int enabled = 0;
403
+ for (uint index = 0; index < count; index++)
404
+ {
405
+ IntPtr entryPointer = IntPtr.Add(buffer, offset + ((int)index * size));
406
+ LUID_AND_ATTRIBUTES entry = (LUID_AND_ATTRIBUTES)Marshal.PtrToStructure(entryPointer, typeof(LUID_AND_ATTRIBUTES));
407
+ if ((entry.Attributes & SePrivilegeEnabled) != 0) enabled++;
408
+ }
409
+ return enabled;
410
+ }
411
+ finally
412
+ {
413
+ Marshal.FreeHGlobal(buffer);
414
+ }
415
+ }
416
+
417
+ private static IntPtr QueryTokenInformation(IntPtr token, int informationClass)
418
+ {
419
+ int length = 0;
420
+ GetTokenInformation(token, informationClass, IntPtr.Zero, 0, out length);
421
+ if (length <= 0) ThrowWin32("size-token-information");
422
+ IntPtr buffer = Marshal.AllocHGlobal(length);
423
+ if (!GetTokenInformation(token, informationClass, buffer, length, out length))
424
+ {
425
+ int error = Marshal.GetLastWin32Error();
426
+ Marshal.FreeHGlobal(buffer);
427
+ throw new LauncherException("read-token-information", error);
428
+ }
429
+ return buffer;
430
+ }
431
+
432
+ private static void ConfigureJob(IntPtr job, UIntPtr processMemory)
433
+ {
434
+ JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
435
+ limits.BasicLimitInformation.LimitFlags = JobObjectLimitKillOnJobClose |
436
+ JobObjectLimitActiveProcess | JobObjectLimitProcessMemory;
437
+ limits.BasicLimitInformation.ActiveProcessLimit = 1;
438
+ limits.ProcessMemoryLimit = processMemory;
439
+ int limitSize = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
440
+ IntPtr limitPointer = Marshal.AllocHGlobal(limitSize);
441
+ try
442
+ {
443
+ Marshal.StructureToPtr(limits, limitPointer, false);
444
+ Check(SetInformationJobObject(job, JobObjectExtendedLimitInformation, limitPointer, (uint)limitSize), "set-job-limits");
445
+ }
446
+ finally
447
+ {
448
+ Marshal.FreeHGlobal(limitPointer);
449
+ }
450
+
451
+ JOBOBJECT_BASIC_UI_RESTRICTIONS ui = new JOBOBJECT_BASIC_UI_RESTRICTIONS();
452
+ ui.UIRestrictionsClass = JobObjectUiLimitHandles | JobObjectUiLimitReadClipboard |
453
+ JobObjectUiLimitWriteClipboard | JobObjectUiLimitSystemParameters |
454
+ JobObjectUiLimitDisplaySettings | JobObjectUiLimitGlobalAtoms |
455
+ JobObjectUiLimitDesktop | JobObjectUiLimitExitWindows;
456
+ int uiSize = Marshal.SizeOf(typeof(JOBOBJECT_BASIC_UI_RESTRICTIONS));
457
+ IntPtr uiPointer = Marshal.AllocHGlobal(uiSize);
458
+ try
459
+ {
460
+ Marshal.StructureToPtr(ui, uiPointer, false);
461
+ Check(SetInformationJobObject(job, JobObjectBasicUiRestrictions, uiPointer, (uint)uiSize), "set-job-ui-limits");
462
+ }
463
+ finally
464
+ {
465
+ Marshal.FreeHGlobal(uiPointer);
466
+ }
467
+
468
+ }
469
+
470
+ private static Task<byte[]> ReadBoundedAsync(IntPtr handle)
471
+ {
472
+ return Task.Factory.StartNew(delegate {
473
+ using (FileStream stream = new FileStream(new Microsoft.Win32.SafeHandles.SafeFileHandle(handle, false), FileAccess.Read, 4096, false))
474
+ using (MemoryStream output = new MemoryStream())
475
+ {
476
+ byte[] buffer = new byte[8192];
477
+ while (true)
478
+ {
479
+ int read = stream.Read(buffer, 0, buffer.Length);
480
+ if (read == 0) break;
481
+ if (output.Length + read > OutputLimit) throw new IOException("Child output exceeded the launcher limit.");
482
+ output.Write(buffer, 0, read);
483
+ }
484
+ return output.ToArray();
485
+ }
486
+ });
487
+ }
488
+
489
+ private static string BuildCommandLine(string executable, string[] args)
490
+ {
491
+ StringBuilder result = new StringBuilder();
492
+ result.Append(QuoteWindowsArgument(executable));
493
+ foreach (string argument in args)
494
+ {
495
+ result.Append(' ');
496
+ result.Append(QuoteWindowsArgument(argument));
497
+ }
498
+ return result.ToString();
499
+ }
500
+
501
+ private static string QuoteWindowsArgument(string value)
502
+ {
503
+ if (value.Length > 0 && value.IndexOfAny(new char[] { ' ', '\t', '\n', '\v', '"' }) < 0) return value;
504
+ StringBuilder result = new StringBuilder("\"");
505
+ int slashes = 0;
506
+ foreach (char character in value)
507
+ {
508
+ if (character == '\\')
509
+ {
510
+ slashes++;
511
+ }
512
+ else if (character == '"')
513
+ {
514
+ result.Append('\\', slashes * 2 + 1);
515
+ result.Append('"');
516
+ slashes = 0;
517
+ }
518
+ else
519
+ {
520
+ result.Append('\\', slashes);
521
+ result.Append(character);
522
+ slashes = 0;
523
+ }
524
+ }
525
+ result.Append('\\', slashes * 2);
526
+ result.Append('"');
527
+ return result.ToString();
528
+ }
529
+
530
+ private static string ToJson(LaunchResult result)
531
+ {
532
+ return "{" +
533
+ "\"protocol\":\"" + Protocol + "\"," +
534
+ "\"launched\":" + Bool(result.Launched) + "," +
535
+ "\"timedOut\":" + Bool(result.TimedOut) + "," +
536
+ "\"exitCode\":" + (result.ExitCode.HasValue ? result.ExitCode.Value.ToString() : "null") + "," +
537
+ "\"isolation\":{" +
538
+ "\"filteredToken\":" + Bool(result.FilteredToken) + "," +
539
+ "\"disableMaxPrivilege\":" + Bool(result.DisableMaxPrivilege) + "," +
540
+ "\"enabledPrivilegeCount\":" + result.EnabledPrivilegeCount.ToString() + "," +
541
+ "\"privilegesBounded\":" + Bool(result.PrivilegesBounded) + "," +
542
+ "\"lowIntegrity\":" + Bool(result.LowIntegrity) + "," +
543
+ "\"assignedBeforeResume\":" + Bool(result.AssignedBeforeResume) + "," +
544
+ "\"handleAllowlist\":" + Bool(result.HandleAllowlist) + "," +
545
+ "\"killOnClose\":" + Bool(result.KillOnClose) + "," +
546
+ "\"activeProcessLimit\":" + Bool(result.ActiveProcessLimit) + "," +
547
+ "\"processMemoryLimit\":" + Bool(result.ProcessMemoryLimit) + "," +
548
+ "\"uiRestrictions\":" + Bool(result.UiRestrictions) + "," +
549
+ "\"writableTempLowIntegrity\":" + Bool(result.WritableTempLowIntegrity) +
550
+ "}," +
551
+ "\"stdoutBase64\":\"" + Convert.ToBase64String(result.StandardOutput) + "\"," +
552
+ "\"stderrBase64\":\"" + Convert.ToBase64String(result.StandardError) + "\"," +
553
+ "\"errorStage\":" + (result.ErrorStage == null ? "null" : "\"" + EscapeJson(result.ErrorStage) + "\"") + "," +
554
+ "\"errorCode\":" + result.ErrorCode.ToString() +
555
+ "}";
556
+ }
557
+
558
+ private static string Bool(bool value) { return value ? "true" : "false"; }
559
+
560
+ private static string EscapeJson(string value)
561
+ {
562
+ return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", "\\r").Replace("\n", "\\n");
563
+ }
564
+
565
+ private static void Check(bool condition, string stage)
566
+ {
567
+ if (!condition) ThrowWin32(stage);
568
+ }
569
+
570
+ private static void ThrowWin32(string stage)
571
+ {
572
+ throw new LauncherException(stage, Marshal.GetLastWin32Error());
573
+ }
574
+
575
+ private static void CloseIfValid(IntPtr handle)
576
+ {
577
+ if (handle != IntPtr.Zero && handle != new IntPtr(-1)) CloseHandle(handle);
578
+ }
579
+
580
+ private sealed class LauncherException : Exception
581
+ {
582
+ public readonly string Stage;
583
+ public readonly int NativeError;
584
+ public LauncherException(string stage, int nativeError) : base(stage + " failed with Windows error " + nativeError + ".")
585
+ {
586
+ Stage = stage;
587
+ NativeError = nativeError;
588
+ }
589
+ }
590
+
591
+ [StructLayout(LayoutKind.Sequential)]
592
+ private struct SID_AND_ATTRIBUTES { public IntPtr Sid; public uint Attributes; }
593
+ [StructLayout(LayoutKind.Sequential)]
594
+ private struct LUID { public uint LowPart; public int HighPart; }
595
+ [StructLayout(LayoutKind.Sequential)]
596
+ private struct LUID_AND_ATTRIBUTES { public LUID Luid; public uint Attributes; }
597
+ [StructLayout(LayoutKind.Sequential)]
598
+ private struct TOKEN_MANDATORY_LABEL { public SID_AND_ATTRIBUTES Label; }
599
+ [StructLayout(LayoutKind.Sequential)]
600
+ private struct SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; [MarshalAs(UnmanagedType.Bool)] public bool bInheritHandle; }
601
+ [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
602
+ private struct STARTUPINFO
603
+ {
604
+ public int cb; public string lpReserved; public string lpDesktop; public string lpTitle;
605
+ public uint dwX; public uint dwY; public uint dwXSize; public uint dwYSize;
606
+ public uint dwXCountChars; public uint dwYCountChars; public uint dwFillAttribute;
607
+ public uint dwFlags; public ushort wShowWindow; public ushort cbReserved2;
608
+ public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError;
609
+ }
610
+ [StructLayout(LayoutKind.Sequential)]
611
+ private struct STARTUPINFOEX { public STARTUPINFO StartupInfo; public IntPtr lpAttributeList; }
612
+ [StructLayout(LayoutKind.Sequential)]
613
+ private struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; }
614
+ [StructLayout(LayoutKind.Sequential)]
615
+ private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
616
+ {
617
+ public long PerProcessUserTimeLimit; public long PerJobUserTimeLimit; public uint LimitFlags;
618
+ public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public uint ActiveProcessLimit;
619
+ public long Affinity; public uint PriorityClass; public uint SchedulingClass;
620
+ }
621
+ [StructLayout(LayoutKind.Sequential)]
622
+ private struct IO_COUNTERS
623
+ {
624
+ public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount;
625
+ public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount;
626
+ }
627
+ [StructLayout(LayoutKind.Sequential)]
628
+ private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
629
+ {
630
+ public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo;
631
+ public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit;
632
+ public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed;
633
+ }
634
+ [StructLayout(LayoutKind.Sequential)]
635
+ private struct JOBOBJECT_BASIC_UI_RESTRICTIONS { public uint UIRestrictionsClass; }
636
+
637
+ [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentProcess();
638
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle(IntPtr handle);
639
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern IntPtr LocalFree(IntPtr handle);
640
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateFile(string fileName, uint access, uint shareMode, ref SECURITY_ATTRIBUTES securityAttributes, uint creationDisposition, uint flagsAndAttributes, IntPtr templateFile);
641
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CreatePipe(out IntPtr read, out IntPtr write, ref SECURITY_ATTRIBUTES attributes, uint size);
642
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetHandleInformation(IntPtr handle, uint mask, uint flags);
643
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
644
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr thread);
645
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
646
+ [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateJobObject(IntPtr attributes, string name);
647
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr job, int informationClass, IntPtr information, uint length);
648
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
649
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool TerminateJobObject(IntPtr job, uint exitCode);
650
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool InitializeProcThreadAttributeList(IntPtr attributeList, int attributeCount, int flags, ref UIntPtr size);
651
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool UpdateProcThreadAttribute(IntPtr attributeList, uint flags, IntPtr attribute, IntPtr value, UIntPtr size, IntPtr previousValue, IntPtr returnSize);
652
+ [DllImport("kernel32.dll")] private static extern void DeleteProcThreadAttributeList(IntPtr attributeList);
653
+
654
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken(IntPtr process, uint access, out IntPtr token);
655
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CreateRestrictedToken(IntPtr existingToken, uint flags, uint disableSidCount, SID_AND_ATTRIBUTES[] disableSids, uint deletePrivilegeCount, IntPtr deletePrivileges, uint restrictedSidCount, SID_AND_ATTRIBUTES[] restrictedSids, out IntPtr newToken);
656
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool SetTokenInformation(IntPtr token, int informationClass, IntPtr information, int length);
657
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool GetTokenInformation(IntPtr token, int informationClass, IntPtr information, int length, out int returnLength);
658
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
659
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern IntPtr GetSidSubAuthority(IntPtr sid, uint subAuthority);
660
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern uint GetLengthSid(IntPtr sid);
661
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CopySid(uint destinationLength, IntPtr destination, IntPtr source);
662
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool ConvertStringSidToSid(string sid, out IntPtr binarySid);
663
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string descriptor, uint revision, out IntPtr securityDescriptor, out uint size);
664
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool SetFileSecurity(string path, uint securityInformation, IntPtr securityDescriptor);
665
+ [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
666
+ private static extern bool CreateProcessAsUser(IntPtr token, string applicationName, StringBuilder commandLine, IntPtr processAttributes, IntPtr threadAttributes, bool inheritHandles, uint creationFlags, IntPtr environment, string currentDirectory, ref STARTUPINFOEX startupInfo, out PROCESS_INFORMATION processInformation);
667
+ }