Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect powershell #265

Merged
merged 9 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/rust-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions src/bindings.toml
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 14 additions & 7 deletions src/cargo-about/generate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
Expand Down Expand Up @@ -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(())
Expand Down
172 changes: 172 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<UnicodeString>();
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::<UnicodeString>() / 2];

while let Some(ph) = handle {
let mut basic_info = std::mem::MaybeUninit::<ProcessBasicInformation>::uninit();
let mut length = 0;
if nt_query_information_process(
ph.handle,
Processinfoclass::ProcessBasicInformation,
basic_info.as_mut_ptr().cast(),
std::mem::size_of::<ProcessBasicInformation>() as _,
&mut length,
) != StatusSuccess
{
break;
}

if length != std::mem::size_of::<ProcessBasicInformation>() 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::<UnicodeString>();
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());
}
}
111 changes: 111 additions & 0 deletions src/win_bindings.rs
Original file line number Diff line number Diff line change
@@ -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<unsafe extern "system" fn()>;
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,
}