@holoscript/holosystem 0.2.4 → 0.2.5

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.
@@ -1,13 +1,19 @@
1
1
  using System;
2
2
  using System.Collections.Generic;
3
+ using System.Diagnostics;
3
4
  using System.IO;
5
+ using System.Net;
6
+ using System.Net.Sockets;
4
7
  using System.Runtime.InteropServices;
8
+ using System.Security.AccessControl;
9
+ using System.Security.Principal;
5
10
  using System.Text;
6
11
  using System.Threading.Tasks;
7
12
 
8
13
  internal static class Program
9
14
  {
10
15
  private const string Protocol = "holoscript.holosystem.windows-sandbox-launch.v1";
16
+ private const string AppContainerProtocol = "holoscript.holosystem.windows-appcontainer-launch.v1";
11
17
  private const string LowIntegritySid = "S-1-16-4096";
12
18
  private const int OutputLimit = 1024 * 1024;
13
19
 
@@ -21,6 +27,9 @@ internal static class Program
21
27
  private const uint SePrivilegeEnabled = 0x2;
22
28
  private const int TokenPrivileges = 3;
23
29
  private const int TokenIntegrityLevel = 25;
30
+ private const int TokenIsAppContainer = 29;
31
+ private const int TokenCapabilities = 30;
32
+ private const int TokenAppContainerSid = 31;
24
33
 
25
34
  private const uint CreateSuspended = 0x00000004;
26
35
  private const uint CreateNoWindow = 0x08000000;
@@ -49,6 +58,11 @@ internal static class Program
49
58
  private const uint JobObjectUiLimitExitWindows = 0x00000080;
50
59
  private const int JobObjectBasicUiRestrictions = 4;
51
60
  private const int JobObjectExtendedLimitInformation = 9;
61
+ private const int ErrorAccessDenied = 5;
62
+ private const int WsaAccessDenied = 10013;
63
+ private const int WsaTimedOut = 10060;
64
+ private static readonly IntPtr ProcThreadAttributeHandleList = new IntPtr(0x00020002);
65
+ private static readonly IntPtr ProcThreadAttributeSecurityCapabilities = new IntPtr(0x00020009);
52
66
 
53
67
  private sealed class Arguments
54
68
  {
@@ -59,6 +73,9 @@ internal static class Program
59
73
  public int TimeoutMilliseconds;
60
74
  public UIntPtr ProcessMemoryBytes;
61
75
  public string[] ChildArguments;
76
+ public bool AppContainerDeny;
77
+ public string ProtectedSentinel;
78
+ public string CanaryExecutable;
62
79
  }
63
80
 
64
81
  private sealed class LaunchResult
@@ -78,6 +95,18 @@ internal static class Program
78
95
  public bool ProcessMemoryLimit;
79
96
  public bool UiRestrictions;
80
97
  public bool WritableTempLowIntegrity;
98
+ public bool AppContainer;
99
+ public bool AppContainerSidMatched;
100
+ public int CapabilityCount;
101
+ public bool SnapshotReadExecuteGrant;
102
+ public bool WritableTempModifyGrant;
103
+ public bool FilesystemCanaryDenied;
104
+ public int FilesystemCanaryError;
105
+ public bool NetworkCanaryDenied;
106
+ public int NetworkCanaryError;
107
+ public bool LoopbackAccepted;
108
+ public bool ProfileDeleted;
109
+ public bool AppContainerMode;
81
110
  public byte[] StandardOutput = new byte[0];
82
111
  public byte[] StandardError = new byte[0];
83
112
  public string ErrorStage;
@@ -126,7 +155,11 @@ internal static class Program
126
155
  "--executable", "--working-directory", "--sandbox-root", "--writable-temp",
127
156
  "--timeout-ms", "--process-memory-bytes"
128
157
  };
129
- if (values.Count != required.Length)
158
+ bool appContainerDeny = values.ContainsKey("--appcontainer-deny") ||
159
+ values.ContainsKey("--protected-sentinel") ||
160
+ values.ContainsKey("--canary-executable");
161
+ int expectedCount = required.Length + (appContainerDeny ? 3 : 0);
162
+ if (values.Count != expectedCount)
130
163
  {
131
164
  throw new ArgumentException("Unknown or missing launcher field.");
132
165
  }
@@ -134,6 +167,14 @@ internal static class Program
134
167
  {
135
168
  if (!values.ContainsKey(key)) throw new ArgumentException("Missing launcher field " + key + ".");
136
169
  }
170
+ if (appContainerDeny &&
171
+ (!values.ContainsKey("--appcontainer-deny") ||
172
+ !values.ContainsKey("--protected-sentinel") ||
173
+ !values.ContainsKey("--canary-executable") ||
174
+ values["--appcontainer-deny"] != "v1"))
175
+ {
176
+ throw new ArgumentException("AppContainer deny mode requires its exact closed fields.");
177
+ }
137
178
 
138
179
  int timeout;
139
180
  ulong memory;
@@ -158,6 +199,22 @@ internal static class Program
158
199
  {
159
200
  throw new ArgumentException("Executable, working directory, and writable temp must stay inside sandbox-root.");
160
201
  }
202
+ string protectedSentinel = appContainerDeny
203
+ ? Path.GetFullPath(values["--protected-sentinel"])
204
+ : null;
205
+ if (appContainerDeny &&
206
+ (IsInsideRoot(protectedSentinel, root, rootPrefix) || !File.Exists(protectedSentinel)))
207
+ {
208
+ throw new ArgumentException("Protected sentinel must be an existing caller-readable file outside sandbox-root.");
209
+ }
210
+ string canaryExecutable = appContainerDeny
211
+ ? Path.GetFullPath(values["--canary-executable"])
212
+ : null;
213
+ if (appContainerDeny &&
214
+ (!IsInsideRoot(canaryExecutable, root, rootPrefix) || !File.Exists(canaryExecutable)))
215
+ {
216
+ throw new ArgumentException("AppContainer canary must be an existing file inside sandbox-root.");
217
+ }
161
218
 
162
219
  string[] child = new string[args.Length - separator - 1];
163
220
  Array.Copy(args, separator + 1, child, 0, child.Length);
@@ -169,6 +226,9 @@ internal static class Program
169
226
  TimeoutMilliseconds = timeout,
170
227
  ProcessMemoryBytes = new UIntPtr(memory),
171
228
  ChildArguments = child,
229
+ AppContainerDeny = appContainerDeny,
230
+ ProtectedSentinel = protectedSentinel,
231
+ CanaryExecutable = canaryExecutable,
172
232
  };
173
233
  }
174
234
 
@@ -184,6 +244,8 @@ internal static class Program
184
244
  IntPtr processToken = IntPtr.Zero;
185
245
  IntPtr restrictedToken = IntPtr.Zero;
186
246
  IntPtr lowSid = IntPtr.Zero;
247
+ IntPtr appContainerSid = IntPtr.Zero;
248
+ IntPtr childToken = IntPtr.Zero;
187
249
  IntPtr job = IntPtr.Zero;
188
250
  IntPtr stdoutRead = IntPtr.Zero;
189
251
  IntPtr stdoutWrite = IntPtr.Zero;
@@ -192,12 +254,16 @@ internal static class Program
192
254
  IntPtr nullInput = IntPtr.Zero;
193
255
  IntPtr attributeList = IntPtr.Zero;
194
256
  IntPtr handleList = IntPtr.Zero;
257
+ IntPtr securityCapabilitiesPointer = IntPtr.Zero;
258
+ string appContainerIdentity = null;
259
+ bool appContainerProfileCreated = false;
195
260
  PROCESS_INFORMATION process = new PROCESS_INFORMATION();
196
261
 
197
262
  try
198
263
  {
199
264
  ApplyLowIntegrityLabel(args.WritableTemp);
200
265
  result.WritableTempLowIntegrity = true;
266
+ result.AppContainerMode = args.AppContainerDeny;
201
267
 
202
268
  uint tokenAccess = TokenAssignPrimary | TokenDuplicate | TokenQuery | TokenAdjustDefault | TokenAdjustSessionId;
203
269
  Check(OpenProcessToken(GetCurrentProcess(), tokenAccess, out processToken), "open-process-token");
@@ -223,6 +289,35 @@ internal static class Program
223
289
  result.ProcessMemoryLimit = true;
224
290
  result.UiRestrictions = true;
225
291
 
292
+ if (args.AppContainerDeny)
293
+ {
294
+ appContainerIdentity = "HoloScript.HoloSystem.WHPX_" + Guid.NewGuid().ToString("N");
295
+ int profileResult = CreateAppContainerProfile(
296
+ appContainerIdentity,
297
+ "HoloSystem WHPX isolation",
298
+ "Ephemeral zero-capability HoloSystem VM host boundary",
299
+ IntPtr.Zero,
300
+ 0,
301
+ out appContainerSid
302
+ );
303
+ if (profileResult != 0)
304
+ {
305
+ throw new LauncherException("create-appcontainer-profile", profileResult);
306
+ }
307
+ appContainerProfileCreated = true;
308
+ SecurityIdentifier appSid = new SecurityIdentifier(appContainerSid);
309
+ GrantDirectoryAccess(args.SandboxRoot, appSid, FileSystemRights.ReadAndExecute | FileSystemRights.ListDirectory);
310
+ result.SnapshotReadExecuteGrant = true;
311
+ GrantDirectoryAccess(args.WritableTemp, appSid, FileSystemRights.Modify);
312
+ result.WritableTempModifyGrant = true;
313
+ HardenProtectedSentinel(args.ProtectedSentinel);
314
+ RunAppContainerCanaries(args, restrictedToken, appContainerSid, job, result);
315
+ if (!result.FilesystemCanaryDenied || !result.NetworkCanaryDenied)
316
+ {
317
+ throw new LauncherException("appcontainer-canary-denial", 0);
318
+ }
319
+ }
320
+
226
321
  SECURITY_ATTRIBUTES pipeSecurity = new SECURITY_ATTRIBUTES();
227
322
  pipeSecurity.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
228
323
  pipeSecurity.bInheritHandle = true;
@@ -235,9 +330,10 @@ internal static class Program
235
330
  Check(nullInput != new IntPtr(-1), "open-null-input");
236
331
 
237
332
  UIntPtr attributeBytes = UIntPtr.Zero;
238
- InitializeProcThreadAttributeList(IntPtr.Zero, 1, 0, ref attributeBytes);
333
+ int attributeCount = args.AppContainerDeny ? 2 : 1;
334
+ InitializeProcThreadAttributeList(IntPtr.Zero, attributeCount, 0, ref attributeBytes);
239
335
  attributeList = Marshal.AllocHGlobal((int)attributeBytes.ToUInt64());
240
- Check(InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeBytes), "initialize-handle-allowlist");
336
+ Check(InitializeProcThreadAttributeList(attributeList, attributeCount, 0, ref attributeBytes), "initialize-handle-allowlist");
241
337
  handleList = Marshal.AllocHGlobal(IntPtr.Size * 3);
242
338
  Marshal.WriteIntPtr(handleList, 0, nullInput);
243
339
  Marshal.WriteIntPtr(handleList, IntPtr.Size, stdoutWrite);
@@ -245,7 +341,7 @@ internal static class Program
245
341
  Check(UpdateProcThreadAttribute(
246
342
  attributeList,
247
343
  0,
248
- new IntPtr(0x00020002),
344
+ ProcThreadAttributeHandleList,
249
345
  handleList,
250
346
  new UIntPtr((uint)(IntPtr.Size * 3)),
251
347
  IntPtr.Zero,
@@ -253,6 +349,27 @@ internal static class Program
253
349
  ), "set-handle-allowlist");
254
350
  result.HandleAllowlist = true;
255
351
 
352
+ if (args.AppContainerDeny)
353
+ {
354
+ SECURITY_CAPABILITIES securityCapabilities = new SECURITY_CAPABILITIES();
355
+ securityCapabilities.AppContainerSid = appContainerSid;
356
+ securityCapabilities.Capabilities = IntPtr.Zero;
357
+ securityCapabilities.CapabilityCount = 0;
358
+ securityCapabilities.Reserved = 0;
359
+ int securityCapabilitiesSize = Marshal.SizeOf(typeof(SECURITY_CAPABILITIES));
360
+ securityCapabilitiesPointer = Marshal.AllocHGlobal(securityCapabilitiesSize);
361
+ Marshal.StructureToPtr(securityCapabilities, securityCapabilitiesPointer, false);
362
+ Check(UpdateProcThreadAttribute(
363
+ attributeList,
364
+ 0,
365
+ ProcThreadAttributeSecurityCapabilities,
366
+ securityCapabilitiesPointer,
367
+ new UIntPtr((uint)securityCapabilitiesSize),
368
+ IntPtr.Zero,
369
+ IntPtr.Zero
370
+ ), "set-appcontainer-security-capabilities");
371
+ }
372
+
256
373
  STARTUPINFOEX startup = new STARTUPINFOEX();
257
374
  startup.StartupInfo.cb = Marshal.SizeOf(typeof(STARTUPINFOEX));
258
375
  startup.StartupInfo.dwFlags = StartfUseStdHandles;
@@ -279,6 +396,22 @@ internal static class Program
279
396
  Check(created, "create-restricted-process");
280
397
  result.Launched = true;
281
398
 
399
+ if (args.AppContainerDeny)
400
+ {
401
+ Check(OpenProcessToken(process.hProcess, TokenQuery, out childToken), "open-appcontainer-process-token");
402
+ result.AppContainer = IsAppContainer(childToken);
403
+ result.AppContainerSidMatched = AppContainerSidMatches(childToken, appContainerSid);
404
+ result.CapabilityCount = TokenGroupCount(childToken, TokenCapabilities);
405
+ result.LowIntegrity = HasLowIntegrity(childToken);
406
+ result.EnabledPrivilegeCount = EnabledPrivilegeCount(childToken);
407
+ result.PrivilegesBounded = result.EnabledPrivilegeCount <= 1;
408
+ if (!result.AppContainer || !result.AppContainerSidMatched || result.CapabilityCount != 0 ||
409
+ !result.LowIntegrity || !result.PrivilegesBounded)
410
+ {
411
+ throw new LauncherException("appcontainer-token-evidence", 0);
412
+ }
413
+ }
414
+
282
415
  CloseHandle(stdoutWrite);
283
416
  stdoutWrite = IntPtr.Zero;
284
417
  CloseHandle(stderrWrite);
@@ -328,11 +461,24 @@ internal static class Program
328
461
  CloseIfValid(nullInput);
329
462
  if (attributeList != IntPtr.Zero) DeleteProcThreadAttributeList(attributeList);
330
463
  if (handleList != IntPtr.Zero) Marshal.FreeHGlobal(handleList);
464
+ if (securityCapabilitiesPointer != IntPtr.Zero) Marshal.FreeHGlobal(securityCapabilitiesPointer);
331
465
  if (attributeList != IntPtr.Zero) Marshal.FreeHGlobal(attributeList);
332
466
  CloseIfValid(job);
467
+ CloseIfValid(childToken);
333
468
  CloseIfValid(restrictedToken);
334
469
  CloseIfValid(processToken);
335
470
  if (lowSid != IntPtr.Zero) LocalFree(lowSid);
471
+ if (appContainerProfileCreated)
472
+ {
473
+ int deleteResult = DeleteAppContainerProfile(appContainerIdentity);
474
+ result.ProfileDeleted = deleteResult == 0;
475
+ if (deleteResult != 0 && result.ErrorStage == null)
476
+ {
477
+ result.ErrorStage = "delete-appcontainer-profile";
478
+ result.ErrorCode = deleteResult;
479
+ }
480
+ }
481
+ if (appContainerSid != IntPtr.Zero) FreeSid(appContainerSid);
336
482
  }
337
483
  return result;
338
484
  }
@@ -352,6 +498,224 @@ internal static class Program
352
498
  }
353
499
  }
354
500
 
501
+ private static void GrantDirectoryAccess(string path, SecurityIdentifier sid, FileSystemRights rights)
502
+ {
503
+ DirectoryInfo directory = new DirectoryInfo(path);
504
+ DirectorySecurity security = directory.GetAccessControl(AccessControlSections.Access);
505
+ security.AddAccessRule(new FileSystemAccessRule(
506
+ sid,
507
+ rights | FileSystemRights.Synchronize,
508
+ InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
509
+ PropagationFlags.None,
510
+ AccessControlType.Allow
511
+ ));
512
+ directory.SetAccessControl(security);
513
+ }
514
+
515
+ private static void HardenProtectedSentinel(string path)
516
+ {
517
+ SecurityIdentifier user = WindowsIdentity.GetCurrent().User;
518
+ if (user == null) throw new InvalidOperationException("Current user SID is unavailable.");
519
+ FileSecurity security = new FileSecurity();
520
+ security.SetOwner(user);
521
+ security.SetAccessRuleProtection(true, false);
522
+ security.AddAccessRule(new FileSystemAccessRule(user, FileSystemRights.FullControl, AccessControlType.Allow));
523
+ File.SetAccessControl(path, security);
524
+ using (FileStream stream = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
525
+ {
526
+ if (stream.Length == 0) throw new InvalidOperationException("Protected sentinel is empty.");
527
+ }
528
+ }
529
+
530
+ private static void RunAppContainerCanaries(
531
+ Arguments args,
532
+ IntPtr restrictedToken,
533
+ IntPtr appContainerSid,
534
+ IntPtr job,
535
+ LaunchResult result)
536
+ {
537
+ TcpListener listener = new TcpListener(IPAddress.Loopback, 0);
538
+ listener.Start(1);
539
+ try
540
+ {
541
+ int port = ((IPEndPoint)listener.LocalEndpoint).Port;
542
+ Dictionary<string, int> evidence = RunAppContainerCanaryChild(
543
+ args,
544
+ restrictedToken,
545
+ appContainerSid,
546
+ job,
547
+ port
548
+ );
549
+ result.FilesystemCanaryError = evidence["filesystemError"];
550
+ result.NetworkCanaryError = evidence["networkError"];
551
+ result.LoopbackAccepted = listener.Pending();
552
+ result.FilesystemCanaryDenied = evidence["appContainer"] == 1 &&
553
+ evidence["capabilityCount"] == 0 &&
554
+ result.FilesystemCanaryError == ErrorAccessDenied;
555
+ result.NetworkCanaryDenied = evidence["appContainer"] == 1 &&
556
+ evidence["capabilityCount"] == 0 &&
557
+ (result.NetworkCanaryError == WsaAccessDenied ||
558
+ result.NetworkCanaryError == WsaTimedOut) &&
559
+ !result.LoopbackAccepted;
560
+ }
561
+ finally
562
+ {
563
+ listener.Stop();
564
+ }
565
+ }
566
+
567
+ private static Dictionary<string, int> RunAppContainerCanaryChild(
568
+ Arguments args,
569
+ IntPtr restrictedToken,
570
+ IntPtr appContainerSid,
571
+ IntPtr job,
572
+ int port)
573
+ {
574
+ IntPtr stdoutRead = IntPtr.Zero;
575
+ IntPtr stdoutWrite = IntPtr.Zero;
576
+ IntPtr stderrRead = IntPtr.Zero;
577
+ IntPtr stderrWrite = IntPtr.Zero;
578
+ IntPtr nullInput = IntPtr.Zero;
579
+ IntPtr attributeList = IntPtr.Zero;
580
+ IntPtr handleList = IntPtr.Zero;
581
+ IntPtr securityCapabilitiesPointer = IntPtr.Zero;
582
+ PROCESS_INFORMATION process = new PROCESS_INFORMATION();
583
+ try
584
+ {
585
+ SECURITY_ATTRIBUTES pipeSecurity = new SECURITY_ATTRIBUTES();
586
+ pipeSecurity.nLength = Marshal.SizeOf(typeof(SECURITY_ATTRIBUTES));
587
+ pipeSecurity.bInheritHandle = true;
588
+ Check(CreatePipe(out stdoutRead, out stdoutWrite, ref pipeSecurity, 0), "create-canary-stdout-pipe");
589
+ Check(SetHandleInformation(stdoutRead, HandleFlagInherit, 0), "protect-canary-stdout-reader");
590
+ Check(CreatePipe(out stderrRead, out stderrWrite, ref pipeSecurity, 0), "create-canary-stderr-pipe");
591
+ Check(SetHandleInformation(stderrRead, HandleFlagInherit, 0), "protect-canary-stderr-reader");
592
+ nullInput = CreateFile("NUL", GenericRead, FileShareRead | FileShareWrite, ref pipeSecurity, OpenExisting, FileAttributeNormal, IntPtr.Zero);
593
+ Check(nullInput != new IntPtr(-1), "open-canary-null-input");
594
+
595
+ UIntPtr attributeBytes = UIntPtr.Zero;
596
+ InitializeProcThreadAttributeList(IntPtr.Zero, 2, 0, ref attributeBytes);
597
+ attributeList = Marshal.AllocHGlobal((int)attributeBytes.ToUInt64());
598
+ Check(InitializeProcThreadAttributeList(attributeList, 2, 0, ref attributeBytes), "initialize-canary-attributes");
599
+ handleList = Marshal.AllocHGlobal(IntPtr.Size * 3);
600
+ Marshal.WriteIntPtr(handleList, 0, nullInput);
601
+ Marshal.WriteIntPtr(handleList, IntPtr.Size, stdoutWrite);
602
+ Marshal.WriteIntPtr(handleList, IntPtr.Size * 2, stderrWrite);
603
+ Check(UpdateProcThreadAttribute(
604
+ attributeList,
605
+ 0,
606
+ ProcThreadAttributeHandleList,
607
+ handleList,
608
+ new UIntPtr((uint)(IntPtr.Size * 3)),
609
+ IntPtr.Zero,
610
+ IntPtr.Zero
611
+ ), "set-canary-handle-allowlist");
612
+
613
+ SECURITY_CAPABILITIES securityCapabilities = new SECURITY_CAPABILITIES();
614
+ securityCapabilities.AppContainerSid = appContainerSid;
615
+ int securityCapabilitiesSize = Marshal.SizeOf(typeof(SECURITY_CAPABILITIES));
616
+ securityCapabilitiesPointer = Marshal.AllocHGlobal(securityCapabilitiesSize);
617
+ Marshal.StructureToPtr(securityCapabilities, securityCapabilitiesPointer, false);
618
+ Check(UpdateProcThreadAttribute(
619
+ attributeList,
620
+ 0,
621
+ ProcThreadAttributeSecurityCapabilities,
622
+ securityCapabilitiesPointer,
623
+ new UIntPtr((uint)securityCapabilitiesSize),
624
+ IntPtr.Zero,
625
+ IntPtr.Zero
626
+ ), "set-canary-appcontainer");
627
+
628
+ string executable = args.CanaryExecutable;
629
+ string[] canaryArguments = {
630
+ "--protected-sentinel",
631
+ args.ProtectedSentinel,
632
+ "--loopback-port",
633
+ port.ToString()
634
+ };
635
+ STARTUPINFOEX startup = new STARTUPINFOEX();
636
+ startup.StartupInfo.cb = Marshal.SizeOf(typeof(STARTUPINFOEX));
637
+ startup.StartupInfo.dwFlags = StartfUseStdHandles;
638
+ startup.StartupInfo.hStdInput = nullInput;
639
+ startup.StartupInfo.hStdOutput = stdoutWrite;
640
+ startup.StartupInfo.hStdError = stderrWrite;
641
+ startup.lpAttributeList = attributeList;
642
+ StringBuilder commandLine = new StringBuilder(BuildCommandLine(executable, canaryArguments));
643
+ uint flags = CreateSuspended | CreateNoWindow | CreateUnicodeEnvironment | ExtendedStartupInfoPresent;
644
+ Check(CreateProcessAsUser(
645
+ restrictedToken,
646
+ executable,
647
+ commandLine,
648
+ IntPtr.Zero,
649
+ IntPtr.Zero,
650
+ true,
651
+ flags,
652
+ IntPtr.Zero,
653
+ args.SandboxRoot,
654
+ ref startup,
655
+ out process
656
+ ), "create-appcontainer-canary");
657
+ CloseHandle(stdoutWrite);
658
+ stdoutWrite = IntPtr.Zero;
659
+ CloseHandle(stderrWrite);
660
+ stderrWrite = IntPtr.Zero;
661
+ Check(AssignProcessToJobObject(job, process.hProcess), "assign-canary-to-job");
662
+ Task<byte[]> stdoutTask = ReadBoundedAsync(stdoutRead);
663
+ Task<byte[]> stderrTask = ReadBoundedAsync(stderrRead);
664
+ uint resumed = ResumeThread(process.hThread);
665
+ if (resumed == UInt32.MaxValue) ThrowWin32("resume-appcontainer-canary");
666
+ uint wait = WaitForSingleObject(process.hProcess, 10000);
667
+ if (wait != WaitObject0)
668
+ {
669
+ TerminateProcess(process.hProcess, 0x7d);
670
+ WaitForSingleObject(process.hProcess, 5000);
671
+ throw new LauncherException("wait-appcontainer-canary", unchecked((int)wait));
672
+ }
673
+ uint exitCode;
674
+ Check(GetExitCodeProcess(process.hProcess, out exitCode), "read-canary-exit");
675
+ Task.WaitAll(new Task[] { stdoutTask, stderrTask }, 5000);
676
+ byte[] stdout = stdoutTask.IsCompleted ? stdoutTask.Result : new byte[0];
677
+ byte[] stderr = stderrTask.IsCompleted ? stderrTask.Result : new byte[0];
678
+ if (exitCode != 0 || stderr.Length != 0) throw new InvalidOperationException("AppContainer canary failed closed.");
679
+ return ParseCanaryEvidence(Encoding.UTF8.GetString(stdout).Trim());
680
+ }
681
+ finally
682
+ {
683
+ CloseIfValid(process.hThread);
684
+ CloseIfValid(process.hProcess);
685
+ CloseIfValid(stdoutRead);
686
+ CloseIfValid(stdoutWrite);
687
+ CloseIfValid(stderrRead);
688
+ CloseIfValid(stderrWrite);
689
+ CloseIfValid(nullInput);
690
+ if (attributeList != IntPtr.Zero) DeleteProcThreadAttributeList(attributeList);
691
+ if (handleList != IntPtr.Zero) Marshal.FreeHGlobal(handleList);
692
+ if (securityCapabilitiesPointer != IntPtr.Zero) Marshal.FreeHGlobal(securityCapabilitiesPointer);
693
+ if (attributeList != IntPtr.Zero) Marshal.FreeHGlobal(attributeList);
694
+ }
695
+ }
696
+
697
+ private static Dictionary<string, int> ParseCanaryEvidence(string value)
698
+ {
699
+ Dictionary<string, int> parsed = new Dictionary<string, int>(StringComparer.Ordinal);
700
+ foreach (string field in value.Split(';'))
701
+ {
702
+ string[] pair = field.Split(new char[] { '=' }, 2);
703
+ int number;
704
+ if (pair.Length != 2 || parsed.ContainsKey(pair[0]) || !Int32.TryParse(pair[1], out number))
705
+ {
706
+ throw new InvalidOperationException("AppContainer canary protocol is invalid.");
707
+ }
708
+ parsed.Add(pair[0], number);
709
+ }
710
+ string[] keys = { "appContainer", "capabilityCount", "filesystemError", "networkError" };
711
+ if (parsed.Count != keys.Length) throw new InvalidOperationException("AppContainer canary protocol is incomplete.");
712
+ foreach (string key in keys)
713
+ {
714
+ if (!parsed.ContainsKey(key)) throw new InvalidOperationException("AppContainer canary field is missing.");
715
+ }
716
+ return parsed;
717
+ }
718
+
355
719
  private static void SetTokenIntegrity(IntPtr token, IntPtr sid)
356
720
  {
357
721
  int sidLength = (int)GetLengthSid(sid);
@@ -414,6 +778,51 @@ internal static class Program
414
778
  }
415
779
  }
416
780
 
781
+ private static bool IsAppContainer(IntPtr token)
782
+ {
783
+ IntPtr buffer = QueryTokenInformation(token, TokenIsAppContainer);
784
+ try
785
+ {
786
+ return Marshal.ReadInt32(buffer) != 0;
787
+ }
788
+ finally
789
+ {
790
+ Marshal.FreeHGlobal(buffer);
791
+ }
792
+ }
793
+
794
+ private static int TokenGroupCount(IntPtr token, int informationClass)
795
+ {
796
+ IntPtr buffer = QueryTokenInformation(token, informationClass);
797
+ try
798
+ {
799
+ return Marshal.ReadInt32(buffer);
800
+ }
801
+ finally
802
+ {
803
+ Marshal.FreeHGlobal(buffer);
804
+ }
805
+ }
806
+
807
+ private static bool AppContainerSidMatches(IntPtr token, IntPtr expectedSid)
808
+ {
809
+ IntPtr buffer = QueryTokenInformation(token, TokenAppContainerSid);
810
+ try
811
+ {
812
+ TOKEN_APPCONTAINER_INFORMATION information =
813
+ (TOKEN_APPCONTAINER_INFORMATION)Marshal.PtrToStructure(
814
+ buffer,
815
+ typeof(TOKEN_APPCONTAINER_INFORMATION)
816
+ );
817
+ return information.TokenAppContainer != IntPtr.Zero &&
818
+ EqualSid(information.TokenAppContainer, expectedSid);
819
+ }
820
+ finally
821
+ {
822
+ Marshal.FreeHGlobal(buffer);
823
+ }
824
+ }
825
+
417
826
  private static IntPtr QueryTokenInformation(IntPtr token, int informationClass)
418
827
  {
419
828
  int length = 0;
@@ -529,12 +938,8 @@ internal static class Program
529
938
 
530
939
  private static string ToJson(LaunchResult result)
531
940
  {
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\":{" +
941
+ string isolation = result.AppContainerMode
942
+ ?
538
943
  "\"filteredToken\":" + Bool(result.FilteredToken) + "," +
539
944
  "\"disableMaxPrivilege\":" + Bool(result.DisableMaxPrivilege) + "," +
540
945
  "\"enabledPrivilegeCount\":" + result.EnabledPrivilegeCount.ToString() + "," +
@@ -546,8 +951,36 @@ internal static class Program
546
951
  "\"activeProcessLimit\":" + Bool(result.ActiveProcessLimit) + "," +
547
952
  "\"processMemoryLimit\":" + Bool(result.ProcessMemoryLimit) + "," +
548
953
  "\"uiRestrictions\":" + Bool(result.UiRestrictions) + "," +
549
- "\"writableTempLowIntegrity\":" + Bool(result.WritableTempLowIntegrity) +
550
- "}," +
954
+ "\"appContainer\":" + Bool(result.AppContainer) + "," +
955
+ "\"appContainerSidMatched\":" + Bool(result.AppContainerSidMatched) + "," +
956
+ "\"capabilityCount\":" + result.CapabilityCount.ToString() + "," +
957
+ "\"snapshotReadExecuteGrant\":" + Bool(result.SnapshotReadExecuteGrant) + "," +
958
+ "\"writableTempModifyGrant\":" + Bool(result.WritableTempModifyGrant) + "," +
959
+ "\"filesystemCanaryDenied\":" + Bool(result.FilesystemCanaryDenied) + "," +
960
+ "\"filesystemCanaryError\":" + result.FilesystemCanaryError.ToString() + "," +
961
+ "\"networkCanaryDenied\":" + Bool(result.NetworkCanaryDenied) + "," +
962
+ "\"networkCanaryError\":" + result.NetworkCanaryError.ToString() + "," +
963
+ "\"loopbackAccepted\":" + Bool(result.LoopbackAccepted) + "," +
964
+ "\"profileDeleted\":" + Bool(result.ProfileDeleted)
965
+ :
966
+ "\"filteredToken\":" + Bool(result.FilteredToken) + "," +
967
+ "\"disableMaxPrivilege\":" + Bool(result.DisableMaxPrivilege) + "," +
968
+ "\"enabledPrivilegeCount\":" + result.EnabledPrivilegeCount.ToString() + "," +
969
+ "\"privilegesBounded\":" + Bool(result.PrivilegesBounded) + "," +
970
+ "\"lowIntegrity\":" + Bool(result.LowIntegrity) + "," +
971
+ "\"assignedBeforeResume\":" + Bool(result.AssignedBeforeResume) + "," +
972
+ "\"handleAllowlist\":" + Bool(result.HandleAllowlist) + "," +
973
+ "\"killOnClose\":" + Bool(result.KillOnClose) + "," +
974
+ "\"activeProcessLimit\":" + Bool(result.ActiveProcessLimit) + "," +
975
+ "\"processMemoryLimit\":" + Bool(result.ProcessMemoryLimit) + "," +
976
+ "\"uiRestrictions\":" + Bool(result.UiRestrictions) + "," +
977
+ "\"writableTempLowIntegrity\":" + Bool(result.WritableTempLowIntegrity);
978
+ return "{" +
979
+ "\"protocol\":\"" + (result.AppContainerMode ? AppContainerProtocol : Protocol) + "\"," +
980
+ "\"launched\":" + Bool(result.Launched) + "," +
981
+ "\"timedOut\":" + Bool(result.TimedOut) + "," +
982
+ "\"exitCode\":" + (result.ExitCode.HasValue ? result.ExitCode.Value.ToString() : "null") + "," +
983
+ "\"isolation\":{" + isolation + "}," +
551
984
  "\"stdoutBase64\":\"" + Convert.ToBase64String(result.StandardOutput) + "\"," +
552
985
  "\"stderrBase64\":\"" + Convert.ToBase64String(result.StandardError) + "\"," +
553
986
  "\"errorStage\":" + (result.ErrorStage == null ? "null" : "\"" + EscapeJson(result.ErrorStage) + "\"") + "," +
@@ -597,6 +1030,16 @@ internal static class Program
597
1030
  [StructLayout(LayoutKind.Sequential)]
598
1031
  private struct TOKEN_MANDATORY_LABEL { public SID_AND_ATTRIBUTES Label; }
599
1032
  [StructLayout(LayoutKind.Sequential)]
1033
+ private struct TOKEN_APPCONTAINER_INFORMATION { public IntPtr TokenAppContainer; }
1034
+ [StructLayout(LayoutKind.Sequential)]
1035
+ private struct SECURITY_CAPABILITIES
1036
+ {
1037
+ public IntPtr AppContainerSid;
1038
+ public IntPtr Capabilities;
1039
+ public uint CapabilityCount;
1040
+ public uint Reserved;
1041
+ }
1042
+ [StructLayout(LayoutKind.Sequential)]
600
1043
  private struct SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; [MarshalAs(UnmanagedType.Bool)] public bool bInheritHandle; }
601
1044
  [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
602
1045
  private struct STARTUPINFO
@@ -643,6 +1086,7 @@ internal static class Program
643
1086
  [DllImport("kernel32.dll", SetLastError = true)] private static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
644
1087
  [DllImport("kernel32.dll", SetLastError = true)] private static extern uint ResumeThread(IntPtr thread);
645
1088
  [DllImport("kernel32.dll", SetLastError = true)] private static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
1089
+ [DllImport("kernel32.dll", SetLastError = true)] private static extern bool TerminateProcess(IntPtr process, uint exitCode);
646
1090
  [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern IntPtr CreateJobObject(IntPtr attributes, string name);
647
1091
  [DllImport("kernel32.dll", SetLastError = true)] private static extern bool SetInformationJobObject(IntPtr job, int informationClass, IntPtr information, uint length);
648
1092
  [DllImport("kernel32.dll", SetLastError = true)] private static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
@@ -658,10 +1102,16 @@ internal static class Program
658
1102
  [DllImport("advapi32.dll", SetLastError = true)] private static extern IntPtr GetSidSubAuthorityCount(IntPtr sid);
659
1103
  [DllImport("advapi32.dll", SetLastError = true)] private static extern IntPtr GetSidSubAuthority(IntPtr sid, uint subAuthority);
660
1104
  [DllImport("advapi32.dll", SetLastError = true)] private static extern uint GetLengthSid(IntPtr sid);
1105
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern bool EqualSid(IntPtr firstSid, IntPtr secondSid);
1106
+ [DllImport("advapi32.dll", SetLastError = true)] private static extern IntPtr FreeSid(IntPtr sid);
661
1107
  [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CopySid(uint destinationLength, IntPtr destination, IntPtr source);
662
1108
  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool ConvertStringSidToSid(string sid, out IntPtr binarySid);
663
1109
  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string descriptor, uint revision, out IntPtr securityDescriptor, out uint size);
664
1110
  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] private static extern bool SetFileSecurity(string path, uint securityInformation, IntPtr securityDescriptor);
665
1111
  [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
666
1112
  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);
1113
+ [DllImport("userenv.dll", CharSet = CharSet.Unicode)]
1114
+ private static extern int CreateAppContainerProfile(string appContainerName, string displayName, string description, IntPtr capabilities, uint capabilityCount, out IntPtr appContainerSid);
1115
+ [DllImport("userenv.dll", CharSet = CharSet.Unicode)]
1116
+ private static extern int DeleteAppContainerProfile(string appContainerName);
667
1117
  }