@sysid/sandbox-runtime-improved 0.0.52-sysid.1 → 0.0.54-sysid.1
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/README.md +5 -2
- package/dist/cli.js +91 -7
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +17 -11
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.d.ts +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.js +16 -2
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +18 -5
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +53 -0
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +46 -0
- package/dist/sandbox/sandbox-config.js.map +1 -1
- package/dist/sandbox/sandbox-manager.d.ts +4 -0
- package/dist/sandbox/sandbox-manager.d.ts.map +1 -1
- package/dist/sandbox/sandbox-manager.js +172 -40
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +12 -3
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.js +18 -4
- package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +222 -0
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -0
- package/dist/sandbox/windows-sandbox-utils.js +433 -0
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -0
- package/dist/vendor/srt-win/Cargo.lock +361 -0
- package/dist/vendor/srt-win/Cargo.toml +46 -0
- package/dist/vendor/srt-win/ci/cleanup.ps1 +49 -0
- package/dist/vendor/srt-win/ci/smoke-exec.ps1 +446 -0
- package/dist/vendor/srt-win/ci/smoke.ps1 +307 -0
- package/dist/vendor/srt-win/src/job.rs +102 -0
- package/dist/vendor/srt-win/src/launch.rs +732 -0
- package/dist/vendor/srt-win/src/lib.rs +20 -0
- package/dist/vendor/srt-win/src/main.rs +669 -0
- package/dist/vendor/srt-win/src/self_protect.rs +169 -0
- package/dist/vendor/srt-win/src/sid.rs +296 -0
- package/dist/vendor/srt-win/src/token.rs +341 -0
- package/dist/vendor/srt-win/src/util.rs +42 -0
- package/dist/vendor/srt-win/src/wfp.rs +992 -0
- package/dist/vendor/srt-win/src/winsta.rs +209 -0
- package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +259 -0
- package/package.json +2 -2
- package/vendor/srt-win/Cargo.lock +361 -0
- package/vendor/srt-win/Cargo.toml +46 -0
- package/vendor/srt-win/ci/cleanup.ps1 +49 -0
- package/vendor/srt-win/ci/smoke-exec.ps1 +446 -0
- package/vendor/srt-win/ci/smoke.ps1 +307 -0
- package/vendor/srt-win/src/job.rs +102 -0
- package/vendor/srt-win/src/launch.rs +732 -0
- package/vendor/srt-win/src/lib.rs +20 -0
- package/vendor/srt-win/src/main.rs +669 -0
- package/vendor/srt-win/src/self_protect.rs +169 -0
- package/vendor/srt-win/src/sid.rs +296 -0
- package/vendor/srt-win/src/token.rs +341 -0
- package/vendor/srt-win/src/util.rs +42 -0
- package/vendor/srt-win/src/wfp.rs +992 -0
- package/vendor/srt-win/src/winsta.rs +209 -0
- package/vendor/srt-win/tests/sd_access_check_matrix.rs +259 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
<#
|
|
2
|
+
srt-win end-to-end smoke test.
|
|
3
|
+
|
|
4
|
+
Exercises the full group + WFP lifecycle against a built srt-win.exe.
|
|
5
|
+
Throws on any assertion failure. Requires elevation (NetLocalGroup*
|
|
6
|
+
and Fwpm* both need admin).
|
|
7
|
+
|
|
8
|
+
Usage (local dev machine):
|
|
9
|
+
pwsh vendor/srt-win/ci/smoke.ps1 .\target\release\srt-win.exe
|
|
10
|
+
|
|
11
|
+
Usage (CI — workflow passes the path):
|
|
12
|
+
pwsh vendor/srt-win/ci/smoke.ps1 vendor\srt-win\target\release\srt-win.exe
|
|
13
|
+
|
|
14
|
+
All WFP operations target $TestSublayer (a fixed test GUID), NOT
|
|
15
|
+
the production default sublayer — safe to run on a dev machine
|
|
16
|
+
that has real sandbox-runtime filters installed; the idempotent
|
|
17
|
+
install's purge-then-re-add only touches the test sublayer.
|
|
18
|
+
|
|
19
|
+
When running under GitHub Actions, the random alt-sublayer GUID is
|
|
20
|
+
also written to $env:GITHUB_ENV so the always()-gated cleanup step
|
|
21
|
+
can remove those filters even if this script throws midway.
|
|
22
|
+
#>
|
|
23
|
+
param(
|
|
24
|
+
[Parameter(Mandatory = $true)]
|
|
25
|
+
[string]$Exe,
|
|
26
|
+
[string]$GroupName = 'srt-ci-test',
|
|
27
|
+
# Distinct from wfp::DEFAULT_SUBLAYER_GUID so local runs never
|
|
28
|
+
# touch a production install.
|
|
29
|
+
[string]$TestSublayer = 'a91b6f12-4c0e-4e30-b1f7-3d52890ce117',
|
|
30
|
+
# Second sublayer for the single-install convenience-subcommand
|
|
31
|
+
# row, so it doesn't perturb the lifecycle section's $TestSublayer.
|
|
32
|
+
[string]$InstallSublayer = 'b2e8a6c4-1f73-4d09-9e25-c7b0d3a48f61'
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
$ErrorActionPreference = 'Stop'
|
|
36
|
+
|
|
37
|
+
if (-not (Test-Path $Exe)) {
|
|
38
|
+
throw "srt-win.exe not found at '$Exe'"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# user_sid logged for debug context only (whose TokenGroups the
|
|
42
|
+
# group-status / fence assertions are evaluating against).
|
|
43
|
+
$me = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
|
44
|
+
Write-Host "srt-win smoke: exe=$Exe group=$GroupName sublayer=$TestSublayer user_sid=$me"
|
|
45
|
+
$sl = @('--sublayer-guid', $TestSublayer)
|
|
46
|
+
# Explicit so the assertions below are deterministic even if the
|
|
47
|
+
# compiled-in default changes.
|
|
48
|
+
$pr = @('--proxy-port-range', '60080-60089')
|
|
49
|
+
|
|
50
|
+
function Run([string[]]$argv) {
|
|
51
|
+
& $Exe @argv
|
|
52
|
+
if ($LASTEXITCODE -ne 0) {
|
|
53
|
+
throw "srt-win $($argv -join ' ') exited $LASTEXITCODE"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function J([string[]]$argv) { Run $argv | ConvertFrom-Json }
|
|
57
|
+
function MustFail([string[]]$argv, [string]$why) {
|
|
58
|
+
& $Exe @argv 2>$null
|
|
59
|
+
if ($LASTEXITCODE -eq 0) {
|
|
60
|
+
throw "expected non-zero ($why): srt-win $($argv -join ' ')"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# The admin mutators below (group create|delete, wfp install|uninstall)
|
|
65
|
+
# self-elevate via maybe_self_elevate (batch 02b). The CI runner is
|
|
66
|
+
# already elevated, so the helper returns None and they run in-process,
|
|
67
|
+
# same as before; the UAC self-elevate relaunch path is interactive-only
|
|
68
|
+
# (see 02b) and is not exercised here.
|
|
69
|
+
# ── group create (idempotent) ────────────────────────────────────────
|
|
70
|
+
Run @('group', 'create', '--name', $GroupName)
|
|
71
|
+
Run @('group', 'create', '--name', $GroupName) # second call must succeed
|
|
72
|
+
|
|
73
|
+
$gs = J @('group', 'status', '--name', $GroupName)
|
|
74
|
+
Write-Host "group status (--name): $($gs | ConvertTo-Json -Compress)"
|
|
75
|
+
# In CI there's no logout, so the new group SID is not yet on the
|
|
76
|
+
# runner's token: `created-not-on-token` is expected. On a dev box
|
|
77
|
+
# that already has the group from a prior run, `ready` is fine too.
|
|
78
|
+
if ($gs.state -notin 'created-not-on-token', 'ready') {
|
|
79
|
+
throw "unexpected group state: $($gs.state)"
|
|
80
|
+
}
|
|
81
|
+
if (-not $gs.sid -or -not $gs.sid.StartsWith('S-1-')) {
|
|
82
|
+
throw "group status did not return a SID"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# --group-sid path returns the same SID.
|
|
86
|
+
$gs2 = J @('group', 'status', '--group-sid', $gs.sid)
|
|
87
|
+
if ($gs2.sid -ne $gs.sid) {
|
|
88
|
+
throw "--group-sid status sid mismatch: $($gs2.sid) vs $($gs.sid)"
|
|
89
|
+
}
|
|
90
|
+
# Unmapped SID via --group-sid reports absent (not created-not-on-token).
|
|
91
|
+
$gsBad = J @('group', 'status', '--group-sid', 'S-1-5-21-1-2-3-9999999')
|
|
92
|
+
if ($gsBad.state -ne 'absent') {
|
|
93
|
+
throw "unmapped --group-sid expected absent, got $($gsBad.state)"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# ── negative input ──────────────────────────────────────────────────
|
|
97
|
+
MustFail @('wfp', 'install', '--group-sid', 'not-a-sid') 'invalid --group-sid'
|
|
98
|
+
MustFail (@('wfp', 'install', '--name', $GroupName, '--proxy-port-range', '100-50') + $sl) 'low>high'
|
|
99
|
+
MustFail (@('wfp', 'install', '--name', $GroupName, '--proxy-port-range', '1-1000') + $sl) 'range too wide'
|
|
100
|
+
MustFail (@('wfp', 'install', '--name', $GroupName, '--proxy-port-range', '60080') + $sl) 'missing dash'
|
|
101
|
+
|
|
102
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
103
|
+
# WFP lifecycle test — uses $GroupName.
|
|
104
|
+
#
|
|
105
|
+
# The WFP filters are machine-wide and keyed on the group SID. We
|
|
106
|
+
# can install/enumerate/uninstall them regardless of whether the
|
|
107
|
+
# group is on the current token, so $GroupName works fine for the
|
|
108
|
+
# *lifecycle* assertions below. It does NOT work for asserting
|
|
109
|
+
# "the broker gets through" — that needs a group already enabled
|
|
110
|
+
# on this token, which $GroupName isn't (no logout in CI). The
|
|
111
|
+
# fence-behaviour section further down uses BUILTIN\Administrators
|
|
112
|
+
# instead.
|
|
113
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
114
|
+
|
|
115
|
+
# ── pre-install absent ───────────────────────────────────────────────
|
|
116
|
+
$pre = J (@('wfp', 'status') + $sl)
|
|
117
|
+
if ($pre.state -ne 'absent') {
|
|
118
|
+
throw "pre-install wfp status expected absent, got $($pre.state)"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# First install via --name.
|
|
122
|
+
Run (@('wfp', 'install', '--name', $GroupName) + $sl + $pr)
|
|
123
|
+
$ws = J (@('wfp', 'status') + $sl)
|
|
124
|
+
Write-Host "wfp status: $($ws | ConvertTo-Json -Compress)"
|
|
125
|
+
if ($ws.state -ne 'installed') { throw "expected installed, got $($ws.state)" }
|
|
126
|
+
if ($ws.filters -lt 8) { throw "expected >=8 filters, got $($ws.filters)" }
|
|
127
|
+
if ($ws.port_range[0] -ne 60080 -or $ws.port_range[1] -ne 60089) {
|
|
128
|
+
throw "expected port_range [60080,60089], got [$($ws.port_range -join ',')]"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Idempotency: second install via --group-sid path leaves the same
|
|
132
|
+
# filter count.
|
|
133
|
+
Run (@('wfp', 'install', '--group-sid', $gs.sid) + $sl + $pr)
|
|
134
|
+
$ws2 = J (@('wfp', 'status') + $sl)
|
|
135
|
+
if ($ws2.filters -ne $ws.filters) {
|
|
136
|
+
throw "idempotency: filter count changed $($ws.filters) -> $($ws2.filters)"
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ── --proxy-port-range override round-trips through status ─────────
|
|
140
|
+
Run (@('wfp', 'install', '--name', $GroupName, '--proxy-port-range', '50000-50001') + $sl)
|
|
141
|
+
$wsR = J (@('wfp', 'status') + $sl)
|
|
142
|
+
if ($wsR.port_range[0] -ne 50000 -or $wsR.port_range[1] -ne 50001) {
|
|
143
|
+
throw "expected port_range [50000,50001], got [$($wsR.port_range -join ',')]"
|
|
144
|
+
}
|
|
145
|
+
# No-flag install: assert the compiled-in DEFAULT_PROXY_PORT_RANGE.
|
|
146
|
+
Run (@('wfp', 'install', '--name', $GroupName) + $sl)
|
|
147
|
+
$wsD = J (@('wfp', 'status') + $sl)
|
|
148
|
+
if ($wsD.port_range[0] -ne 60080 -or $wsD.port_range[1] -ne 60089) {
|
|
149
|
+
throw "no-flag default expected [60080,60089], got [$($wsD.port_range -join ',')]"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# ── --sublayer-guid isolation ────────────────────────────────────────
|
|
153
|
+
# Persist the alt GUID so an always()-gated cleanup step can remove
|
|
154
|
+
# its filters even if this script throws midway.
|
|
155
|
+
$altGuid = [guid]::NewGuid().ToString()
|
|
156
|
+
if ($env:GITHUB_ENV) {
|
|
157
|
+
Add-Content $env:GITHUB_ENV "SRT_ALT_GUID=$altGuid"
|
|
158
|
+
}
|
|
159
|
+
Run (@('wfp', 'install', '--name', $GroupName, '--sublayer-guid', $altGuid) + $pr)
|
|
160
|
+
$alt = J @('wfp', 'status', '--sublayer-guid', $altGuid)
|
|
161
|
+
if ($alt.state -ne 'installed') {
|
|
162
|
+
throw "alt sublayer expected installed, got $($alt.state)"
|
|
163
|
+
}
|
|
164
|
+
# Test sublayer is still its own thing.
|
|
165
|
+
$stillTest = J (@('wfp', 'status') + $sl)
|
|
166
|
+
if ($stillTest.filters -ne $ws.filters) {
|
|
167
|
+
throw "test sublayer perturbed by alt install"
|
|
168
|
+
}
|
|
169
|
+
Run @('wfp', 'uninstall', '--sublayer-guid', $altGuid)
|
|
170
|
+
$altGone = J @('wfp', 'status', '--sublayer-guid', $altGuid)
|
|
171
|
+
if ($altGone.state -ne 'absent') {
|
|
172
|
+
throw "alt sublayer expected absent after uninstall, got $($altGone.state)"
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# ── teardown: uninstall on test sublayer ────────────────────────────
|
|
176
|
+
Run (@('wfp', 'uninstall') + $sl)
|
|
177
|
+
$post = J (@('wfp', 'status') + $sl)
|
|
178
|
+
if ($post.state -ne 'absent') {
|
|
179
|
+
throw "post-uninstall expected absent, got $($post.state)"
|
|
180
|
+
}
|
|
181
|
+
# Idempotent no-op: second uninstall must also exit 0.
|
|
182
|
+
Run (@('wfp', 'uninstall') + $sl)
|
|
183
|
+
|
|
184
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
185
|
+
# WFP fence-behaviour test — uses BUILTIN\Administrators (S-1-5-32-544).
|
|
186
|
+
#
|
|
187
|
+
# Why a different group: the fence relies on AccessCheck against the
|
|
188
|
+
# connecting token, so to assert "broker passes filter 1" we need a
|
|
189
|
+
# group that's *already enabled* on this token. $GroupName was just
|
|
190
|
+
# created and won't be in TokenGroups until a fresh logon. Admins is
|
|
191
|
+
# reliably enabled on the GHA runner.
|
|
192
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
193
|
+
|
|
194
|
+
$adminsSid = 'S-1-5-32-544'
|
|
195
|
+
Run (@('wfp', 'install', '--group-sid', $adminsSid) + $sl + $pr)
|
|
196
|
+
try {
|
|
197
|
+
# Broker-side: this process has Admins enabled, so filter 1
|
|
198
|
+
# (PERMIT group-enabled) should let the connect through.
|
|
199
|
+
$r = curl.exe -s -m 10 -o NUL -w "%{http_code}" https://example.com
|
|
200
|
+
if ($LASTEXITCODE -ne 0 -or $r -ne '200') {
|
|
201
|
+
throw "broker egress through filter 1 expected 200, got exit=$LASTEXITCODE code='$r'"
|
|
202
|
+
}
|
|
203
|
+
Write-Host "fence: broker egress OK ($r)"
|
|
204
|
+
|
|
205
|
+
# Child-side (group deny-only) assertion lands in batch 02 once
|
|
206
|
+
# `srt-win exec` exists; that batch's smoke-exec.ps1 will run
|
|
207
|
+
# srt-win exec --group-sid S-1-5-32-544 -- curl https://example.com
|
|
208
|
+
# and assert it is BLOCKED.
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
Run (@('wfp', 'uninstall') + $sl)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ── group teardown ───────────────────────────────────────────────────
|
|
215
|
+
Run @('group', 'delete', '--name', $GroupName)
|
|
216
|
+
$gd = J @('group', 'status', '--name', $GroupName)
|
|
217
|
+
if ($gd.state -ne 'absent') {
|
|
218
|
+
throw "post-delete group expected absent, got $($gd.state)"
|
|
219
|
+
}
|
|
220
|
+
# Idempotent no-op: second delete must also exit 0.
|
|
221
|
+
Run @('group', 'delete', '--name', $GroupName)
|
|
222
|
+
|
|
223
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
224
|
+
# Single-step `install` / `uninstall` convenience subcommands.
|
|
225
|
+
#
|
|
226
|
+
# Distinct group name + sublayer so this section is isolated from the
|
|
227
|
+
# lifecycle assertions above. Verifies that one `install` call leaves
|
|
228
|
+
# both the group present AND the WFP filters installed, and that one
|
|
229
|
+
# `uninstall` call removes both.
|
|
230
|
+
# ═════════════════════════════════════════════════════════════════════
|
|
231
|
+
|
|
232
|
+
$instGrp = "$GroupName-inst"
|
|
233
|
+
$isl = @('--sublayer-guid', $InstallSublayer)
|
|
234
|
+
|
|
235
|
+
Run (@('install', '--name', $instGrp) + $isl + $pr)
|
|
236
|
+
$ig = J @('group', 'status', '--name', $instGrp)
|
|
237
|
+
if ($ig.state -notin 'created-not-on-token', 'ready') {
|
|
238
|
+
throw "install: group expected created-not-on-token/ready, got $($ig.state)"
|
|
239
|
+
}
|
|
240
|
+
$iw = J (@('wfp', 'status') + $isl)
|
|
241
|
+
if ($iw.state -ne 'installed' -or $iw.filters -lt 8) {
|
|
242
|
+
throw "install: wfp expected installed/>=8, got $($iw.state)/$($iw.filters)"
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
# Idempotency, same range: second install with identical flags is
|
|
246
|
+
# a no-op (exit 0, "already installed").
|
|
247
|
+
$out = & $Exe @(@('install', '--name', $instGrp) + $isl + $pr) 2>&1 | Out-String
|
|
248
|
+
if ($LASTEXITCODE -ne 0) {
|
|
249
|
+
throw "install idempotency: same-range expected exit 0, got $LASTEXITCODE"
|
|
250
|
+
}
|
|
251
|
+
if ($out -notmatch 'already installed') {
|
|
252
|
+
throw "install idempotency: expected 'already installed' in output, got '$out'"
|
|
253
|
+
}
|
|
254
|
+
$iw2 = J (@('wfp', 'status') + $isl)
|
|
255
|
+
if ($iw2.filters -ne $iw.filters) {
|
|
256
|
+
throw "install idempotency: filters $($iw.filters) -> $($iw2.filters)"
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Conflict, different range without --force: exits 13.
|
|
260
|
+
& $Exe @(@('install', '--name', $instGrp, '--proxy-port-range', '50000-50001') + $isl) 2>$null
|
|
261
|
+
if ($LASTEXITCODE -ne 13) {
|
|
262
|
+
throw "install conflict: different-range without --force expected exit 13, got $LASTEXITCODE"
|
|
263
|
+
}
|
|
264
|
+
# Original range still in place.
|
|
265
|
+
$iw3 = J (@('wfp', 'status') + $isl)
|
|
266
|
+
if ($iw3.port_range[0] -ne 60080) {
|
|
267
|
+
throw "install conflict: range was overwritten without --force"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# --force replaces.
|
|
271
|
+
Run (@('install', '--name', $instGrp, '--proxy-port-range', '50000-50001', '--force') + $isl)
|
|
272
|
+
$iwF = J (@('wfp', 'status') + $isl)
|
|
273
|
+
if ($iwF.port_range[0] -ne 50000 -or $iwF.port_range[1] -ne 50001) {
|
|
274
|
+
throw "install --force: expected port_range [50000,50001], got [$($iwF.port_range -join ',')]"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
# --group-sid path (group externally managed; here it already
|
|
278
|
+
# exists from the --name install above). With matching range
|
|
279
|
+
# this is the same-range no-op.
|
|
280
|
+
Run (@('install', '--group-sid', $ig.sid, '--proxy-port-range', '50000-50001') + $isl)
|
|
281
|
+
|
|
282
|
+
# Distinct exit code 11: group step fails on a malformed
|
|
283
|
+
# --group-sid (canonicalize_sid rejects). --force so the
|
|
284
|
+
# pre-check (which would otherwise fire on the existing
|
|
285
|
+
# install above) is skipped and we reach the group step.
|
|
286
|
+
& $Exe @(@('install', '--group-sid', 'not-a-sid', '--force') + $isl) 2>$null
|
|
287
|
+
if ($LASTEXITCODE -ne 11) {
|
|
288
|
+
throw "install: invalid --group-sid expected exit 11, got $LASTEXITCODE"
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
# Uninstall removes filters only — group remains.
|
|
292
|
+
Run (@('uninstall') + $isl)
|
|
293
|
+
$uw = J (@('wfp', 'status') + $isl)
|
|
294
|
+
if ($uw.state -ne 'absent') {
|
|
295
|
+
throw "uninstall: wfp expected absent, got $($uw.state)"
|
|
296
|
+
}
|
|
297
|
+
$ug = J @('group', 'status', '--name', $instGrp)
|
|
298
|
+
if ($ug.state -notin 'created-not-on-token', 'ready') {
|
|
299
|
+
throw "uninstall: group should be left intact, got $($ug.state)"
|
|
300
|
+
}
|
|
301
|
+
# Idempotent no-op: second uninstall must also exit 0.
|
|
302
|
+
Run (@('uninstall') + $isl)
|
|
303
|
+
|
|
304
|
+
# Explicit group teardown.
|
|
305
|
+
Run @('group', 'delete', '--name', $instGrp)
|
|
306
|
+
|
|
307
|
+
Write-Host 'srt-win smoke: OK'
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//! Job-object wrapper for the sandbox child.
|
|
2
|
+
//!
|
|
3
|
+
//! Two roles:
|
|
4
|
+
//! 1. `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE` containment — the sandboxed
|
|
5
|
+
//! process tree dies with the broker.
|
|
6
|
+
//! 2. `JOBOBJECT_BASIC_UI_RESTRICTIONS` — block clipboard, global
|
|
7
|
+
//! atoms, system-parameter writes, display-settings changes,
|
|
8
|
+
//! desktop switching, ExitWindows, and cross-job USER/GDI handle
|
|
9
|
+
//! access. The bits are listed individually rather than relying
|
|
10
|
+
//! on a hypothetical `_ALL` so the enforced surface is auditable
|
|
11
|
+
//! from this call site.
|
|
12
|
+
//!
|
|
13
|
+
//! Both must be set BEFORE `AssignProcessToJobObject` so they're in
|
|
14
|
+
//! effect from the moment the suspended child is assigned (the caller
|
|
15
|
+
//! resumes the thread only after `assign`).
|
|
16
|
+
|
|
17
|
+
use anyhow::{Context, Result};
|
|
18
|
+
use std::ffi::c_void;
|
|
19
|
+
use std::mem::{size_of, zeroed};
|
|
20
|
+
use windows::Win32::Foundation::{CloseHandle, HANDLE};
|
|
21
|
+
use windows::Win32::System::JobObjects::{
|
|
22
|
+
AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
|
|
23
|
+
JobObjectBasicUIRestrictions, JobObjectExtendedLimitInformation,
|
|
24
|
+
JOBOBJECT_BASIC_UI_RESTRICTIONS, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
|
25
|
+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, JOB_OBJECT_UILIMIT_DESKTOP,
|
|
26
|
+
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS, JOB_OBJECT_UILIMIT_EXITWINDOWS,
|
|
27
|
+
JOB_OBJECT_UILIMIT_GLOBALATOMS, JOB_OBJECT_UILIMIT_HANDLES,
|
|
28
|
+
JOB_OBJECT_UILIMIT_READCLIPBOARD, JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS,
|
|
29
|
+
JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/// RAII job object. `Drop` closes the handle; with
|
|
33
|
+
/// `KILL_ON_JOB_CLOSE` set, that terminates every process still in
|
|
34
|
+
/// the job.
|
|
35
|
+
pub struct Job(HANDLE);
|
|
36
|
+
|
|
37
|
+
impl Job {
|
|
38
|
+
/// Create an unnamed job with kill-on-close + full UI lockdown.
|
|
39
|
+
pub fn new() -> Result<Self> {
|
|
40
|
+
// Wrap the raw handle in `Self` immediately so a `?` from
|
|
41
|
+
// either `SetInformationJobObject` below still closes it.
|
|
42
|
+
let job = unsafe {
|
|
43
|
+
Self(CreateJobObjectW(None, None).context("CreateJobObjectW")?)
|
|
44
|
+
};
|
|
45
|
+
unsafe {
|
|
46
|
+
let mut ext: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = zeroed();
|
|
47
|
+
ext.BasicLimitInformation.LimitFlags =
|
|
48
|
+
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
|
49
|
+
SetInformationJobObject(
|
|
50
|
+
job.0,
|
|
51
|
+
JobObjectExtendedLimitInformation,
|
|
52
|
+
&ext as *const _ as *const c_void,
|
|
53
|
+
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
|
54
|
+
)
|
|
55
|
+
.context("SetInformationJobObject(KILL_ON_JOB_CLOSE)")?;
|
|
56
|
+
|
|
57
|
+
// READCLIPBOARD — block OpenClipboard for read
|
|
58
|
+
// WRITECLIPBOARD — block SetClipboardData
|
|
59
|
+
// HANDLES — block USER/GDI handles from outside the job
|
|
60
|
+
// GLOBALATOMS — block GlobalAddAtom (atom-table IPC)
|
|
61
|
+
// SYSTEMPARAMETERS — block SystemParametersInfoW(SPI_SET*)
|
|
62
|
+
// DISPLAYSETTINGS — block ChangeDisplaySettings
|
|
63
|
+
// DESKTOP — block SwitchDesktop / SetThreadDesktop
|
|
64
|
+
// EXITWINDOWS — block sandbox-initiated logoff/shutdown
|
|
65
|
+
let ui_bits = JOB_OBJECT_UILIMIT_READCLIPBOARD
|
|
66
|
+
| JOB_OBJECT_UILIMIT_WRITECLIPBOARD
|
|
67
|
+
| JOB_OBJECT_UILIMIT_HANDLES
|
|
68
|
+
| JOB_OBJECT_UILIMIT_GLOBALATOMS
|
|
69
|
+
| JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS
|
|
70
|
+
| JOB_OBJECT_UILIMIT_DISPLAYSETTINGS
|
|
71
|
+
| JOB_OBJECT_UILIMIT_DESKTOP
|
|
72
|
+
| JOB_OBJECT_UILIMIT_EXITWINDOWS;
|
|
73
|
+
let ui = JOBOBJECT_BASIC_UI_RESTRICTIONS {
|
|
74
|
+
UIRestrictionsClass: ui_bits,
|
|
75
|
+
};
|
|
76
|
+
SetInformationJobObject(
|
|
77
|
+
job.0,
|
|
78
|
+
JobObjectBasicUIRestrictions,
|
|
79
|
+
&ui as *const _ as *const c_void,
|
|
80
|
+
size_of::<JOBOBJECT_BASIC_UI_RESTRICTIONS>() as u32,
|
|
81
|
+
)
|
|
82
|
+
.context("SetInformationJobObject(BasicUIRestrictions)")?;
|
|
83
|
+
}
|
|
84
|
+
Ok(job)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Assign a (suspended) process to the job.
|
|
88
|
+
pub fn assign(&self, proc: HANDLE) -> Result<()> {
|
|
89
|
+
unsafe {
|
|
90
|
+
AssignProcessToJobObject(self.0, proc)
|
|
91
|
+
.context("AssignProcessToJobObject")
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl Drop for Job {
|
|
97
|
+
fn drop(&mut self) {
|
|
98
|
+
unsafe {
|
|
99
|
+
let _ = CloseHandle(self.0);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|