windows-development-sandbox 1.14.0 → 1.16.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 (100) hide show
  1. package/config.example.json +2 -1
  2. package/dist/app/features/accounts/accounts.def.js +4 -3
  3. package/dist/app/features/defs.d.ts +4 -2
  4. package/dist/app/features/defs.js +26 -12
  5. package/dist/app/features/projects/projects.def.d.ts +1 -1
  6. package/dist/app/features/projects/projects.run.d.ts +1 -1
  7. package/dist/app/features/projects/projects.run.js +10 -2
  8. package/dist/app/features/sandbox/sandbox.def.js +12 -6
  9. package/dist/app/features/sessions/sessions.def.js +1 -1
  10. package/dist/app/features/settings/categories.def.js +1 -0
  11. package/dist/bin/agent-sandbox.js +2 -2
  12. package/dist/bin/{chunk-F7IMME5G.js → chunk-7EXDUNE7.js} +98 -79
  13. package/dist/bin/{chunk-BQ23GBLC.js → chunk-HIC4CUMY.js} +1925 -1430
  14. package/dist/bin/{chunk-LQHEUN2A.js → chunk-PXTLKIV4.js} +1 -1
  15. package/dist/bin/claude-host.js +2 -2
  16. package/dist/bin/claude-sbx.js +2 -2
  17. package/dist/bin/codex-sbx.js +2 -2
  18. package/dist/bin/gemini-sbx.js +2 -2
  19. package/dist/bin/sandbox.js +2 -2
  20. package/dist/bin/sbx-hide.js +2 -2
  21. package/dist/bin/sbx-show.js +2 -2
  22. package/dist/cli/commands/projects/here.js +2 -0
  23. package/dist/cli/index.js +4 -1
  24. package/dist/cli/report.d.ts +7 -0
  25. package/dist/cli/report.js +31 -0
  26. package/dist/cli/services/health-service.js +15 -3
  27. package/dist/cli/services/projects-service.d.ts +8 -3
  28. package/dist/cli/services/projects-service.js +21 -4
  29. package/dist/core/config/defaults.js +1 -0
  30. package/dist/core/schema/config.d.ts +4 -0
  31. package/dist/core/schema/projects.d.ts +4 -0
  32. package/dist/core/schema/projects.js +3 -0
  33. package/dist/guest/boot/boot.js +8 -2
  34. package/dist/guest/daemon/daemon.js +3 -0
  35. package/dist/guest/daemon/live-folders.d.ts +48 -0
  36. package/dist/guest/daemon/live-folders.js +245 -0
  37. package/dist/guest/daemon/live.d.ts +4 -0
  38. package/dist/guest/daemon/live.js +57 -0
  39. package/dist/guest/entries/daemon.js +4 -2
  40. package/dist/guest/runtime/csc.d.ts +1 -0
  41. package/dist/guest/runtime/csc.js +1 -0
  42. package/dist/guest/runtime/secrets.d.ts +5 -2
  43. package/dist/guest/runtime/secrets.js +12 -5
  44. package/dist/host/engines/hcs/live-folders.d.ts +117 -0
  45. package/dist/host/engines/hcs/live-folders.js +347 -0
  46. package/dist/host/engines/index.d.ts +1 -0
  47. package/dist/host/engines/index.js +1 -0
  48. package/dist/host/launch/generate.js +8 -1
  49. package/dist/host/projects/join.d.ts +4 -0
  50. package/dist/host/projects/join.js +11 -3
  51. package/dist/host/projects/mapper.d.ts +34 -5
  52. package/dist/host/projects/mapper.js +88 -20
  53. package/dist/host/projects/registry.d.ts +2 -0
  54. package/dist/host/projects/registry.js +1 -1
  55. package/dist/host/secrets/launch-secrets.js +2 -1
  56. package/dist/runtime/agent.js +14 -0
  57. package/dist/runtime/daemon.js +466 -132
  58. package/dist/runtime/db.js +14 -0
  59. package/dist/runtime/main.js +26 -3
  60. package/dist/runtime/netproxy.js +14 -0
  61. package/dist/runtime/view.js +14 -0
  62. package/dist/selftest/accounts.test.js +22 -0
  63. package/dist/selftest/app.test.js +2 -2
  64. package/dist/selftest/index.js +2 -0
  65. package/dist/selftest/layout.test.js +1 -0
  66. package/dist/selftest/live-folders.test.d.ts +2 -0
  67. package/dist/selftest/live-folders.test.js +588 -0
  68. package/dist/selftest/projects.test.js +7 -7
  69. package/dist/selftest/support/engine-golden.js +1 -0
  70. package/dist/selftest/support/fixtures.d.ts +1 -0
  71. package/dist/selftest/support/live.js +2 -0
  72. package/dist/shared/channel/index.d.ts +1 -0
  73. package/dist/shared/channel/index.js +1 -0
  74. package/dist/shared/channel/live.d.ts +53 -0
  75. package/dist/shared/channel/live.js +92 -0
  76. package/dist/shared/channel/run-dir.d.ts +5 -0
  77. package/dist/shared/channel/run-dir.js +11 -0
  78. package/dist/shared/channel/secrets.d.ts +1 -0
  79. package/dist/shared/channel/secrets.js +5 -0
  80. package/dist/shared/paths/projects.d.ts +6 -1
  81. package/dist/shared/paths/projects.js +21 -1
  82. package/dist/sync/watcher.js +9 -0
  83. package/dist/web/index.html +2 -2
  84. package/dist/web/main-JTH3FT4O.js +221 -0
  85. package/dist/web/{main-UAQPI5XD.css → main-XT7T6ODM.css} +1 -1
  86. package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
  87. package/native/csharp/HcsHost/FolderAccess.cs +361 -0
  88. package/native/csharp/HcsHost/Hcs.cs +31 -3
  89. package/native/csharp/HcsHost/Program.cs +54 -6
  90. package/native/csharp/HcsHost/Share.cs +64 -11
  91. package/native/csharp/HcsHost/WsbGuard.cs +130 -0
  92. package/native/csharp/HcsHost/WsbShare.cs +295 -0
  93. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  94. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  95. package/native/scripts/contained-common.ps1 +4 -0
  96. package/native/scripts/hcs-live-share.ps1 +191 -0
  97. package/package.json +1 -1
  98. package/sandbox/csharp/BindMount.cs +120 -0
  99. package/schemas/config.schema.json +8 -0
  100. package/dist/web/main-6XNBTJLT.js +0 -221
@@ -1,8 +1,9 @@
1
- // Live folders (PROTOTYPE, not wired into the CLI; docs/design/hcs-live-folders.md): a vSMB share added to or removed
2
- // from a RUNNING compute system with HcsModifyComputeSystem. The request is built here from checked fields, never
3
- // passed through: only ResourcePath "VirtualMachine/Devices/VirtualSmb/Shares", only Add or Remove, a share name with
4
- // this tool's prefix, and fixed options (NoDirectmap always; read-only unless asked; never TakeBackupPrivilege,
5
- // UseShareRootIdentity, AllowedFiles or any other option that changes whose rights the guest uses).
1
+ // Live folders (docs/design/hcs-live-folders.md): a vSMB share added to or removed from a RUNNING compute system with
2
+ // HcsModifyComputeSystem. The request is built here from checked fields, never passed through: only ResourcePath
3
+ // "VirtualMachine/Devices/VirtualSmb/Shares", only Add or Remove, a share name with this tool's prefix, and fixed
4
+ // options (NoDirectmap always; read-only unless asked; never TakeBackupPrivilege, UseShareRootIdentity, AllowedFiles or
5
+ // any other option that changes whose rights the guest uses). `share` uses it on our own systems, `wsb-share` on the
6
+ // running Windows Sandbox (WsbShare.cs).
6
7
  using System;
7
8
  using System.Collections.Generic;
8
9
  using System.IO;
@@ -25,9 +26,23 @@ namespace AgentSandbox.HcsHost
25
26
  public string Name;
26
27
  public string Path;
27
28
  public bool ReadOnly = true;
29
+ /// <summary>wsb-share: this tool's storage and data folders, never shared (the data folder holds secrets\).</summary>
30
+ public string StorageRoot;
31
+ public string DataPath;
32
+ /// <summary>wsb-share: the Windows user who asked (must be the one running HcsHost: no other account's prompt).</summary>
33
+ public string UserSid;
28
34
 
29
35
  /// <summary>Reads and checks a request file. Null with `problem` set when it is not allowed.</summary>
30
36
  public static ShareRequest Read(string file, out string problem)
37
+ {
38
+ return Read(file, null, out problem);
39
+ }
40
+
41
+ /// <summary>
42
+ /// The same, and when `sha256` is given the exact bytes parsed must have that SHA-256 (a file swapped after the
43
+ /// elevated script checked it is caught here).
44
+ /// </summary>
45
+ public static ShareRequest Read(string file, string sha256, out string problem)
31
46
  {
32
47
  FileInfo info = new FileInfo(file);
33
48
  if (!info.Exists || info.Length > MaxRequestBytes)
@@ -35,10 +50,27 @@ namespace AgentSandbox.HcsHost
35
50
  problem = "the request must be a file of at most 64 KB";
36
51
  return null;
37
52
  }
53
+ byte[] bytes = File.ReadAllBytes(file);
54
+ if (bytes.Length > MaxRequestBytes)
55
+ {
56
+ problem = "the request must be a file of at most 64 KB";
57
+ return null;
58
+ }
59
+ if (sha256 != null && !string.Equals(Sha256Hex(bytes), sha256, StringComparison.OrdinalIgnoreCase))
60
+ {
61
+ problem = "the request file changed after it was checked";
62
+ return null;
63
+ }
38
64
  Dictionary<string, object> raw;
39
65
  try
40
66
  {
41
- raw = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(File.ReadAllText(file, Encoding.UTF8));
67
+ string text = new UTF8Encoding(false, true).GetString(bytes).TrimStart('\uFEFF');
68
+ raw = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(text);
69
+ }
70
+ catch (DecoderFallbackException)
71
+ {
72
+ problem = "the request is not UTF-8";
73
+ return null;
42
74
  }
43
75
  catch (ArgumentException)
44
76
  {
@@ -55,6 +87,9 @@ namespace AgentSandbox.HcsHost
55
87
  string action = Text(raw, "action");
56
88
  r.Name = Text(raw, "name");
57
89
  r.Path = Text(raw, "path");
90
+ r.StorageRoot = Text(raw, "storageRoot");
91
+ r.DataPath = Text(raw, "dataPath");
92
+ r.UserSid = Text(raw, "userSid");
58
93
  object ro;
59
94
  if (raw != null && raw.TryGetValue("readOnly", out ro))
60
95
  {
@@ -80,6 +115,14 @@ namespace AgentSandbox.HcsHost
80
115
  return problem == null ? r : null;
81
116
  }
82
117
 
118
+ public static string Sha256Hex(byte[] bytes)
119
+ {
120
+ using (System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create())
121
+ {
122
+ return BitConverter.ToString(sha.ComputeHash(bytes)).Replace("-", "").ToLowerInvariant();
123
+ }
124
+ }
125
+
83
126
  private static string Text(Dictionary<string, object> raw, string key)
84
127
  {
85
128
  object value;
@@ -150,17 +193,27 @@ namespace AgentSandbox.HcsHost
150
193
  /// <summary>The ModifySettingRequest (HCS schema; learn.microsoft.com .../hcsmodifycomputesystem).</summary>
151
194
  public string ToModifySettingRequest()
152
195
  {
196
+ return Build(Add, Name, Add ? System.IO.Path.GetFullPath(Path.TrimEnd('\\')) : null, ReadOnly);
197
+ }
198
+
199
+ /// <summary>The one place a share request is written: the fixed resource path and options.</summary>
200
+ public static string Build(bool add, string name, string folder, bool readOnly)
201
+ {
202
+ if (name == null || !SafeName.IsMatch(name))
203
+ {
204
+ throw new ArgumentException("not one of this tool's share names");
205
+ }
153
206
  StringBuilder sb = new StringBuilder();
154
207
  sb.Append("{\"ResourcePath\":").Append(Json.Quote(ResourcePath));
155
- sb.Append(",\"RequestType\":").Append(Json.Quote(Add ? "Add" : "Remove"));
156
- sb.Append(",\"Settings\":{\"Name\":").Append(Json.Quote(Name));
157
- if (Add)
208
+ sb.Append(",\"RequestType\":").Append(Json.Quote(add ? "Add" : "Remove"));
209
+ sb.Append(",\"Settings\":{\"Name\":").Append(Json.Quote(name));
210
+ if (add)
158
211
  {
159
- sb.Append(",\"Path\":").Append(Json.Quote(System.IO.Path.GetFullPath(Path.TrimEnd('\\'))));
212
+ sb.Append(",\"Path\":").Append(Json.Quote(folder));
160
213
  // hcsshim's read-only defaults (internal/uvm/vsmb.go DefaultVSMBOptions) plus NoDirectmap: a share that
161
214
  // was never direct-mapped can be removed again while the system runs.
162
215
  sb.Append(",\"Options\":{\"NoDirectmap\":true");
163
- if (ReadOnly)
216
+ if (readOnly)
164
217
  {
165
218
  sb.Append(",\"ReadOnly\":true,\"ShareRead\":true,\"CacheIo\":true,\"PseudoOplocks\":true");
166
219
  }
@@ -0,0 +1,130 @@
1
+ // The one exception to "only systems this tool created" (docs/design/hcs-live-folders.md, section 4): the wsb-share
2
+ // verb may add a vSMB share to the running Windows Sandbox. It acts only when exactly one compute system is a running
3
+ // VirtualMachine with a GUID id and an Owner from a fixed list, and that id is the one asked for. An unknown Owner,
4
+ // none or two candidates (another user's sandbox, Application Guard) all fail closed. Ids with this tool's own prefix
5
+ // are refused here, and `share` refuses GUIDs, so neither verb reaches the other's systems.
6
+ using System;
7
+ using System.Collections;
8
+ using System.Collections.Generic;
9
+ using System.Text.RegularExpressions;
10
+ using System.Web.Script.Serialization;
11
+
12
+ namespace AgentSandbox.HcsHost
13
+ {
14
+ public static class WsbGuard
15
+ {
16
+ /// <summary>The Owner Windows Sandbox's compute system is reported to have (unconfirmed: the PC test records it).</summary>
17
+ private static readonly string[] Owners = new string[] { "Madrid" };
18
+ private static readonly Regex GuidId = new Regex("^[0-9A-Fa-f]{8}-([0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$");
19
+
20
+ /// <summary>Null when `id` has the shape of Windows Sandbox's id (a GUID, never one of ours).</summary>
21
+ public static string IdProblem(string id)
22
+ {
23
+ if (id == null || id.StartsWith(Guard.IdPrefix, StringComparison.OrdinalIgnoreCase) || !GuidId.IsMatch(id))
24
+ {
25
+ return "only the running Windows Sandbox (a GUID id) can be given a live folder";
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /// <summary>One system from HCS JSON: its Id, SystemType, Owner and State ("" when missing).</summary>
31
+ private static string Field(Dictionary<string, object> s, string key)
32
+ {
33
+ object value;
34
+ return s != null && s.TryGetValue(key, out value) && value is string ? (string)value : "";
35
+ }
36
+
37
+ private static bool IsCandidate(Dictionary<string, object> s)
38
+ {
39
+ return Field(s, "SystemType") == "VirtualMachine" && Field(s, "State") == "Running" &&
40
+ Array.IndexOf(Owners, Field(s, "Owner")) >= 0 && GuidId.IsMatch(Field(s, "Id"));
41
+ }
42
+
43
+ /// <summary>
44
+ /// Pure: the Windows Sandbox candidate in HcsEnumerateComputeSystems' answer. Null (with `id`) when there is
45
+ /// exactly one; otherwise why not.
46
+ /// </summary>
47
+ public static string Candidate(string enumerateJson, out string id)
48
+ {
49
+ id = null;
50
+ object parsed;
51
+ try
52
+ {
53
+ parsed = new JavaScriptSerializer().DeserializeObject(enumerateJson ?? "");
54
+ }
55
+ catch (ArgumentException)
56
+ {
57
+ return "the list of compute systems is not JSON";
58
+ }
59
+ catch (InvalidOperationException)
60
+ {
61
+ return "the list of compute systems is not JSON";
62
+ }
63
+ IEnumerable systems = parsed as object[];
64
+ if (systems == null)
65
+ {
66
+ return "the list of compute systems is not a list";
67
+ }
68
+ List<string> found = new List<string>();
69
+ foreach (object o in systems)
70
+ {
71
+ Dictionary<string, object> s = o as Dictionary<string, object>;
72
+ if (IsCandidate(s))
73
+ {
74
+ found.Add(Field(s, "Id"));
75
+ }
76
+ }
77
+ if (found.Count != 1)
78
+ {
79
+ return "exactly one running Windows Sandbox is needed, and " + found.Count + " were found (another user's sandbox or Application Guard counts too; a Windows Sandbox whose Owner is not " + string.Join(", ", Owners) + " is not recognised)";
80
+ }
81
+ id = found[0];
82
+ return null;
83
+ }
84
+
85
+ /// <summary>Pure: null when the candidate is the system asked for (case-insensitive GUIDs).</summary>
86
+ public static string MatchProblem(string candidate, string wanted)
87
+ {
88
+ if (wanted != null && wanted.Length > 0 && !string.Equals(candidate, wanted, StringComparison.OrdinalIgnoreCase))
89
+ {
90
+ return "the running Windows Sandbox is not the one this live folder was recorded for (it was restarted)";
91
+ }
92
+ return null;
93
+ }
94
+
95
+ /// <summary>Pure: null when an opened system's own properties still say it is the running Windows Sandbox `id`.</summary>
96
+ public static string PropertiesProblem(string propertiesJson, string id)
97
+ {
98
+ Dictionary<string, object> s;
99
+ try
100
+ {
101
+ s = new JavaScriptSerializer().Deserialize<Dictionary<string, object>>(propertiesJson ?? "");
102
+ }
103
+ catch (ArgumentException)
104
+ {
105
+ return "its properties are not JSON";
106
+ }
107
+ catch (InvalidOperationException)
108
+ {
109
+ return "its properties are not JSON";
110
+ }
111
+ if (!IsCandidate(s) || !string.Equals(Field(s, "Id"), id, StringComparison.OrdinalIgnoreCase))
112
+ {
113
+ return "the opened system is no longer the running Windows Sandbox";
114
+ }
115
+ return null;
116
+ }
117
+
118
+ /// <summary>Live: asks HCS for every system and finds Windows Sandbox (null with `id`, or why not).</summary>
119
+ public static string Find(out string id)
120
+ {
121
+ id = null;
122
+ HcsReply reply = Hcs.Enumerate(null);
123
+ if (!reply.Ok)
124
+ {
125
+ return "the compute systems could not be listed: " + reply.Message();
126
+ }
127
+ return Candidate(reply.Json, out id);
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,295 @@
1
+ // wsb-share (docs/design/hcs-live-folders.md): adds or removes one live folder on the running Windows Sandbox, run by
2
+ // native\scripts\hcs-live-share.ps1 from an administrator-only copy of this exe. One process holds one handle to the
3
+ // system for the whole add:
4
+ // 1. the request (its exact bytes hashed and compared), the folder (FolderAccess) and the system (WsbGuard: exactly
5
+ // one candidate, the id asked for if one was recorded, its own properties re-read through the handle);
6
+ // 2. a canary: a fresh folder next to this exe (Administrators, SYSTEM and the Virtual Machines group only) holding a
7
+ // random 128-bit token, shared read-only under a random name; canary.json in the request's folder tells the
8
+ // unelevated caller its name; the token our sandbox's daemon read from that share must come back in answer.txt
9
+ // within the time limit;
10
+ // 3. only then the real folder is shared. The canary share and folder are always removed again.
11
+ // It never terminates, pauses or saves the system, and every HCS operation waits at most 60 seconds.
12
+ using System;
13
+ using System.Collections.Generic;
14
+ using System.IO;
15
+ using System.Security.AccessControl;
16
+ using System.Security.Cryptography;
17
+ using System.Security.Principal;
18
+ using System.Text;
19
+ using System.Text.RegularExpressions;
20
+ using System.Threading;
21
+
22
+ namespace AgentSandbox.HcsHost
23
+ {
24
+ public static class WsbShare
25
+ {
26
+ private const uint OperationMs = 60000;
27
+ private const int CanaryWaitMs = 45000;
28
+ private const int PollMs = 250;
29
+ private const string CanaryFile = "canary.json";
30
+ private const string AnswerFile = "answer.txt";
31
+ private const string TokenFile = "token.txt";
32
+ private static readonly Regex Token = new Regex("^[0-9a-f]{32}$");
33
+ private static readonly SecurityIdentifier VirtualMachines = new SecurityIdentifier("S-1-5-83-0");
34
+
35
+ /// <summary>Adds or removes per the request; `values` say what happened (id, canary, the share request).</summary>
36
+ public static bool Run(string requestFile, string sha256, IDictionary<string, string> values, out string message)
37
+ {
38
+ string problem;
39
+ ShareRequest request = ShareRequest.Read(requestFile, sha256, out problem);
40
+ if (problem != null)
41
+ {
42
+ message = "Refused: " + problem;
43
+ return false;
44
+ }
45
+ // Elevated with another account's password (a standard user's prompt): the profile, access and ownership
46
+ // checks would be that account's, so it is refused. Live folders need your own administrator account.
47
+ string me = WindowsIdentity.GetCurrent().User.Value;
48
+ if (string.IsNullOrEmpty(request.UserSid) || !string.Equals(request.UserSid, me, StringComparison.OrdinalIgnoreCase))
49
+ {
50
+ message = "Refused: the permission prompt was answered with another account; live folders need your own administrator account";
51
+ return false;
52
+ }
53
+ string wanted = string.IsNullOrEmpty(request.Id) ? null : request.Id;
54
+ if (wanted != null && (problem = WsbGuard.IdProblem(wanted)) != null)
55
+ {
56
+ message = "Refused: " + problem;
57
+ return false;
58
+ }
59
+ if (!request.Add && wanted == null)
60
+ {
61
+ message = "Refused: a live folder is only removed from the sandbox it was added to (give its id)";
62
+ return false;
63
+ }
64
+ string folder = null;
65
+ IntPtr held = IntPtr.Zero;
66
+ if (request.Add && (problem = FolderAccess.Problem(request.Path, request.ReadOnly, request.StorageRoot, request.DataPath, out folder, out held)) != null)
67
+ {
68
+ message = "Refused: " + problem;
69
+ return false;
70
+ }
71
+ try
72
+ {
73
+ return WithFolder(request, requestFile, folder, held, wanted, values, out message);
74
+ }
75
+ finally
76
+ {
77
+ FolderAccess.Close(held);
78
+ }
79
+ }
80
+
81
+ /// <summary>The rest of Run, while the folder to share is held open (it cannot be renamed or replaced).</summary>
82
+ private static bool WithFolder(ShareRequest request, string requestFile, string folder, IntPtr held, string wanted, IDictionary<string, string> values, out string message)
83
+ {
84
+ string problem;
85
+ string id;
86
+ if ((problem = WsbGuard.Find(out id)) != null || (problem = WsbGuard.MatchProblem(id, wanted)) != null)
87
+ {
88
+ message = "Refused: " + problem;
89
+ return false;
90
+ }
91
+ values["id"] = id;
92
+ IntPtr system;
93
+ int hr = Hcs.Open(id, out system);
94
+ if (hr != 0)
95
+ {
96
+ HcsReply failed = new HcsReply();
97
+ failed.HResult = hr;
98
+ message = "Windows Sandbox could not be opened: " + failed.Message();
99
+ return false;
100
+ }
101
+ try
102
+ {
103
+ HcsReply props = Hcs.PropertiesOf(system);
104
+ if (!props.Ok || (problem = WsbGuard.PropertiesProblem(props.Json, id)) != null)
105
+ {
106
+ message = "Refused: " + (problem ?? "its properties could not be read: " + props.Message());
107
+ return false;
108
+ }
109
+ if (!request.Add)
110
+ {
111
+ string remove = ShareRequest.Build(false, request.Name, null, true);
112
+ values["request"] = remove;
113
+ return Reply(Hcs.ModifyOn(system, remove, OperationMs), "removed", out message);
114
+ }
115
+ return AddWithCanary(system, request, folder, held, Path.GetDirectoryName(Path.GetFullPath(requestFile)), values, out message);
116
+ }
117
+ finally
118
+ {
119
+ Hcs.Close(system);
120
+ }
121
+ }
122
+
123
+ private static bool Reply(HcsReply reply, string done, out string message)
124
+ {
125
+ message = reply.Ok ? done : "The Host Compute Service said: " + reply.Message() + " (0x" + reply.HResult.ToString("X8") + ")";
126
+ return reply.Ok;
127
+ }
128
+
129
+ private static string RandomHex(int bytes)
130
+ {
131
+ byte[] b = new byte[bytes];
132
+ using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
133
+ {
134
+ rng.GetBytes(b);
135
+ }
136
+ return BitConverter.ToString(b).Replace("-", "").ToLowerInvariant();
137
+ }
138
+
139
+ /// <summary>A new folder only Administrators, SYSTEM and the VM worker processes can use (made with that DACL).</summary>
140
+ private static string MakeCanaryFolder(string token)
141
+ {
142
+ string here = Path.GetDirectoryName(typeof(WsbShare).Assembly.Location);
143
+ string dir = Path.Combine(here, "canary-" + RandomHex(8));
144
+ if (Directory.Exists(dir) || File.Exists(dir))
145
+ {
146
+ throw new IOException("the canary folder exists already");
147
+ }
148
+ DirectorySecurity security = new DirectorySecurity();
149
+ security.SetAccessRuleProtection(true, false);
150
+ security.SetOwner(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null));
151
+ InheritanceFlags inherit = InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit;
152
+ security.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), FileSystemRights.FullControl, inherit, PropagationFlags.None, AccessControlType.Allow));
153
+ security.AddAccessRule(new FileSystemAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), FileSystemRights.FullControl, inherit, PropagationFlags.None, AccessControlType.Allow));
154
+ security.AddAccessRule(new FileSystemAccessRule(VirtualMachines, FileSystemRights.ReadAndExecute, inherit, PropagationFlags.None, AccessControlType.Allow));
155
+ Directory.CreateDirectory(dir, security);
156
+ File.WriteAllText(Path.Combine(dir, TokenFile), token, new UTF8Encoding(false));
157
+ return dir;
158
+ }
159
+
160
+ /// <summary>Writes a small file whole: a temp name first, then moved into place.</summary>
161
+ private static void WriteWhole(string file, string text)
162
+ {
163
+ // The caller's own temp folder: checked for a link right before writing, and only a new file is made there
164
+ // (never over an existing file or link).
165
+ string dir = Path.GetDirectoryName(file);
166
+ if ((File.GetAttributes(dir) & FileAttributes.ReparsePoint) != 0)
167
+ {
168
+ throw new IOException("the request folder became a link");
169
+ }
170
+ string temp = file + "." + RandomHex(4) + ".tmp";
171
+ byte[] bytes = new UTF8Encoding(false).GetBytes(text);
172
+ using (FileStream stream = new FileStream(temp, FileMode.CreateNew, FileAccess.Write, FileShare.None))
173
+ {
174
+ stream.Write(bytes, 0, bytes.Length);
175
+ }
176
+ File.Move(temp, file);
177
+ }
178
+
179
+ /// <summary>The token the caller relays from our sandbox's daemon (32 hex digits), or null until it is there.</summary>
180
+ private static string ReadAnswer(string file)
181
+ {
182
+ try
183
+ {
184
+ FileInfo info = new FileInfo(file);
185
+ if (!info.Exists || info.Length > 64)
186
+ {
187
+ return null;
188
+ }
189
+ string text = File.ReadAllText(file, Encoding.ASCII).Trim();
190
+ return text.Length > 0 ? text : null;
191
+ }
192
+ catch (IOException)
193
+ {
194
+ return null; // still being written
195
+ }
196
+ catch (UnauthorizedAccessException)
197
+ {
198
+ return null;
199
+ }
200
+ }
201
+
202
+ private static bool FixedTimeEquals(string a, string b)
203
+ {
204
+ if (a == null || b == null || a.Length != b.Length)
205
+ {
206
+ return false;
207
+ }
208
+ int diff = 0;
209
+ for (int i = 0; i < a.Length; i++)
210
+ {
211
+ diff |= a[i] ^ b[i];
212
+ }
213
+ return diff == 0;
214
+ }
215
+
216
+ private static bool AddWithCanary(IntPtr system, ShareRequest request, string folder, IntPtr held, string exchange, IDictionary<string, string> values, out string message)
217
+ {
218
+ string token = RandomHex(16);
219
+ string canaryName = "agentsbx-" + RandomHex(8);
220
+ string canaryDir = null;
221
+ bool canaryAdded = false;
222
+ try
223
+ {
224
+ canaryDir = MakeCanaryFolder(token);
225
+ // Removed in finally even when the add fails: an add that timed out may still complete.
226
+ canaryAdded = true;
227
+ HcsReply added = Hcs.ModifyOn(system, ShareRequest.Build(true, canaryName, canaryDir, true), OperationMs);
228
+ if (!added.Ok)
229
+ {
230
+ values["canary"] = "not added";
231
+ return Reply(added, "", out message);
232
+ }
233
+ WriteWhole(Path.Combine(exchange, CanaryFile), "{\"share\":" + Json.Quote(canaryName) + "}");
234
+ string answer = null;
235
+ for (int waited = 0; waited < CanaryWaitMs && answer == null; waited += PollMs)
236
+ {
237
+ Thread.Sleep(PollMs);
238
+ answer = ReadAnswer(Path.Combine(exchange, AnswerFile));
239
+ }
240
+ if (answer == null || answer == "none")
241
+ {
242
+ values["canary"] = answer == null ? "no answer" : "not read";
243
+ message = "The sandbox did not read the test share" + (answer == null ? " in time" : "") + ", so this may not be your sandbox; nothing was shared";
244
+ return false;
245
+ }
246
+ if (!Token.IsMatch(answer) || !FixedTimeEquals(answer, token))
247
+ {
248
+ values["canary"] = "wrong token";
249
+ message = "The test share came back with the wrong token, so this is not your sandbox; nothing was shared";
250
+ return false;
251
+ }
252
+ values["canary"] = "matched";
253
+ string add = ShareRequest.Build(true, request.Name, folder, request.ReadOnly);
254
+ values["request"] = add;
255
+ HcsReply real = Hcs.ModifyOn(system, add, OperationMs);
256
+ string moved = real.Ok ? FolderAccess.MovedProblem(held, folder) : null;
257
+ if (!real.Ok || moved != null)
258
+ {
259
+ // Failed, timed out (it may still complete) or the folder moved: the share is taken away again.
260
+ HcsReply undone = Hcs.ModifyOn(system, ShareRequest.Build(false, request.Name, null, true), OperationMs);
261
+ values["undone"] = undone.Ok ? "yes" : undone.Message();
262
+ if (moved != null)
263
+ {
264
+ message = "Refused: " + moved;
265
+ return false;
266
+ }
267
+ }
268
+ return Reply(real, "added", out message);
269
+ }
270
+ finally
271
+ {
272
+ if (canaryAdded)
273
+ {
274
+ HcsReply removed = Hcs.ModifyOn(system, ShareRequest.Build(false, canaryName, null, true), OperationMs);
275
+ values["canaryRemoved"] = removed.Ok ? "yes" : removed.Message();
276
+ }
277
+ if (canaryDir != null)
278
+ {
279
+ try
280
+ {
281
+ Directory.Delete(canaryDir, true);
282
+ }
283
+ catch (IOException)
284
+ {
285
+ // Still open by the VM: the elevated script deletes the whole work folder afterwards.
286
+ }
287
+ catch (UnauthorizedAccessException)
288
+ {
289
+ // The same.
290
+ }
291
+ }
292
+ }
293
+ }
294
+ }
295
+ }
@@ -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.14.0.0")]
12
- [assembly: AssemblyFileVersion("1.14.0.0")]
13
- [assembly: AssemblyInformationalVersion("1.14.0")]
11
+ [assembly: AssemblyVersion("1.16.0.0")]
12
+ [assembly: AssemblyFileVersion("1.16.0.0")]
13
+ [assembly: AssemblyInformationalVersion("1.16.0")]
14
14
  [assembly: ComVisible(false)]
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
9
9
  [assembly: AssemblyCompany("windows-development-sandbox (open source)")]
10
10
  [assembly: AssemblyProduct("Windows Development Sandbox")]
11
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")]
12
+ [assembly: AssemblyVersion("1.16.0.0")]
13
+ [assembly: AssemblyFileVersion("1.16.0.0")]
14
+ [assembly: AssemblyInformationalVersion("1.16.0")]
15
15
  [assembly: ComVisible(false)]
@@ -15,6 +15,10 @@ function Add-Note([string]$Text) {
15
15
  $script:Lines.Add($Text)
16
16
  }
17
17
 
18
+ function Get-Notes {
19
+ return @($script:Lines)
20
+ }
21
+
18
22
  function Test-PlainAbsolute([string]$Path) {
19
23
  if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
20
24
  if ($Path -notmatch $script:PathRule) { return $false }