From c64807dc6672a2ff479f8cc196f19633f0db9800 Mon Sep 17 00:00:00 2001 From: Jake Shadle Date: Mon, 18 Nov 2024 16:14:23 +0100 Subject: [PATCH] Detect powershell (#265) --- .github/workflows/rust-ci.yml | 9 ++ Cargo.lock | 1 + Cargo.toml | 2 + src/bindings.toml | 22 +++++ src/cargo-about/generate.rs | 21 +++-- src/lib.rs | 172 ++++++++++++++++++++++++++++++++++ src/win_bindings.rs | 111 ++++++++++++++++++++++ 7 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 src/bindings.toml create mode 100644 src/win_bindings.rs diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index 9112c58..dfd7a2d 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -45,7 +45,16 @@ jobs: - name: cargo test build run: cargo build --tests --release - name: cargo test + shell: bash run: cargo test --release + - name: detects powershell + if: ${{ matrix.os != 'macos-14' }} + shell: pwsh + run: cargo test --release -- --ignored is_powershell_true + - name: doesn't detect powershell + if: ${{ matrix.os != 'macos-14' }} + shell: bash + run: cargo test --release -- --ignored is_powershell_false msrv-check: name: Minimum Stable Rust Version Check diff --git a/Cargo.lock b/Cargo.lock index 7f618a8..c5f625e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,7 @@ dependencies = [ "home", "ignore", "krates", + "libc", "log", "mimalloc", "nu-ansi-term", diff --git a/Cargo.toml b/Cargo.toml index 5c14e68..50e43f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,8 @@ home = "0.5" ignore = "0.4" # Dependency graphing krates = { version = "0.17.1", features = ["metadata"] } +# Parent process retrieval +libc = "0.2" # Logging macros log = "0.4" # Better heap allocator over system one (usually) diff --git a/src/bindings.toml b/src/bindings.toml new file mode 100644 index 0000000..236f757 --- /dev/null +++ b/src/bindings.toml @@ -0,0 +1,22 @@ +output = "win_bindings.rs" +binds = [ + "MAX_PATH", + "NtClose", + "NtOpenProcess", + "NtQueryInformationProcess", + "ProcessBasicInformation", + "ProcessImageFileName", + "PROCESS_BASIC_INFORMATION", + "PROCESS_QUERY_INFORMATION", + "STATUS_SUCCESS", + "UNICODE_STRING", +] + +[bind-mode] +mode = "minwin" + +[bind-mode.config] +enum-style = "minwin" +fix-naming = true +use-rust-casing = true +linking-style = "raw-dylib" diff --git a/src/cargo-about/generate.rs b/src/cargo-about/generate.rs index 8c5daea..cceb9bc 100644 --- a/src/cargo-about/generate.rs +++ b/src/cargo-about/generate.rs @@ -170,6 +170,15 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> { "handlebars template(s) must be specified when using handlebars output format" ); + // Check if the parent process is powershell, if it is, assume that it will + // screw up the output https://github.com/EmbarkStudios/cargo-about/issues/198 + // and inform the user about the -o, --output-file option + let redirect_stdout = + args.output_file.is_none() || args.output_file.as_deref() == Some(Path::new("-")); + if redirect_stdout { + 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"); + } + rayon::scope(|s| { s.spawn(|_| { log::info!("gathering crates for {manifest_path}"); @@ -289,13 +298,11 @@ pub fn cmd(args: Args, color: crate::Color) -> anyhow::Result<()> { serde_json::to_string(&input)? }; - match args.output_file.as_ref() { - None => println!("{output}"), - Some(path) if path == Path::new("-") => println!("{output}"), - Some(path) => { - std::fs::write(path, output) - .with_context(|| format!("output file {path} could not be written"))?; - } + if let Some(path) = &args.output_file.filter(|_| !redirect_stdout) { + std::fs::write(path, output) + .with_context(|| format!("output file {path} could not be written"))?; + } else { + println!("{output}"); } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 4014d2c..29b0f69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -216,3 +216,175 @@ pub fn validate_sha256(buffer: &str, expected: &str) -> anyhow::Result<()> { Ok(()) } + +#[cfg(target_family = "unix")] +#[allow(unsafe_code)] +pub fn is_powershell_parent() -> bool { + if !cfg!(target_os = "linux") { + // Making the assumption that no one on MacOS or any of the *BSDs uses powershell... + return false; + } + + // SAFETY: no invariants to uphold + let mut parent_id = Some(unsafe { libc::getppid() }); + + while let Some(ppid) = parent_id { + let Ok(cmd) = std::fs::read_to_string(format!("/proc/{ppid}/cmdline")) else { + break; + }; + + let Some(proc) = cmd + .split('\0') + .next() + .and_then(|path| path.split('/').last()) + else { + break; + }; + + if proc == "pwsh" { + return true; + } + + let Ok(status) = std::fs::read_to_string(format!("/proc/{ppid}/status")) else { + break; + }; + + for line in status.lines() { + let Some(ppid) = line.strip_prefix("PPid:\t") else { + continue; + }; + + parent_id = ppid.parse().ok(); + break; + } + } + + false +} + +#[cfg(target_family = "windows")] +mod win_bindings; + +#[cfg(target_family = "windows")] +#[allow(unsafe_code)] +pub fn is_powershell_parent() -> bool { + use std::os::windows::ffi::OsStringExt as _; + use win_bindings::*; + + struct NtHandle { + handle: isize, + } + + impl Drop for NtHandle { + fn drop(&mut self) { + if self.handle != -1 { + unsafe { + nt_close(self.handle); + } + } + } + } + + let mut handle = Some(NtHandle { handle: -1 }); + + unsafe { + let reset = |fname: &mut [u16]| { + let ustr = &mut *fname.as_mut_ptr().cast::(); + ustr.length = 0; + ustr.maximum_length = MaxPath as _; + }; + + // The API for this is extremely irritating, the struct and string buffer + // need to be the same :/ + let mut file_name = [0u16; MaxPath as usize + std::mem::size_of::() / 2]; + + while let Some(ph) = handle { + let mut basic_info = std::mem::MaybeUninit::::uninit(); + let mut length = 0; + if nt_query_information_process( + ph.handle, + Processinfoclass::ProcessBasicInformation, + basic_info.as_mut_ptr().cast(), + std::mem::size_of::() as _, + &mut length, + ) != StatusSuccess + { + break; + } + + if length != std::mem::size_of::() as u32 { + break; + } + + let basic_info = basic_info.assume_init(); + reset(&mut file_name); + + let ppid = basic_info.inherited_from_unique_process_id as isize; + + if ppid == 0 || ppid == -1 { + break; + } + + let mut parent_handle = -1; + let obj_attr = std::mem::zeroed(); + let client_id = ClientId { + unique_process: ppid, + unique_thread: 0, + }; + if nt_open_process( + &mut parent_handle, + ProcessAccessRights::ProcessQueryInformation, + &obj_attr, + &client_id, + ) != StatusSuccess + { + break; + } + + handle = Some(NtHandle { + handle: parent_handle, + }); + + if nt_query_information_process( + parent_handle, + Processinfoclass::ProcessImageFileName, + file_name.as_mut_ptr().cast(), + (file_name.len() * 2) as _, + &mut length, + ) != StatusSuccess + { + break; + } + + let ustr = &*file_name.as_ptr().cast::(); + let os = std::ffi::OsString::from_wide(std::slice::from_raw_parts( + ustr.buffer, + (ustr.length >> 1) as usize, + )); + + let path = std::path::Path::new(&os); + if let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) { + if stem == "pwsh" || stem == "powershell" { + return true; + } + } + } + + false + } +} + +#[cfg(test)] +mod test { + #[test] + #[ignore = "call when actually run from powershell"] + fn is_powershell_true() { + assert!(super::is_powershell_parent()); + } + + #[test] + #[ignore = "call when not actually run from powershell"] + fn is_powershell_false() { + assert!(!super::is_powershell_parent()); + } +} diff --git a/src/win_bindings.rs b/src/win_bindings.rs new file mode 100644 index 0000000..eab6f1d --- /dev/null +++ b/src/win_bindings.rs @@ -0,0 +1,111 @@ +//! Bindings generated by `minwin` 0.1.0 +#![allow( + non_snake_case, + non_upper_case_globals, + non_camel_case_types, + clippy::upper_case_acronyms +)] +#[link(name = "ntdll", kind = "raw-dylib")] +extern "system" { + #[link_name = "NtClose"] + pub fn nt_close(handle: Handle) -> Ntstatus; + #[link_name = "NtOpenProcess"] + pub fn nt_open_process( + process_handle: *mut Handle, + desired_access: u32, + object_attributes: *const ObjectAttributes, + client_id: *const ClientId, + ) -> Ntstatus; + #[link_name = "NtQueryInformationProcess"] + pub fn nt_query_information_process( + process_handle: Handle, + process_information_class: Processinfoclass::Enum, + process_information: *mut ::core::ffi::c_void, + process_information_length: u32, + return_length: *mut u32, + ) -> Ntstatus; +} +pub const MaxPath: u32 = 260; +#[repr(C)] +pub struct ClientId { + pub unique_process: Handle, + pub unique_thread: Handle, +} +pub type Handle = isize; +#[repr(C)] +pub struct ListEntry { + pub flink: *mut ListEntry, + pub blink: *mut ListEntry, +} +pub type Ntstatus = i32; +pub const StatusSuccess: Ntstatus = 0; +#[repr(C)] +pub struct ObjectAttributes { + pub length: u32, + pub root_directory: Handle, + pub object_name: *mut UnicodeString, + pub attributes: u32, + pub security_descriptor: *mut ::core::ffi::c_void, + pub security_quality_of_service: *mut ::core::ffi::c_void, +} +#[repr(C)] +pub struct Peb { + pub reserved1: [u8; 2], + pub being_debugged: u8, + pub reserved2: [u8; 1], + pub reserved3: [*mut ::core::ffi::c_void; 2], + pub ldr: *mut PebLdrData, + pub process_parameters: *mut RtlUserProcessParameters, + pub reserved4: [*mut ::core::ffi::c_void; 3], + pub atl_thunk_s_list_ptr: *mut ::core::ffi::c_void, + pub reserved5: *mut ::core::ffi::c_void, + pub reserved6: u32, + pub reserved7: *mut ::core::ffi::c_void, + pub reserved8: u32, + pub atl_thunk_s_list_ptr32: u32, + pub reserved9: [*mut ::core::ffi::c_void; 45], + pub reserved10: [u8; 96], + pub post_process_init_routine: PpsPostProcessInitRoutine, + pub reserved11: [u8; 128], + pub reserved12: [*mut ::core::ffi::c_void; 1], + pub session_id: u32, +} +#[repr(C)] +pub struct PebLdrData { + pub reserved1: [u8; 8], + pub reserved2: [*mut ::core::ffi::c_void; 3], + pub in_memory_order_module_list: ListEntry, +} +pub type PpsPostProcessInitRoutine = ::core::option::Option; +pub mod ProcessAccessRights { + pub type Enum = u32; + pub const ProcessQueryInformation: Enum = 1024; +} +#[repr(C)] +pub struct ProcessBasicInformation { + pub exit_status: Ntstatus, + pub peb_base_address: *mut Peb, + pub affinity_mask: usize, + pub base_priority: i32, + pub unique_process_id: usize, + pub inherited_from_unique_process_id: usize, +} +pub mod Processinfoclass { + pub type Enum = i32; + pub const ProcessBasicInformation: Enum = 0; + pub const ProcessImageFileName: Enum = 27; +} +pub type Pwstr = *mut u16; +#[repr(C)] +pub struct RtlUserProcessParameters { + pub reserved1: [u8; 16], + pub reserved2: [*mut ::core::ffi::c_void; 10], + pub image_path_name: UnicodeString, + pub command_line: UnicodeString, +} +#[repr(C)] +pub struct UnicodeString { + pub length: u16, + pub maximum_length: u16, + pub buffer: Pwstr, +}