@sysid/sandbox-runtime-improved 0.0.56-sysid.1 → 0.0.61-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 +43 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sandbox/credential-mask-files.d.ts +72 -0
- package/dist/sandbox/credential-mask-files.d.ts.map +1 -0
- package/dist/sandbox/credential-mask-files.js +146 -0
- package/dist/sandbox/credential-mask-files.js.map +1 -0
- package/dist/sandbox/credential-sentinel.d.ts +72 -0
- package/dist/sandbox/credential-sentinel.d.ts.map +1 -0
- package/dist/sandbox/credential-sentinel.js +130 -0
- package/dist/sandbox/credential-sentinel.js.map +1 -0
- package/dist/sandbox/domain-pattern.d.ts +36 -0
- package/dist/sandbox/domain-pattern.d.ts.map +1 -0
- package/dist/sandbox/domain-pattern.js +60 -0
- package/dist/sandbox/domain-pattern.js.map +1 -0
- package/dist/sandbox/http-proxy.d.ts +32 -1
- package/dist/sandbox/http-proxy.d.ts.map +1 -1
- package/dist/sandbox/http-proxy.js +10 -2
- package/dist/sandbox/http-proxy.js.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.d.ts +15 -0
- package/dist/sandbox/linux-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/linux-sandbox-utils.js +106 -47
- package/dist/sandbox/linux-sandbox-utils.js.map +1 -1
- package/dist/sandbox/listen-in-range.d.ts +12 -0
- package/dist/sandbox/listen-in-range.d.ts.map +1 -0
- package/dist/sandbox/listen-in-range.js +43 -0
- package/dist/sandbox/listen-in-range.js.map +1 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts +11 -0
- package/dist/sandbox/macos-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/macos-sandbox-utils.js +69 -14
- package/dist/sandbox/macos-sandbox-utils.js.map +1 -1
- package/dist/sandbox/mitm-ca.d.ts +18 -2
- package/dist/sandbox/mitm-ca.d.ts.map +1 -1
- package/dist/sandbox/mitm-ca.js +49 -9
- package/dist/sandbox/mitm-ca.js.map +1 -1
- package/dist/sandbox/mitm-leaf.d.ts.map +1 -1
- package/dist/sandbox/mitm-leaf.js +24 -6
- package/dist/sandbox/mitm-leaf.js.map +1 -1
- package/dist/sandbox/mux-proxy.d.ts +59 -0
- package/dist/sandbox/mux-proxy.d.ts.map +1 -0
- package/dist/sandbox/mux-proxy.js +159 -0
- package/dist/sandbox/mux-proxy.js.map +1 -0
- package/dist/sandbox/request-filter.d.ts +11 -1
- package/dist/sandbox/request-filter.d.ts.map +1 -1
- package/dist/sandbox/request-filter.js.map +1 -1
- package/dist/sandbox/sandbox-config.d.ts +254 -25
- package/dist/sandbox/sandbox-config.d.ts.map +1 -1
- package/dist/sandbox/sandbox-config.js +200 -13
- 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 +601 -194
- package/dist/sandbox/sandbox-manager.js.map +1 -1
- package/dist/sandbox/sandbox-schemas.d.ts +15 -0
- package/dist/sandbox/sandbox-schemas.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.d.ts +42 -6
- package/dist/sandbox/sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/sandbox-utils.js +115 -27
- package/dist/sandbox/sandbox-utils.js.map +1 -1
- package/dist/sandbox/socks-proxy.d.ts +11 -5
- package/dist/sandbox/socks-proxy.d.ts.map +1 -1
- package/dist/sandbox/socks-proxy.js +13 -81
- package/dist/sandbox/socks-proxy.js.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.d.ts +2 -2
- package/dist/sandbox/tls-terminate-proxy.d.ts.map +1 -1
- package/dist/sandbox/tls-terminate-proxy.js +31 -5
- package/dist/sandbox/tls-terminate-proxy.js.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.d.ts +347 -11
- package/dist/sandbox/windows-sandbox-utils.d.ts.map +1 -1
- package/dist/sandbox/windows-sandbox-utils.js +437 -45
- package/dist/sandbox/windows-sandbox-utils.js.map +1 -1
- package/package.json +7 -4
- package/vendor/seccomp/build.ts +8 -30
- package/vendor/srt-win/build.ts +21 -0
- package/dist/vendor/seccomp/Dockerfile.build +0 -6
- package/dist/vendor/seccomp/arm64/apply-seccomp +0 -0
- package/dist/vendor/seccomp/build.ts +0 -91
- package/dist/vendor/seccomp/x64/apply-seccomp +0 -0
- package/dist/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/dist/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/dist/vendor/srt-win/Cargo.lock +0 -361
- package/dist/vendor/srt-win/Cargo.toml +0 -46
- package/dist/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/dist/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/dist/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/dist/vendor/srt-win/src/job.rs +0 -102
- package/dist/vendor/srt-win/src/launch.rs +0 -732
- package/dist/vendor/srt-win/src/lib.rs +0 -20
- package/dist/vendor/srt-win/src/main.rs +0 -669
- package/dist/vendor/srt-win/src/self_protect.rs +0 -169
- package/dist/vendor/srt-win/src/sid.rs +0 -296
- package/dist/vendor/srt-win/src/token.rs +0 -341
- package/dist/vendor/srt-win/src/util.rs +0 -42
- package/dist/vendor/srt-win/src/wfp.rs +0 -992
- package/dist/vendor/srt-win/src/winsta.rs +0 -209
- package/dist/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
- package/vendor/seccomp-src/apply-seccomp.c +0 -280
- package/vendor/seccomp-src/seccomp-unix-block.c +0 -148
- package/vendor/srt-win/Cargo.lock +0 -361
- package/vendor/srt-win/Cargo.toml +0 -46
- package/vendor/srt-win/ci/cleanup.ps1 +0 -49
- package/vendor/srt-win/ci/smoke-exec.ps1 +0 -446
- package/vendor/srt-win/ci/smoke.ps1 +0 -307
- package/vendor/srt-win/src/job.rs +0 -102
- package/vendor/srt-win/src/launch.rs +0 -732
- package/vendor/srt-win/src/lib.rs +0 -20
- package/vendor/srt-win/src/main.rs +0 -669
- package/vendor/srt-win/src/self_protect.rs +0 -169
- package/vendor/srt-win/src/sid.rs +0 -296
- package/vendor/srt-win/src/token.rs +0 -341
- package/vendor/srt-win/src/util.rs +0 -42
- package/vendor/srt-win/src/wfp.rs +0 -992
- package/vendor/srt-win/src/winsta.rs +0 -209
- package/vendor/srt-win/tests/sd_access_check_matrix.rs +0 -259
|
@@ -1,732 +0,0 @@
|
|
|
1
|
-
//! `srt-win exec`: build the deny-only-group restricted token,
|
|
2
|
-
//! self-protect the broker, spawn the target suspended under a
|
|
3
|
-
//! locked-down job + non-interactive desktop + mitigation-policy
|
|
4
|
-
//! stack + explicit handle whitelist, resume, wait, propagate exit
|
|
5
|
-
//! code.
|
|
6
|
-
//!
|
|
7
|
-
//! Stateless — no marker file, no proxy thread, no FS-deny
|
|
8
|
-
//! handling here. Network egress for the child reaches the host's
|
|
9
|
-
//! JS-side proxies (whose ports the caller passes) via the WFP
|
|
10
|
-
//! loopback permit installed by `srt-win wfp install`.
|
|
11
|
-
|
|
12
|
-
use anyhow::{anyhow, Context, Result};
|
|
13
|
-
use std::ffi::c_void;
|
|
14
|
-
use std::mem::{size_of, zeroed};
|
|
15
|
-
use std::path::Path;
|
|
16
|
-
use windows::core::{PCWSTR, PWSTR};
|
|
17
|
-
use windows::Win32::Foundation::{
|
|
18
|
-
CloseHandle, SetHandleInformation, HANDLE, HANDLE_FLAG_INHERIT,
|
|
19
|
-
WAIT_OBJECT_0,
|
|
20
|
-
};
|
|
21
|
-
use windows::Win32::System::Console::{
|
|
22
|
-
GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
|
|
23
|
-
};
|
|
24
|
-
use windows::Win32::System::Threading::{
|
|
25
|
-
CreateProcessAsUserW, DeleteProcThreadAttributeList, GetExitCodeProcess,
|
|
26
|
-
InitializeProcThreadAttributeList, ResumeThread, TerminateProcess,
|
|
27
|
-
UpdateProcThreadAttribute, WaitForSingleObject, CREATE_SUSPENDED,
|
|
28
|
-
CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, INFINITE,
|
|
29
|
-
LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_INFORMATION,
|
|
30
|
-
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
|
31
|
-
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, STARTUPINFOEXW, STARTUPINFOW,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
use crate::job::Job;
|
|
35
|
-
use crate::self_protect;
|
|
36
|
-
use crate::sid::{self, GroupState};
|
|
37
|
-
use crate::token::{self, open_self_token, to_primary};
|
|
38
|
-
use crate::util::{pcwstr, wstr};
|
|
39
|
-
use crate::winsta::WinStaDesk;
|
|
40
|
-
|
|
41
|
-
// ─── RAII handle wrappers ───────────────────────────────────────────
|
|
42
|
-
|
|
43
|
-
/// Owns a kernel `HANDLE`; `CloseHandle` on drop. For tokens and
|
|
44
|
-
/// the like — anything where the only cleanup is `CloseHandle`.
|
|
45
|
-
struct OwnedHandle(HANDLE);
|
|
46
|
-
impl OwnedHandle {
|
|
47
|
-
fn raw(&self) -> HANDLE { self.0 }
|
|
48
|
-
}
|
|
49
|
-
impl Drop for OwnedHandle {
|
|
50
|
-
fn drop(&mut self) {
|
|
51
|
-
if !self.0.is_invalid() {
|
|
52
|
-
unsafe { let _ = CloseHandle(self.0); }
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/// Owns a freshly-spawned (suspended) child. If dropped before
|
|
58
|
-
/// [`defuse`] is called, terminates the child — so an error
|
|
59
|
-
/// between `CreateProcessAsUserW` and `WaitForSingleObject`
|
|
60
|
-
/// can't orphan a suspended process that's not yet in the job.
|
|
61
|
-
/// Always closes both handles on drop.
|
|
62
|
-
struct SpawnedChild {
|
|
63
|
-
pi: PROCESS_INFORMATION,
|
|
64
|
-
armed: bool,
|
|
65
|
-
}
|
|
66
|
-
impl SpawnedChild {
|
|
67
|
-
fn new(pi: PROCESS_INFORMATION) -> Self {
|
|
68
|
-
Self { pi, armed: true }
|
|
69
|
-
}
|
|
70
|
-
fn process(&self) -> HANDLE { self.pi.hProcess }
|
|
71
|
-
fn thread(&self) -> HANDLE { self.pi.hThread }
|
|
72
|
-
/// Disarm the terminate-on-drop. Call after the child has been
|
|
73
|
-
/// assigned to the job AND resumed — past that point
|
|
74
|
-
/// `KILL_ON_JOB_CLOSE` covers cleanup.
|
|
75
|
-
fn defuse(&mut self) { self.armed = false; }
|
|
76
|
-
}
|
|
77
|
-
impl Drop for SpawnedChild {
|
|
78
|
-
fn drop(&mut self) {
|
|
79
|
-
unsafe {
|
|
80
|
-
if self.armed {
|
|
81
|
-
let _ = TerminateProcess(self.pi.hProcess, 1);
|
|
82
|
-
}
|
|
83
|
-
let _ = CloseHandle(self.pi.hThread);
|
|
84
|
-
let _ = CloseHandle(self.pi.hProcess);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── Process-creation mitigation-policy bits ────────────────────────
|
|
90
|
-
//
|
|
91
|
-
// The `windows` crate exposes `PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY`
|
|
92
|
-
// but not the per-bit DWORD64 macros (they're winnt.h preprocessor
|
|
93
|
-
// `#define`s, still absent as of 0.62). Each policy occupies a 4-bit
|
|
94
|
-
// slot in the u64; `..._ALWAYS_ON` flips bit 0 of its slot.
|
|
95
|
-
//
|
|
96
|
-
// Only mitigations that don't break Node/Python JIT or mingw-built
|
|
97
|
-
// shells are enabled here. Specifically NOT enabled:
|
|
98
|
-
// - `IMAGE_LOAD_PREFER_SYSTEM32` — flips DLL search-order so System32
|
|
99
|
-
// wins over the EXE's directory; breaks the cygwin1.dll /
|
|
100
|
-
// msys-2.0.dll resolution model.
|
|
101
|
-
// - `CONTROL_FLOW_GUARD_ALWAYS_ON` — forces CFG even when the EXE
|
|
102
|
-
// wasn't built with `/guard:cf`; stock mingw-built `bash.exe`
|
|
103
|
-
// dies in `dofork`. CFG is defense-in-depth, not a primary
|
|
104
|
-
// boundary.
|
|
105
|
-
|
|
106
|
-
/// Bit 32 — block legacy AppInit / IME / Winsock-LSP DLL injection
|
|
107
|
-
/// and `SetWindowsHookEx`.
|
|
108
|
-
const MITIGATION_EXTENSION_POINT_DISABLE: u64 = 1u64 << 32;
|
|
109
|
-
/// Bit 48 — block GDI from loading non-system fonts (historic
|
|
110
|
-
/// kernel font-parser RCE surface; sandbox children are
|
|
111
|
-
/// console/network workloads).
|
|
112
|
-
const MITIGATION_FONT_DISABLE: u64 = 1u64 << 48;
|
|
113
|
-
/// Bit 52 — refuse `LoadLibrary` from UNC / network paths.
|
|
114
|
-
const MITIGATION_IMAGE_LOAD_NO_REMOTE: u64 = 1u64 << 52;
|
|
115
|
-
/// Bit 56 — refuse `LoadLibrary` of any image whose mandatory label
|
|
116
|
-
/// is Low IL.
|
|
117
|
-
const MITIGATION_IMAGE_LOAD_NO_LOW_LABEL: u64 = 1u64 << 56;
|
|
118
|
-
|
|
119
|
-
/// Inputs to a single `srt-win exec` invocation.
|
|
120
|
-
pub struct ExecSpec<'a> {
|
|
121
|
-
/// Discriminator group SID (already resolved by the caller from
|
|
122
|
-
/// `--name` / `--group-sid`).
|
|
123
|
-
pub group_sid: &'a str,
|
|
124
|
-
/// Skip the "is `group_sid` enabled in the broker's token"
|
|
125
|
-
/// pre-flight check. **Fail-open** — the WFP fence relies on
|
|
126
|
-
/// the broker having the group enabled; with this set the
|
|
127
|
-
/// child may run with weaker isolation if the install was
|
|
128
|
-
/// incomplete. Surfaced as an explicit CLI flag (not an env
|
|
129
|
-
/// var) so the bypass is intentional and not accidentally
|
|
130
|
-
/// inherited from a parent's environment. Used only by CI
|
|
131
|
-
/// runners that create the group in-job and cannot
|
|
132
|
-
/// logout/login mid-run.
|
|
133
|
-
pub skip_group_check: bool,
|
|
134
|
-
/// Target executable.
|
|
135
|
-
pub target_exe: &'a Path,
|
|
136
|
-
/// Target argv (everything after the target).
|
|
137
|
-
pub target_args: &'a [String],
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/// Run the target under the sandbox and return its exit code.
|
|
141
|
-
pub fn run(spec: &ExecSpec<'_>) -> Result<u32> {
|
|
142
|
-
// 1) Pre-flight: the group must be enabled in the broker's
|
|
143
|
-
// token. `Absent` means the user hasn't logged out + back
|
|
144
|
-
// in since `group create`. `DenyOnly` means we're already
|
|
145
|
-
// inside a sandbox child — refuse.
|
|
146
|
-
match sid::group_state_for_self(spec.group_sid)? {
|
|
147
|
-
GroupState::Enabled => {}
|
|
148
|
-
GroupState::Absent if spec.skip_group_check => {
|
|
149
|
-
eprintln!(
|
|
150
|
-
"srt-win: WARNING: --skip-group-check is set and \
|
|
151
|
-
group {} is absent from the broker's TokenGroups. \
|
|
152
|
-
The WFP fence may not be in effect for this \
|
|
153
|
-
process tree. This bypass is intended ONLY for \
|
|
154
|
-
ephemeral CI runners.",
|
|
155
|
-
spec.group_sid
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
GroupState::Absent => {
|
|
159
|
-
return Err(anyhow!(
|
|
160
|
-
"group {} is not present in the broker's \
|
|
161
|
-
TokenGroups. Log out and back in to refresh group \
|
|
162
|
-
membership, then retry. (Run `srt-win group status` \
|
|
163
|
-
to confirm.) Pass --skip-group-check to bypass in CI.",
|
|
164
|
-
spec.group_sid
|
|
165
|
-
));
|
|
166
|
-
}
|
|
167
|
-
GroupState::DenyOnly => {
|
|
168
|
-
return Err(anyhow!(
|
|
169
|
-
"group {} is deny-only in this token — the broker \
|
|
170
|
-
itself is running inside a sandbox child. Refusing \
|
|
171
|
-
to launch.",
|
|
172
|
-
spec.group_sid
|
|
173
|
-
));
|
|
174
|
-
}
|
|
175
|
-
GroupState::Present => {
|
|
176
|
-
return Err(anyhow!(
|
|
177
|
-
"group {} is present but neither enabled nor \
|
|
178
|
-
deny-only (unexpected token attribute state).",
|
|
179
|
-
spec.group_sid
|
|
180
|
-
));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// 2) Self-protect: rewrite the broker process DACL so the
|
|
185
|
-
// child can't `OpenProcess` us. Best-effort — log on
|
|
186
|
-
// failure but don't abort, since a broker without
|
|
187
|
-
// self-protect is still strictly safer than no sandbox.
|
|
188
|
-
if let Err(e) = self_protect::install_broker_dacl(spec.group_sid) {
|
|
189
|
-
eprintln!("srt-win: WARNING: install_broker_dacl: {e:#}");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// 3) Restricted token. Each handle is RAII-owned so any `?`
|
|
193
|
-
// below closes whatever was already opened.
|
|
194
|
-
let self_tok = OwnedHandle(open_self_token()?);
|
|
195
|
-
let restricted = OwnedHandle(
|
|
196
|
-
token::make_sandbox_token(self_tok.raw(), spec.group_sid)
|
|
197
|
-
.context("make_sandbox_token")?,
|
|
198
|
-
);
|
|
199
|
-
let primary = OwnedHandle(
|
|
200
|
-
to_primary(restricted.raw()).context("to_primary")?,
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
// 4) Job.
|
|
204
|
-
let job = Job::new().context("Job::new")?;
|
|
205
|
-
|
|
206
|
-
// 5) Window station + desktop.
|
|
207
|
-
let mut winsta = WinStaDesk::new().context("WinStaDesk::new")?;
|
|
208
|
-
|
|
209
|
-
// 6) Env block — verbatim passthrough of the broker's own
|
|
210
|
-
// environment (the TS caller populates it with the full proxy
|
|
211
|
-
// set; see `build_env_block`).
|
|
212
|
-
let mut env = build_env_block();
|
|
213
|
-
|
|
214
|
-
// 7) Command line + application name.
|
|
215
|
-
let cmdline = build_cmdline(spec.target_exe, spec.target_args);
|
|
216
|
-
let mut cmdline_w = wstr(&cmdline);
|
|
217
|
-
let app_w = wstr(&spec.target_exe.display().to_string());
|
|
218
|
-
|
|
219
|
-
// 8) PROC_THREAD_ATTRIBUTE_LIST: mitigation policy + explicit
|
|
220
|
-
// handle whitelist.
|
|
221
|
-
let mitigation: u64 = MITIGATION_EXTENSION_POINT_DISABLE
|
|
222
|
-
| MITIGATION_FONT_DISABLE
|
|
223
|
-
| MITIGATION_IMAGE_LOAD_NO_REMOTE
|
|
224
|
-
| MITIGATION_IMAGE_LOAD_NO_LOW_LABEL;
|
|
225
|
-
let mut handle_list = collect_inheritable_std_handles();
|
|
226
|
-
if handle_list.is_empty() {
|
|
227
|
-
return Err(anyhow!(
|
|
228
|
-
"no std handle is inheritable; refusing to spawn. \
|
|
229
|
-
srt-win exec requires the broker have at least one \
|
|
230
|
-
console-attached stdio stream."
|
|
231
|
-
));
|
|
232
|
-
}
|
|
233
|
-
let mut attrs = ProcThreadAttrs::new(2)?;
|
|
234
|
-
attrs.set_mitigation_policy(&mitigation)?;
|
|
235
|
-
attrs.set_handle_list(&mut handle_list)?;
|
|
236
|
-
|
|
237
|
-
// 9) STARTUPINFOEXW.
|
|
238
|
-
let mut six: STARTUPINFOEXW = unsafe { zeroed() };
|
|
239
|
-
six.StartupInfo.cb = size_of::<STARTUPINFOEXW>() as u32;
|
|
240
|
-
six.lpAttributeList = attrs.list();
|
|
241
|
-
six.StartupInfo.lpDesktop = PWSTR(winsta.desktop_name_ptr());
|
|
242
|
-
|
|
243
|
-
// 10) Spawn suspended.
|
|
244
|
-
let mut pi: PROCESS_INFORMATION = unsafe { zeroed() };
|
|
245
|
-
unsafe {
|
|
246
|
-
CreateProcessAsUserW(
|
|
247
|
-
Some(primary.raw()),
|
|
248
|
-
pcwstr(&app_w),
|
|
249
|
-
Some(PWSTR(cmdline_w.as_mut_ptr())),
|
|
250
|
-
None,
|
|
251
|
-
None,
|
|
252
|
-
// Must be TRUE for `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`
|
|
253
|
-
// to take effect (documented Vista-era quirk: with
|
|
254
|
-
// FALSE the kernel ignores the attribute entirely).
|
|
255
|
-
true,
|
|
256
|
-
CREATE_SUSPENDED
|
|
257
|
-
| CREATE_UNICODE_ENVIRONMENT
|
|
258
|
-
| EXTENDED_STARTUPINFO_PRESENT,
|
|
259
|
-
Some(env.as_mut_ptr() as *const c_void),
|
|
260
|
-
// Inherit cwd.
|
|
261
|
-
PCWSTR::null(),
|
|
262
|
-
// STARTUPINFOEXW is layout-compatible (StartupInfo is
|
|
263
|
-
// first member); EXTENDED_STARTUPINFO_PRESENT tells the
|
|
264
|
-
// kernel to read past it for lpAttributeList.
|
|
265
|
-
&six.StartupInfo as *const STARTUPINFOW,
|
|
266
|
-
&mut pi,
|
|
267
|
-
)
|
|
268
|
-
.with_context(|| {
|
|
269
|
-
format!(
|
|
270
|
-
"CreateProcessAsUserW({})",
|
|
271
|
-
spec.target_exe.display()
|
|
272
|
-
)
|
|
273
|
-
})?;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// The child exists, suspended, NOT yet in the job. Wrap it
|
|
277
|
-
// in a guard so any `?` from here to `defuse()` terminates
|
|
278
|
-
// it — `KILL_ON_JOB_CLOSE` can't help until after `assign`.
|
|
279
|
-
let mut child = SpawnedChild::new(pi);
|
|
280
|
-
|
|
281
|
-
// 11) Assign to job → resume. ResumeThread returns the
|
|
282
|
-
// previous suspend count, or u32::MAX on failure — a
|
|
283
|
-
// failure here would leave the child suspended in the
|
|
284
|
-
// job and `WaitForSingleObject(INFINITE)` below would
|
|
285
|
-
// hang the broker forever. Check before defusing the
|
|
286
|
-
// terminate-on-drop guard.
|
|
287
|
-
job.assign(child.process())?;
|
|
288
|
-
let prev_suspend = unsafe { ResumeThread(child.thread()) };
|
|
289
|
-
if prev_suspend == u32::MAX {
|
|
290
|
-
return Err(anyhow!(
|
|
291
|
-
"ResumeThread: {}",
|
|
292
|
-
std::io::Error::last_os_error()
|
|
293
|
-
));
|
|
294
|
-
}
|
|
295
|
-
// From here the job owns lifetime; disarm terminate-on-drop.
|
|
296
|
-
child.defuse();
|
|
297
|
-
|
|
298
|
-
// 12) Wait + collect exit code.
|
|
299
|
-
let rc = unsafe { WaitForSingleObject(child.process(), INFINITE) };
|
|
300
|
-
if rc != WAIT_OBJECT_0 {
|
|
301
|
-
eprintln!("srt-win: WaitForSingleObject returned 0x{:x}", rc.0);
|
|
302
|
-
}
|
|
303
|
-
let mut code: u32 = 0;
|
|
304
|
-
unsafe {
|
|
305
|
-
GetExitCodeProcess(child.process(), &mut code)
|
|
306
|
-
.context("GetExitCodeProcess")?;
|
|
307
|
-
}
|
|
308
|
-
// `child` (closes hProcess/hThread), `primary`/`restricted`/
|
|
309
|
-
// `self_tok` (CloseHandle) all drop here.
|
|
310
|
-
// Keep `attrs` (its backing buffer + the borrowed `mitigation`
|
|
311
|
-
// and `handle_list`), `winsta`, and `job` alive until here.
|
|
312
|
-
// The kernel snapshots the attribute list at CreateProcess
|
|
313
|
-
// time, but DeleteProcThreadAttributeList (in attrs.drop) may
|
|
314
|
-
// re-read pointers; and the WS+desktop must outlive the
|
|
315
|
-
// child's attach during process creation.
|
|
316
|
-
drop(attrs);
|
|
317
|
-
drop(handle_list);
|
|
318
|
-
drop(winsta);
|
|
319
|
-
drop(job);
|
|
320
|
-
Ok(code)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// ─── Environment block ──────────────────────────────────────────────
|
|
324
|
-
|
|
325
|
-
/// Build a `CREATE_UNICODE_ENVIRONMENT` block from the broker's own
|
|
326
|
-
/// environment, **verbatim**.
|
|
327
|
-
///
|
|
328
|
-
/// `srt-win exec` is a dumb passthrough for proxy configuration: it
|
|
329
|
-
/// does NOT synthesize `HTTP_PROXY` / `ALL_PROXY` / `NO_PROXY` and has
|
|
330
|
-
/// no `--http-proxy` / `--socks-proxy` flags. The single source of
|
|
331
|
-
/// proxy env is the TS `generateProxyEnvVars`, which the caller merges
|
|
332
|
-
/// into the environment it spawns `srt-win exec` with; this function
|
|
333
|
-
/// just forwards that environment to the child. No proxy value is
|
|
334
|
-
/// invented, no inherited var is stripped or blanked.
|
|
335
|
-
///
|
|
336
|
-
/// Entries are sorted case-insensitively by name for block ordering —
|
|
337
|
-
/// not the strict case-insensitive Unicode collation the
|
|
338
|
-
/// `CreateProcess` docs describe, but in practice the loader and
|
|
339
|
-
/// `GetEnvironmentVariableW` don't enforce ordering; `cmd /c set` and
|
|
340
|
-
/// every consumer we've tested work regardless. Names are NOT folded
|
|
341
|
-
/// or deduplicated, so if both `HTTP_PROXY` and `http_proxy` are
|
|
342
|
-
/// present both survive into the child.
|
|
343
|
-
///
|
|
344
|
-
/// The only adjustment on top of the verbatim copy is restoring the
|
|
345
|
-
/// missing-case variants of `*_PROXY` variables (see
|
|
346
|
-
/// [`add_proxy_case_twins`]) — casing repair of caller-provided
|
|
347
|
-
/// values, not proxy synthesis. Nothing else is added: consumers that
|
|
348
|
-
/// need the broker's identity (e.g. the self-protect probe) discover
|
|
349
|
-
/// it by walking the parent-process chain rather than via an
|
|
350
|
-
/// environment variable.
|
|
351
|
-
fn build_env_block() -> Vec<u16> {
|
|
352
|
-
let mut entries: Vec<(String, String)> = std::env::vars().collect();
|
|
353
|
-
|
|
354
|
-
add_proxy_case_twins(&mut entries);
|
|
355
|
-
|
|
356
|
-
// Order the block case-insensitively by name; values pass through
|
|
357
|
-
// verbatim. No dedup — case-variant duplicates are preserved.
|
|
358
|
-
entries.sort_by_cached_key(|(k, _)| k.to_ascii_uppercase());
|
|
359
|
-
|
|
360
|
-
// Encode: `KEY=VALUE\0`… `\0`.
|
|
361
|
-
let mut out: Vec<u16> = Vec::new();
|
|
362
|
-
for (k, v) in entries {
|
|
363
|
-
out.extend(k.encode_utf16());
|
|
364
|
-
out.push(b'=' as u16);
|
|
365
|
-
out.extend(v.encode_utf16());
|
|
366
|
-
out.push(0);
|
|
367
|
-
}
|
|
368
|
-
out.push(0);
|
|
369
|
-
out
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/// Re-add the missing-case variants of `*_PROXY` variables (the host
|
|
373
|
-
/// spawn layer collapses case-duplicate keys) so that Cygwin/MSYS2
|
|
374
|
-
/// programs — which see a case-sensitive environment — still find
|
|
375
|
-
/// them. For every entry whose name ends with `_PROXY`
|
|
376
|
-
/// (case-insensitively), the all-uppercase and all-lowercase forms of
|
|
377
|
-
/// that name are appended where missing, with the same value. Existing
|
|
378
|
-
/// keys are never overwritten and nothing is added for names that are
|
|
379
|
-
/// not `*_PROXY`.
|
|
380
|
-
fn add_proxy_case_twins(entries: &mut Vec<(String, String)>) {
|
|
381
|
-
let mut names: std::collections::HashSet<String> =
|
|
382
|
-
entries.iter().map(|(k, _)| k.clone()).collect();
|
|
383
|
-
let mut twins: Vec<(String, String)> = Vec::new();
|
|
384
|
-
for (k, v) in entries.iter() {
|
|
385
|
-
if !k.to_ascii_uppercase().ends_with("_PROXY") {
|
|
386
|
-
continue;
|
|
387
|
-
}
|
|
388
|
-
for form in [k.to_ascii_uppercase(), k.to_ascii_lowercase()] {
|
|
389
|
-
if !names.contains(&form) {
|
|
390
|
-
names.insert(form.clone());
|
|
391
|
-
twins.push((form, v.clone()));
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
entries.extend(twins);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// ─── Command-line quoting ───────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
/// MSVCRT / `CommandLineToArgvW` quoting for one argument.
|
|
401
|
-
/// Public so `main.rs`'s self-elevate path can rebuild
|
|
402
|
-
/// `lpParameters` from `std::env::args()`.
|
|
403
|
-
pub fn quote_arg(a: &str) -> String {
|
|
404
|
-
if !a.is_empty()
|
|
405
|
-
&& !a
|
|
406
|
-
.chars()
|
|
407
|
-
.any(|c| matches!(c, ' ' | '\t' | '"' | '\\'))
|
|
408
|
-
{
|
|
409
|
-
return a.to_string();
|
|
410
|
-
}
|
|
411
|
-
let mut out = String::with_capacity(a.len() + 2);
|
|
412
|
-
out.push('"');
|
|
413
|
-
let mut backslashes = 0usize;
|
|
414
|
-
for c in a.chars() {
|
|
415
|
-
match c {
|
|
416
|
-
'\\' => {
|
|
417
|
-
backslashes += 1;
|
|
418
|
-
out.push('\\');
|
|
419
|
-
}
|
|
420
|
-
'"' => {
|
|
421
|
-
// Double the run of backslashes, then escape the
|
|
422
|
-
// quote.
|
|
423
|
-
for _ in 0..backslashes {
|
|
424
|
-
out.push('\\');
|
|
425
|
-
}
|
|
426
|
-
out.push('\\');
|
|
427
|
-
out.push('"');
|
|
428
|
-
backslashes = 0;
|
|
429
|
-
}
|
|
430
|
-
_ => {
|
|
431
|
-
backslashes = 0;
|
|
432
|
-
out.push(c);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
// Trailing backslash run before the closing quote must double.
|
|
437
|
-
for _ in 0..backslashes {
|
|
438
|
-
out.push('\\');
|
|
439
|
-
}
|
|
440
|
-
out.push('"');
|
|
441
|
-
out
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
fn target_is_cmd(exe: &Path) -> bool {
|
|
445
|
-
exe.file_name()
|
|
446
|
-
.and_then(|n| n.to_str())
|
|
447
|
-
.map(|s| {
|
|
448
|
-
s.eq_ignore_ascii_case("cmd.exe") || s.eq_ignore_ascii_case("cmd")
|
|
449
|
-
})
|
|
450
|
-
.unwrap_or(false)
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/// Build the full command line.
|
|
454
|
-
///
|
|
455
|
-
/// **Non-cmd targets:** every arg is MSVCRT-quoted via
|
|
456
|
-
/// [`quote_arg`] so `CommandLineToArgvW` in the child recovers
|
|
457
|
-
/// the exact argv.
|
|
458
|
-
///
|
|
459
|
-
/// **`cmd.exe` targets:** cmd does NOT use `CommandLineToArgvW`;
|
|
460
|
-
/// it parses `lpCommandLine` itself. With `/s`, it strips the
|
|
461
|
-
/// first and last `"` of the post-`/c` portion and runs what's
|
|
462
|
-
/// between *verbatim* under cmd's own rules. The caller is
|
|
463
|
-
/// expected to include `/s`; without it cmd falls back to the
|
|
464
|
-
/// legacy "if exactly two quotes and they wrap a runnable
|
|
465
|
-
/// command, strip them; otherwise leave alone" heuristic, and
|
|
466
|
-
/// the wrapper quote may not strip cleanly. (Batch 03's
|
|
467
|
-
/// `wrapWithSandboxArgv` always passes `/d /s /c`.) So we:
|
|
468
|
-
/// 1. Emit the exe + flags up to and including `/c|/k|/r`
|
|
469
|
-
/// using `quote_arg` (these are simple tokens; quoting is
|
|
470
|
-
/// a no-op unless the exe path has spaces).
|
|
471
|
-
/// 2. Join the remaining argv elements with single spaces —
|
|
472
|
-
/// this is the user's cmd command string, reconstructed.
|
|
473
|
-
/// 3. Wrap that in ONE outer `"…"` pair for `/s` to strip.
|
|
474
|
-
///
|
|
475
|
-
/// The post-`/c` content is **passed through unmodified**. We
|
|
476
|
-
/// do NOT caret-escape `& | < > ^ ( )` and do NOT touch `"` —
|
|
477
|
-
/// the contract is "this is a cmd.exe command string" and the
|
|
478
|
-
/// caller (batch-03's `wrapWithSandboxArgv`) supplies it as
|
|
479
|
-
/// such. `&` chains commands, `"…"` quotes — exactly as the
|
|
480
|
-
/// user typed. The child IS the sandbox, so cmd metachars here
|
|
481
|
-
/// are the user's tool, not an escape vector. (The Phase-6 N1
|
|
482
|
-
/// host-shell injection concern was about the OUTER spawn,
|
|
483
|
-
/// which is solved by argv-mode in batch 03; this is the inner
|
|
484
|
-
/// sandboxed cmd.)
|
|
485
|
-
///
|
|
486
|
-
/// An earlier revision per-arg-doubled `"` → `""`, which cmd
|
|
487
|
-
/// treats as a quote-state *toggle*, not a literal — that
|
|
488
|
-
/// mis-parsed payloads containing `&` and was reverted.
|
|
489
|
-
pub fn build_cmdline(exe: &Path, args: &[String]) -> String {
|
|
490
|
-
let cmd_split = if target_is_cmd(exe) {
|
|
491
|
-
args.iter().position(|a| {
|
|
492
|
-
matches!(a.to_ascii_lowercase().as_str(), "/c" | "/k" | "/r")
|
|
493
|
-
})
|
|
494
|
-
} else {
|
|
495
|
-
None
|
|
496
|
-
};
|
|
497
|
-
let mut s = quote_arg(&exe.display().to_string());
|
|
498
|
-
match cmd_split {
|
|
499
|
-
Some(p) => {
|
|
500
|
-
for a in &args[..=p] {
|
|
501
|
-
s.push(' ');
|
|
502
|
-
s.push_str("e_arg(a));
|
|
503
|
-
}
|
|
504
|
-
// One outer pair of quotes around the whole post-/c
|
|
505
|
-
// command for `/s` to strip; contents verbatim.
|
|
506
|
-
s.push_str(" \"");
|
|
507
|
-
s.push_str(&args[p + 1..].join(" "));
|
|
508
|
-
s.push('"');
|
|
509
|
-
}
|
|
510
|
-
None => {
|
|
511
|
-
for a in args {
|
|
512
|
-
s.push(' ');
|
|
513
|
-
s.push_str("e_arg(a));
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
s
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// ─── PROC_THREAD_ATTRIBUTE_LIST helper ──────────────────────────────
|
|
521
|
-
|
|
522
|
-
/// RAII wrapper over an opaque `LPPROC_THREAD_ATTRIBUTE_LIST`.
|
|
523
|
-
/// `Drop` calls `DeleteProcThreadAttributeList`. The values passed
|
|
524
|
-
/// to [`set_*`] must outlive `self` — the kernel reads them by
|
|
525
|
-
/// pointer at `CreateProcess` time.
|
|
526
|
-
struct ProcThreadAttrs {
|
|
527
|
-
storage: Vec<u8>,
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
impl ProcThreadAttrs {
|
|
531
|
-
fn new(count: u32) -> Result<Self> {
|
|
532
|
-
let mut size = 0usize;
|
|
533
|
-
// Sizing call — expected to fail with
|
|
534
|
-
// ERROR_INSUFFICIENT_BUFFER and write the required size.
|
|
535
|
-
unsafe {
|
|
536
|
-
let _ = InitializeProcThreadAttributeList(
|
|
537
|
-
None, count, None, &mut size,
|
|
538
|
-
);
|
|
539
|
-
}
|
|
540
|
-
if size == 0 {
|
|
541
|
-
return Err(anyhow!(
|
|
542
|
-
"InitializeProcThreadAttributeList sizing returned 0"
|
|
543
|
-
));
|
|
544
|
-
}
|
|
545
|
-
let mut storage = vec![0u8; size];
|
|
546
|
-
unsafe {
|
|
547
|
-
InitializeProcThreadAttributeList(
|
|
548
|
-
Some(LPPROC_THREAD_ATTRIBUTE_LIST(
|
|
549
|
-
storage.as_mut_ptr() as *mut c_void,
|
|
550
|
-
)),
|
|
551
|
-
count,
|
|
552
|
-
None,
|
|
553
|
-
&mut size,
|
|
554
|
-
)
|
|
555
|
-
.context("InitializeProcThreadAttributeList")?;
|
|
556
|
-
}
|
|
557
|
-
Ok(Self { storage })
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
fn list(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST {
|
|
561
|
-
LPPROC_THREAD_ATTRIBUTE_LIST(self.storage.as_mut_ptr() as *mut c_void)
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
fn set_mitigation_policy(&mut self, policy: &u64) -> Result<()> {
|
|
565
|
-
unsafe {
|
|
566
|
-
UpdateProcThreadAttribute(
|
|
567
|
-
self.list(),
|
|
568
|
-
0,
|
|
569
|
-
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY as usize,
|
|
570
|
-
Some(policy as *const u64 as *const c_void),
|
|
571
|
-
size_of::<u64>(),
|
|
572
|
-
None,
|
|
573
|
-
None,
|
|
574
|
-
)
|
|
575
|
-
.context("UpdateProcThreadAttribute(MITIGATION_POLICY)")
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/// `UpdateProcThreadAttribute(HANDLE_LIST)` requires at least
|
|
580
|
-
/// one entry — Windows rejects an empty list with
|
|
581
|
-
/// `ERROR_BAD_LENGTH`. The caller is expected to have filtered
|
|
582
|
-
/// already.
|
|
583
|
-
fn set_handle_list(&mut self, handles: &mut [HANDLE]) -> Result<()> {
|
|
584
|
-
debug_assert!(!handles.is_empty());
|
|
585
|
-
unsafe {
|
|
586
|
-
UpdateProcThreadAttribute(
|
|
587
|
-
self.list(),
|
|
588
|
-
0,
|
|
589
|
-
PROC_THREAD_ATTRIBUTE_HANDLE_LIST as usize,
|
|
590
|
-
Some(handles.as_ptr() as *const c_void),
|
|
591
|
-
std::mem::size_of_val(handles),
|
|
592
|
-
None,
|
|
593
|
-
None,
|
|
594
|
-
)
|
|
595
|
-
.context("UpdateProcThreadAttribute(HANDLE_LIST)")
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
impl Drop for ProcThreadAttrs {
|
|
601
|
-
fn drop(&mut self) {
|
|
602
|
-
unsafe {
|
|
603
|
-
DeleteProcThreadAttributeList(self.list());
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/// Mark this process's std handles inheritable and return the ones
|
|
609
|
-
/// that succeeded. We need at least one entry to satisfy the
|
|
610
|
-
/// kernel's `HANDLE_LIST` length check; the std handles are the
|
|
611
|
-
/// natural minimal set since the child attaches to the same
|
|
612
|
-
/// console anyway.
|
|
613
|
-
fn collect_inheritable_std_handles() -> Vec<HANDLE> {
|
|
614
|
-
let mut out = Vec::with_capacity(3);
|
|
615
|
-
for which in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
|
|
616
|
-
let h = match unsafe { GetStdHandle(which) } {
|
|
617
|
-
Ok(h) => h,
|
|
618
|
-
Err(_) => continue,
|
|
619
|
-
};
|
|
620
|
-
if h.0.is_null() || (h.0 as isize) == -1 {
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
// Best-effort: a detached broker may have non-inheritable
|
|
624
|
-
// (or pseudo) handles here; skip rather than fail.
|
|
625
|
-
let r = unsafe {
|
|
626
|
-
SetHandleInformation(h, HANDLE_FLAG_INHERIT.0, HANDLE_FLAG_INHERIT)
|
|
627
|
-
};
|
|
628
|
-
if r.is_ok() {
|
|
629
|
-
out.push(h);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
out
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
#[cfg(test)]
|
|
636
|
-
mod tests {
|
|
637
|
-
use super::*;
|
|
638
|
-
|
|
639
|
-
#[test]
|
|
640
|
-
fn quote_arg_simple() {
|
|
641
|
-
assert_eq!(quote_arg("foo"), "foo");
|
|
642
|
-
assert_eq!(quote_arg(""), "\"\"");
|
|
643
|
-
assert_eq!(quote_arg("a b"), "\"a b\"");
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
#[test]
|
|
647
|
-
fn quote_arg_backslash_quote() {
|
|
648
|
-
// a\"b → "a\\\"b"
|
|
649
|
-
assert_eq!(quote_arg(r#"a\"b"#), r#""a\\\"b""#);
|
|
650
|
-
// trailing backslashes double before closing quote
|
|
651
|
-
assert_eq!(quote_arg(r"a\"), r#""a\\""#);
|
|
652
|
-
assert_eq!(quote_arg(r"a\\"), r#""a\\\\""#);
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
#[test]
|
|
656
|
-
fn build_cmdline_cmd_passthrough() {
|
|
657
|
-
let exe = Path::new(r"C:\Windows\System32\cmd.exe");
|
|
658
|
-
// post-/c content is wrapped once in "…" for /s to strip;
|
|
659
|
-
// inner quotes and metachars are NOT touched.
|
|
660
|
-
let line = build_cmdline(
|
|
661
|
-
exe,
|
|
662
|
-
&["/d".into(), "/s".into(), "/c".into(),
|
|
663
|
-
r#"echo "x & y""#.into()],
|
|
664
|
-
);
|
|
665
|
-
assert_eq!(
|
|
666
|
-
line,
|
|
667
|
-
r#""C:\Windows\System32\cmd.exe" /d /s /c "echo "x & y"""#
|
|
668
|
-
);
|
|
669
|
-
// Multiple post-/c argv elements are joined with a space.
|
|
670
|
-
let line2 = build_cmdline(
|
|
671
|
-
exe,
|
|
672
|
-
&["/c".into(), "echo".into(), "a".into(), "&".into(),
|
|
673
|
-
"echo".into(), "b".into()],
|
|
674
|
-
);
|
|
675
|
-
assert_eq!(
|
|
676
|
-
line2,
|
|
677
|
-
r#""C:\Windows\System32\cmd.exe" /c "echo a & echo b""#
|
|
678
|
-
);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
#[test]
|
|
682
|
-
fn build_cmdline_cmd_no_split_when_no_c_flag() {
|
|
683
|
-
// cmd.exe without /c|/k|/r → MSVCRT quoting throughout.
|
|
684
|
-
let exe = Path::new("cmd.exe");
|
|
685
|
-
let line = build_cmdline(exe, &["/?".into()]);
|
|
686
|
-
assert_eq!(line, r#"cmd.exe /?"#);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
#[test]
|
|
690
|
-
fn build_cmdline_non_cmd_uses_msvcrt_quoting() {
|
|
691
|
-
let exe = Path::new(r"C:\foo\bar.exe");
|
|
692
|
-
let args = vec![r#"a "b"#.into()];
|
|
693
|
-
let line = build_cmdline(exe, &args);
|
|
694
|
-
assert!(line.ends_with(r#""a \"b""#), "got: {line}");
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
#[test]
|
|
698
|
-
fn proxy_case_twins_suffix_rule_covers_any_proxy_var() {
|
|
699
|
-
let mut entries = vec![
|
|
700
|
-
// Any *_PROXY name is twinned, not just the standard trio.
|
|
701
|
-
(
|
|
702
|
-
"GRPC_PROXY".to_string(),
|
|
703
|
-
"socks5h://localhost:60081".to_string(),
|
|
704
|
-
),
|
|
705
|
-
// Mixed-case input → BOTH canonical forms appended.
|
|
706
|
-
("Http_Proxy".to_string(), "http://localhost:60080".to_string()),
|
|
707
|
-
// Names that merely contain or extend the suffix are not.
|
|
708
|
-
("FOO_PROXYX".to_string(), "x".to_string()),
|
|
709
|
-
("PATH".to_string(), r"C:\Windows".to_string()),
|
|
710
|
-
];
|
|
711
|
-
add_proxy_case_twins(&mut entries);
|
|
712
|
-
let matching = |name: &str| {
|
|
713
|
-
entries
|
|
714
|
-
.iter()
|
|
715
|
-
.filter(|(k, _)| k == name)
|
|
716
|
-
.collect::<Vec<_>>()
|
|
717
|
-
};
|
|
718
|
-
assert_eq!(matching("grpc_proxy").len(), 1);
|
|
719
|
-
assert_eq!(matching("grpc_proxy")[0].1, "socks5h://localhost:60081");
|
|
720
|
-
assert_eq!(matching("GRPC_PROXY").len(), 1);
|
|
721
|
-
// Mixed-case original is preserved AND both canonical forms added.
|
|
722
|
-
assert_eq!(matching("Http_Proxy").len(), 1);
|
|
723
|
-
assert_eq!(matching("HTTP_PROXY").len(), 1);
|
|
724
|
-
assert_eq!(matching("http_proxy").len(), 1);
|
|
725
|
-
assert_eq!(matching("http_proxy")[0].1, "http://localhost:60080");
|
|
726
|
-
// Non-matching names untouched, nothing appended for them.
|
|
727
|
-
assert_eq!(matching("FOO_PROXYX").len(), 1);
|
|
728
|
-
assert!(matching("foo_proxyx").is_empty());
|
|
729
|
-
assert_eq!(matching("PATH").len(), 1);
|
|
730
|
-
assert!(matching("path").is_empty());
|
|
731
|
-
}
|
|
732
|
-
}
|