windows-development-sandbox 1.13.0 → 1.14.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.
Files changed (125) hide show
  1. package/dist/app/features/accounts/accounts.def.d.ts +1 -0
  2. package/dist/app/features/accounts/accounts.def.js +12 -3
  3. package/dist/app/features/accounts/accounts.run.d.ts +1 -0
  4. package/dist/app/features/accounts/accounts.run.js +20 -11
  5. package/dist/app/features/defs.d.ts +1 -0
  6. package/dist/app/features/settings/settings-file.d.ts +7 -0
  7. package/dist/app/features/settings/settings-file.js +19 -2
  8. package/dist/app/features/settings/settings-rows.js +9 -1
  9. package/dist/app/features/storage/storage.run.js +1 -1
  10. package/dist/bin/agent-sandbox.js +7 -1
  11. package/dist/bin/chunk-BQ23GBLC.js +78790 -0
  12. package/dist/bin/chunk-F7IMME5G.js +10432 -0
  13. package/dist/bin/chunk-LQHEUN2A.js +7 -0
  14. package/dist/bin/claude-host.js +10 -4
  15. package/dist/bin/claude-sbx.js +8 -2
  16. package/dist/bin/codex-sbx.js +8 -2
  17. package/dist/bin/gemini-sbx.js +8 -2
  18. package/dist/bin/sandbox.js +7 -2
  19. package/dist/bin/sbx-hide.js +8 -2
  20. package/dist/bin/sbx-show.js +8 -2
  21. package/dist/cli/commands/launch/launch.js +1 -1
  22. package/dist/cli/commands/launch/prepare.js +24 -7
  23. package/dist/cli/commands/launch/running.js +4 -0
  24. package/dist/cli/commands/repair/checks/contained.js +1 -1
  25. package/dist/cli/commands/storage/offer.js +1 -1
  26. package/dist/cli/menu/live.d.ts +29 -0
  27. package/dist/cli/menu/live.js +92 -21
  28. package/dist/cli/menu/menu.d.ts +2 -1
  29. package/dist/cli/menu/menu.js +16 -2
  30. package/dist/cli/menu/pages/list-page.d.ts +5 -0
  31. package/dist/cli/menu/pages/list-page.js +19 -0
  32. package/dist/cli/menu/shell.d.ts +2 -0
  33. package/dist/cli/menu/shell.js +1 -1
  34. package/dist/cli/services/accounts-service.d.ts +7 -0
  35. package/dist/cli/services/accounts-service.js +20 -0
  36. package/dist/cli/tui/app.d.ts +4 -0
  37. package/dist/cli/tui/app.js +5 -0
  38. package/dist/cli/tui/terminal.d.ts +6 -0
  39. package/dist/cli/tui/terminal.js +8 -1
  40. package/dist/cli/validate.js +8 -4
  41. package/dist/core/package-info.js +1 -1
  42. package/dist/guest/daemon/sessions.d.ts +7 -0
  43. package/dist/guest/daemon/sessions.js +23 -2
  44. package/dist/guest/entries/daemon.js +1 -0
  45. package/dist/guest/sessions/pty-host.d.ts +10 -0
  46. package/dist/guest/sessions/pty-host.js +48 -3
  47. package/dist/host/channel/channel.js +1 -1
  48. package/dist/host/install/install-record.d.ts +1 -0
  49. package/dist/host/install/install-record.js +2 -0
  50. package/dist/host/sandbox/contained-deps.d.ts +2 -1
  51. package/dist/host/sandbox/contained-deps.js +6 -4
  52. package/dist/host/sandbox/platform.d.ts +2 -0
  53. package/dist/host/sandbox/platform.js +19 -4
  54. package/dist/host/sharing/lifecycle.js +9 -1
  55. package/dist/host/storage/contained/contained-storage.js +13 -0
  56. package/dist/host/storage/contained/disk-port.d.ts +5 -0
  57. package/dist/host/storage/contained/helper-port.d.ts +72 -0
  58. package/dist/host/storage/contained/helper-port.js +162 -0
  59. package/dist/host/storage/contained/index.d.ts +1 -0
  60. package/dist/host/storage/contained/index.js +1 -0
  61. package/dist/host/storage/contained/state.d.ts +3 -2
  62. package/dist/host/storage/contained/state.js +5 -2
  63. package/dist/host/web/bridge.d.ts +5 -0
  64. package/dist/host/web/bridge.js +11 -1
  65. package/dist/host/web/server.d.ts +2 -0
  66. package/dist/host/web/server.js +11 -1
  67. package/dist/runtime/agent.js +9 -0
  68. package/dist/runtime/daemon.js +57 -3
  69. package/dist/runtime/db.js +9 -0
  70. package/dist/runtime/main.js +195 -12
  71. package/dist/runtime/netproxy.js +9 -0
  72. package/dist/runtime/view.js +195 -12
  73. package/dist/selftest/channel.test.js +72 -1
  74. package/dist/selftest/contained.test.js +1 -37
  75. package/dist/selftest/hcs.test.js +5 -2
  76. package/dist/selftest/index.js +2 -0
  77. package/dist/selftest/install.test.js +11 -0
  78. package/dist/selftest/layout.test.js +10 -1
  79. package/dist/selftest/live-daemon.test.js +51 -1
  80. package/dist/selftest/live-terminal.test.js +112 -4
  81. package/dist/selftest/menu-perf.test.js +198 -12
  82. package/dist/selftest/menu.test.js +4 -0
  83. package/dist/selftest/storage-helper.test.d.ts +2 -0
  84. package/dist/selftest/storage-helper.test.js +493 -0
  85. package/dist/selftest/support/vhdx.d.ts +14 -0
  86. package/dist/selftest/support/vhdx.js +72 -0
  87. package/dist/selftest/web.test.js +14 -2
  88. package/dist/shared/channel/run-dir.d.ts +6 -0
  89. package/dist/shared/channel/run-dir.js +9 -0
  90. package/dist/shared/pty/index.d.ts +3 -1
  91. package/dist/shared/pty/index.js +3 -1
  92. package/dist/shared/pty/key-gate.d.ts +24 -0
  93. package/dist/shared/pty/key-gate.js +45 -0
  94. package/dist/shared/pty/mouse-modes.d.ts +27 -0
  95. package/dist/shared/pty/mouse-modes.js +155 -0
  96. package/dist/shared/pty/terminal.d.ts +0 -5
  97. package/dist/shared/pty/terminal.js +51 -20
  98. package/dist/sync/watcher.js +1 -1
  99. package/dist/web/index.html +1 -1
  100. package/dist/web/{main-Z2WYAE2Z.js → main-6XNBTJLT.js} +34 -34
  101. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  102. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  103. package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
  104. package/native/csharp/StorageHelper/Client.cs +297 -0
  105. package/native/csharp/StorageHelper/Log.cs +177 -0
  106. package/native/csharp/StorageHelper/MountRecords.cs +160 -0
  107. package/native/csharp/StorageHelper/Operations.cs +596 -0
  108. package/native/csharp/StorageHelper/Pins.cs +270 -0
  109. package/native/csharp/StorageHelper/Privileges.cs +78 -0
  110. package/native/csharp/StorageHelper/Program.cs +349 -0
  111. package/native/csharp/StorageHelper/Protocol.cs +107 -0
  112. package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
  113. package/native/csharp/StorageHelper/Rules.cs +156 -0
  114. package/native/csharp/StorageHelper/Server.cs +656 -0
  115. package/native/csharp/StorageHelper/Settings.cs +297 -0
  116. package/native/csharp/StorageHelper/StorageService.cs +53 -0
  117. package/native/csharp/StorageHelper/UserSettings.cs +206 -0
  118. package/native/csharp/StorageHelper/Vhdx.cs +230 -0
  119. package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
  120. package/native/csharp/StorageHelper/Volumes.cs +379 -0
  121. package/native/csharp/StorageHelper/Watchdog.cs +128 -0
  122. package/native/csharp/StorageHelper/Win32.cs +250 -0
  123. package/native/scripts/set-storage-helper.ps1 +203 -0
  124. package/package.json +1 -1
  125. package/sandbox/csharp/PtyHost.cs +236 -1
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.12.0.0")]
12
- [assembly: AssemblyFileVersion("1.12.0.0")]
13
- [assembly: AssemblyInformationalVersion("1.12.0")]
11
+ [assembly: AssemblyVersion("1.14.0.0")]
12
+ [assembly: AssemblyFileVersion("1.14.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.14.0")]
14
14
  [assembly: ComVisible(false)]
15
15
  // computecore.dll and computestorage.dll are not KnownDLLs: load them from System32 only, never from the exe's folder.
16
16
  [assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
8
8
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
9
9
  [assembly: AssemblyProduct("Windows Development Sandbox")]
10
10
  [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
11
- [assembly: AssemblyVersion("1.12.0.0")]
12
- [assembly: AssemblyFileVersion("1.12.0.0")]
13
- [assembly: AssemblyInformationalVersion("1.12.0")]
11
+ [assembly: AssemblyVersion("1.14.0.0")]
12
+ [assembly: AssemblyFileVersion("1.14.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.14.0")]
14
14
  [assembly: ComVisible(false)]
@@ -0,0 +1,15 @@
1
+ // What AgentSandboxStorage.exe says about itself in its file properties (publisher, product, description, version), so
2
+ // Windows, antivirus software and you can tell what it is. It is built by the all-users installer from the sources next
3
+ // to this file, into the Program Files install folder.
4
+ using System.Reflection;
5
+ using System.Runtime.InteropServices;
6
+
7
+ [assembly: AssemblyTitle("Windows Development Sandbox storage helper")]
8
+ [assembly: AssemblyDescription("Service of windows-development-sandbox (open source, MIT): mounts and dismounts a user's contained-storage disk image without a permission prompt, after checking it is that user's own image in that user's own folder. Source: https://github.com/JoshDiDuca/windows-development-sandbox")]
9
+ [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
10
+ [assembly: AssemblyProduct("Windows Development Sandbox")]
11
+ [assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
12
+ [assembly: AssemblyVersion("1.14.0.0")]
13
+ [assembly: AssemblyFileVersion("1.14.0.0")]
14
+ [assembly: AssemblyInformationalVersion("1.14.0")]
15
+ [assembly: ComVisible(false)]
@@ -0,0 +1,297 @@
1
+ // Who is on the other end of the pipe, from Windows and never from the message: the token of the connected client
2
+ // (ImpersonateNamedPipeClient, taken and reverted at once), checked to be a real interactive user (impersonation level,
3
+ // not anonymous or a service account, medium integrity or higher, not an app container or restricted token, a member
4
+ // of the "Agent Sandbox Users" group or Administrators), and the client process (GetNamedPipeClientProcessId), whose
5
+ // user must be the same. The token is kept only to read that user's settings file and environment.
6
+ using System;
7
+ using System.ComponentModel;
8
+ using System.Runtime.InteropServices;
9
+ using System.Security.Principal;
10
+ using System.Text;
11
+ using Microsoft.Win32.SafeHandles;
12
+
13
+ namespace AgentSandbox.StorageHelper
14
+ {
15
+ public sealed class Client : IDisposable
16
+ {
17
+ private const uint TokenQuery = 0x0008;
18
+ private const uint TokenDuplicate = 0x0002;
19
+ private const uint TokenImpersonate = 0x0004;
20
+ private const int TokenUserClass = 1;
21
+ private const int TokenImpersonationLevelClass = 9;
22
+ private const int TokenIntegrityLevelClass = 25;
23
+ private const int TokenIsAppContainerClass = 29;
24
+ private const int SecurityImpersonation = 2;
25
+ private const int MediumIntegrity = 0x2000;
26
+ private const uint ProcessQueryLimited = 0x1000;
27
+ private static readonly string[] ServiceAccounts = { "S-1-5-7", "S-1-5-18", "S-1-5-19", "S-1-5-20" };
28
+ private static readonly SecurityIdentifier Administrators = new SecurityIdentifier("S-1-5-32-544");
29
+
30
+ private Client(SafeAccessTokenHandle token, SecurityIdentifier sid, int pid, string program)
31
+ {
32
+ Token = token;
33
+ Sid = sid;
34
+ Pid = pid;
35
+ Program = program;
36
+ }
37
+
38
+ public SafeAccessTokenHandle Token { get; private set; }
39
+ public SecurityIdentifier Sid { get; private set; }
40
+ public int Pid { get; private set; }
41
+ public string Program { get; private set; }
42
+
43
+ public string Account
44
+ {
45
+ get
46
+ {
47
+ try
48
+ {
49
+ return Sid.Translate(typeof(NTAccount)).Value;
50
+ }
51
+ catch (Exception)
52
+ {
53
+ return "";
54
+ }
55
+ }
56
+ }
57
+
58
+ private static class NativeMethods
59
+ {
60
+ [DllImport("kernel32.dll", SetLastError = true)]
61
+ [return: MarshalAs(UnmanagedType.Bool)]
62
+ public static extern bool GetNamedPipeClientProcessId(SafeHandle pipe, out uint processId);
63
+
64
+ [DllImport("advapi32.dll", SetLastError = true)]
65
+ [return: MarshalAs(UnmanagedType.Bool)]
66
+ public static extern bool ImpersonateNamedPipeClient(SafeHandle pipe);
67
+
68
+ [DllImport("advapi32.dll", SetLastError = true)]
69
+ [return: MarshalAs(UnmanagedType.Bool)]
70
+ public static extern bool RevertToSelf();
71
+
72
+ [DllImport("kernel32.dll")]
73
+ public static extern IntPtr GetCurrentThread();
74
+
75
+ [DllImport("advapi32.dll", SetLastError = true)]
76
+ [return: MarshalAs(UnmanagedType.Bool)]
77
+ public static extern bool OpenThreadToken(IntPtr thread, uint access, [MarshalAs(UnmanagedType.Bool)] bool openAsSelf, out SafeAccessTokenHandle token);
78
+
79
+ [DllImport("advapi32.dll", SetLastError = true)]
80
+ [return: MarshalAs(UnmanagedType.Bool)]
81
+ public static extern bool GetTokenInformation(SafeAccessTokenHandle token, int infoClass, byte[] info, int length, out int returned);
82
+
83
+ [DllImport("advapi32.dll", EntryPoint = "GetTokenInformation", SetLastError = true)]
84
+ [return: MarshalAs(UnmanagedType.Bool)]
85
+ public static extern bool GetTokenInformationPtr(SafeAccessTokenHandle token, int infoClass, IntPtr info, int length, out int returned);
86
+
87
+ [DllImport("advapi32.dll", SetLastError = true)]
88
+ [return: MarshalAs(UnmanagedType.Bool)]
89
+ public static extern bool IsTokenRestricted(SafeAccessTokenHandle token);
90
+
91
+ [DllImport("advapi32.dll", SetLastError = true)]
92
+ [return: MarshalAs(UnmanagedType.Bool)]
93
+ public static extern bool CheckTokenMembership(SafeAccessTokenHandle token, byte[] sid, [MarshalAs(UnmanagedType.Bool)] out bool isMember);
94
+
95
+ [DllImport("kernel32.dll", SetLastError = true)]
96
+ public static extern IntPtr OpenProcess(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint processId);
97
+
98
+ [DllImport("advapi32.dll", SetLastError = true)]
99
+ [return: MarshalAs(UnmanagedType.Bool)]
100
+ public static extern bool OpenProcessToken(IntPtr process, uint access, out SafeAccessTokenHandle token);
101
+
102
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
103
+ [return: MarshalAs(UnmanagedType.Bool)]
104
+ public static extern bool QueryFullProcessImageNameW(IntPtr process, uint flags, StringBuilder name, ref uint size);
105
+
106
+ [DllImport("kernel32.dll", SetLastError = true)]
107
+ [return: MarshalAs(UnmanagedType.Bool)]
108
+ public static extern bool CloseHandle(IntPtr handle);
109
+ }
110
+
111
+ /// <summary>
112
+ /// The user of the process at the other end of the pipe, before anything is read (to count connections): its
113
+ /// SID, or "pid:N" when it cannot be told. Only for counting; never for deciding who the caller is.
114
+ /// </summary>
115
+ public static string ProcessUserOf(SafeHandle pipe)
116
+ {
117
+ uint pid;
118
+ if (!NativeMethods.GetNamedPipeClientProcessId(pipe, out pid))
119
+ {
120
+ return "unknown";
121
+ }
122
+ string program;
123
+ SecurityIdentifier user = ProcessUser(pid, out program);
124
+ return user == null ? "pid:" + pid : user.Value;
125
+ }
126
+
127
+ /// <summary>The checked client of a pipe that has already received the request (impersonation needs that).</summary>
128
+ public static Client Of(SafeHandle h, SecurityIdentifier group)
129
+ {
130
+ uint pid;
131
+ if (!NativeMethods.GetNamedPipeClientProcessId(h, out pid))
132
+ {
133
+ throw Win32.LastError("Could not tell which program is calling");
134
+ }
135
+ SafeAccessTokenHandle token;
136
+ if (!NativeMethods.ImpersonateNamedPipeClient(h))
137
+ {
138
+ throw Win32.LastError("Could not tell who is calling");
139
+ }
140
+ try
141
+ {
142
+ if (!NativeMethods.OpenThreadToken(NativeMethods.GetCurrentThread(), TokenQuery | TokenDuplicate | TokenImpersonate, true, out token))
143
+ {
144
+ throw Win32.LastError("Could not tell who is calling");
145
+ }
146
+ }
147
+ finally
148
+ {
149
+ if (!NativeMethods.RevertToSelf())
150
+ {
151
+ // Running on as the caller would be worse than stopping: end the service.
152
+ Environment.FailFast("RevertToSelf failed");
153
+ }
154
+ }
155
+ try
156
+ {
157
+ SecurityIdentifier sid = Check(token, group);
158
+ string program;
159
+ SecurityIdentifier processUser = ProcessUser(pid, out program);
160
+ if (processUser == null || !processUser.Equals(sid))
161
+ {
162
+ throw new RefusedException("The calling program does not run as the user who called.");
163
+ }
164
+ return new Client(token, sid, (int)pid, program);
165
+ }
166
+ catch
167
+ {
168
+ token.Dispose();
169
+ throw;
170
+ }
171
+ }
172
+
173
+ /// <summary>The user of a token that may use this service, or an exception that says why not.</summary>
174
+ public static SecurityIdentifier Check(SafeAccessTokenHandle token, SecurityIdentifier group)
175
+ {
176
+ byte[] level = Info(token, TokenImpersonationLevelClass);
177
+ if (BitConverter.ToInt32(level, 0) < SecurityImpersonation)
178
+ {
179
+ throw new RefusedException("The caller did not allow the service to act as them.");
180
+ }
181
+ SecurityIdentifier sid = SidIn(token, TokenUserClass);
182
+ if (Array.IndexOf(ServiceAccounts, sid.Value) >= 0 || sid.IsWellKnown(WellKnownSidType.AnonymousSid))
183
+ {
184
+ throw new RefusedException("Service accounts cannot use contained storage.");
185
+ }
186
+ SecurityIdentifier label = SidIn(token, TokenIntegrityLevelClass);
187
+ byte[] labelBytes = new byte[label.BinaryLength];
188
+ label.GetBinaryForm(labelBytes, 0);
189
+ int rid = BitConverter.ToInt32(labelBytes, labelBytes.Length - 4);
190
+ if (rid < MediumIntegrity)
191
+ {
192
+ throw new RefusedException("A program at low integrity cannot use contained storage.");
193
+ }
194
+ if (BitConverter.ToInt32(Info(token, TokenIsAppContainerClass), 0) != 0)
195
+ {
196
+ throw new RefusedException("An app container cannot use contained storage.");
197
+ }
198
+ if (NativeMethods.IsTokenRestricted(token))
199
+ {
200
+ throw new RefusedException("A restricted program cannot use contained storage.");
201
+ }
202
+ if (!IsMember(token, group) && !IsMember(token, Administrators))
203
+ {
204
+ throw new RefusedException("Only members of the Agent Sandbox Users group may use this (sign out and in after being added).");
205
+ }
206
+ return sid;
207
+ }
208
+
209
+ private static bool IsMember(SafeAccessTokenHandle token, SecurityIdentifier sid)
210
+ {
211
+ if (sid == null)
212
+ {
213
+ return false;
214
+ }
215
+ byte[] bytes = new byte[sid.BinaryLength];
216
+ sid.GetBinaryForm(bytes, 0);
217
+ bool member;
218
+ if (!NativeMethods.CheckTokenMembership(token, bytes, out member))
219
+ {
220
+ throw Win32.LastError("Could not check the caller's groups");
221
+ }
222
+ return member;
223
+ }
224
+
225
+ private static byte[] Info(SafeAccessTokenHandle token, int infoClass)
226
+ {
227
+ byte[] buffer = new byte[1024];
228
+ int returned;
229
+ if (!NativeMethods.GetTokenInformation(token, infoClass, buffer, buffer.Length, out returned))
230
+ {
231
+ throw Win32.LastError("Could not read the caller's token");
232
+ }
233
+ return buffer;
234
+ }
235
+
236
+ /// <summary>The SID of a TOKEN_USER or TOKEN_MANDATORY_LABEL (a SID_AND_ATTRIBUTES pointing into the same buffer).</summary>
237
+ private static SecurityIdentifier SidIn(SafeAccessTokenHandle token, int infoClass)
238
+ {
239
+ IntPtr buffer = Marshal.AllocHGlobal(1024);
240
+ try
241
+ {
242
+ int returned;
243
+ if (!NativeMethods.GetTokenInformationPtr(token, infoClass, buffer, 1024, out returned))
244
+ {
245
+ throw Win32.LastError("Could not read the caller's token");
246
+ }
247
+ return new SecurityIdentifier(Marshal.ReadIntPtr(buffer));
248
+ }
249
+ finally
250
+ {
251
+ Marshal.FreeHGlobal(buffer);
252
+ }
253
+ }
254
+
255
+ private static SecurityIdentifier ProcessUser(uint pid, out string program)
256
+ {
257
+ program = "";
258
+ IntPtr process = NativeMethods.OpenProcess(ProcessQueryLimited, false, pid);
259
+ if (process == IntPtr.Zero)
260
+ {
261
+ return null;
262
+ }
263
+ try
264
+ {
265
+ StringBuilder name = new StringBuilder(1024);
266
+ uint size = (uint)name.Capacity;
267
+ if (NativeMethods.QueryFullProcessImageNameW(process, 0, name, ref size))
268
+ {
269
+ program = name.ToString();
270
+ }
271
+ SafeAccessTokenHandle token;
272
+ if (!NativeMethods.OpenProcessToken(process, TokenQuery, out token))
273
+ {
274
+ return null;
275
+ }
276
+ using (token)
277
+ {
278
+ return UserOf(token);
279
+ }
280
+ }
281
+ finally
282
+ {
283
+ NativeMethods.CloseHandle(process);
284
+ }
285
+ }
286
+
287
+ public static SecurityIdentifier UserOf(SafeAccessTokenHandle token)
288
+ {
289
+ return new WindowsIdentity(token.DangerousGetHandle()).User;
290
+ }
291
+
292
+ public void Dispose()
293
+ {
294
+ Token.Dispose();
295
+ }
296
+ }
297
+ }
@@ -0,0 +1,177 @@
1
+ // The service's log: one line per call (when, who, what, the result), in %ProgramData%\AgentSandbox\logs\
2
+ // storage-helper.log. The folder must belong to administrators with nobody else allowed to change it, and must be
3
+ // exactly where it says (no link on the way); otherwise the service does not start, since it writes there as SYSTEM.
4
+ // The file is opened without following links and must have one name only; past 1 MB it is renamed (by its handle) to
5
+ // storage-helper.log.1. Text from outside (a set name, a path, a reason) is reduced to printable ASCII first. Writing
6
+ // never throws: a full disk must not change what a call does.
7
+ using System;
8
+ using System.ComponentModel;
9
+ using System.IO;
10
+ using System.Runtime.InteropServices;
11
+ using System.Security.AccessControl;
12
+ using System.Security.Principal;
13
+ using System.Text;
14
+ using Microsoft.Win32.SafeHandles;
15
+
16
+ namespace AgentSandbox.StorageHelper
17
+ {
18
+ public sealed class Log
19
+ {
20
+ private const long MaxBytes = 1024 * 1024;
21
+ private const uint OpenAlways = 4;
22
+ private const uint FileAppendData = 0x0004;
23
+ private const int FileRenameInfoClass = 3;
24
+ private static readonly SecurityIdentifier SystemSid = new SecurityIdentifier("S-1-5-18");
25
+ private static readonly SecurityIdentifier AdministratorsSid = new SecurityIdentifier("S-1-5-32-544");
26
+ private readonly object gate = new object();
27
+ private readonly string file;
28
+
29
+ private Log(string file)
30
+ {
31
+ this.file = file;
32
+ }
33
+
34
+ private static class NativeMethods
35
+ {
36
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
37
+ public static extern SafeFileHandle CreateFileW(string fileName, uint access, uint share, IntPtr security, uint creation, uint flags, IntPtr template);
38
+
39
+ [DllImport("kernel32.dll", SetLastError = true)]
40
+ [return: MarshalAs(UnmanagedType.Bool)]
41
+ public static extern bool SetFileInformationByHandle(SafeFileHandle file, int infoClass, byte[] info, int size);
42
+ }
43
+
44
+ /// <summary>The log in this folder (made, admin-only, when missing). Throws when the folder is not safe to write as SYSTEM.</summary>
45
+ public static Log InFolder(string folder, bool requireAdminOnly)
46
+ {
47
+ if (!Directory.Exists(folder))
48
+ {
49
+ DirectorySecurity security = new DirectorySecurity();
50
+ security.SetAccessRuleProtection(true, false);
51
+ security.AddAccessRule(new FileSystemAccessRule(SystemSid, FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
52
+ security.AddAccessRule(new FileSystemAccessRule(AdministratorsSid, FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, PropagationFlags.None, AccessControlType.Allow));
53
+ Directory.CreateDirectory(folder, security);
54
+ }
55
+ using (SafeFileHandle h = Win32.OpenNoFollow(folder, Win32.ReadControl | Win32.FileReadAttributes | Win32.Synchronize, Win32.FileShareRead | Win32.FileShareWrite))
56
+ {
57
+ if (h == null)
58
+ {
59
+ throw new IOException("The log folder could not be made: " + folder);
60
+ }
61
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, folder);
62
+ if ((info.FileAttributes & Win32.AttributeReparsePoint) != 0 || !Win32.IsAt(h, folder))
63
+ {
64
+ throw new IOException("The log folder is a link or behind one: " + folder);
65
+ }
66
+ if (requireAdminOnly)
67
+ {
68
+ RawSecurityDescriptor sd = Win32.SecurityOf(h);
69
+ bool adminOwned = sd.Owner != null && (sd.Owner.Equals(SystemSid) || sd.Owner.Equals(AdministratorsSid));
70
+ if (!adminOwned || Pins.OthersWhoMayChange(sd, SystemSid) != null)
71
+ {
72
+ throw new IOException("Someone who is not an administrator may change the log folder " + folder + "; run the installer again.");
73
+ }
74
+ }
75
+ }
76
+ return new Log(Path.Combine(folder, "storage-helper.log"));
77
+ }
78
+
79
+ public string File
80
+ {
81
+ get { return file; }
82
+ }
83
+
84
+ public void Write(string sid, string account, int pid, string program, string verb, string set, string result)
85
+ {
86
+ string line = Now() +
87
+ " sid=" + Safe.Text(sid, 200) +
88
+ " user=" + Quote(account, 200) +
89
+ " pid=" + pid +
90
+ " program=" + Quote(program, 300) +
91
+ " verb=" + Safe.Text(verb, 20) +
92
+ " set=" + Quote(set, 80) +
93
+ " result=" + Quote(result, 600);
94
+ Append(line);
95
+ }
96
+
97
+ public void Note(string text)
98
+ {
99
+ Append(Now() + " " + Quote(text, 600));
100
+ }
101
+
102
+ private static string Now()
103
+ {
104
+ return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
105
+ }
106
+
107
+ private static string Quote(string text, int max)
108
+ {
109
+ return "\"" + Safe.Text(text, max).Replace("\"", "'") + "\"";
110
+ }
111
+
112
+ private void Append(string line)
113
+ {
114
+ lock (gate)
115
+ {
116
+ try
117
+ {
118
+ using (SafeFileHandle h = OpenLog(FileAppendData | Win32.FileReadAttributes | Win32.Synchronize | Win32.Delete))
119
+ {
120
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, file);
121
+ long size = ((long)info.FileSizeHigh << 32) | info.FileSizeLow;
122
+ if (size > MaxBytes)
123
+ {
124
+ RenameTo(h, file + ".1");
125
+ }
126
+ }
127
+ using (SafeFileHandle h = OpenLog(FileAppendData | Win32.FileReadAttributes | Win32.Synchronize))
128
+ using (FileStream stream = new FileStream(h, FileAccess.Write))
129
+ {
130
+ byte[] bytes = Encoding.ASCII.GetBytes(line + "\r\n");
131
+ stream.Seek(0, SeekOrigin.End);
132
+ stream.Write(bytes, 0, bytes.Length);
133
+ }
134
+ }
135
+ catch (Exception)
136
+ {
137
+ // The log is a record, not a gate: a full disk or a locked file must not change what the call does.
138
+ }
139
+ }
140
+ }
141
+
142
+ /// <summary>The log file, made when missing, never through a link, never a file with a second name.</summary>
143
+ private SafeFileHandle OpenLog(uint access)
144
+ {
145
+ SafeFileHandle h = NativeMethods.CreateFileW(file, access, Win32.FileShareRead, IntPtr.Zero, OpenAlways, Win32.FlagOpenReparsePoint, IntPtr.Zero);
146
+ if (h.IsInvalid)
147
+ {
148
+ Win32Exception e = Win32.LastError("Could not open the log");
149
+ h.Dispose();
150
+ throw e;
151
+ }
152
+ Win32.ByHandleFileInformation info = Win32.InfoOf(h, file);
153
+ if ((info.FileAttributes & (Win32.AttributeReparsePoint | Win32.AttributeDirectory)) != 0 || info.NumberOfLinks != 1)
154
+ {
155
+ h.Dispose();
156
+ throw new IOException("The log is a link or has a second name; not writing to it.");
157
+ }
158
+ return h;
159
+ }
160
+
161
+ private static void RenameTo(SafeFileHandle h, string target)
162
+ {
163
+ // FILE_RENAME_INFO: BOOLEAN ReplaceIfExists; HANDLE RootDirectory; DWORD FileNameLength; WCHAR FileName[].
164
+ byte[] name = Encoding.Unicode.GetBytes(target);
165
+ int pointer = IntPtr.Size;
166
+ int nameOffset = pointer * 2 + 4;
167
+ byte[] info = new byte[nameOffset + name.Length + 2];
168
+ info[0] = 1;
169
+ BitConverter.GetBytes(name.Length).CopyTo(info, pointer * 2);
170
+ name.CopyTo(info, nameOffset);
171
+ if (!NativeMethods.SetFileInformationByHandle(h, FileRenameInfoClass, info, info.Length))
172
+ {
173
+ throw Win32.LastError("Could not rotate the log");
174
+ }
175
+ }
176
+ }
177
+ }
@@ -0,0 +1,160 @@
1
+ // What the service has mounted, so nothing is left behind: one line per mount (user SID, storage folder, data set) in
2
+ // %ProgramData%\AgentSandbox\storage-helper\mounts.txt, a folder only administrators may change. When the service starts
3
+ // it takes off mount points whose disk is gone (the PC restarted while mounted; the Windows permission prompt's script
4
+ // would otherwise find a stale mount point), and uninstalling runs `release-all`, which dismounts every recorded set with
5
+ // the usual checks. The file is only ever read and written by this service as SYSTEM.
6
+ using System;
7
+ using System.Collections.Generic;
8
+ using System.IO;
9
+ using System.Security.Principal;
10
+ using System.Text;
11
+
12
+ namespace AgentSandbox.StorageHelper
13
+ {
14
+ public sealed class MountRecord
15
+ {
16
+ public MountRecord(string sid, string storageRoot, string set)
17
+ {
18
+ Sid = sid;
19
+ StorageRoot = storageRoot;
20
+ Set = set;
21
+ }
22
+
23
+ public string Sid { get; private set; }
24
+ public string StorageRoot { get; private set; }
25
+ public string Set { get; private set; }
26
+
27
+ public string Key
28
+ {
29
+ get { return (Sid + "\t" + StorageRoot + "\t" + Set).ToUpperInvariant(); }
30
+ }
31
+ }
32
+
33
+ public sealed class MountRecords
34
+ {
35
+ private const int MaxRecords = 1000;
36
+ private const int MaxPerUser = 8;
37
+ private readonly object gate = new object();
38
+ private readonly string file;
39
+
40
+ public MountRecords(string folder)
41
+ {
42
+ // Checked like the log folder: administrators only, no link on the way.
43
+ Log.InFolder(folder, true);
44
+ file = Path.Combine(folder, "mounts.txt");
45
+ }
46
+
47
+ public List<MountRecord> Read()
48
+ {
49
+ lock (gate)
50
+ {
51
+ List<MountRecord> records = new List<MountRecord>();
52
+ if (!File.Exists(file))
53
+ {
54
+ return records;
55
+ }
56
+ foreach (string line in File.ReadAllLines(file, Encoding.UTF8))
57
+ {
58
+ string[] parts = line.Split('\t');
59
+ if (parts.Length != 3 || !Rules.IsSetName(parts[2]) || !Rules.IsPlainAbsolute(parts[1]))
60
+ {
61
+ continue;
62
+ }
63
+ try
64
+ {
65
+ new SecurityIdentifier(parts[0]);
66
+ }
67
+ catch (ArgumentException)
68
+ {
69
+ continue;
70
+ }
71
+ records.Add(new MountRecord(parts[0], parts[1], parts[2]));
72
+ if (records.Count >= MaxRecords)
73
+ {
74
+ break;
75
+ }
76
+ }
77
+ return records;
78
+ }
79
+ }
80
+
81
+ /// <summary>True when this record is there already or there is room for it (at most 8 per user, 1000 in all).</summary>
82
+ public bool HasRoomFor(MountRecord record)
83
+ {
84
+ lock (gate)
85
+ {
86
+ int mine = 0;
87
+ List<MountRecord> records = Read();
88
+ foreach (MountRecord r in records)
89
+ {
90
+ if (r.Key == record.Key)
91
+ {
92
+ return true;
93
+ }
94
+ if (string.Equals(r.Sid, record.Sid, StringComparison.OrdinalIgnoreCase))
95
+ {
96
+ mine++;
97
+ }
98
+ }
99
+ return mine < MaxPerUser && records.Count < MaxRecords;
100
+ }
101
+ }
102
+
103
+ public void Add(MountRecord record)
104
+ {
105
+ lock (gate)
106
+ {
107
+ List<MountRecord> records = Read();
108
+ foreach (MountRecord r in records)
109
+ {
110
+ if (r.Key == record.Key)
111
+ {
112
+ return;
113
+ }
114
+ }
115
+ records.Add(record);
116
+ Save(records);
117
+ }
118
+ }
119
+
120
+ public void Remove(MountRecord record)
121
+ {
122
+ lock (gate)
123
+ {
124
+ List<MountRecord> kept = new List<MountRecord>();
125
+ foreach (MountRecord r in Read())
126
+ {
127
+ if (r.Key != record.Key)
128
+ {
129
+ kept.Add(r);
130
+ }
131
+ }
132
+ Save(kept);
133
+ }
134
+ }
135
+
136
+ private void Save(List<MountRecord> records)
137
+ {
138
+ StringBuilder sb = new StringBuilder();
139
+ int count = 0;
140
+ foreach (MountRecord r in records)
141
+ {
142
+ if (count++ >= MaxRecords)
143
+ {
144
+ break;
145
+ }
146
+ sb.Append(r.Sid).Append('\t').Append(r.StorageRoot).Append('\t').Append(r.Set).Append("\r\n");
147
+ }
148
+ string temp = file + ".tmp";
149
+ File.WriteAllText(temp, sb.ToString(), new UTF8Encoding(false));
150
+ if (File.Exists(file))
151
+ {
152
+ File.Replace(temp, file, null);
153
+ }
154
+ else
155
+ {
156
+ File.Move(temp, file);
157
+ }
158
+ }
159
+ }
160
+ }