Skip to content

Commit c64807d

Browse files
authored
Detect powershell (#265)
1 parent 0f8c369 commit c64807d

File tree

7 files changed

+331
-7
lines changed

7 files changed

+331
-7
lines changed

.github/workflows/rust-ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,16 @@ jobs:
4545
- name: cargo test build
4646
run: cargo build --tests --release
4747
- name: cargo test
48+
shell: bash
4849
run: cargo test --release
50+
- name: detects powershell
51+
if: ${{ matrix.os != 'macos-14' }}
52+
shell: pwsh
53+
run: cargo test --release -- --ignored is_powershell_true
54+
- name: doesn't detect powershell
55+
if: ${{ matrix.os != 'macos-14' }}
56+
shell: bash
57+
run: cargo test --release -- --ignored is_powershell_false
4958

5059
msrv-check:
5160
name: Minimum Stable Rust Version Check

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ home = "0.5"
5555
ignore = "0.4"
5656
# Dependency graphing
5757
krates = { version = "0.17.1", features = ["metadata"] }
58+
# Parent process retrieval
59+
libc = "0.2"
5860
# Logging macros
5961
log = "0.4"
6062
# Better heap allocator over system one (usually)

src/bindings.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
output = "win_bindings.rs"
2+
binds = [
3+
"MAX_PATH",
4+
"NtClose",
5+
"NtOpenProcess",
6+
"NtQueryInformationProcess",
7+
"ProcessBasicInformation",
8+
"ProcessImageFileName",
9+
"PROCESS_BASIC_INFORMATION",
10+
"PROCESS_QUERY_INFORMATION",
11+
"STATUS_SUCCESS",
12+
"UNICODE_STRING",
13+
]
14+
15+
[bind-mode]
16+
mode = "minwin"
17+
18+
[bind-mode.config]
19+
enum-style = "minwin"
20+
fix-naming = true
21+
use-rust-casing = true
22+
linking-style = "raw-dylib"

src/cargo-about/generate.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,15 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> {
170170
"handlebars template(s) must be specified when using handlebars output format"
171171
);
172172

173+
// Check if the parent process is powershell, if it is, assume that it will
174+
// screw up the output https://github.com/EmbarkStudios/cargo-about/issues/198
175+
// and inform the user about the -o, --output-file option
176+
let redirect_stdout =
177+
args.output_file.is_none() || args.output_file.as_deref() == Some(Path::new("-"));
178+
if redirect_stdout {
179+
anyhow::ensure!(!cargo_about::is_powershell_parent(), "cargo-about should not redirect its output in powershell, please use the -o, --output-file option to redirect to a file to avoid powershell encoding issues");
180+
}
181+
173182
rayon::scope(|s| {
174183
s.spawn(|_| {
175184
log::info!("gathering crates for {manifest_path}");
@@ -289,13 +298,11 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> {
289298
serde_json::to_string(&input)?
290299
};
291300

292-
match args.output_file.as_ref() {
293-
None => println!("{output}"),
294-
Some(path) if path == Path::new("-") => println!("{output}"),
295-
Some(path) => {
296-
std::fs::write(path, output)
297-
.with_context(|| format!("output file {path} could not be written"))?;
298-
}
301+
if let Some(path) = &args.output_file.filter(|_| !redirect_stdout) {
302+
std::fs::write(path, output)
303+
.with_context(|| format!("output file {path} could not be written"))?;
304+
} else {
305+
println!("{output}");
299306
}
300307

301308
Ok(())

src/lib.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,175 @@ pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> {
216216

217217
Ok(())
218218
}
219+
220+
#[cfg(target_family = "unix")]
221+
#[allow(unsafe_code)]
222+
pub fn is_powershell_parent() -> bool {
223+
if !cfg!(target_os = "linux") {
224+
// Making the assumption that no one on MacOS or any of the *BSDs uses powershell...
225+
return false;
226+
}
227+
228+
// SAFETY: no invariants to uphold
229+
let mut parent_id = Some(unsafe { libc::getppid() });
230+
231+
while let Some(ppid) = parent_id {
232+
let Ok(cmd) = std::fs::read_to_string(format!("/proc/{ppid}/cmdline")) else {
233+
break;
234+
};
235+
236+
let Some(proc) = cmd
237+
.split('\0')
238+
.next()
239+
.and_then(|path| path.split('/').last())
240+
else {
241+
break;
242+
};
243+
244+
if proc == "pwsh" {
245+
return true;
246+
}
247+
248+
let Ok(status) = std::fs::read_to_string(format!("/proc/{ppid}/status")) else {
249+
break;
250+
};
251+
252+
for line in status.lines() {
253+
let Some(ppid) = line.strip_prefix("PPid:\t") else {
254+
continue;
255+
};
256+
257+
parent_id = ppid.parse().ok();
258+
break;
259+
}
260+
}
261+
262+
false
263+
}
264+
265+
#[cfg(target_family = "windows")]
266+
mod win_bindings;
267+
268+
#[cfg(target_family = "windows")]
269+
#[allow(unsafe_code)]
270+
pub fn is_powershell_parent() -> bool {
271+
use std::os::windows::ffi::OsStringExt as _;
272+
use win_bindings::*;
273+
274+
struct NtHandle {
275+
handle: isize,
276+
}
277+
278+
impl Drop for NtHandle {
279+
fn drop(&mut self) {
280+
if self.handle != -1 {
281+
unsafe {
282+
nt_close(self.handle);
283+
}
284+
}
285+
}
286+
}
287+
288+
let mut handle = Some(NtHandle { handle: -1 });
289+
290+
unsafe {
291+
let reset = |fname: &mut [u16]| {
292+
let ustr = &mut *fname.as_mut_ptr().cast::<UnicodeString>();
293+
ustr.length = 0;
294+
ustr.maximum_length = MaxPath as _;
295+
};
296+
297+
// The API for this is extremely irritating, the struct and string buffer
298+
// need to be the same :/
299+
let mut file_name = [0u16; MaxPath as usize + std::mem::size_of::<UnicodeString>() / 2];
300+
301+
while let Some(ph) = handle {
302+
let mut basic_info = std::mem::MaybeUninit::<ProcessBasicInformation>::uninit();
303+
let mut length = 0;
304+
if nt_query_information_process(
305+
ph.handle,
306+
Processinfoclass::ProcessBasicInformation,
307+
basic_info.as_mut_ptr().cast(),
308+
std::mem::size_of::<ProcessBasicInformation>() as _,
309+
&mut length,
310+
) != StatusSuccess
311+
{
312+
break;
313+
}
314+
315+
if length != std::mem::size_of::<ProcessBasicInformation>() as u32 {
316+
break;
317+
}
318+
319+
let basic_info = basic_info.assume_init();
320+
reset(&mut file_name);
321+
322+
let ppid = basic_info.inherited_from_unique_process_id as isize;
323+
324+
if ppid == 0 || ppid == -1 {
325+
break;
326+
}
327+
328+
let mut parent_handle = -1;
329+
let obj_attr = std::mem::zeroed();
330+
let client_id = ClientId {
331+
unique_process: ppid,
332+
unique_thread: 0,
333+
};
334+
if nt_open_process(
335+
&mut parent_handle,
336+
ProcessAccessRights::ProcessQueryInformation,
337+
&obj_attr,
338+
&client_id,
339+
) != StatusSuccess
340+
{
341+
break;
342+
}
343+
344+
handle = Some(NtHandle {
345+
handle: parent_handle,
346+
});
347+
348+
if nt_query_information_process(
349+
parent_handle,
350+
Processinfoclass::ProcessImageFileName,
351+
file_name.as_mut_ptr().cast(),
352+
(file_name.len() * 2) as _,
353+
&mut length,
354+
) != StatusSuccess
355+
{
356+
break;
357+
}
358+
359+
let ustr = &*file_name.as_ptr().cast::<UnicodeString>();
360+
let os = std::ffi::OsString::from_wide(std::slice::from_raw_parts(
361+
ustr.buffer,
362+
(ustr.length >> 1) as usize,
363+
));
364+
365+
let path = std::path::Path::new(&os);
366+
if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) {
367+
if stem == "pwsh" || stem == "powershell" {
368+
return true;
369+
}
370+
}
371+
}
372+
373+
false
374+
}
375+
}
376+
377+
#[cfg(test)]
378+
mod test {
379+
#[test]
380+
#[ignore = "call when actually run from powershell"]
381+
fn is_powershell_true() {
382+
assert!(super::is_powershell_parent());
383+
}
384+
385+
#[test]
386+
#[ignore = "call when not actually run from powershell"]
387+
fn is_powershell_false() {
388+
assert!(!super::is_powershell_parent());
389+
}
390+
}

src/win_bindings.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! Bindings generated by `minwin` 0.1.0
2+
#![allow(
3+
non_snake_case,
4+
non_upper_case_globals,
5+
non_camel_case_types,
6+
clippy::upper_case_acronyms
7+
)]
8+
#[link(name = "ntdll", kind = "raw-dylib")]
9+
extern "system" {
10+
#[link_name = "NtClose"]
11+
pub fn nt_close(handle: Handle) -> Ntstatus;
12+
#[link_name = "NtOpenProcess"]
13+
pub fn nt_open_process(
14+
process_handle: *mut Handle,
15+
desired_access: u32,
16+
object_attributes: *const ObjectAttributes,
17+
client_id: *const ClientId,
18+
) -> Ntstatus;
19+
#[link_name = "NtQueryInformationProcess"]
20+
pub fn nt_query_information_process(
21+
process_handle: Handle,
22+
process_information_class: Processinfoclass::Enum,
23+
process_information: *mut ::core::ffi::c_void,
24+
process_information_length: u32,
25+
return_length: *mut u32,
26+
) -> Ntstatus;
27+
}
28+
pub const MaxPath: u32 = 260;
29+
#[repr(C)]
30+
pub struct ClientId {
31+
pub unique_process: Handle,
32+
pub unique_thread: Handle,
33+
}
34+
pub type Handle = isize;
35+
#[repr(C)]
36+
pub struct ListEntry {
37+
pub flink: *mut ListEntry,
38+
pub blink: *mut ListEntry,
39+
}
40+
pub type Ntstatus = i32;
41+
pub const StatusSuccess: Ntstatus = 0;
42+
#[repr(C)]
43+
pub struct ObjectAttributes {
44+
pub length: u32,
45+
pub root_directory: Handle,
46+
pub object_name: *mut UnicodeString,
47+
pub attributes: u32,
48+
pub security_descriptor: *mut ::core::ffi::c_void,
49+
pub security_quality_of_service: *mut ::core::ffi::c_void,
50+
}
51+
#[repr(C)]
52+
pub struct Peb {
53+
pub reserved1: [u8; 2],
54+
pub being_debugged: u8,
55+
pub reserved2: [u8; 1],
56+
pub reserved3: [*mut ::core::ffi::c_void; 2],
57+
pub ldr: *mut PebLdrData,
58+
pub process_parameters: *mut RtlUserProcessParameters,
59+
pub reserved4: [*mut ::core::ffi::c_void; 3],
60+
pub atl_thunk_s_list_ptr: *mut ::core::ffi::c_void,
61+
pub reserved5: *mut ::core::ffi::c_void,
62+
pub reserved6: u32,
63+
pub reserved7: *mut ::core::ffi::c_void,
64+
pub reserved8: u32,
65+
pub atl_thunk_s_list_ptr32: u32,
66+
pub reserved9: [*mut ::core::ffi::c_void; 45],
67+
pub reserved10: [u8; 96],
68+
pub post_process_init_routine: PpsPostProcessInitRoutine,
69+
pub reserved11: [u8; 128],
70+
pub reserved12: [*mut ::core::ffi::c_void; 1],
71+
pub session_id: u32,
72+
}
73+
#[repr(C)]
74+
pub struct PebLdrData {
75+
pub reserved1: [u8; 8],
76+
pub reserved2: [*mut ::core::ffi::c_void; 3],
77+
pub in_memory_order_module_list: ListEntry,
78+
}
79+
pub type PpsPostProcessInitRoutine = ::core::option::Option<unsafe extern "system" fn()>;
80+
pub mod ProcessAccessRights {
81+
pub type Enum = u32;
82+
pub const ProcessQueryInformation: Enum = 1024;
83+
}
84+
#[repr(C)]
85+
pub struct ProcessBasicInformation {
86+
pub exit_status: Ntstatus,
87+
pub peb_base_address: *mut Peb,
88+
pub affinity_mask: usize,
89+
pub base_priority: i32,
90+
pub unique_process_id: usize,
91+
pub inherited_from_unique_process_id: usize,
92+
}
93+
pub mod Processinfoclass {
94+
pub type Enum = i32;
95+
pub const ProcessBasicInformation: Enum = 0;
96+
pub const ProcessImageFileName: Enum = 27;
97+
}
98+
pub type Pwstr = *mut u16;
99+
#[repr(C)]
100+
pub struct RtlUserProcessParameters {
101+
pub reserved1: [u8; 16],
102+
pub reserved2: [*mut ::core::ffi::c_void; 10],
103+
pub image_path_name: UnicodeString,
104+
pub command_line: UnicodeString,
105+
}
106+
#[repr(C)]
107+
pub struct UnicodeString {
108+
pub length: u16,
109+
pub maximum_length: u16,
110+
pub buffer: Pwstr,
111+
}

0 commit comments

Comments
 (0)