py_spy/
python_process_info.rs

1use regex::Regex;
2#[cfg(windows)]
3use regex::RegexBuilder;
4#[cfg(windows)]
5use std::collections::HashMap;
6use std::mem::size_of;
7use std::path::Path;
8use std::slice;
9
10use anyhow::{Context, Error, Result};
11use lazy_static::lazy_static;
12use proc_maps::{get_process_maps, MapRange};
13#[cfg(not(target_os = "macos"))]
14use remoteprocess::Pid;
15use remoteprocess::ProcessMemory;
16
17use crate::binary_parser::{parse_binary, BinaryInfo};
18use crate::config::Config;
19use crate::python_bindings::{
20    pyruntime, v2_7_15, v3_10_0, v3_11_0, v3_12_0, v3_13_0, v3_3_7, v3_5_5, v3_6_6, v3_7_0, v3_8_0,
21    v3_9_5,
22};
23use crate::python_interpreters::{InterpreterState, ThreadState};
24use crate::stack_trace::get_stack_traces;
25use crate::version::Version;
26
27/// Holds information about the python process: memory map layout, parsed binary info
28/// for python /libpython etc.
29pub struct PythonProcessInfo {
30    pub python_binary: Option<BinaryInfo>,
31    // if python was compiled with './configure --enabled-shared', code/symbols will
32    // be in a libpython.so file instead of the executable. support that.
33    pub libpython_binary: Option<BinaryInfo>,
34    pub maps: Box<dyn ContainsAddr>,
35    pub python_filename: std::path::PathBuf,
36    #[cfg(target_os = "linux")]
37    pub dockerized: bool,
38}
39
40impl PythonProcessInfo {
41    pub fn new(process: &remoteprocess::Process) -> Result<PythonProcessInfo, Error> {
42        let filename = process
43            .exe()
44            .context("Failed to get process executable name. Check that the process is running.")?;
45
46        #[cfg(windows)]
47        let filename = filename.to_lowercase();
48
49        #[cfg(windows)]
50        let is_python_bin = |pathname: &str| pathname.to_lowercase() == filename;
51
52        #[cfg(not(windows))]
53        let is_python_bin = |pathname: &str| pathname == filename;
54
55        // get virtual memory layout
56        let maps = get_process_maps(process.pid)?;
57        info!("Got virtual memory maps from pid {}:", process.pid);
58        for map in &maps {
59            debug!(
60                "map: {:016x}-{:016x} {}{}{} {}",
61                map.start(),
62                map.start() + map.size(),
63                if map.is_read() { 'r' } else { '-' },
64                if map.is_write() { 'w' } else { '-' },
65                if map.is_exec() { 'x' } else { '-' },
66                map.filename()
67                    .unwrap_or(&std::path::PathBuf::from(""))
68                    .display()
69            );
70        }
71
72        // parse the main python binary
73        let (python_binary, python_filename) = {
74            // Get the memory address for the executable by matching against virtual memory maps
75            let map = maps.iter().find(|m| {
76                if let Some(pathname) = m.filename() {
77                    if let Some(pathname) = pathname.to_str() {
78                        #[cfg(not(windows))]
79                        {
80                            return is_python_bin(pathname) && m.is_exec();
81                        }
82                        #[cfg(windows)]
83                        {
84                            return is_python_bin(pathname);
85                        }
86                    }
87                }
88                false
89            });
90
91            let map = match map {
92                Some(map) => map,
93                None => {
94                    warn!("Failed to find '{}' in virtual memory maps, falling back to first map region", filename);
95                    // If we failed to find the executable in the virtual memory maps, just take the first file we find
96                    // sometimes on windows get_process_exe returns stale info =( https://github.com/benfred/py-spy/issues/40
97                    // and on all operating systems I've tried, the exe is the first region in the maps
98                    maps.first().ok_or_else(|| {
99                        format_err!("Failed to get virtual memory maps from process")
100                    })?
101                }
102            };
103
104            #[cfg(not(target_os = "linux"))]
105            let filename = std::path::PathBuf::from(filename);
106
107            // use filename through /proc/pid/exe which works across docker namespaces and
108            // handles if the file was deleted
109            #[cfg(target_os = "linux")]
110            let filename = std::path::PathBuf::from(format!("/proc/{}/exe", process.pid));
111
112            // TODO: consistent types? u64 -> usize? for map.start etc
113            let python_binary = parse_binary(&filename, map.start() as u64, map.size() as u64);
114
115            // windows symbols are stored in separate files (.pdb), load
116            #[cfg(windows)]
117            let python_binary = python_binary.and_then(|mut pb| {
118                get_windows_python_symbols(process.pid, &filename, map.start() as u64)
119                    .map(|symbols| {
120                        pb.symbols.extend(symbols);
121                        pb
122                    })
123                    .map_err(|err| err.into())
124            });
125
126            // For OSX, need to adjust main binary symbols by subtracting _mh_execute_header
127            // (which we've added to by map.start already, so undo that here)
128            #[cfg(target_os = "macos")]
129            let python_binary = python_binary.map(|mut pb| {
130                let offset = pb.symbols["_mh_execute_header"] - map.start() as u64;
131                for address in pb.symbols.values_mut() {
132                    *address -= offset;
133                }
134
135                if pb.bss_addr != 0 {
136                    pb.bss_addr -= offset;
137                }
138                pb
139            });
140
141            (python_binary, filename)
142        };
143
144        // likewise handle libpython for python versions compiled with --enabled-shared
145        let libpython_binary = {
146            let libmaps: Vec<_> = maps
147                .iter()
148                .filter(|m| {
149                    if let Some(pathname) = m.filename() {
150                        if let Some(pathname) = pathname.to_str() {
151                            #[cfg(not(windows))]
152                            {
153                                return is_python_lib(pathname) && m.is_exec();
154                            }
155                            #[cfg(windows)]
156                            {
157                                return is_python_lib(pathname);
158                            }
159                        }
160                    }
161                    false
162                })
163                .collect();
164
165            let mut libpython_binary: Option<BinaryInfo> = None;
166
167            #[cfg(not(target_os = "linux"))]
168            let libpython_option = if !libmaps.is_empty() {
169                Some(&libmaps[0])
170            } else {
171                None
172            };
173            #[cfg(target_os = "linux")]
174            let libpython_option = libmaps.iter().min_by_key(|m| m.offset);
175
176            if let Some(libpython) = libpython_option {
177                if let Some(filename) = &libpython.filename() {
178                    info!("Found libpython binary @ {}", filename.display());
179
180                    // on linux the process could be running in docker, access the filename through procfs
181                    #[cfg(target_os = "linux")]
182                    let filename = &std::path::PathBuf::from(format!(
183                        "/proc/{}/root{}",
184                        process.pid,
185                        filename.display()
186                    ));
187
188                    #[allow(unused_mut)]
189                    let mut parsed =
190                        parse_binary(filename, libpython.start() as u64, libpython.size() as u64)?;
191                    #[cfg(windows)]
192                    parsed.symbols.extend(get_windows_python_symbols(
193                        process.pid,
194                        filename,
195                        libpython.start() as u64,
196                    )?);
197                    libpython_binary = Some(parsed);
198                }
199            }
200
201            // On OSX, it's possible that the Python library is a dylib loaded up from the system
202            // framework (like /System/Library/Frameworks/Python.framework/Versions/2.7/Python)
203            // In this case read in the dyld_info information and figure out the filename from there
204            #[cfg(target_os = "macos")]
205            {
206                if libpython_binary.is_none() {
207                    use proc_maps::mac_maps::get_dyld_info;
208                    let dyld_infos = get_dyld_info(process.pid)?;
209
210                    for dyld in &dyld_infos {
211                        let segname =
212                            unsafe { std::ffi::CStr::from_ptr(dyld.segment.segname.as_ptr()) };
213                        debug!(
214                            "dyld: {:016x}-{:016x} {:10} {}",
215                            dyld.segment.vmaddr,
216                            dyld.segment.vmaddr + dyld.segment.vmsize,
217                            segname.to_string_lossy(),
218                            dyld.filename.display()
219                        );
220                    }
221
222                    let python_dyld_data = dyld_infos.iter().find(|m| {
223                        if let Some(filename) = m.filename.to_str() {
224                            return is_python_framework(filename)
225                                && m.segment.segname[0..7] == [95, 95, 68, 65, 84, 65, 0];
226                        }
227                        false
228                    });
229
230                    if let Some(libpython) = python_dyld_data {
231                        info!(
232                            "Found libpython binary from dyld @ {}",
233                            libpython.filename.display()
234                        );
235
236                        let mut binary = parse_binary(
237                            &libpython.filename,
238                            libpython.segment.vmaddr,
239                            libpython.segment.vmsize,
240                        )?;
241
242                        // TODO: bss addr offsets returned from parsing binary are wrong
243                        // (assumes data section isn't split from text section like done here).
244                        // BSS occurs somewhere in the data section, just scan that
245                        // (could later tighten this up to look at segment sections too)
246                        binary.bss_addr = libpython.segment.vmaddr;
247                        binary.bss_size = libpython.segment.vmsize;
248                        libpython_binary = Some(binary);
249                    }
250                }
251            }
252
253            libpython_binary
254        };
255
256        // If we have a libpython binary - we can tolerate failures on parsing the main python binary.
257        let python_binary = match libpython_binary {
258            None => Some(python_binary.context("Failed to parse python binary")?),
259            _ => python_binary.ok(),
260        };
261
262        #[cfg(target_os = "linux")]
263        let dockerized = is_dockerized(process.pid).unwrap_or(false);
264
265        Ok(PythonProcessInfo {
266            python_binary,
267            libpython_binary,
268            maps: Box::new(maps),
269            python_filename,
270            #[cfg(target_os = "linux")]
271            dockerized,
272        })
273    }
274
275    pub fn get_symbol(&self, symbol: &str) -> Option<&u64> {
276        if let Some(ref pb) = self.python_binary {
277            if let Some(addr) = pb.symbols.get(symbol) {
278                info!("got symbol {} (0x{:016x}) from python binary", symbol, addr);
279                return Some(addr);
280            }
281        }
282
283        if let Some(ref binary) = self.libpython_binary {
284            if let Some(addr) = binary.symbols.get(symbol) {
285                info!(
286                    "got symbol {} (0x{:016x}) from libpython binary",
287                    symbol, addr
288                );
289                return Some(addr);
290            }
291        }
292        None
293    }
294}
295
296/// Returns the version of python running in the process.
297pub fn get_python_version<P>(python_info: &PythonProcessInfo, process: &P) -> Result<Version, Error>
298where
299    P: ProcessMemory,
300{
301    // If possible, grab the sys.version string from the processes memory (mac osx).
302    if let Some(&addr) = python_info
303        .get_symbol("Py_GetVersion.version")
304        .or_else(|| python_info.get_symbol("version"))
305    {
306        info!("Getting version from symbol address");
307        if let Ok(bytes) = process.copy(addr as usize, 128) {
308            if let Ok(version) = Version::scan_bytes(&bytes) {
309                return Ok(version);
310            }
311        }
312    }
313
314    // otherwise get version info from scanning BSS section for sys.version string
315    if let Some(ref pb) = python_info.python_binary {
316        info!("Getting version from python binary BSS");
317        let bss = process.copy(pb.bss_addr as usize, pb.bss_size as usize)?;
318        match Version::scan_bytes(&bss) {
319            Ok(version) => return Ok(version),
320            Err(err) => info!("Failed to get version from BSS section: {}", err),
321        }
322    }
323
324    // try again if there is a libpython.so
325    if let Some(ref libpython) = python_info.libpython_binary {
326        info!("Getting version from libpython BSS");
327        let bss = process.copy(libpython.bss_addr as usize, libpython.bss_size as usize)?;
328        match Version::scan_bytes(&bss) {
329            Ok(version) => return Ok(version),
330            Err(err) => info!("Failed to get version from libpython BSS section: {}", err),
331        }
332    }
333
334    // the python_filename might have the version encoded in it (/usr/bin/python3.5 etc).
335    // try reading that in (will miss patch level on python, but that shouldn't matter)
336    info!(
337        "Trying to get version from path: {}",
338        python_info.python_filename.display()
339    );
340    let path = Path::new(&python_info.python_filename);
341    if let Some(python) = path.file_name() {
342        if let Some(python) = python.to_str() {
343            if let Some(stripped_python) = python.strip_prefix("python") {
344                let tokens: Vec<&str> = stripped_python.split('.').collect();
345                if tokens.len() >= 2 {
346                    if let (Ok(major), Ok(minor)) =
347                        (tokens[0].parse::<u64>(), tokens[1].parse::<u64>())
348                    {
349                        return Ok(Version {
350                            major,
351                            minor,
352                            patch: 0,
353                            release_flags: "".to_owned(),
354                            build_metadata: None,
355                        });
356                    }
357                }
358            }
359        }
360    }
361    Err(format_err!(
362        "Failed to find python version from target process"
363    ))
364}
365
366pub fn get_interpreter_address<P>(
367    python_info: &PythonProcessInfo,
368    process: &P,
369    version: &Version,
370) -> Result<usize, Error>
371where
372    P: ProcessMemory,
373{
374    // get the address of the main PyInterpreterState object from loaded symbols if we can
375    // (this tends to be faster than scanning through the bss section)
376    match version {
377        Version {
378            major: 3,
379            minor: 13,
380            ..
381        } => {
382            if let Some(&addr) = python_info.get_symbol("_PyRuntime") {
383                // figure out the interpreters_head location using the debug_offsets
384                let debug_offsets: v3_13_0::_Py_DebugOffsets =
385                    process.copy_struct(addr as usize)?;
386                let addr = process.copy_struct(
387                    addr as usize + debug_offsets.runtime_state.interpreters_head as usize,
388                )?;
389
390                // Make sure the interpreter addr is valid before returning
391                match check_interpreter_addresses(&[addr], &*python_info.maps, process, version) {
392                    Ok(addr) => return Ok(addr),
393                    Err(_) => {
394                        warn!(
395                            "Interpreter address from _PyRuntime symbol is invalid {:016x}",
396                            addr
397                        );
398                    }
399                };
400            }
401        }
402        Version {
403            major: 3,
404            minor: 7..=12,
405            ..
406        } => {
407            if let Some(&addr) = python_info.get_symbol("_PyRuntime") {
408                let addr = process
409                    .copy_struct(addr as usize + pyruntime::get_interp_head_offset(version))?;
410
411                // Make sure the interpreter addr is valid before returning
412                match check_interpreter_addresses(&[addr], &*python_info.maps, process, version) {
413                    Ok(addr) => return Ok(addr),
414                    Err(_) => {
415                        warn!(
416                            "Interpreter address from _PyRuntime symbol is invalid {:016x}",
417                            addr
418                        );
419                    }
420                };
421            }
422        }
423        _ => {
424            if let Some(&addr) = python_info.get_symbol("interp_head") {
425                let addr = process.copy_struct(addr as usize)?;
426                match check_interpreter_addresses(&[addr], &*python_info.maps, process, version) {
427                    Ok(addr) => return Ok(addr),
428                    Err(_) => {
429                        warn!(
430                            "Interpreter address from interp_head symbol is invalid {:016x}",
431                            addr
432                        );
433                    }
434                };
435            }
436        }
437    };
438    info!("Failed to find runtime address from symbols, scanning BSS section from main binary");
439
440    // try scanning the BSS section of the binary for things that might be the interpreterstate
441    let err = if let Some(ref pb) = python_info.python_binary {
442        match get_interpreter_address_from_binary(pb, &*python_info.maps, process, version) {
443            Ok(addr) => return Ok(addr),
444            err => Some(err),
445        }
446    } else {
447        None
448    };
449    // Before giving up, try again if there is a libpython.so
450    if let Some(ref lpb) = python_info.libpython_binary {
451        info!("Failed to get interpreter from binary BSS, scanning libpython BSS");
452        match get_interpreter_address_from_binary(lpb, &*python_info.maps, process, version) {
453            Ok(addr) => Ok(addr),
454            lib_err => err.unwrap_or(lib_err),
455        }
456    } else {
457        err.expect("Both python and libpython are invalid.")
458    }
459}
460
461fn get_interpreter_address_from_binary<P>(
462    binary: &BinaryInfo,
463    maps: &dyn ContainsAddr,
464    process: &P,
465    version: &Version,
466) -> Result<usize, Error>
467where
468    P: ProcessMemory,
469{
470    // First check the pyruntime section it was found
471    if binary.pyruntime_addr != 0 {
472        let bss = process.copy(
473            binary.pyruntime_addr as usize,
474            binary.pyruntime_size as usize,
475        )?;
476        #[allow(clippy::cast_ptr_alignment)]
477        let addrs = unsafe {
478            slice::from_raw_parts(bss.as_ptr() as *const usize, bss.len() / size_of::<usize>())
479        };
480        if let Ok(addr) = check_interpreter_addresses(addrs, maps, process, version) {
481            return Ok(addr);
482        }
483    }
484
485    // We're going to scan the BSS/data section for things, and try to narrowly scan things that
486    // look like pointers to PyinterpreterState
487    let bss = process.copy(binary.bss_addr as usize, binary.bss_size as usize)?;
488
489    #[allow(clippy::cast_ptr_alignment)]
490    let addrs = unsafe {
491        slice::from_raw_parts(bss.as_ptr() as *const usize, bss.len() / size_of::<usize>())
492    };
493    check_interpreter_addresses(addrs, maps, process, version)
494}
495
496// Checks whether a block of memory (from BSS/.data etc) contains pointers that are pointing
497// to a valid PyInterpreterState
498fn check_interpreter_addresses<P>(
499    addrs: &[usize],
500    maps: &dyn ContainsAddr,
501    process: &P,
502    version: &Version,
503) -> Result<usize, Error>
504where
505    P: ProcessMemory,
506{
507    // This function does all the work, but needs a type of the interpreter
508    fn check<I, P>(addrs: &[usize], maps: &dyn ContainsAddr, process: &P) -> Result<usize, Error>
509    where
510        I: InterpreterState,
511        P: ProcessMemory,
512    {
513        for &addr in addrs {
514            if maps.contains_addr(addr) {
515                // get the pythreadstate pointer from the interpreter object, and if it is also
516                // a valid pointer then load it up.
517                let threadstate_ptr_ptr = I::threadstate_ptr_ptr(addr);
518                let maybe_threads = process
519                    .copy_struct(threadstate_ptr_ptr as usize)
520                    .context("Failed to copy PyThreadState head pointer");
521
522                let threads: *const I::ThreadState = match maybe_threads {
523                    Ok(threads) => threads,
524                    Err(_) => continue,
525                };
526
527                if maps.contains_addr(threads as usize) {
528                    // If the threadstate points back to the interpreter like we expect, then
529                    // this is almost certainly the address of the intrepreter
530                    let thread = match process.copy_pointer(threads) {
531                        Ok(thread) => thread,
532                        Err(_) => continue,
533                    };
534
535                    // as a final sanity check, try getting the stack_traces, and only return if this works
536                    if thread.interp() as usize == addr
537                        && get_stack_traces::<I, P>(addr, process, 0, None).is_ok()
538                    {
539                        return Ok(addr);
540                    }
541                }
542            }
543        }
544        Err(format_err!(
545            "Failed to find a python interpreter in the .data section"
546        ))
547    }
548
549    // different versions have different layouts, check as appropriate
550    match version {
551        Version {
552            major: 2,
553            minor: 3..=7,
554            ..
555        } => check::<v2_7_15::_is, P>(addrs, maps, process),
556        Version {
557            major: 3, minor: 3, ..
558        } => check::<v3_3_7::_is, P>(addrs, maps, process),
559        Version {
560            major: 3,
561            minor: 4..=5,
562            ..
563        } => check::<v3_5_5::_is, P>(addrs, maps, process),
564        Version {
565            major: 3, minor: 6, ..
566        } => check::<v3_6_6::_is, P>(addrs, maps, process),
567        Version {
568            major: 3, minor: 7, ..
569        } => check::<v3_7_0::_is, P>(addrs, maps, process),
570        Version {
571            major: 3,
572            minor: 8,
573            patch: 0,
574            ..
575        } => match version.release_flags.as_ref() {
576            "a1" | "a2" | "a3" => check::<v3_7_0::_is, P>(addrs, maps, process),
577            _ => check::<v3_8_0::_is, P>(addrs, maps, process),
578        },
579        Version {
580            major: 3, minor: 8, ..
581        } => check::<v3_8_0::_is, P>(addrs, maps, process),
582        Version {
583            major: 3, minor: 9, ..
584        } => check::<v3_9_5::_is, P>(addrs, maps, process),
585        Version {
586            major: 3,
587            minor: 10,
588            ..
589        } => check::<v3_10_0::_is, P>(addrs, maps, process),
590        Version {
591            major: 3,
592            minor: 11,
593            ..
594        } => check::<v3_11_0::_is, P>(addrs, maps, process),
595        Version {
596            major: 3,
597            minor: 12,
598            ..
599        } => check::<v3_12_0::_is, P>(addrs, maps, process),
600        Version {
601            major: 3,
602            minor: 13,
603            ..
604        } => check::<v3_13_0::_is, P>(addrs, maps, process),
605        _ => Err(format_err!("Unsupported version of Python: {}", version)),
606    }
607}
608
609pub fn get_threadstate_address<P>(
610    interpreter_address: usize,
611    python_info: &PythonProcessInfo,
612    process: &P,
613    version: &Version,
614    config: &Config,
615) -> Result<usize, Error>
616where
617    P: ProcessMemory,
618{
619    let threadstate_address = match version {
620        Version {
621            major: 3,
622            minor: 13,
623            ..
624        } => {
625            let gil_ptr = interpreter_address + std::mem::offset_of!(v3_13_0::_is, ceval.gil);
626            let gil = process.copy_struct::<usize>(gil_ptr)?;
627            gil
628        }
629        Version {
630            major: 3,
631            minor: 12,
632            ..
633        } => {
634            let gil_ptr = interpreter_address + std::mem::offset_of!(v3_12_0::_is, ceval.gil);
635            let gil: usize = process.copy_struct(gil_ptr)?;
636            gil
637        }
638        Version {
639            major: 3,
640            minor: 7..=11,
641            ..
642        } => match python_info.get_symbol("_PyRuntime") {
643            Some(&addr) => {
644                if let Some(offset) = pyruntime::get_tstate_current_offset(version) {
645                    info!("Found _PyRuntime @ 0x{:016x}, getting gilstate.tstate_current from offset 0x{:x}",
646                            addr, offset);
647                    addr as usize + offset
648                } else {
649                    error_if_gil(
650                        config,
651                        version,
652                        "unknown pyruntime.gilstate.tstate_current offset",
653                    )?;
654                    0
655                }
656            }
657            None => {
658                error_if_gil(config, version, "failed to find _PyRuntime symbol")?;
659                0
660            }
661        },
662        _ => match python_info.get_symbol("_PyThreadState_Current") {
663            Some(&addr) => {
664                info!("Found _PyThreadState_Current @ 0x{:016x}", addr);
665                addr as usize
666            }
667            None => {
668                error_if_gil(
669                    config,
670                    version,
671                    "failed to find _PyThreadState_Current symbol",
672                )?;
673                0
674            }
675        },
676    };
677
678    Ok(threadstate_address)
679}
680
681fn error_if_gil(config: &Config, version: &Version, msg: &str) -> Result<(), Error> {
682    lazy_static! {
683        static ref WARNED: std::sync::atomic::AtomicBool =
684            std::sync::atomic::AtomicBool::new(false);
685    }
686
687    if config.gil_only {
688        if !WARNED.load(std::sync::atomic::Ordering::Relaxed) {
689            // only print this once
690            eprintln!(
691                "Cannot detect GIL holding in version '{}' on the current platform (reason: {})",
692                version, msg
693            );
694            eprintln!("Please open an issue in https://github.com/benfred/py-spy with the Python version and your platform.");
695            WARNED.store(true, std::sync::atomic::Ordering::Relaxed);
696        }
697        Err(format_err!(
698            "Cannot detect GIL holding in version '{}' on the current platform (reason: {})",
699            version,
700            msg
701        ))
702    } else {
703        warn!("Unable to detect GIL usage: {}", msg);
704        Ok(())
705    }
706}
707
708pub trait ContainsAddr {
709    fn contains_addr(&self, addr: usize) -> bool;
710}
711
712impl ContainsAddr for Vec<MapRange> {
713    #[cfg(windows)]
714    fn contains_addr(&self, _addr: usize) -> bool {
715        // On windows, we can't just check if a pointer is valid by looking to see if it points
716        // to something in the virtual memory map. Brute-force it instead
717        true
718    }
719
720    #[cfg(not(windows))]
721    fn contains_addr(&self, addr: usize) -> bool {
722        proc_maps::maps_contain_addr(addr, self)
723    }
724}
725
726#[cfg(target_os = "linux")]
727fn is_dockerized(pid: Pid) -> Result<bool, Error> {
728    let self_mnt = std::fs::read_link("/proc/self/ns/mnt")?;
729    let target_mnt = std::fs::read_link(format!("/proc/{}/ns/mnt", pid))?;
730    Ok(self_mnt != target_mnt)
731}
732
733// We can't use goblin to parse external symbol files (like in a separate .pdb file) on windows,
734// So use the win32 api to load up the couple of symbols we need on windows. Note:
735// we still can get export's from the PE file
736#[cfg(windows)]
737pub fn get_windows_python_symbols(
738    pid: Pid,
739    filename: &Path,
740    offset: u64,
741) -> std::io::Result<HashMap<String, u64>> {
742    use proc_maps::win_maps::SymbolLoader;
743
744    let handler = SymbolLoader::new(pid)?;
745    let _module = handler.load_module(filename)?; // need to keep this module in scope
746
747    let mut ret = HashMap::new();
748
749    // currently we only need a subset of symbols, and enumerating the symbols is
750    // expensive (via SymEnumSymbolsW), so rather than load up all symbols like we
751    // do for goblin, just load the the couple we need directly.
752    for symbol in ["_PyThreadState_Current", "interp_head", "_PyRuntime"].iter() {
753        if let Ok((base, addr)) = handler.address_from_name(symbol) {
754            // If we have a module base (ie from PDB), need to adjust by the offset
755            // otherwise seems like we can take address directly
756            let addr = if base == 0 {
757                addr
758            } else {
759                offset + addr - base
760            };
761            ret.insert(String::from(*symbol), addr);
762        }
763    }
764
765    Ok(ret)
766}
767
768#[cfg(any(target_os = "linux", target_os = "freebsd"))]
769pub fn is_python_lib(pathname: &str) -> bool {
770    lazy_static! {
771        static ref RE: Regex = Regex::new(r"/libpython\d.\d\d?(m|d|u)?.so").unwrap();
772    }
773    RE.is_match(pathname)
774}
775
776#[cfg(target_os = "macos")]
777pub fn is_python_lib(pathname: &str) -> bool {
778    lazy_static! {
779        static ref RE: Regex = Regex::new(r"/libpython\d.\d\d?(m|d|u)?.(dylib|so)$").unwrap();
780    }
781    RE.is_match(pathname) || is_python_framework(pathname)
782}
783
784#[cfg(windows)]
785pub fn is_python_lib(pathname: &str) -> bool {
786    lazy_static! {
787        static ref RE: Regex = RegexBuilder::new(r"\\python\d\d\d?(m|d|u)?.dll$")
788            .case_insensitive(true)
789            .build()
790            .unwrap();
791    }
792    RE.is_match(pathname)
793}
794
795#[cfg(target_os = "macos")]
796pub fn is_python_framework(pathname: &str) -> bool {
797    pathname.ends_with("/Python") && !pathname.contains("Python.app")
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803
804    #[cfg(target_os = "macos")]
805    #[test]
806    fn test_is_python_lib() {
807        assert!(is_python_lib("~/Anaconda2/lib/libpython2.7.dylib"));
808
809        // python lib configured with --with-pydebug (flag: d)
810        assert!(is_python_lib("/lib/libpython3.4d.dylib"));
811
812        // configured --with-pymalloc (flag: m)
813        assert!(is_python_lib("/usr/local/lib/libpython3.8m.dylib"));
814
815        // python2 configured with --with-wide-unicode (flag: u)
816        assert!(is_python_lib("./libpython2.7u.dylib"));
817
818        assert!(!is_python_lib("/libboost_python.dylib"));
819        assert!(!is_python_lib("/lib/heapq.cpython-36m-darwin.dylib"));
820    }
821
822    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
823    #[test]
824    fn test_is_python_lib() {
825        // libpython bundled by pyinstaller https://github.com/benfred/py-spy/issues/42
826        assert!(is_python_lib("/tmp/_MEIOqzg01/libpython2.7.so.1.0"));
827
828        // test debug/malloc/unicode flags
829        assert!(is_python_lib("./libpython2.7.so"));
830        assert!(is_python_lib("/usr/lib/libpython3.4d.so"));
831        assert!(is_python_lib("/usr/local/lib/libpython3.8m.so"));
832        assert!(is_python_lib("/usr/lib/libpython2.7u.so"));
833
834        // don't blindly match libraries with python in the name (boost_python etc)
835        assert!(!is_python_lib("/usr/lib/libboost_python.so"));
836        assert!(!is_python_lib(
837            "/usr/lib/x86_64-linux-gnu/libboost_python-py27.so.1.58.0"
838        ));
839        assert!(!is_python_lib("/usr/lib/libboost_python-py35.so"));
840    }
841
842    #[cfg(windows)]
843    #[test]
844    fn test_is_python_lib() {
845        assert!(is_python_lib(
846            "C:\\Users\\test\\AppData\\Local\\Programs\\Python\\Python37\\python37.dll"
847        ));
848        // .NET host via https://github.com/pythonnet/pythonnet
849        assert!(is_python_lib(
850            "C:\\Users\\test\\AppData\\Local\\Programs\\Python\\Python37\\python37.DLL"
851        ));
852    }
853
854    #[cfg(target_os = "macos")]
855    #[test]
856    fn test_python_frameworks() {
857        // homebrew v2
858        assert!(!is_python_framework("/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python"));
859        assert!(is_python_framework(
860            "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Python"
861        ));
862
863        // System python from osx 10.13.6 (high sierra)
864        assert!(!is_python_framework("/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python"));
865        assert!(is_python_framework(
866            "/System/Library/Frameworks/Python.framework/Versions/2.7/Python"
867        ));
868
869        // pyenv 3.6.6 with OSX framework enabled (https://github.com/benfred/py-spy/issues/15)
870        // env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.6.6
871        assert!(is_python_framework(
872            "/Users/ben/.pyenv/versions/3.6.6/Python.framework/Versions/3.6/Python"
873        ));
874        assert!(!is_python_framework("/Users/ben/.pyenv/versions/3.6.6/Python.framework/Versions/3.6/Resources/Python.app/Contents/MacOS/Python"));
875
876        // single file pyinstaller
877        assert!(is_python_framework(
878            "/private/var/folders/3x/qy479lpd1fb2q88lc9g4d3kr0000gn/T/_MEI2Akvi8/Python"
879        ));
880    }
881}