Skip to main content

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_14_0, v3_3_7, v3_5_5, v3_6_6,
21    v3_7_0, v3_8_0, 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    // Try getting the Py_Version symbol (points to 32 bit encoded version)
302    if let Some(&addr) = python_info.get_symbol("Py_Version") {
303        let version: u32 = process
304            .copy_struct(addr as usize)
305            .context("Failed to copy Py_Version symbol")?;
306
307        // decode u32 version via the _Py_PACK_FULL_VERSION logic
308        let major: u64 = ((version >> 24) & 0xff).into();
309        let minor: u64 = ((version >> 16) & 0xff).into();
310        let patch: u64 = ((version >> 8) & 0xff).into();
311        let release_level = (version >> 4) & 0xf;
312        let release_serial = (version) & 0xf;
313        let release_flags = match release_level {
314            0xA => format!("a{}", release_serial),
315            0xB => format!("b{}", release_serial),
316            0xC => format!("rc{}", release_serial),
317            _ => "".to_owned(),
318        };
319
320        let version = Version {
321            major,
322            minor,
323            patch,
324            release_flags,
325            build_metadata: None,
326        };
327        info!("Got version {} from Py_Version symbol", version);
328        return Ok(version);
329    }
330
331    // If possible, grab the sys.version string from the processes memory (mac osx).
332    if let Some(&addr) = python_info
333        .get_symbol("Py_GetVersion.version")
334        .or_else(|| python_info.get_symbol("version"))
335    {
336        info!("Getting version from symbol address");
337        if let Ok(bytes) = process.copy(addr as usize, 128) {
338            if let Ok(version) = Version::scan_bytes(&bytes) {
339                return Ok(version);
340            }
341        }
342    }
343
344    // otherwise get version info from scanning BSS section for sys.version string
345    if let Some(ref pb) = python_info.python_binary {
346        info!("Getting version from python binary BSS");
347        let bss = process.copy(pb.bss_addr as usize, pb.bss_size as usize)?;
348        match Version::scan_bytes(&bss) {
349            Ok(version) => return Ok(version),
350            Err(err) => info!("Failed to get version from BSS section: {}", err),
351        }
352    }
353
354    // try again if there is a libpython.so
355    if let Some(ref libpython) = python_info.libpython_binary {
356        info!("Getting version from libpython BSS");
357        let bss = process.copy(libpython.bss_addr as usize, libpython.bss_size as usize)?;
358        match Version::scan_bytes(&bss) {
359            Ok(version) => return Ok(version),
360            Err(err) => info!("Failed to get version from libpython BSS section: {}", err),
361        }
362    }
363
364    // the python_filename might have the version encoded in it (/usr/bin/python3.5 etc).
365    // try reading that in (will miss patch level on python, but that shouldn't matter)
366    info!(
367        "Trying to get version from path: {}",
368        python_info.python_filename.display()
369    );
370    let path = Path::new(&python_info.python_filename);
371    if let Some(python) = path.file_name() {
372        if let Some(python) = python.to_str() {
373            if let Some(stripped_python) = python.strip_prefix("python") {
374                let tokens: Vec<&str> = stripped_python.split('.').collect();
375                if tokens.len() >= 2 {
376                    if let (Ok(major), Ok(minor)) =
377                        (tokens[0].parse::<u64>(), tokens[1].parse::<u64>())
378                    {
379                        return Ok(Version {
380                            major,
381                            minor,
382                            patch: 0,
383                            release_flags: "".to_owned(),
384                            build_metadata: None,
385                        });
386                    }
387                }
388            }
389        }
390    }
391    Err(format_err!(
392        "Failed to find python version from target process"
393    ))
394}
395
396pub fn get_interpreter_address<P>(
397    python_info: &PythonProcessInfo,
398    process: &P,
399    version: &Version,
400) -> Result<usize, Error>
401where
402    P: ProcessMemory,
403{
404    // get the address of the main PyInterpreterState object from loaded symbols if we can
405    // (this tends to be faster than scanning through the bss section)
406    match get_interpreter_address_from_symbols(python_info, process, version) {
407        Ok(addr) => {
408            // Check that the symbol address is valid before returning
409            match check_interpreter_addresses(&[addr], &*python_info.maps, process, version) {
410                Ok(addr) => return Ok(addr),
411                Err(_) => {
412                    warn!("Interpreter address from symbol is invalid {:016x}", addr);
413                }
414            };
415        }
416        Err(err) => {
417            info!("Failed to get interpreter address from symbols {:?}, scanning BSS section from main binary", err)
418        }
419    }
420
421    // try scanning the BSS section of the binary for things that might be the interpreterstate
422    let err = if let Some(ref pb) = python_info.python_binary {
423        match get_interpreter_address_from_binary(pb, &*python_info.maps, process, version) {
424            Ok(addr) => return Ok(addr),
425            err => Some(err),
426        }
427    } else {
428        None
429    };
430
431    // Before giving up, try again if there is a libpython.so
432    if let Some(ref lpb) = python_info.libpython_binary {
433        info!("Failed to get interpreter from binary BSS, scanning libpython BSS");
434        match get_interpreter_address_from_binary(lpb, &*python_info.maps, process, version) {
435            Ok(addr) => Ok(addr),
436            lib_err => err.unwrap_or(lib_err),
437        }
438    } else {
439        err.expect("Both python and libpython are invalid.")
440    }
441}
442
443// Gets the address of the main PyInterpreterState object from loaded symbols
444fn get_interpreter_address_from_symbols<P>(
445    python_info: &PythonProcessInfo,
446    process: &P,
447    version: &Version,
448) -> Result<usize, Error>
449where
450    P: ProcessMemory,
451{
452    match version {
453        Version {
454            major: 3,
455            minor: 13..=14,
456            ..
457        } => {
458            if let Some(&pyruntime_addr) = python_info.get_symbol("_PyRuntime") {
459                // figure out the interpreters_head location using the debug_offsets
460                match version {
461                    Version {
462                        major: 3,
463                        minor: 14,
464                        ..
465                    } => {
466                        let debug_offsets: v3_14_0::_Py_DebugOffsets =
467                            process.copy_struct(pyruntime_addr as usize)?;
468                        return process
469                            .copy_struct(
470                                pyruntime_addr as usize
471                                    + debug_offsets.runtime_state.interpreters_head as usize,
472                            )
473                            .context(
474                                "Failed to copy py_debug_offsets.runtime_state.interpreters_head",
475                            );
476                    }
477                    _ => {
478                        let debug_offsets: v3_13_0::_Py_DebugOffsets =
479                            process.copy_struct(pyruntime_addr as usize)?;
480                        return process
481                            .copy_struct(
482                                pyruntime_addr as usize
483                                    + debug_offsets.runtime_state.interpreters_head as usize,
484                            )
485                            .context(
486                                "Failed to copy py_debug_offsets.runtime_state.interpreters_head",
487                            );
488                    }
489                };
490            }
491        }
492        Version {
493            major: 3,
494            minor: 7..=12,
495            ..
496        } => {
497            if let Some(&addr) = python_info.get_symbol("_PyRuntime") {
498                return process
499                    .copy_struct(addr as usize + pyruntime::get_interp_head_offset(version))
500                    .context("Failed to copy interpreters_head");
501            }
502        }
503        _ => {
504            if let Some(&addr) = python_info.get_symbol("interp_head") {
505                return process
506                    .copy_struct(addr as usize)
507                    .context("Failed to copy interp_head");
508            }
509        }
510    };
511    return Err(format_err!(
512        "Failed to find _PyRuntime address from symbols"
513    ));
514}
515
516fn get_interpreter_address_from_binary<P>(
517    binary: &BinaryInfo,
518    maps: &dyn ContainsAddr,
519    process: &P,
520    version: &Version,
521) -> Result<usize, Error>
522where
523    P: ProcessMemory,
524{
525    // First check the pyruntime section it was found
526    if binary.pyruntime_addr != 0 {
527        let bss = process.copy(
528            binary.pyruntime_addr as usize,
529            binary.pyruntime_size as usize,
530        )?;
531        #[allow(clippy::cast_ptr_alignment)]
532        let addrs = unsafe {
533            slice::from_raw_parts(bss.as_ptr() as *const usize, bss.len() / size_of::<usize>())
534        };
535        if let Ok(addr) = check_interpreter_addresses(addrs, maps, process, version) {
536            return Ok(addr);
537        }
538    }
539
540    // We're going to scan the BSS/data section for things, and try to narrowly scan things that
541    // look like pointers to PyinterpreterState
542    let bss = process.copy(binary.bss_addr as usize, binary.bss_size as usize)?;
543
544    #[allow(clippy::cast_ptr_alignment)]
545    let addrs = unsafe {
546        slice::from_raw_parts(bss.as_ptr() as *const usize, bss.len() / size_of::<usize>())
547    };
548    check_interpreter_addresses(addrs, maps, process, version)
549}
550
551// Checks whether a block of memory (from BSS/.data etc) contains pointers that are pointing
552// to a valid PyInterpreterState
553fn check_interpreter_addresses<P>(
554    addrs: &[usize],
555    maps: &dyn ContainsAddr,
556    process: &P,
557    version: &Version,
558) -> Result<usize, Error>
559where
560    P: ProcessMemory,
561{
562    // This function does all the work, but needs a type of the interpreter
563    fn check<I, P>(addrs: &[usize], maps: &dyn ContainsAddr, process: &P) -> Result<usize, Error>
564    where
565        I: InterpreterState,
566        P: ProcessMemory,
567    {
568        for &addr in addrs {
569            if maps.contains_addr(addr) {
570                // get the pythreadstate pointer from the interpreter object, and if it is also
571                // a valid pointer then load it up.
572                let threadstate_ptr_ptr = I::threadstate_ptr_ptr(addr);
573                let maybe_threads = process
574                    .copy_struct(threadstate_ptr_ptr as usize)
575                    .context("Failed to copy PyThreadState head pointer");
576
577                let threads: *const I::ThreadState = match maybe_threads {
578                    Ok(threads) => threads,
579                    Err(_) => continue,
580                };
581
582                if maps.contains_addr(threads as usize) {
583                    // If the threadstate points back to the interpreter like we expect, then
584                    // this is almost certainly the address of the intrepreter
585                    let thread = match process.copy_pointer(threads) {
586                        Ok(thread) => thread,
587                        Err(_) => continue,
588                    };
589
590                    // as a final sanity check, try getting the stack_traces, and only return if this works
591                    if thread.interp() as usize == addr
592                        && get_stack_traces::<I, P>(addr, process, 0, None).is_ok()
593                    {
594                        return Ok(addr);
595                    }
596                }
597            }
598        }
599        Err(format_err!(
600            "Failed to find a python interpreter in the .data section"
601        ))
602    }
603
604    // different versions have different layouts, check as appropriate
605    match version {
606        Version {
607            major: 2,
608            minor: 3..=7,
609            ..
610        } => check::<v2_7_15::_is, P>(addrs, maps, process),
611        Version {
612            major: 3, minor: 3, ..
613        } => check::<v3_3_7::_is, P>(addrs, maps, process),
614        Version {
615            major: 3,
616            minor: 4..=5,
617            ..
618        } => check::<v3_5_5::_is, P>(addrs, maps, process),
619        Version {
620            major: 3, minor: 6, ..
621        } => check::<v3_6_6::_is, P>(addrs, maps, process),
622        Version {
623            major: 3, minor: 7, ..
624        } => check::<v3_7_0::_is, P>(addrs, maps, process),
625        Version {
626            major: 3, minor: 8, ..
627        } => check::<v3_8_0::_is, P>(addrs, maps, process),
628        Version {
629            major: 3, minor: 9, ..
630        } => check::<v3_9_5::_is, P>(addrs, maps, process),
631        Version {
632            major: 3,
633            minor: 10,
634            ..
635        } => check::<v3_10_0::_is, P>(addrs, maps, process),
636        Version {
637            major: 3,
638            minor: 11,
639            ..
640        } => check::<v3_11_0::_is, P>(addrs, maps, process),
641        Version {
642            major: 3,
643            minor: 12,
644            ..
645        } => check::<v3_12_0::_is, P>(addrs, maps, process),
646        Version {
647            major: 3,
648            minor: 13,
649            ..
650        } => check::<v3_13_0::_is, P>(addrs, maps, process),
651        Version {
652            major: 3,
653            minor: 14,
654            ..
655        } => check::<v3_14_0::_is, P>(addrs, maps, process),
656        _ => Err(format_err!("Unsupported version of Python: {}", version)),
657    }
658}
659
660pub fn get_threadstate_address<P>(
661    interpreter_address: usize,
662    python_info: &PythonProcessInfo,
663    process: &P,
664    version: &Version,
665    config: &Config,
666) -> Result<usize, Error>
667where
668    P: ProcessMemory,
669{
670    let threadstate_address = match version {
671        Version {
672            major: 3,
673            minor: 13..=14,
674            ..
675        } => {
676            let gil_ptr = interpreter_address + std::mem::offset_of!(v3_13_0::_is, ceval.gil);
677            process.copy_struct::<usize>(gil_ptr)?
678        }
679        Version {
680            major: 3,
681            minor: 12,
682            ..
683        } => {
684            let gil_ptr = interpreter_address + std::mem::offset_of!(v3_12_0::_is, ceval.gil);
685            let gil: usize = process.copy_struct(gil_ptr)?;
686            gil
687        }
688        Version {
689            major: 3,
690            minor: 7..=11,
691            ..
692        } => match python_info.get_symbol("_PyRuntime") {
693            Some(&addr) => {
694                if let Some(offset) = pyruntime::get_tstate_current_offset(version) {
695                    info!("Found _PyRuntime @ 0x{:016x}, getting gilstate.tstate_current from offset 0x{:x}",
696                            addr, offset);
697                    addr as usize + offset
698                } else {
699                    error_if_gil(
700                        config,
701                        version,
702                        "unknown pyruntime.gilstate.tstate_current offset",
703                    )?;
704                    0
705                }
706            }
707            None => {
708                error_if_gil(config, version, "failed to find _PyRuntime symbol")?;
709                0
710            }
711        },
712        _ => match python_info.get_symbol("_PyThreadState_Current") {
713            Some(&addr) => {
714                info!("Found _PyThreadState_Current @ 0x{:016x}", addr);
715                addr as usize
716            }
717            None => {
718                error_if_gil(
719                    config,
720                    version,
721                    "failed to find _PyThreadState_Current symbol",
722                )?;
723                0
724            }
725        },
726    };
727
728    Ok(threadstate_address)
729}
730
731fn error_if_gil(config: &Config, version: &Version, msg: &str) -> Result<(), Error> {
732    lazy_static! {
733        static ref WARNED: std::sync::atomic::AtomicBool =
734            std::sync::atomic::AtomicBool::new(false);
735    }
736
737    if config.gil_only {
738        if !WARNED.load(std::sync::atomic::Ordering::Relaxed) {
739            // only print this once
740            eprintln!(
741                "Cannot detect GIL holding in version '{version}' on the current platform (reason: {msg})"
742            );
743            eprintln!("Please open an issue in https://github.com/benfred/py-spy with the Python version and your platform.");
744            WARNED.store(true, std::sync::atomic::Ordering::Relaxed);
745        }
746        Err(format_err!(
747            "Cannot detect GIL holding in version '{}' on the current platform (reason: {})",
748            version,
749            msg
750        ))
751    } else {
752        warn!("Unable to detect GIL usage: {}", msg);
753        Ok(())
754    }
755}
756
757pub trait ContainsAddr {
758    fn contains_addr(&self, addr: usize) -> bool;
759}
760
761impl ContainsAddr for Vec<MapRange> {
762    #[cfg(windows)]
763    fn contains_addr(&self, _addr: usize) -> bool {
764        // On windows, we can't just check if a pointer is valid by looking to see if it points
765        // to something in the virtual memory map. Brute-force it instead
766        true
767    }
768
769    #[cfg(not(windows))]
770    fn contains_addr(&self, addr: usize) -> bool {
771        proc_maps::maps_contain_addr(addr, self)
772    }
773}
774
775#[cfg(target_os = "linux")]
776fn is_dockerized(pid: Pid) -> Result<bool, Error> {
777    let self_mnt = std::fs::read_link("/proc/self/ns/mnt")?;
778    let target_mnt = std::fs::read_link(format!("/proc/{}/ns/mnt", pid))?;
779    Ok(self_mnt != target_mnt)
780}
781
782// We can't use goblin to parse external symbol files (like in a separate .pdb file) on windows,
783// So use the win32 api to load up the couple of symbols we need on windows. Note:
784// we still can get export's from the PE file
785#[cfg(windows)]
786pub fn get_windows_python_symbols(
787    pid: Pid,
788    filename: &Path,
789    offset: u64,
790) -> std::io::Result<HashMap<String, u64>> {
791    use proc_maps::win_maps::SymbolLoader;
792
793    let handler = SymbolLoader::new(pid)?;
794    let _module = handler.load_module(filename)?; // need to keep this module in scope
795
796    let mut ret = HashMap::new();
797
798    // currently we only need a subset of symbols, and enumerating the symbols is
799    // expensive (via SymEnumSymbolsW), so rather than load up all symbols like we
800    // do for goblin, just load the the couple we need directly.
801    for symbol in ["_PyThreadState_Current", "interp_head", "_PyRuntime"].iter() {
802        if let Ok((base, addr)) = handler.address_from_name(symbol) {
803            // If we have a module base (ie from PDB), need to adjust by the offset
804            // otherwise seems like we can take address directly
805            let addr = if base == 0 {
806                addr
807            } else {
808                offset + addr - base
809            };
810            ret.insert(String::from(*symbol), addr);
811        }
812    }
813
814    Ok(ret)
815}
816
817#[cfg(any(target_os = "linux", target_os = "freebsd"))]
818pub fn is_python_lib(pathname: &str) -> bool {
819    lazy_static! {
820        static ref RE: Regex = Regex::new(r"/libpython\d.\d\d?(m|d|u)?.so").unwrap();
821    }
822    RE.is_match(pathname)
823}
824
825#[cfg(target_os = "macos")]
826pub fn is_python_lib(pathname: &str) -> bool {
827    lazy_static! {
828        static ref RE: Regex = Regex::new(r"/libpython\d.\d\d?(m|d|u)?.(dylib|so)$").unwrap();
829    }
830    RE.is_match(pathname) || is_python_framework(pathname)
831}
832
833#[cfg(windows)]
834pub fn is_python_lib(pathname: &str) -> bool {
835    lazy_static! {
836        static ref RE: Regex = RegexBuilder::new(r"\\python\d\d\d?(m|d|u)?.dll$")
837            .case_insensitive(true)
838            .build()
839            .unwrap();
840    }
841    RE.is_match(pathname)
842}
843
844#[cfg(target_os = "macos")]
845pub fn is_python_framework(pathname: &str) -> bool {
846    pathname.ends_with("/Python") && !pathname.contains("Python.app")
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    #[cfg(target_os = "macos")]
854    #[test]
855    fn test_is_python_lib() {
856        assert!(is_python_lib("~/Anaconda2/lib/libpython2.7.dylib"));
857
858        // python lib configured with --with-pydebug (flag: d)
859        assert!(is_python_lib("/lib/libpython3.4d.dylib"));
860
861        // configured --with-pymalloc (flag: m)
862        assert!(is_python_lib("/usr/local/lib/libpython3.8m.dylib"));
863
864        // python2 configured with --with-wide-unicode (flag: u)
865        assert!(is_python_lib("./libpython2.7u.dylib"));
866
867        assert!(!is_python_lib("/libboost_python.dylib"));
868        assert!(!is_python_lib("/lib/heapq.cpython-36m-darwin.dylib"));
869    }
870
871    #[cfg(any(target_os = "linux", target_os = "freebsd"))]
872    #[test]
873    fn test_is_python_lib() {
874        // libpython bundled by pyinstaller https://github.com/benfred/py-spy/issues/42
875        assert!(is_python_lib("/tmp/_MEIOqzg01/libpython2.7.so.1.0"));
876
877        // test debug/malloc/unicode flags
878        assert!(is_python_lib("./libpython2.7.so"));
879        assert!(is_python_lib("/usr/lib/libpython3.4d.so"));
880        assert!(is_python_lib("/usr/local/lib/libpython3.8m.so"));
881        assert!(is_python_lib("/usr/lib/libpython2.7u.so"));
882
883        // don't blindly match libraries with python in the name (boost_python etc)
884        assert!(!is_python_lib("/usr/lib/libboost_python.so"));
885        assert!(!is_python_lib(
886            "/usr/lib/x86_64-linux-gnu/libboost_python-py27.so.1.58.0"
887        ));
888        assert!(!is_python_lib("/usr/lib/libboost_python-py35.so"));
889    }
890
891    #[cfg(windows)]
892    #[test]
893    fn test_is_python_lib() {
894        assert!(is_python_lib(
895            "C:\\Users\\test\\AppData\\Local\\Programs\\Python\\Python37\\python37.dll"
896        ));
897        // .NET host via https://github.com/pythonnet/pythonnet
898        assert!(is_python_lib(
899            "C:\\Users\\test\\AppData\\Local\\Programs\\Python\\Python37\\python37.DLL"
900        ));
901    }
902
903    #[cfg(target_os = "macos")]
904    #[test]
905    fn test_python_frameworks() {
906        // homebrew v2
907        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"));
908        assert!(is_python_framework(
909            "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/Python"
910        ));
911
912        // System python from osx 10.13.6 (high sierra)
913        assert!(!is_python_framework("/System/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python"));
914        assert!(is_python_framework(
915            "/System/Library/Frameworks/Python.framework/Versions/2.7/Python"
916        ));
917
918        // pyenv 3.6.6 with OSX framework enabled (https://github.com/benfred/py-spy/issues/15)
919        // env PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install 3.6.6
920        assert!(is_python_framework(
921            "/Users/ben/.pyenv/versions/3.6.6/Python.framework/Versions/3.6/Python"
922        ));
923        assert!(!is_python_framework("/Users/ben/.pyenv/versions/3.6.6/Python.framework/Versions/3.6/Resources/Python.app/Contents/MacOS/Python"));
924
925        // single file pyinstaller
926        assert!(is_python_framework(
927            "/private/var/folders/3x/qy479lpd1fb2q88lc9g4d3kr0000gn/T/_MEI2Akvi8/Python"
928        ));
929    }
930}