windows-development-sandbox 1.13.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config.example.json +2 -1
- package/dist/app/features/defs.d.ts +1 -1
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/projects/projects.run.js +10 -2
- package/dist/app/features/sandbox/sandbox.def.js +3 -1
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +1 -1
- package/dist/cli/commands/projects/here.js +2 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/services/health-service.js +15 -3
- package/dist/cli/services/projects-service.d.ts +8 -3
- package/dist/cli/services/projects-service.js +21 -4
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/schema/config.d.ts +4 -0
- package/dist/core/schema/projects.d.ts +4 -0
- package/dist/core/schema/projects.js +3 -0
- package/dist/guest/boot/boot.js +8 -2
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/daemon/live-folders.d.ts +48 -0
- package/dist/guest/daemon/live-folders.js +245 -0
- package/dist/guest/daemon/live.d.ts +4 -0
- package/dist/guest/daemon/live.js +57 -0
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +5 -2
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +5 -2
- package/dist/guest/runtime/secrets.js +12 -5
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/engines/hcs/live-folders.d.ts +117 -0
- package/dist/host/engines/hcs/live-folders.js +347 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/launch/generate.js +8 -1
- package/dist/host/projects/join.d.ts +4 -0
- package/dist/host/projects/join.js +11 -3
- package/dist/host/projects/mapper.d.ts +34 -5
- package/dist/host/projects/mapper.js +88 -20
- package/dist/host/projects/registry.d.ts +2 -0
- package/dist/host/projects/registry.js +1 -1
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +23 -0
- package/dist/runtime/daemon.js +523 -135
- package/dist/runtime/db.js +23 -0
- package/dist/runtime/main.js +221 -15
- package/dist/runtime/netproxy.js +23 -0
- package/dist/runtime/view.js +209 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +1 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-folders.test.d.ts +2 -0
- package/dist/selftest/live-folders.test.js +588 -0
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/projects.test.js +7 -7
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/live.js +2 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/live.d.ts +53 -0
- package/dist/shared/channel/live.js +92 -0
- package/dist/shared/channel/run-dir.d.ts +11 -0
- package/dist/shared/channel/run-dir.js +20 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +5 -0
- package/dist/shared/paths/projects.d.ts +6 -1
- package/dist/shared/paths/projects.js +21 -1
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +10 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
- package/native/csharp/HcsHost/FolderAccess.cs +361 -0
- package/native/csharp/HcsHost/Hcs.cs +31 -3
- package/native/csharp/HcsHost/Program.cs +54 -6
- package/native/csharp/HcsHost/Share.cs +64 -11
- package/native/csharp/HcsHost/WsbGuard.cs +130 -0
- package/native/csharp/HcsHost/WsbShare.cs +295 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/contained-common.ps1 +4 -0
- package/native/scripts/hcs-live-share.ps1 +191 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/BindMount.cs +120 -0
- package/sandbox/csharp/PtyHost.cs +236 -1
- package/schemas/config.schema.json +8 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// Live folders (
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
|
|
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(
|
|
156
|
-
sb.Append(",\"Settings\":{\"Name\":").Append(Json.Quote(
|
|
157
|
-
if (
|
|
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(
|
|
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 (
|
|
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.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
11
|
+
[assembly: AssemblyVersion("1.15.0.0")]
|
|
12
|
+
[assembly: AssemblyFileVersion("1.15.0.0")]
|
|
13
|
+
[assembly: AssemblyInformationalVersion("1.15.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.15.0.0")]
|
|
13
|
+
[assembly: AssemblyFileVersion("1.15.0.0")]
|
|
14
|
+
[assembly: AssemblyInformationalVersion("1.15.0")]
|
|
15
|
+
[assembly: ComVisible(false)]
|