use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::ptr::NonNull;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use pyo3::IntoPyObjectExt;
use pyo3::class::basic::CompareOp;
use pyo3::exceptions::{PyAttributeError, PyTypeError};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyBytes, PyDict, PyList};
use record::{Diagnostic, Record};
use crate::backend::MemoryOps;
use crate::bugchecks::{analyze_bugcheck, bugcheck_from_dump_info, current_bugcheck};
use crate::dbg_backend::{ContinueDisposition, WatchpointAccess, halt_unreachable_reason};
use crate::dump_writer::{collect_dump_metadata, write_kernel_dump};
use crate::error::Error;
use crate::exception_policy::{
ExceptionPolicyFinalAction, ExceptionPolicyMode, exception_alias, parse_exception_code,
};
use crate::expr::Expr;
use crate::gdb::breakpoints::{Breakpoint as CoreBreakpoint, BreakpointConfig};
use crate::guest::ProcessInfo;
use crate::kd::KdMemorySource;
use crate::output;
use crate::phys::PhysMem;
use crate::repl::ReplState;
use crate::session::{ContinueOutcome, Session, processor_index_from_backend_thread_id};
use crate::symbols::{
FieldValue, ParsedType, TypeInfo, le_uint, parse_source_paths, parse_symbol_sources,
};
use crate::target::MemorySearchMatch as CoreMemorySearchMatch;
use crate::target::cpu::parse_msr_name;
use crate::target::heap::HeapSelector;
use crate::target::meta::decode_error_code;
use crate::target::mm::{AddressModule as CoreAddressModule, MemoryRegionInfo};
use crate::target::mm::{PfnSelector, PoolType, PoolUsageSort};
use crate::target::sched::ApcSelector;
use crate::target::{SelectedFrame, ThreadInfo};
use crate::trapframe::read_ktrap_frame_at_or_current;
use crate::triage_report::TriageReport;
use crate::types::VirtAddr;
use crate::view::{self, View};
use crate::{Backend, TargetSpec};
pub mod embed;
pub mod record;
static NEVER_CANCEL: AtomicBool = AtomicBool::new(false);
const MAX_READ_LEN: usize = 1 << 28; const MAX_SEARCH_LEN: usize = 1 << 30;
pyo3::create_exception!(_ntoseye, NtoseyeError, pyo3::exceptions::PyException);
pyo3::create_exception!(_ntoseye, MemoryAccessError, NtoseyeError);
fn err(e: Error) -> PyErr {
let msg = e.to_string();
match e {
Error::BadVirtualAddress(_)
| Error::AddressNotInDump(_)
| Error::BadPhysicalAddress(_)
| Error::PartialRead(_)
| Error::PartialWrite(_)
| Error::BufferNotEnough
| Error::InvalidRange => MemoryAccessError::new_err(msg),
_ => NtoseyeError::new_err(msg),
}
}
fn raise(msg: impl std::fmt::Display) -> PyErr {
NtoseyeError::new_err(msg.to_string())
}
fn decode_c_string(buf: &[u8]) -> String {
let end = buf.iter().position(|&b| b == 0).unwrap_or(buf.len());
buf[..end].iter().map(|&b| b as char).collect()
}
fn walk_list_bases(dbg: &Debugger, head: u64, link_offset: u64) -> PyResult<Vec<u64>> {
dbg.inner
.target
.walk_list(VirtAddr(head), link_offset)
.map_err(err)
}
fn view_record<'py>(py: Python<'py>, v: &view::View) -> PyResult<Bound<'py, Record>> {
view::to_py(py, v)?
.cast_into::<Record>()
.map_err(|e| raise(e.to_string()))
}
fn view_list<'py>(py: Python<'py>, v: &view::View) -> PyResult<Bound<'py, PyList>> {
view::to_py(py, v)?
.cast_into::<PyList>()
.map_err(|e| raise(e.to_string()))
}
fn breakpoint_id_arg(value: &Bound<'_, PyAny>, session_id: usize) -> PyResult<u32> {
if let Ok(bp) = value.extract::<PyRef<'_, Breakpoint>>() {
if bp.session_id != session_id {
return Err(raise(
"breakpoint handle belongs to a different debugger session",
));
}
return Ok(bp.id());
}
value
.extract::<u32>()
.map_err(|_| PyTypeError::new_err("expected a breakpoint id or ntoseye.Breakpoint handle"))
}
fn breakpoint_target_arg(
dbg: &Debugger,
target: &Bound<'_, PyAny>,
) -> PyResult<(u64, Option<String>)> {
if let Ok(addr) = target.extract::<u64>() {
return Ok((addr, None));
}
if let Ok(expr) = target.extract::<String>() {
let addr = Expr::eval(&expr, &dbg.inner.target).map_err(err)?.0;
return Ok((addr, Some(expr)));
}
Err(PyTypeError::new_err(
"expected a breakpoint address or debugger expression",
))
}
fn breakpoint_config_arg(
dbg: &Debugger,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
) -> PyResult<BreakpointConfig> {
let scope = process
.map(|pid| dbg.inner.breakpoint_scope_for_pid(pid))
.transpose()
.map_err(err)?;
let thread = thread
.map(|ethread| dbg.inner.breakpoint_thread_for_ethread(ethread))
.transpose()
.map_err(err)?;
Ok(BreakpointConfig {
condition,
condition_expr: None,
pass_count,
one_shot,
action,
scope,
thread,
processor,
skip_prologue: false,
})
}
fn exception_code_arg(value: &Bound<'_, PyAny>) -> PyResult<u32> {
if let Ok(code) = value.extract::<u64>() {
return u32::try_from(code)
.map_err(|_| raise(format!("exception code {code:#x} does not fit in 32 bits")));
}
let text: String = value
.extract()
.map_err(|_| PyTypeError::new_err("expected an exception code (int) or alias (str)"))?;
parse_exception_code(&text).map_err(raise)
}
fn msr_arg(value: &Bound<'_, PyAny>) -> PyResult<u32> {
if let Ok(number) = value.extract::<u64>() {
return u32::try_from(number)
.map_err(|_| raise(format!("MSR {number:#x} does not fit in 32 bits")));
}
let text: String = value
.extract()
.map_err(|_| PyTypeError::new_err("expected an MSR number or IA32_* name"))?;
parse_msr_name(&text).ok_or_else(|| raise(format!("unknown MSR name '{text}'")))
}
fn selected_frame_record<'py>(
py: Python<'py>,
frame: &SelectedFrame,
) -> PyResult<Bound<'py, Record>> {
view_record(
py,
&View::Object(vec![
("index", View::Num(frame.index as u64)),
("ip", View::Hex(frame.ip)),
("sp", View::Hex(frame.sp)),
]),
)
}
#[pyclass(unsendable)]
pub struct Debugger {
inner: SessionHandle,
}
enum SessionHandle {
Owned(Box<Session>),
Borrowed {
ptr: NonNull<Session>,
valid: Arc<AtomicBool>,
},
}
impl SessionHandle {
fn is_owned(&self) -> bool {
matches!(self, SessionHandle::Owned(_))
}
}
const STALE_BORROW: &str = "ntoseye: use of a Debugger (or a Struct/Type derived from it) after the REPL command \
that created it returned; borrowed handles are valid only inside that command and must \
not be stashed across calls";
impl Deref for SessionHandle {
type Target = Session;
fn deref(&self) -> &Session {
match self {
SessionHandle::Owned(s) => s,
SessionHandle::Borrowed { ptr, valid } => {
assert!(valid.load(Ordering::Relaxed), "{STALE_BORROW}");
unsafe { ptr.as_ref() }
}
}
}
}
impl DerefMut for SessionHandle {
fn deref_mut(&mut self) -> &mut Session {
match self {
SessionHandle::Owned(s) => s,
SessionHandle::Borrowed { ptr, valid } => {
assert!(valid.load(Ordering::Relaxed), "{STALE_BORROW}");
unsafe { ptr.as_mut() }
}
}
}
}
#[derive(Clone)]
struct BreakpointSnapshot {
id: u32,
address: Option<u64>,
enabled: bool,
resolved: bool,
deferred: bool,
specification: Option<String>,
symbol: Option<String>,
scope: String,
thread: Option<String>,
processor: Option<u16>,
condition: Option<String>,
pass_count: u64,
hit_count: u64,
remaining_pass_count: u64,
one_shot: bool,
action: Option<String>,
temporary: bool,
watch_access: Option<String>,
watch_length: Option<u8>,
}
impl BreakpointSnapshot {
fn from_core(bp: &CoreBreakpoint) -> Self {
Self {
id: bp.id,
address: bp.resolved_address().map(|address| address.0),
enabled: bp.enabled,
resolved: bp.resolved,
deferred: bp.deferred(),
specification: bp.specification().map(str::to_string),
symbol: bp.symbol.clone(),
scope: bp.scope.label(),
thread: bp.thread.as_ref().map(|thread| thread.label()),
processor: bp.processor,
condition: bp.condition.clone(),
pass_count: bp.pass_count,
hit_count: bp.hit_count,
remaining_pass_count: bp.remaining_pass_count,
one_shot: bp.one_shot,
action: bp.action.clone(),
temporary: bp.temporary,
watch_access: bp.watch_access_name().map(str::to_string),
watch_length: bp.watch_length(),
}
}
fn placeholder(id: u32, address: u64, symbol: Option<String>, temporary: bool) -> Self {
Self {
id,
address: Some(address),
enabled: true,
resolved: true,
deferred: false,
specification: None,
symbol,
scope: "unknown".to_string(),
thread: None,
processor: None,
condition: None,
pass_count: 0,
hit_count: 0,
remaining_pass_count: 0,
one_shot: false,
action: None,
temporary,
watch_access: None,
watch_length: None,
}
}
}
#[pyclass(unsendable)]
pub struct Breakpoint {
dbg: Option<Py<Debugger>>,
session_id: usize,
snapshot: BreakpointSnapshot,
}
impl Breakpoint {
fn live_snapshot(&self, py: Python<'_>) -> Option<BreakpointSnapshot> {
let dbg = self.dbg.as_ref()?.borrow(py);
if dbg.session_id() != self.session_id {
return None;
}
dbg.inner
.breakpoint(self.snapshot.id)
.map(BreakpointSnapshot::from_core)
}
fn with_current<T>(&self, py: Python<'_>, read: impl FnOnce(&BreakpointSnapshot) -> T) -> T {
match self.live_snapshot(py) {
Some(live) => read(&live),
None => read(&self.snapshot),
}
}
fn require_live_debugger<'py>(&'py self, py: Python<'py>) -> PyResult<PyRefMut<'py, Debugger>> {
let Some(dbg) = &self.dbg else {
return Err(raise(
"breakpoint handle is not attached to a live debugger",
));
};
let dbg = dbg.borrow_mut(py);
if dbg.session_id() != self.session_id {
return Err(raise(
"breakpoint handle belongs to a different debugger session",
));
}
Ok(dbg)
}
}
#[pymethods]
impl Breakpoint {
#[getter]
fn id(&self) -> u32 {
self.snapshot.id
}
#[getter]
fn address(&self, py: Python<'_>) -> Option<u64> {
self.with_current(py, |bp| bp.address)
}
#[getter]
fn symbol(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.symbol.clone())
}
#[getter]
fn scope(&self, py: Python<'_>) -> String {
self.with_current(py, |bp| bp.scope.clone())
}
#[getter]
fn thread(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.thread.clone())
}
#[getter]
fn processor(&self, py: Python<'_>) -> Option<u16> {
self.with_current(py, |bp| bp.processor)
}
#[getter]
fn condition(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.condition.clone())
}
#[getter]
fn resolved(&self, py: Python<'_>) -> bool {
self.with_current(py, |bp| bp.resolved)
}
#[getter]
fn deferred(&self, py: Python<'_>) -> bool {
self.with_current(py, |bp| bp.deferred)
}
#[getter]
fn specification(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.specification.clone())
}
#[getter]
fn pass_count(&self, py: Python<'_>) -> u64 {
self.with_current(py, |bp| bp.pass_count)
}
#[getter]
fn hit_count(&self, py: Python<'_>) -> u64 {
self.with_current(py, |bp| bp.hit_count)
}
#[getter]
fn remaining_pass_count(&self, py: Python<'_>) -> u64 {
self.with_current(py, |bp| bp.remaining_pass_count)
}
#[getter]
fn one_shot(&self, py: Python<'_>) -> bool {
self.with_current(py, |bp| bp.one_shot)
}
#[getter]
fn action(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.action.clone())
}
#[getter]
fn temporary(&self, py: Python<'_>) -> bool {
self.with_current(py, |bp| bp.temporary)
}
#[getter]
fn watchpoint(&self, py: Python<'_>) -> bool {
self.with_current(py, |bp| bp.watch_access.is_some())
}
#[getter]
fn watch_access(&self, py: Python<'_>) -> Option<String> {
self.with_current(py, |bp| bp.watch_access.clone())
}
#[getter]
fn watch_length(&self, py: Python<'_>) -> Option<u8> {
self.with_current(py, |bp| bp.watch_length)
}
#[getter]
fn valid(&self, py: Python<'_>) -> bool {
self.live_snapshot(py).is_some()
}
fn is_valid(&self, py: Python<'_>) -> bool {
self.valid(py)
}
#[getter]
fn enabled(&self, py: Python<'_>) -> bool {
self.live_snapshot(py).map(|bp| bp.enabled).unwrap_or(false)
}
#[setter]
fn set_enabled(&self, py: Python<'_>, enabled: bool) -> PyResult<()> {
if enabled {
self.enable(py)
} else {
self.disable(py)
}
}
#[setter]
fn set_condition(&self, py: Python<'_>, condition: Option<String>) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.inner
.set_breakpoint_condition(self.snapshot.id, condition)
.map_err(err)
}
#[setter]
fn set_pass_count(&self, py: Python<'_>, pass_count: u64) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.inner
.breakpoints
.set_pass_count(self.snapshot.id, pass_count)
.map_err(err)
}
#[setter]
fn set_action(&self, py: Python<'_>, action: Option<String>) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.inner
.breakpoints
.set_action(self.snapshot.id, action)
.map_err(err)
}
#[setter]
fn set_one_shot(&self, py: Python<'_>, one_shot: bool) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.inner
.breakpoints
.set_one_shot(self.snapshot.id, one_shot)
.map_err(err)
}
fn clear(&self, py: Python<'_>) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.require_halted("breakpoint.clear")?;
dbg.inner.remove_breakpoint(self.snapshot.id).map_err(err)
}
fn delete(&self, py: Python<'_>) -> PyResult<()> {
self.clear(py)
}
fn enable(&self, py: Python<'_>) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.require_halted("breakpoint.enable")?;
dbg.inner.enable_breakpoint(self.snapshot.id).map_err(err)
}
fn disable(&self, py: Python<'_>) -> PyResult<()> {
let mut dbg = self.require_live_debugger(py)?;
dbg.require_halted("breakpoint.disable")?;
dbg.inner.disable_breakpoint(self.snapshot.id).map_err(err)
}
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let enabled = self.enabled(py);
self.with_current(py, |snapshot| {
let d = PyDict::new(py);
d.set_item("id", snapshot.id)?;
d.set_item("address", snapshot.address)?;
d.set_item("enabled", enabled)?;
d.set_item("resolved", snapshot.resolved)?;
d.set_item("deferred", snapshot.deferred)?;
d.set_item("specification", &snapshot.specification)?;
d.set_item("symbol", &snapshot.symbol)?;
d.set_item("scope", &snapshot.scope)?;
d.set_item("condition", &snapshot.condition)?;
d.set_item("pass_count", snapshot.pass_count)?;
d.set_item("hit_count", snapshot.hit_count)?;
d.set_item("remaining_pass_count", snapshot.remaining_pass_count)?;
d.set_item("one_shot", snapshot.one_shot)?;
d.set_item("action", &snapshot.action)?;
d.set_item("temporary", snapshot.temporary)?;
d.set_item("watchpoint", snapshot.watch_access.is_some())?;
d.set_item("watch_access", &snapshot.watch_access)?;
d.set_item("watch_length", snapshot.watch_length)?;
Ok(d)
})
}
fn __richcmp__(&self, other: PyRef<'_, Breakpoint>, op: CompareOp) -> bool {
let equal = self.session_id == other.session_id && self.snapshot.id == other.snapshot.id;
match op {
CompareOp::Eq => equal,
CompareOp::Ne => !equal,
_ => false,
}
}
fn __hash__(&self) -> isize {
(self.session_id as isize).wrapping_mul(31) ^ self.snapshot.id as isize
}
fn __repr__(&self, py: Python<'_>) -> String {
let state = if self.valid(py) { "valid" } else { "invalid" };
self.with_current(py, |snapshot| {
let kind = if snapshot.watch_access.is_some() {
"Watchpoint"
} else {
"Breakpoint"
};
let location = snapshot
.address
.map(|address| format!("at {address:#x}"))
.unwrap_or_else(|| "deferred".to_string());
let symbol = snapshot
.symbol
.as_ref()
.map(|symbol| format!(" {symbol}"))
.unwrap_or_default();
format!("<{kind} #{} {location}{symbol} {state}>", snapshot.id)
})
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
enum StopKind {
Breakpoint,
Watchpoint,
Bugcheck,
Exception,
Step,
TargetReloaded,
#[default]
Running,
Halted,
}
impl StopKind {
fn as_str(self) -> &'static str {
match self {
Self::Breakpoint => "breakpoint",
Self::Watchpoint => "watchpoint",
Self::Bugcheck => "bugcheck",
Self::Exception => "exception",
Self::Step => "step",
Self::TargetReloaded => "target_reloaded",
Self::Running => "running",
Self::Halted => "halted",
}
}
}
#[derive(Default)]
struct StopOutcomeData {
kind: StopKind,
rip: Option<u64>,
symbol: Option<String>,
attached_process: Option<ProcessInfo>,
stopped_process: Option<ProcessInfo>,
stopped_thread: Option<ThreadInfo>,
breakpoints: Vec<BreakpointSnapshot>,
address: Option<u64>,
temporary: Option<bool>,
condition_error: Option<String>,
exception_code: Option<u32>,
first_chance: Option<bool>,
exception_address: Option<u64>,
bugcheck_info: Option<Py<PyAny>>,
kernel_base: Option<u64>,
coherent: Option<bool>,
}
#[pyclass(unsendable)]
pub struct StopOutcome {
dbg: Py<Debugger>,
session_id: usize,
data: StopOutcomeData,
}
impl StopOutcome {
fn breakpoint_handles(&self, py: Python<'_>) -> Vec<Breakpoint> {
self.data
.breakpoints
.iter()
.cloned()
.map(|snapshot| Breakpoint {
dbg: Some(self.dbg.clone_ref(py)),
session_id: self.session_id,
snapshot,
})
.collect()
}
}
#[pyclass(unsendable, skip_from_py_object)]
#[derive(Clone)]
pub struct AddressModule {
name: String,
base: u64,
size: u32,
offset: u64,
}
impl From<CoreAddressModule> for AddressModule {
fn from(module: CoreAddressModule) -> Self {
Self {
name: module.name,
base: module.base.0,
size: module.size,
offset: module.offset,
}
}
}
#[pymethods]
impl AddressModule {
#[getter]
fn name(&self) -> &str {
&self.name
}
#[getter]
fn base(&self) -> u64 {
self.base
}
#[getter]
fn size(&self) -> u32 {
self.size
}
#[getter]
fn offset(&self) -> u64 {
self.offset
}
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let d = PyDict::new(py);
d.set_item("name", &self.name)?;
d.set_item("base", self.base)?;
d.set_item("size", self.size)?;
d.set_item("offset", self.offset)?;
Ok(d)
}
fn __repr__(&self) -> String {
format!("<AddressModule {} base={:#x}>", self.name, self.base)
}
}
#[pyclass(unsendable, skip_from_py_object)]
#[derive(Clone)]
pub struct MemoryRegion {
start: u64,
end: u64,
protection: Option<u64>,
vad_type: Option<u64>,
private_memory: Option<bool>,
commit_charge: Option<u64>,
details: Option<String>,
}
impl From<MemoryRegionInfo> for MemoryRegion {
fn from(region: MemoryRegionInfo) -> Self {
Self {
start: region.start.0,
end: region.end.0,
protection: region.protection,
vad_type: region.vad_type,
private_memory: region.private_memory,
commit_charge: region.commit_charge,
details: region.details,
}
}
}
#[pymethods]
impl MemoryRegion {
#[getter]
fn start(&self) -> u64 {
self.start
}
#[getter]
fn end(&self) -> u64 {
self.end
}
#[getter]
fn protection(&self) -> Option<u64> {
self.protection
}
#[getter]
fn vad_type(&self) -> Option<u64> {
self.vad_type
}
#[getter]
fn private_memory(&self) -> Option<bool> {
self.private_memory
}
#[getter]
fn commit_charge(&self) -> Option<u64> {
self.commit_charge
}
#[getter]
fn details(&self) -> Option<String> {
self.details.clone()
}
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let d = PyDict::new(py);
d.set_item("start", self.start)?;
d.set_item("end", self.end)?;
d.set_item("protection", self.protection)?;
d.set_item("vad_type", self.vad_type)?;
d.set_item("private_memory", self.private_memory)?;
d.set_item("commit_charge", self.commit_charge)?;
d.set_item("details", self.details.clone())?;
Ok(d)
}
fn __repr__(&self) -> String {
format!("<MemoryRegion {:#x}..{:#x}>", self.start, self.end)
}
}
#[pyclass(unsendable, skip_from_py_object)]
#[derive(Clone)]
pub struct MemorySearchMatch {
address: u64,
offset: u64,
symbol: Option<String>,
kind: String,
module: Option<AddressModule>,
section: Option<String>,
va_type: Option<String>,
region: Option<MemoryRegion>,
}
impl MemorySearchMatch {
fn from_core(hit: CoreMemorySearchMatch) -> Self {
Self {
address: hit.address.0,
offset: hit.offset,
symbol: hit.symbol,
kind: hit.description.kind.to_string(),
module: hit.description.module.map(AddressModule::from),
section: hit.description.section,
va_type: hit.description.va_type,
region: hit.description.region.map(MemoryRegion::from),
}
}
}
#[pymethods]
impl MemorySearchMatch {
#[getter]
fn address(&self) -> u64 {
self.address
}
#[getter]
fn offset(&self) -> u64 {
self.offset
}
#[getter]
fn symbol(&self) -> Option<String> {
self.symbol.clone()
}
#[getter]
fn kind(&self) -> &str {
&self.kind
}
#[getter]
fn module(&self) -> Option<AddressModule> {
self.module.clone()
}
#[getter]
fn section(&self) -> Option<String> {
self.section.clone()
}
#[getter]
fn va_type(&self) -> Option<String> {
self.va_type.clone()
}
#[getter]
fn region(&self) -> Option<MemoryRegion> {
self.region.clone()
}
fn to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let d = PyDict::new(py);
d.set_item("address", self.address)?;
d.set_item("offset", self.offset)?;
d.set_item("symbol", self.symbol.clone())?;
d.set_item("kind", &self.kind)?;
if let Some(module) = &self.module {
d.set_item("module", module.to_dict(py)?)?;
} else {
d.set_item("module", py.None())?;
}
d.set_item("section", self.section.clone())?;
d.set_item("va_type", self.va_type.clone())?;
if let Some(region) = &self.region {
d.set_item("region", region.to_dict(py)?)?;
} else {
d.set_item("region", py.None())?;
}
Ok(d)
}
fn __repr__(&self) -> String {
match &self.symbol {
Some(symbol) => format!("<MemorySearchMatch {:#x} {}>", self.address, symbol),
None => format!("<MemorySearchMatch {:#x}>", self.address),
}
}
}
#[pymethods]
impl StopOutcome {
#[getter]
fn reason(&self) -> &'static str {
self.data.kind.as_str()
}
#[getter]
fn running(&self) -> bool {
self.data.kind == StopKind::Running
}
#[getter]
fn breakpoint_stop(&self) -> bool {
matches!(self.data.kind, StopKind::Breakpoint | StopKind::Watchpoint)
}
#[getter]
fn watchpoint(&self) -> bool {
self.data.kind == StopKind::Watchpoint
}
#[getter]
fn exception(&self) -> bool {
self.data.kind == StopKind::Exception
}
#[getter]
fn step(&self) -> bool {
self.data.kind == StopKind::Step
}
#[getter]
fn bugcheck(&self) -> bool {
self.data.kind == StopKind::Bugcheck
}
#[getter]
fn target_reloaded(&self) -> bool {
self.data.kind == StopKind::TargetReloaded
}
#[getter]
fn halted(&self) -> bool {
self.data.kind == StopKind::Halted
}
#[getter]
fn terminal(&self) -> bool {
self.bugcheck() || self.target_reloaded()
}
#[getter]
fn rip(&self) -> Option<u64> {
self.data.rip
}
#[getter]
fn symbol(&self) -> Option<String> {
self.data.symbol.clone()
}
#[getter]
fn attached_process<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.data
.attached_process
.as_ref()
.map(|p| view_record(py, &view::process(p)))
.transpose()
}
#[getter]
fn stopped_process<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.data
.stopped_process
.as_ref()
.map(|p| view_record(py, &view::process(p)))
.transpose()
}
#[getter]
fn stopped_thread<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.data
.stopped_thread
.as_ref()
.map(|t| view_record(py, &view::thread(t, None)))
.transpose()
}
#[getter]
fn breakpoints(&self, py: Python<'_>) -> Vec<Breakpoint> {
self.breakpoint_handles(py)
}
#[getter]
fn breakpoint(&self, py: Python<'_>) -> Option<Breakpoint> {
self.breakpoint_handles(py).into_iter().next()
}
#[getter]
fn breakpoint_ids(&self) -> Vec<u32> {
self.data.breakpoints.iter().map(|bp| bp.id).collect()
}
#[getter]
fn breakpoint_id(&self) -> Option<u32> {
self.data.breakpoints.first().map(|bp| bp.id)
}
#[getter]
fn address(&self) -> Option<u64> {
self.data.address
}
#[getter]
fn temporary(&self) -> Option<bool> {
self.data.temporary
}
#[getter]
fn condition_error(&self) -> Option<String> {
self.data.condition_error.clone()
}
#[getter]
fn exception_code(&self) -> Option<u32> {
self.data.exception_code
}
#[getter]
fn first_chance(&self) -> Option<bool> {
self.data.first_chance
}
#[getter]
fn exception_address(&self) -> Option<u64> {
self.data.exception_address
}
#[getter]
fn bugcheck_info(&self, py: Python<'_>) -> Option<Py<PyAny>> {
self.data
.bugcheck_info
.as_ref()
.map(|info| info.clone_ref(py))
}
#[getter]
fn kernel_base(&self) -> Option<u64> {
self.data.kernel_base
}
#[getter]
fn coherent(&self) -> Option<bool> {
self.data.coherent
}
fn __repr__(&self) -> String {
match self.data.rip {
Some(rip) => format!("<StopOutcome {} rip={:#x}>", self.reason(), rip),
None => format!("<StopOutcome {}>", self.reason()),
}
}
}
#[pymethods]
impl Debugger {
fn read<'py>(&self, py: Python<'py>, addr: u64, len: usize) -> PyResult<Bound<'py, PyBytes>> {
if len > MAX_READ_LEN {
return Err(raise(format!(
"read length {len} exceeds cap {MAX_READ_LEN} (0x{MAX_READ_LEN:x})"
)));
}
let mut buf = vec![0u8; len];
self.inner
.read_masked(VirtAddr(addr), &mut buf)
.map_err(err)?;
Ok(PyBytes::new(py, &buf))
}
fn search(&self, start: u64, pattern: &[u8], length: usize) -> PyResult<Vec<u64>> {
if length > MAX_SEARCH_LEN {
return Err(raise(format!(
"search length {length} exceeds cap {MAX_SEARCH_LEN} (0x{MAX_SEARCH_LEN:x})"
)));
}
self.inner
.target
.search(VirtAddr(start), pattern, length)
.map_err(err)
}
fn search_details(
&self,
start: u64,
pattern: &[u8],
length: usize,
) -> PyResult<Vec<MemorySearchMatch>> {
if length > MAX_SEARCH_LEN {
return Err(raise(format!(
"search length {length} exceeds cap {MAX_SEARCH_LEN} (0x{MAX_SEARCH_LEN:x})"
)));
}
self.inner
.target
.search_details(VirtAddr(start), pattern, length)
.map(|matches| {
matches
.into_iter()
.map(MemorySearchMatch::from_core)
.collect()
})
.map_err(err)
}
fn write(&self, addr: u64, data: &[u8]) -> PyResult<()> {
self.inner
.target
.current_process()
.map_err(err)?
.memory()
.write_bytes(VirtAddr(addr), data)
.map_err(err)
}
fn read_u8(&self, addr: u64) -> PyResult<u8> {
Ok(self.read_fixed::<1>(addr)?[0])
}
fn read_u16(&self, addr: u64) -> PyResult<u16> {
Ok(u16::from_le_bytes(self.read_fixed::<2>(addr)?))
}
fn read_u32(&self, addr: u64) -> PyResult<u32> {
Ok(u32::from_le_bytes(self.read_fixed::<4>(addr)?))
}
fn read_u64(&self, addr: u64) -> PyResult<u64> {
Ok(u64::from_le_bytes(self.read_fixed::<8>(addr)?))
}
fn write_u8(&self, addr: u64, value: u8) -> PyResult<()> {
self.write(addr, &value.to_le_bytes())
}
fn write_u16(&self, addr: u64, value: u16) -> PyResult<()> {
self.write(addr, &value.to_le_bytes())
}
fn write_u32(&self, addr: u64, value: u32) -> PyResult<()> {
self.write(addr, &value.to_le_bytes())
}
fn write_u64(&self, addr: u64, value: u64) -> PyResult<()> {
self.write(addr, &value.to_le_bytes())
}
#[getter]
fn pointer_size(&self) -> u64 {
8
}
fn read_pointer(&self, addr: u64) -> PyResult<u64> {
self.read_u64(addr)
}
#[pyo3(signature = (addr, max_len=256))]
fn read_string(&self, addr: u64, max_len: usize) -> PyResult<String> {
self.inner
.target
.read_c_string(VirtAddr(addr), max_len)
.map_err(err)
}
#[pyo3(signature = (addr, max_len=256))]
fn read_wstring(&self, addr: u64, max_len: usize) -> PyResult<String> {
let mut units = Vec::with_capacity(max_len.min(256));
let mut cursor = addr;
while units.len() < max_len {
let unit = self.read_u16(cursor)?;
if unit == 0 {
break;
}
units.push(unit);
cursor += 2;
}
Ok(String::from_utf16_lossy(&units))
}
fn read_unicode_string(&self, addr: u64) -> PyResult<String> {
self.inner
.target
.read_unicode_string(VirtAddr(addr))
.map_err(err)
}
fn read_ansi_string(&self, addr: u64) -> PyResult<String> {
let length = self.read_u16(addr)?;
let buffer = self.read_pointer(addr + 8)?;
if length == 0 || buffer == 0 {
return Ok(String::new());
}
let mut buf = vec![0u8; usize::from(length)];
self.inner
.read_masked(VirtAddr(buffer), &mut buf)
.map_err(err)?;
Ok(buf.iter().map(|&b| b as char).collect())
}
fn eval(&self, expr: &str) -> PyResult<u64> {
Expr::eval(expr, &self.inner.target)
.map(|v| v.0)
.map_err(err)
}
fn read_register(&mut self, name: &str) -> PyResult<u64> {
self.require_halted("read_register")?;
let regs = self.inner.read_registers().map_err(err)?;
self.inner.register_map.read_u64(name, ®s).map_err(err)
}
fn registers(&mut self) -> PyResult<HashMap<String, u64>> {
self.require_halted("registers")?;
let regs = self.inner.read_registers().map_err(err)?;
Ok(self.inner.register_map.to_hashmap(®s))
}
fn write_register(&mut self, name: &str, value: u64) -> PyResult<()> {
self.require_halted("write_register")?;
self.inner.write_register(name, value).map_err(err)
}
#[pyo3(signature = (disposition = "handled"))]
fn cont(&mut self, disposition: &str) -> PyResult<()> {
let disposition = disposition.parse::<ContinueDisposition>().map_err(err)?;
self.inner.resume_with_disposition(disposition).map_err(err)
}
#[pyo3(signature = (timeout_ms=None))]
fn wait_for_stop<'py>(
slf: Bound<'py, Self>,
py: Python<'py>,
timeout_ms: Option<u64>,
) -> PyResult<StopOutcome> {
Self::stop_outcome(slf, py, |dbg| match timeout_ms {
Some(ms) => dbg
.inner
.wait_for_stop_bounded(Some(Duration::from_millis(ms)), &NEVER_CANCEL)
.map_err(err),
None => Self::wait_until_stop(py, |slice| {
dbg.inner
.wait_for_stop_bounded(Some(slice), &NEVER_CANCEL)
.map_err(err)
}),
})
}
#[pyo3(signature = (timeout_ms=None, *, disposition = "handled"))]
fn run<'py>(
slf: Bound<'py, Self>,
py: Python<'py>,
timeout_ms: Option<u64>,
disposition: &str,
) -> PyResult<StopOutcome> {
let disposition = disposition.parse::<ContinueDisposition>().map_err(err)?;
Self::stop_outcome(slf, py, |dbg| match timeout_ms {
Some(ms) => dbg
.inner
.continue_until_break(Some(Duration::from_millis(ms)), &NEVER_CANCEL, disposition)
.map_err(err),
None => Self::wait_until_stop(py, |slice| {
dbg.inner
.continue_until_break(Some(slice), &NEVER_CANCEL, disposition)
.map_err(err)
}),
})
}
fn step<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
Self::stop_outcome(slf, py, |dbg| {
dbg.require_halted("step")?;
dbg.inner.step().map_err(err)?;
let regs = dbg.inner.read_registers().map_err(err)?;
let rip = dbg.inner.register_map.read_u64("rip", ®s).map_err(err)?;
Ok(ContinueOutcome::Step { rip })
})
}
fn step_over<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
Self::stop_outcome(slf, py, |dbg| {
dbg.require_halted("step_over")?;
dbg.inner.step_over(&NEVER_CANCEL).map_err(err)
})
}
fn step_out<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
Self::stop_outcome(slf, py, |dbg| {
dbg.require_halted("step_out")?;
dbg.inner.step_out(&NEVER_CANCEL).map_err(err)
})
}
fn interrupt(&mut self) -> PyResult<()> {
self.inner.interrupt().map(|_| ()).map_err(err)
}
#[pyo3(signature = (addr, process=None))]
fn page_in(&mut self, addr: u64, process: Option<u64>) -> PyResult<bool> {
self.require_halted("page_in")?;
let report = self.inner.page_in(VirtAddr(addr), process).map_err(err)?;
if !report.from_worker {
return Err(err(Error::DebugInfo(
"the target stopped for another reason before the worker reported".into(),
)));
}
Ok(report.resident)
}
fn set_current_thread(&mut self, thread: &str) -> PyResult<()> {
self.inner.set_current_thread(thread).map_err(err)
}
#[getter]
fn current_thread(&self) -> String {
self.inner.current_thread.clone()
}
#[pyo3(signature = (thread))]
fn select_thread(&mut self, thread: Option<u64>) -> PyResult<Option<String>> {
self.require_halted("select_thread")?;
let Some(value) = thread else {
self.inner.reset_windows_thread().map_err(err)?;
return Ok(None);
};
let info = self.inner.find_windows_thread(value).map_err(err)?;
self.inner.select_windows_thread(&info).map_err(err)
}
fn selected_thread<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.inner
.target
.windows_thread_selection
.as_ref()
.map(|thread| view_record(py, &view::thread(thread, None)))
.transpose()
}
fn thread(slf: Bound<'_, Self>, thread: u64) -> PyResult<Struct> {
let (info, ethread) = {
let mut dbg = slf.borrow_mut();
let found = dbg.inner.find_windows_thread(thread).map_err(err)?;
(dbg.resolve_type("_ETHREAD")?, found.ethread.0)
};
Ok(Struct {
dbg: slf.unbind(),
name: "_ETHREAD".to_string(),
info,
base: ethread,
})
}
#[pyo3(signature = (index))]
fn select_frame<'py>(
&mut self,
py: Python<'py>,
index: Option<usize>,
) -> PyResult<Option<Bound<'py, Record>>> {
self.require_halted("select_frame")?;
let Some(index) = index else {
self.inner.clear_selected_frame();
return Ok(None);
};
let frame = self.inner.select_frame_index(index).map_err(err)?;
Ok(Some(selected_frame_record(py, &frame)?))
}
fn selected_frame<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.inner
.target
.selected_frame
.as_ref()
.map(|frame| selected_frame_record(py, frame))
.transpose()
}
#[pyo3(signature = (thread, limit=64))]
fn backtrace_thread<'py>(
&mut self,
py: Python<'py>,
thread: u64,
limit: usize,
) -> PyResult<Bound<'py, Record>> {
let info = self.inner.find_windows_thread(thread).map_err(err)?;
let trace = self
.inner
.backtrace_thread(&info, limit.clamp(1, 4096))
.map_err(err)?;
let frames = View::List(
trace
.stacktrace
.frames
.iter()
.map(view::stack_frame)
.collect(),
);
view_record(
py,
&View::Object(vec![
("ethread", View::Hex(info.ethread.0)),
("tid", View::OptNum(info.tid)),
(
"source",
View::Str(format!("{:?}", trace.source).to_ascii_lowercase()),
),
("frames", frames),
]),
)
}
#[pyo3(signature = (target, timeout_ms=None))]
fn run_to<'py>(
slf: Bound<'py, Self>,
py: Python<'py>,
target: &Bound<'_, PyAny>,
timeout_ms: Option<u64>,
) -> PyResult<StopOutcome> {
let (addr, _) = breakpoint_target_arg(&slf.borrow(), target)?;
Self::stop_outcome(slf, py, |dbg| {
dbg.require_halted("run_to")?;
match timeout_ms {
Some(ms) => {
let deadline = std::time::Instant::now() + Duration::from_millis(ms);
let cancel = AtomicBool::new(false);
std::thread::scope(|scope| {
let cancel_ref = &cancel;
let handle = scope.spawn(move || {
while std::time::Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(20));
if cancel_ref.load(Ordering::Relaxed) {
return;
}
}
cancel_ref.store(true, Ordering::Relaxed);
});
let outcome = dbg.inner.run_to(VirtAddr(addr), &cancel).map_err(err);
cancel.store(true, Ordering::Relaxed);
let _ = handle.join();
match outcome {
Ok(ContinueOutcome::Running) if !dbg.inner.backend.is_running() => {
let rip = dbg
.inner
.read_registers()
.ok()
.and_then(|r| dbg.inner.register_map.read_u64("rip", &r).ok())
.unwrap_or(0);
Ok(ContinueOutcome::Halted { rip })
}
other => other,
}
})
}
None => dbg.inner.run_to(VirtAddr(addr), &NEVER_CANCEL).map_err(err),
}
})
}
#[pyo3(signature = (code, mode, *, disposition=None))]
fn set_exception_policy(
&mut self,
code: &Bound<'_, PyAny>,
mode: &str,
disposition: Option<&str>,
) -> PyResult<()> {
let code = exception_code_arg(code)?;
let mode = match mode {
"break" => ExceptionPolicyMode::Break,
"second_chance" => ExceptionPolicyMode::SecondChance,
"notify" => ExceptionPolicyMode::Notify,
"ignore" => ExceptionPolicyMode::Ignore,
other => {
return Err(raise(format!(
"unknown exception policy mode '{other}' (break, second_chance, notify, ignore)"
)));
}
};
let final_action = disposition
.map(|text| text.parse::<ContinueDisposition>().map_err(err))
.transpose()?
.map(ExceptionPolicyFinalAction::Continue);
self.inner
.exception_policies
.set_with_options(code, mode, None, final_action);
Ok(())
}
fn exception_policies<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let rows = self
.inner
.exception_policies
.entries()
.map(|(code, policy)| {
let mode = match policy.mode {
ExceptionPolicyMode::Break => "break",
ExceptionPolicyMode::SecondChance => "second_chance",
ExceptionPolicyMode::Notify => "notify",
ExceptionPolicyMode::Ignore => "ignore",
};
let disposition = match policy.final_action {
Some(ExceptionPolicyFinalAction::Continue(disposition)) => {
Some(disposition.name().to_string())
}
Some(ExceptionPolicyFinalAction::Break) => Some("break".to_string()),
None => None,
};
View::Object(vec![
("code", View::Hex(u64::from(code))),
(
"alias",
View::OptStr(exception_alias(code).map(str::to_string)),
),
("mode", View::Str(mode.to_string())),
("disposition", View::OptStr(disposition)),
("command", View::OptStr(policy.command.clone())),
])
})
.collect();
view_list(py, &View::List(rows))
}
fn reset_exception_policies(&mut self) {
self.inner.exception_policies.reset();
}
fn is_running(&self) -> bool {
self.inner.backend.is_running()
}
fn status<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
view_record(py, &view::run_status(&self.inner.run_status()))
}
fn bugcheck<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
match current_bugcheck(&self.inner.target)
.or_else(|| bugcheck_from_dump_info(&self.inner.target))
{
Some(analysis) => Ok(Some(view_record(py, &view::bugcheck(&analysis))?)),
None => Ok(None),
}
}
fn triage<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let report = TriageReport::build(&mut self.inner);
view_record(py, &view::triage_report(&report, usize::MAX))
}
fn reload(&mut self) -> PyResult<()> {
self.inner.reload().map_err(err)
}
#[getter]
fn symbol_path(&self) -> Vec<String> {
self.inner
.target
.symbols
.symbol_sources()
.iter()
.map(ToString::to_string)
.collect()
}
#[setter]
fn set_symbol_path(&self, sources: Vec<String>) {
self.inner
.target
.symbols
.set_symbol_sources(parse_symbol_sources(&sources));
}
fn add_symbol_path(&self, source: &str) {
for source in parse_symbol_sources(&[source]) {
self.inner.target.symbols.append_symbol_source(source);
}
}
fn reset_symbol_path(&self) {
self.inner.target.symbols.reset_symbol_sources();
}
#[getter]
fn source_path(&self) -> Vec<String> {
self.inner
.target
.symbols
.source_paths()
.iter()
.map(ToString::to_string)
.collect()
}
#[setter]
fn set_source_path(&self, paths: Vec<String>) {
self.inner
.target
.symbols
.set_source_paths(parse_source_paths(&paths));
}
fn add_source_path(&self, mapping: &str) {
for mapping in parse_source_paths(&[mapping]) {
self.inner.target.symbols.append_source_path(mapping);
}
}
#[pyo3(signature = (module=None))]
fn reload_symbols<'py>(
&mut self,
py: Python<'py>,
module: Option<&str>,
) -> PyResult<Bound<'py, Record>> {
let report = self
.inner
.target
.reload_module_symbols(module)
.map_err(err)?;
let session: &mut Session = &mut self.inner;
session
.breakpoints
.resolve_symbolic(session.backend.as_mut(), &session.target)
.map_err(err)?;
view_record(py, &view::module_symbol_report(&report))
}
fn write_dump(&mut self, py: Python<'_>, path: &str) -> PyResult<u64> {
self.require_halted("write_dump")?;
if matches!(&*self.inner.target.phys, PhysMem::Dmp(_)) {
return Err(raise("write_dump is not applicable to a static crash dump"));
}
let metadata = collect_dump_metadata(&mut self.inner).map_err(err)?;
let memory = &*self.inner.target.phys;
let interrupted = AtomicBool::new(false);
let unreadable = py.detach(|| {
write_kernel_dump(
path,
memory,
&metadata,
|| interrupted.load(Ordering::Relaxed),
|| {},
)
});
unreadable.map_err(err)
}
#[pyo3(signature = (filter=None))]
fn processes(slf: Bound<'_, Self>, filter: Option<String>) -> PyResult<Vec<Struct>> {
let (info, addrs) = {
let dbg = slf.borrow();
let info = dbg.resolve_type("_EPROCESS")?;
let addrs: Vec<u64> = dbg
.inner
.target
.matching_processes(filter.as_deref())
.map_err(err)?
.iter()
.map(|p| p.eprocess_va.0)
.collect();
(info, addrs)
};
Ok(addrs
.into_iter()
.map(|base| Struct {
dbg: slf.clone().unbind(),
name: "_EPROCESS".to_string(),
info: Arc::clone(&info),
base,
})
.collect())
}
fn process(slf: Bound<'_, Self>, target: &str) -> PyResult<Struct> {
let mut matches = Self::processes(slf, Some(target.to_string()))?;
match matches.len() {
0 => Err(raise(format!("no process matches '{target}'"))),
1 => Ok(matches.pop().unwrap()),
n => Err(raise(format!(
"'{target}' is ambiguous ({n} matches); use a pid or processes(filter)"
))),
}
}
fn symbol_candidates<'py>(&self, py: Python<'py>, name: &str) -> PyResult<Bound<'py, PyList>> {
let rows = self
.inner
.target
.symbol_candidates(name)
.iter()
.map(view::symbol_candidate)
.collect();
view_list(py, &view::View::List(rows))
}
fn nearest_symbol<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let address = VirtAddr(addr);
view_record(
py,
&view::nearest_symbol(
address,
self.inner.target.nearest_symbol_current_context(address),
),
)
}
#[pyo3(signature = (query, limit = 50))]
fn search_symbols<'py>(
&self,
py: Python<'py>,
query: &str,
limit: usize,
) -> PyResult<Bound<'py, PyList>> {
if !(1..=500).contains(&limit) {
return Err(raise("limit must be in range 1-500"));
}
let rows = self
.inner
.target
.search_symbols(query, limit)
.iter()
.map(view::symbol_search_match)
.collect();
view_list(py, &view::View::List(rows))
}
fn source_location<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Option<Bound<'py, Record>>> {
self.inner
.target
.source_location(VirtAddr(addr))
.map(|location| view_record(py, &view::source_location(&location)))
.transpose()
}
fn source_addresses(&self, file: &str, line: u32) -> Vec<u64> {
self.inner
.target
.source_addresses(file, line)
.into_iter()
.map(|address| address.0)
.collect()
}
#[pyo3(signature = (addr=None))]
fn procedure_locals<'py>(
&mut self,
py: Python<'py>,
addr: Option<u64>,
) -> PyResult<Bound<'py, PyList>> {
let address = match addr {
Some(addr) => VirtAddr(addr),
None => match self.inner.target.selected_frame.as_ref() {
Some(frame) => VirtAddr(frame.ip),
None => {
self.require_halted("procedure_locals")?;
let regs = self.inner.read_registers().map_err(err)?;
VirtAddr(
self.inner
.register_map
.read_u64("rip", ®s)
.map_err(err)?,
)
}
},
};
let locals = self
.inner
.target
.procedure_locals(address)
.map_err(err)?
.unwrap_or_default();
let rows = locals
.iter()
.map(|local| view::procedure_local(&self.inner.target, address, local))
.collect();
view_list(py, &view::View::List(rows))
}
fn type_size(&self, ty: &str) -> PyResult<u64> {
Ok(self.resolve_type(ty)?.size as u64)
}
fn offset_of(&self, ty: &str, field: &str) -> PyResult<u64> {
self.resolve_type(ty)?.field_offset(field).map_err(err)
}
fn fields<'py>(&self, py: Python<'py>, ty: &str) -> PyResult<Bound<'py, Record>> {
let info = self.resolve_type(ty)?;
view_record(py, &view::type_layout(ty, &info))
}
fn enum_values(&self, name: &str) -> PyResult<Vec<(String, i64)>> {
self.inner
.target
.symbols
.find_enum_across_modules(self.inner.target.current_dtb(), name)
.ok_or_else(|| raise(format!("unknown enum: {name}")))
}
fn read_struct<'py>(
&self,
py: Python<'py>,
ty: &str,
addr: u64,
) -> PyResult<Bound<'py, PyDict>> {
let info = self.resolve_type(ty)?;
let mut buf = vec![0u8; info.size];
self.inner
.read_masked(VirtAddr(addr), &mut buf)
.map_err(err)?;
let d = PyDict::new(py);
for (name, value) in info.decode_fields(&buf) {
match value {
FieldValue::Int(n) | FieldValue::Pointer(n) | FieldValue::Bitfield(n) => {
d.set_item(name, n)?
}
FieldValue::Bytes(b) => d.set_item(name, PyBytes::new(py, &b))?,
}
}
Ok(d)
}
#[pyo3(name = "type")]
fn py_type(slf: Bound<'_, Self>, name: &str) -> PyResult<Type> {
let info = slf.borrow().resolve_type(name)?;
Ok(Type {
dbg: slf.unbind(),
name: name.to_string(),
info,
})
}
fn walk_list(
slf: Bound<'_, Self>,
record_type: &str,
link_field: &str,
head: u64,
) -> PyResult<Vec<Struct>> {
let (record_ti, bases) = {
let dbg = slf.borrow();
let record_ti = dbg.resolve_type(record_type)?;
let link_offset = record_ti.field_offset(link_field).map_err(err)?;
let bases = walk_list_bases(&dbg, head, link_offset)?;
(record_ti, bases)
};
Ok(bases
.into_iter()
.map(|base| Struct {
dbg: slf.clone().unbind(),
name: record_type.to_string(),
info: Arc::clone(&record_ti),
base,
})
.collect())
}
fn disassemble<'py>(
&self,
py: Python<'py>,
addr: u64,
count: usize,
) -> PyResult<Bound<'py, PyList>> {
let rows = self.inner.disassemble(VirtAddr(addr), count).map_err(err)?;
view_list(
py,
&view::View::List(rows.iter().map(view::disasm_row).collect()),
)
}
#[pyo3(signature = (address=None))]
fn inspect_trap_frame<'py>(
&self,
py: Python<'py>,
address: Option<u64>,
) -> PyResult<Bound<'py, Record>> {
let frame = read_ktrap_frame_at_or_current(&self.inner.target, address.map(VirtAddr))
.map_err(err)?;
let rip_symbol = self
.inner
.target
.closest_symbol_current_context(VirtAddr(frame.instruction_pointer()));
view_record(py, &view::trap_frame(&frame, rip_symbol))
}
#[pyo3(signature = (limit = 64))]
fn backtrace<'py>(&mut self, py: Python<'py>, limit: usize) -> PyResult<Bound<'py, PyList>> {
self.require_halted("backtrace")?;
let trace = self.inner.backtrace(limit).map_err(err)?;
view_list(
py,
&view::View::List(trace.frames.iter().map(view::stack_frame).collect()),
)
}
fn closest_symbol(&self, addr: u64) -> Option<String> {
self.inner
.target
.closest_symbol_current_context(VirtAddr(addr))
}
fn current_dtb(&self) -> u64 {
self.inner.target.current_dtb()
}
fn pte_walk<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let walk = self
.inner
.target
.pte_traverse(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::pte_walk(&walk))
}
fn describe_address<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let d = self
.inner
.target
.describe_address(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::address_description(&d))
}
fn inspect_irp<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let irp = self.inner.target.inspect_irp(VirtAddr(addr)).map_err(err)?;
view_record(py, &view::irp(&irp))
}
fn inspect_driver_object<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, Record>> {
let d = self
.inner
.target
.inspect_driver_object(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::driver_object(&self.inner.target, &d))
}
fn inspect_device_object<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, Record>> {
let d = self
.inner
.target
.inspect_device_object(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::device_object(&d))
}
fn inspect_object_header<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, Record>> {
let o = self
.inner
.target
.inspect_object_header(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::object_header(&o))
}
#[pyo3(signature = (limit = 256))]
fn handles<'py>(&self, py: Python<'py>, limit: usize) -> PyResult<Bound<'py, Record>> {
let summary = self.inner.target.enumerate_handles(limit).map_err(err)?;
view_record(py, &view::handle_table(&summary))
}
fn inspect_handle<'py>(&self, py: Python<'py>, handle: u64) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.inspect_handle(handle).map_err(err)?;
view_record(py, &view::handle_entry(&detail))
}
fn inspect_process_token<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let token = self.inner.target.inspect_process_token().map_err(err)?;
view_record(py, &view::token(&token))
}
fn inspect_file_object<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let file = self
.inner
.target
.inspect_file_object(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::file_object(&file))
}
fn inspect_resource<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, Record>> {
let resource = self
.inner
.target
.inspect_resource(VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::resource(&resource))
}
#[pyo3(signature = (limit = 256))]
fn resources<'py>(&self, py: Python<'py>, limit: usize) -> PyResult<Bound<'py, Record>> {
let resources = self.inner.target.enumerate_resources(limit).map_err(err)?;
view_record(py, &view::resource_list(&resources))
}
#[pyo3(signature = (process_limit = 64))]
fn memory_usage<'py>(
&self,
py: Python<'py>,
process_limit: usize,
) -> PyResult<Bound<'py, Record>> {
let summary = self
.inner
.target
.memory_use_summary(process_limit)
.map_err(err)?;
view_record(py, &view::memory_usage(&summary))
}
fn notify_callbacks<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let cbs = self
.inner
.target
.enumerate_notify_callbacks()
.map_err(err)?;
let dtb = self.inner.target.guest().map_err(err)?.ntoskrnl.dtb();
let rows: Vec<view::View> = cbs
.iter()
.map(|c| {
let symbol = self
.inner
.target
.symbols
.format_closest_symbol_for_address(dtb, c.function);
view::notify_callback(c, symbol)
})
.collect();
view_list(py, &view::View::List(rows))
}
fn ssdt<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let tables = self.inner.target.dump_ssdt().map_err(err)?;
let rows: Vec<view::View> = tables.iter().map(view::ssdt_table).collect();
view_list(py, &view::View::List(rows))
}
fn discover_irps<'py>(
&self,
py: Python<'py>,
filter: Option<String>,
) -> PyResult<Bound<'py, PyList>> {
let hits = self
.inner
.target
.discover_irps(filter.as_deref())
.map_err(err)?;
let rows: Vec<view::View> = hits.iter().map(view::irp_hit).collect();
view_list(py, &view::View::List(rows))
}
fn attach_process(&mut self, pid: u64) -> PyResult<String> {
let name = self.inner.target.attach(pid).map(|r| r.name).map_err(err)?;
self.inner.reconcile_breakpoints_if_symbols_changed();
Ok(name)
}
fn detach(&mut self) {
self.inner.target.detach();
}
fn current_process<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, Record>>> {
self.inner
.target
.current_process_info
.as_ref()
.map(|p| view_record(py, &view::process(p)))
.transpose()
}
#[pyo3(signature = (pid = None))]
fn memory_map<'py>(&self, py: Python<'py>, pid: Option<u64>) -> PyResult<Bound<'py, PyList>> {
let process = match pid {
Some(pid) => self
.inner
.target
.matching_processes(Some(&pid.to_string()))
.map_err(err)?
.into_iter()
.find(|p| p.pid == pid)
.ok_or_else(|| raise(format!("no process with pid {pid}")))?,
None => self
.inner
.target
.current_process_info
.clone()
.ok_or_else(|| {
raise("no process attached; pass pid or call attach_process(pid)")
})?,
};
let regions = self
.inner
.target
.enumerate_vad_regions_for_process_info(&process)
.map_err(err)?;
let rows = regions.iter().map(view::memory_region).collect();
view_list(py, &view::View::List(rows))
}
fn kernel_modules<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let mods = self
.inner
.target
.kernel_modules_with_versions()
.map_err(err)?;
view_list(
py,
&view::View::List(mods.iter().map(view::module).collect()),
)
}
fn modules<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let mods = self.inner.target.modules_with_versions().map_err(err)?;
view_list(
py,
&view::View::List(mods.iter().map(view::module).collect()),
)
}
fn driver_objects<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let drivers = self.inner.target.enumerate_driver_objects().map_err(err)?;
view_list(
py,
&view::View::List(drivers.iter().map(view::driver_object_info).collect()),
)
}
fn threads<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let (threads, active) = self.inner.windows_threads().map_err(err)?;
let rows = threads
.iter()
.map(|t| view::thread(t, active.get(&t.ethread.0).map(String::as_str)))
.collect();
view_list(py, &view::View::List(rows))
}
fn vcpus<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
self.require_halted("vcpus")?;
let rows = self
.inner
.vcpus()
.map_err(err)?
.iter()
.map(view::vcpu)
.collect();
view_list(py, &view::View::List(rows))
}
fn capabilities<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
let rows = self.inner.capabilities();
view_list(
py,
&view::View::List(rows.iter().map(view::capability).collect()),
)
}
#[pyo3(signature = (since_seq=0))]
fn debug_log<'py>(&self, py: Python<'py>, since_seq: u64) -> PyResult<Bound<'py, Record>> {
view_record(
py,
&view::debug_log(&self.inner.read_debug_output(since_seq)),
)
}
fn notices(&mut self) -> Vec<String> {
self.inner.take_notices()
}
#[pyo3(signature = (target, condition=None, *, pass_count=0, one_shot=false, process=None, thread=None, processor=None, action=None))]
fn breakpoint(
slf: Bound<'_, Self>,
target: &Bound<'_, PyAny>,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
) -> PyResult<Breakpoint> {
let id = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("breakpoint")?;
let (addr, symbol) = breakpoint_target_arg(&dbg, target)?;
let config = breakpoint_config_arg(
&dbg, condition, pass_count, one_shot, process, thread, processor, action,
)?;
dbg.inner
.add_breakpoint(VirtAddr(addr), symbol, config)
.map_err(err)?
};
Self::breakpoint_handle(&slf, id)
}
#[pyo3(signature = (symbol, condition=None, *, pass_count=0, one_shot=false, process=None, thread=None, processor=None, action=None))]
fn set_symbol_breakpoint(
slf: Bound<'_, Self>,
symbol: String,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
) -> PyResult<Breakpoint> {
let id = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("set_symbol_breakpoint")?;
let config = breakpoint_config_arg(
&dbg, condition, pass_count, one_shot, process, thread, processor, action,
)?;
dbg.inner
.add_symbol_breakpoint(symbol, config)
.map_err(err)?
};
Self::breakpoint_handle(&slf, id)
}
#[pyo3(signature = (pattern, condition=None, *, pass_count=0, one_shot=false, process=None, thread=None, processor=None, action=None, limit=256))]
fn set_pattern_breakpoints(
slf: Bound<'_, Self>,
pattern: &str,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
limit: usize,
) -> PyResult<Vec<Breakpoint>> {
let (ids, errors) = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("set_pattern_breakpoints")?;
let config = breakpoint_config_arg(
&dbg, condition, pass_count, one_shot, process, thread, processor, action,
)?;
dbg.inner
.add_pattern_breakpoints(pattern, config, limit.clamp(1, 4096))
.map_err(err)?
};
let handles: Vec<Breakpoint> = ids
.into_iter()
.map(|id| Self::breakpoint_handle(&slf, id))
.collect::<PyResult<_>>()?;
if let Some(first) = errors.first() {
return Err(raise(format!(
"{} of {} matching symbols failed to install (first: {first})",
errors.len(),
errors.len() + handles.len()
)));
}
if handles.is_empty() {
return Err(raise(format!("no symbols match '{pattern}'")));
}
Ok(handles)
}
#[pyo3(signature = (file, line, condition=None, *, pass_count=0, one_shot=false, process=None, thread=None, processor=None, action=None))]
fn set_source_breakpoint(
slf: Bound<'_, Self>,
file: &str,
line: u32,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
) -> PyResult<Vec<Breakpoint>> {
let ids = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("set_source_breakpoint")?;
let config = breakpoint_config_arg(
&dbg, condition, pass_count, one_shot, process, thread, processor, action,
)?;
dbg.inner
.add_source_breakpoint(format!("{file}:{line}"), config)
.map_err(err)?
};
ids.into_iter()
.map(|id| Self::breakpoint_handle(&slf, id))
.collect()
}
#[pyo3(signature = (target, *, access="write", length=1, condition=None, pass_count=0, one_shot=false, process=None, thread=None, processor=None, action=None))]
fn watchpoint(
slf: Bound<'_, Self>,
target: &Bound<'_, PyAny>,
access: &str,
length: u8,
condition: Option<String>,
pass_count: u64,
one_shot: bool,
process: Option<u64>,
thread: Option<u64>,
processor: Option<u16>,
action: Option<String>,
) -> PyResult<Breakpoint> {
let id = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("watchpoint")?;
let access = access.parse::<WatchpointAccess>().map_err(err)?;
let (addr, symbol) = breakpoint_target_arg(&dbg, target)?;
let config = breakpoint_config_arg(
&dbg, condition, pass_count, one_shot, process, thread, processor, action,
)?;
dbg.inner
.add_watchpoint(VirtAddr(addr), access, length, symbol, config)
.map_err(err)?
};
Self::breakpoint_handle(&slf, id)
}
fn clear_breakpoint(&mut self, id: &Bound<'_, PyAny>) -> PyResult<()> {
let id = breakpoint_id_arg(id, self.session_id())?;
self.require_halted("clear_breakpoint")?;
self.inner.remove_breakpoint(id).map_err(err)
}
fn enable_breakpoint(&mut self, id: &Bound<'_, PyAny>) -> PyResult<()> {
let id = breakpoint_id_arg(id, self.session_id())?;
self.require_halted("enable_breakpoint")?;
self.inner.enable_breakpoint(id).map_err(err)
}
fn disable_breakpoint(&mut self, id: &Bound<'_, PyAny>) -> PyResult<()> {
let id = breakpoint_id_arg(id, self.session_id())?;
self.require_halted("disable_breakpoint")?;
self.inner.disable_breakpoint(id).map_err(err)
}
fn breakpoints(slf: Bound<'_, Self>) -> Vec<Breakpoint> {
let dbg = slf.borrow();
let session_id = dbg.session_id();
dbg.inner
.list_breakpoints()
.into_iter()
.map(|b| Breakpoint {
dbg: Some(slf.clone().unbind()),
session_id,
snapshot: BreakpointSnapshot::from_core(b),
})
.collect()
}
#[pyo3(signature = (processor=None))]
fn inspect_pcr<'py>(
&mut self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.inspect_pcr(processor).map_err(err)?;
view_record(py, &view::cpu::pcr(&detail))
}
#[pyo3(signature = (processor=None))]
fn inspect_prcb<'py>(
&self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.target.inspect_prcb(processor).map_err(err)?;
view_record(py, &view::cpu::prcb(&detail))
}
#[pyo3(signature = (processor=None))]
fn inspect_irql<'py>(
&self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.target.inspect_irql(processor).map_err(err)?;
view_record(py, &view::cpu::irql(&detail))
}
#[pyo3(signature = (vector=None, processor=None))]
fn inspect_idt<'py>(
&mut self,
py: Python<'py>,
vector: Option<u16>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.inspect_idt(processor, vector).map_err(err)?;
view_record(py, &view::cpu::idt(&detail))
}
#[pyo3(signature = (processor=None))]
fn inspect_gdt<'py>(
&mut self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.inspect_gdt(processor).map_err(err)?;
view_record(py, &view::cpu::gdt(&detail))
}
#[pyo3(signature = (processor=None))]
fn inspect_cpuinfo<'py>(
&self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let processor = self.processor_arg(processor);
let detail = self.inner.target.inspect_cpuinfo(processor).map_err(err)?;
view_record(py, &view::cpu::cpuinfo(&detail))
}
#[pyo3(signature = (msr, processor=None))]
fn read_msr(&mut self, msr: &Bound<'_, PyAny>, processor: Option<u16>) -> PyResult<u64> {
let msr = msr_arg(msr)?;
let processor = self.processor_arg(processor);
self.inner.read_msr(processor, msr).map_err(err)
}
#[pyo3(signature = (msr, value, processor=None))]
fn write_msr(
&mut self,
msr: &Bound<'_, PyAny>,
value: u64,
processor: Option<u16>,
) -> PyResult<()> {
let msr = msr_arg(msr)?;
let processor = self.processor_arg(processor);
self.inner.write_msr(processor, msr, value).map_err(err)
}
#[pyo3(signature = (include_idle=false, include_stacks=false))]
fn running<'py>(
&self,
py: Python<'py>,
include_idle: bool,
include_stacks: bool,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.inspect_running(include_idle, include_stacks)
.map_err(err)?;
view_record(py, &view::sched::running(&detail))
}
#[pyo3(signature = (processor=None))]
fn ready_queues<'py>(
&self,
py: Python<'py>,
processor: Option<u16>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_ready_queues(processor)
.map_err(err)?;
view_record(py, &view::sched::ready_queues(&detail))
}
fn dpc_queues<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.inspect_dpc_queues().map_err(err)?;
view_record(py, &view::sched::dpc_queues(&detail))
}
fn timers<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.timer_list().map_err(err)?;
view_record(py, &view::sched::timer_list(&detail))
}
fn inspect_timer<'py>(&self, py: Python<'py>, address: u64) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_timer(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::sched::timer(&detail))
}
#[pyo3(signature = (target=None))]
fn apcs<'py>(
&mut self,
py: Python<'py>,
target: Option<&Bound<'_, PyAny>>,
) -> PyResult<Bound<'py, Record>> {
let selector = match target {
None => ApcSelector::CurrentThread,
Some(value) => {
if let Ok(number) = value.extract::<u64>() {
if self.inner.find_windows_thread(number).is_ok() {
ApcSelector::Thread(VirtAddr(number))
} else {
ApcSelector::Process(number)
}
} else {
let text: String = value.extract().map_err(|_| {
PyTypeError::new_err(
"apcs target must be None, \"*\", an int, or a process name",
)
})?;
if text == "*" {
ApcSelector::All
} else if text == "." {
ApcSelector::CurrentThread
} else {
let processes = self
.inner
.target
.matching_processes(Some(&text))
.map_err(err)?;
match processes.as_slice() {
[process] => ApcSelector::Process(process.pid),
[] => return Err(raise(format!("no process matches '{text}'"))),
many => {
return Err(raise(format!(
"ambiguous process '{text}': {} matches",
many.len()
)));
}
}
}
}
}
};
let detail = self.inner.inspect_apcs(selector).map_err(err)?;
view_record(py, &view::sched::apcs(&detail))
}
#[pyo3(signature = (level=0, filter=None))]
fn stacks<'py>(
&mut self,
py: Python<'py>,
level: u8,
filter: Option<&str>,
) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.inspect_stacks(level, filter).map_err(err)?;
view_record(py, &view::sched::stacks(&detail))
}
#[pyo3(signature = (address=None))]
fn inspect_peb<'py>(
&self,
py: Python<'py>,
address: Option<u64>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_peb(address.map(VirtAddr))
.map_err(err)?;
view_record(py, &view::usermode::peb(&detail))
}
#[pyo3(signature = (address=None))]
fn inspect_teb<'py>(
&self,
py: Python<'py>,
address: Option<u64>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_teb(address.map(VirtAddr))
.map_err(err)?;
view_record(py, &view::usermode::teb(&detail))
}
#[pyo3(signature = (containing=None))]
fn loader_modules<'py>(
&self,
py: Python<'py>,
containing: Option<u64>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.loader_modules(containing.map(VirtAddr))
.map_err(err)?;
view_record(py, &view::usermode::loader_modules(&detail))
}
fn last_error<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.last_error().map_err(err)?;
view_record(py, &view::usermode::last_error(&detail))
}
#[pyo3(signature = (module, include_diffs=false))]
fn check_image<'py>(
&self,
py: Python<'py>,
module: &str,
include_diffs: bool,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.check_image(module, include_diffs)
.map_err(err)?;
view_record(py, &view::usermode::image_check(&detail))
}
fn heap_summary<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.heap_summary().map_err(err)?;
view_record(py, &view::heap::heap_summary(&detail))
}
#[pyo3(signature = (heap, list_entries=false))]
fn inspect_heap<'py>(
&self,
py: Python<'py>,
heap: u64,
list_entries: bool,
) -> PyResult<Bound<'py, Record>> {
let summary = self.inner.target.heap_summary().map_err(err)?;
let selector = if (heap as usize) < summary.heaps.len() {
HeapSelector::Index(heap as usize)
} else {
HeapSelector::Address(VirtAddr(heap))
};
let detail = self
.inner
.target
.inspect_heap(selector, list_entries)
.map_err(err)?;
view_record(py, &view::heap::heap(&detail))
}
fn find_heap_block<'py>(&self, py: Python<'py>, address: u64) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.find_heap_block(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::heap::heap_block_search(&detail))
}
#[pyo3(signature = (include_processes=true))]
fn inspect_vm<'py>(
&self,
py: Python<'py>,
include_processes: bool,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_vm(include_processes)
.map_err(err)?;
view_record(py, &view::mm::vm(&detail))
}
#[pyo3(signature = (value, physical_address=false))]
fn inspect_pfn<'py>(
&self,
py: Python<'py>,
value: u64,
physical_address: bool,
) -> PyResult<Bound<'py, Record>> {
let selector = if physical_address {
PfnSelector::PhysicalAddress(value)
} else {
PfnSelector::Pfn(value)
};
let detail = self.inner.target.inspect_pfn(selector).map_err(err)?;
view_record(py, &view::mm::pfn(&detail))
}
#[pyo3(signature = (addr, dtb=None))]
fn vtop(&self, addr: u64, dtb: Option<u64>) -> PyResult<Option<u64>> {
self.inner
.target
.virt_to_phys(dtb, VirtAddr(addr))
.map_err(err)
}
#[pyo3(signature = (addr, dtb=None))]
fn inspect_translation<'py>(
&self,
py: Python<'py>,
addr: u64,
dtb: Option<u64>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.vtop(dtb.unwrap_or(0), VirtAddr(addr))
.map_err(err)?;
view_record(py, &view::mm::vtop(&detail))
}
fn ptov<'py>(&self, py: Python<'py>, physical: u64) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.ptov(physical).map_err(err)?;
view_record(py, &view::mm::ptov(&detail))
}
fn inspect_pool<'py>(&self, py: Python<'py>, address: u64) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_pool(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::mm::pool_page(&detail))
}
#[pyo3(signature = (tag=None, *, sort="tag", include_counts=false))]
fn pool_usage<'py>(
&self,
py: Python<'py>,
tag: Option<&str>,
sort: &str,
include_counts: bool,
) -> PyResult<Bound<'py, Record>> {
let sort = match sort {
"tag" => PoolUsageSort::Tag,
"nonpaged" => PoolUsageSort::NonPagedBytes,
"paged" => PoolUsageSort::PagedBytes,
other => {
return Err(raise(format!(
"unknown pool_usage sort '{other}' (tag, nonpaged, paged)"
)));
}
};
let detail = self
.inner
.target
.pool_usage(sort, tag, include_counts)
.map_err(err)?;
view_record(py, &view::mm::pool_usage(&detail))
}
#[pyo3(signature = (tag, pool_type=None))]
fn pool_find<'py>(
&self,
py: Python<'py>,
tag: &str,
pool_type: Option<&str>,
) -> PyResult<Bound<'py, Record>> {
let pool_type = match pool_type {
None => None,
Some("nonpaged") => Some(PoolType::NonPaged),
Some("paged") => Some(PoolType::Paged),
Some(other) => {
return Err(raise(format!(
"unknown pool type '{other}' (nonpaged, paged)"
)));
}
};
let detail = self.inner.target.pool_find(tag, pool_type).map_err(err)?;
view_record(py, &view::mm::pool_find(&detail))
}
fn lookaside_lists<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.lookaside_lists().map_err(err)?;
view_record(py, &view::mm::lookaside_lists(&detail))
}
fn inspect_lookaside<'py>(
&self,
py: Python<'py>,
address: u64,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_lookaside(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::mm::lookaside(&detail))
}
fn read_physical<'py>(
&self,
py: Python<'py>,
addr: u64,
len: usize,
) -> PyResult<Bound<'py, PyBytes>> {
if len > MAX_READ_LEN {
return Err(raise(format!(
"read length {len} exceeds cap {MAX_READ_LEN} (0x{MAX_READ_LEN:x})"
)));
}
let mut buf = vec![0u8; len];
self.inner
.target
.read_physical(addr, &mut buf)
.map_err(err)?;
Ok(PyBytes::new(py, &buf))
}
fn write_physical(&self, addr: u64, data: &[u8]) -> PyResult<()> {
self.inner.target.write_physical(addr, data).map_err(err)
}
#[pyo3(signature = (address, annotate_well_known=false))]
fn inspect_security_descriptor<'py>(
&self,
py: Python<'py>,
address: u64,
annotate_well_known: bool,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_security_descriptor(VirtAddr(address), annotate_well_known)
.map_err(err)?;
view_record(py, &view::security::security_descriptor(&detail))
}
fn inspect_acl<'py>(&self, py: Python<'py>, address: u64) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_acl(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::security::acl(&detail))
}
fn inspect_sid<'py>(&self, py: Python<'py>, address: u64) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_sid(VirtAddr(address))
.map_err(err)?;
view_record(py, &view::security::sid(&detail))
}
fn inspect_object_security<'py>(
&self,
py: Python<'py>,
object: u64,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_object_security(VirtAddr(object))
.map_err(err)?;
view_record(py, &view::security::object_security(&detail))
}
#[pyo3(signature = (session=None))]
fn sessions<'py>(&self, py: Python<'py>, session: Option<i64>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.sessions(session).map_err(err)?;
view_record(py, &view::security::sessions(&detail))
}
#[pyo3(signature = (session=None, detailed=false, image_glob=None))]
fn session_processes<'py>(
&self,
py: Python<'py>,
session: Option<i64>,
detailed: bool,
image_glob: Option<&str>,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.session_processes(session, detailed, image_glob)
.map_err(err)?;
view_record(py, &view::security::session_processes(&detail))
}
#[pyo3(signature = (node=None, recurse=false))]
fn inspect_devnode<'py>(
&self,
py: Python<'py>,
node: Option<u64>,
recurse: bool,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_devnode(node.map(VirtAddr), recurse)
.map_err(err)?;
view_record(py, &view::pnp::devnode(&detail))
}
fn inspect_device_stack<'py>(
&self,
py: Python<'py>,
device_or_node: u64,
) -> PyResult<Bound<'py, Record>> {
let detail = self
.inner
.target
.inspect_device_stack(VirtAddr(device_or_node))
.map_err(err)?;
view_record(py, &view::pnp::device_stack(&detail))
}
fn pnp_triage<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.pnp_triage().map_err(err)?;
view_record(py, &view::pnp::pnp_triage(&detail))
}
fn verifier_status<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.verifier_status().map_err(err)?;
view_record(py, &view::meta::verifier(&detail))
}
fn verifier_driver<'py>(&self, py: Python<'py>, module: &str) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.verifier_driver(module).map_err(err)?;
view_record(py, &view::meta::verifier_driver(&detail))
}
fn version<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target_version().map_err(err)?;
view_record(py, &view::meta::target_version(&detail))
}
fn target_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, Record>> {
let detail = self.inner.target.target_time().map_err(err)?;
view_record(py, &view::meta::target_time(&detail))
}
#[staticmethod]
fn decode_error<'py>(py: Python<'py>, code: u64) -> PyResult<Bound<'py, Record>> {
view_record(py, &view::meta::error_code(&decode_error_code(code)))
}
fn run_command(&mut self, line: &str) -> PyResult<String> {
if line.trim().is_empty() {
return Ok(String::new());
}
let mut state = ReplState::for_oneshot(&mut self.inner);
state.line = line.trim().to_string();
let (result, text) = output::capture(|| state.dispatch_line(line));
result.map_err(err)?;
Ok(text)
}
fn close(&mut self) -> PyResult<()> {
if self.inner.is_owned() {
self.inner.cleanup_for_exit().map_err(err)?;
}
Ok(())
}
fn __enter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __exit__(
&mut self,
_exc_type: &Bound<'_, PyAny>,
_exc_value: &Bound<'_, PyAny>,
_traceback: &Bound<'_, PyAny>,
) -> PyResult<bool> {
self.close()?;
Ok(false)
}
fn __repr__(&self) -> String {
format!("<ntoseye.Debugger thread={}>", self.inner.current_thread)
}
}
impl Debugger {
pub fn from_session_ref(session: &mut Session, valid: Arc<AtomicBool>) -> Self {
Debugger {
inner: SessionHandle::Borrowed {
ptr: NonNull::from(session),
valid,
},
}
}
fn session_id(&self) -> usize {
self.inner.id()
}
fn require_halted(&mut self, operation: &str) -> PyResult<()> {
self.inner.settle_pending_stop().map_err(err)?;
if self.inner.backend.is_running() {
match halt_unreachable_reason(&*self.inner.backend) {
Some(reason) => Err(raise(format!(
"{operation} needs a halted target; {reason}"
))),
None => Err(raise(format!(
"{operation} requires the VM to be halted; call interrupt() first"
))),
}
} else {
Ok(())
}
}
fn resolve_type(&self, name: &str) -> PyResult<Arc<TypeInfo>> {
let symbols = &self.inner.target.symbols;
let dtb = self.inner.target.current_dtb();
symbols
.find_type_across_modules(dtb, name)
.ok_or_else(|| raise(symbols.unresolved_type_message(dtb, name)))
}
fn processor_arg(&self, processor: Option<u16>) -> u16 {
processor.unwrap_or_else(|| {
processor_index_from_backend_thread_id(&self.inner.current_thread).unwrap_or(0)
})
}
fn breakpoint_handle(slf: &Bound<'_, Self>, id: u32) -> PyResult<Breakpoint> {
let dbg = slf.borrow();
let snapshot = dbg
.inner
.breakpoint(id)
.map(BreakpointSnapshot::from_core)
.ok_or_else(|| raise(format!("breakpoint {id} disappeared after install")))?;
Ok(Breakpoint {
dbg: Some(slf.clone().unbind()),
session_id: dbg.session_id(),
snapshot,
})
}
fn continue_outcome_data(
&mut self,
py: Python<'_>,
outcome: ContinueOutcome,
) -> PyResult<StopOutcomeData> {
let (stopped_process, stopped_thread) = self.inner.stopped_context();
let attached_process = self.inner.target.current_process_info.clone();
let symbol_at = |rip: u64| {
self.inner
.target
.closest_symbol_current_context(VirtAddr(rip))
};
let data = match outcome {
ContinueOutcome::Breakpoint {
id,
address,
symbol,
temporary,
rip,
condition_error,
..
} => {
let snapshot = self
.inner
.breakpoint(id)
.map(BreakpointSnapshot::from_core)
.unwrap_or_else(|| {
BreakpointSnapshot::placeholder(id, address, symbol.clone(), temporary)
});
let kind = if snapshot.watch_access.is_some() {
StopKind::Watchpoint
} else {
StopKind::Breakpoint
};
StopOutcomeData {
kind,
rip: Some(rip),
symbol: snapshot
.symbol
.clone()
.or_else(|| symbol.or_else(|| symbol_at(rip))),
attached_process: attached_process.clone(),
stopped_process: stopped_process.clone(),
stopped_thread: stopped_thread.clone(),
breakpoints: vec![snapshot],
address: Some(address),
temporary: Some(temporary),
condition_error,
..Default::default()
}
}
ContinueOutcome::Bugcheck { rip, info } => {
let analysis = info
.map(|i| analyze_bugcheck(&self.inner.target, &i))
.or_else(|| current_bugcheck(&self.inner.target))
.or_else(|| bugcheck_from_dump_info(&self.inner.target));
let bugcheck_info = analysis
.map(|a| view_record(py, &view::bugcheck(&a)).map(|d| d.into_any().unbind()))
.transpose()?;
StopOutcomeData {
kind: StopKind::Bugcheck,
rip,
symbol: rip.and_then(symbol_at),
bugcheck_info,
..Default::default()
}
}
ContinueOutcome::Stopped {
rip,
exception_code,
first_chance,
exception_address,
} => StopOutcomeData {
kind: StopKind::Exception,
rip: Some(rip),
symbol: symbol_at(rip),
attached_process: attached_process.clone(),
stopped_process: stopped_process.clone(),
stopped_thread: stopped_thread.clone(),
exception_code,
first_chance,
exception_address,
..Default::default()
},
ContinueOutcome::Step { rip } => StopOutcomeData {
kind: StopKind::Step,
rip: Some(rip),
symbol: symbol_at(rip),
attached_process: attached_process.clone(),
stopped_process: stopped_process.clone(),
stopped_thread: stopped_thread.clone(),
..Default::default()
},
ContinueOutcome::TargetReloaded {
kernel_base,
coherent,
} => StopOutcomeData {
kind: StopKind::TargetReloaded,
kernel_base,
coherent: Some(coherent),
..Default::default()
},
ContinueOutcome::Running => StopOutcomeData::default(),
ContinueOutcome::Halted { rip } => StopOutcomeData {
kind: StopKind::Halted,
rip: Some(rip),
symbol: symbol_at(rip),
..Default::default()
},
};
Ok(data)
}
fn stop_outcome(
slf: Bound<'_, Self>,
py: Python<'_>,
advance: impl FnOnce(&mut Debugger) -> PyResult<ContinueOutcome>,
) -> PyResult<StopOutcome> {
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
let outcome = advance(&mut dbg)?;
(dbg.session_id(), dbg.continue_outcome_data(py, outcome)?)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
fn wait_until_stop(
py: Python<'_>,
mut wait: impl FnMut(Duration) -> PyResult<ContinueOutcome>,
) -> PyResult<ContinueOutcome> {
loop {
match wait(Duration::from_secs(1))? {
ContinueOutcome::Running => py.check_signals()?,
other => return Ok(other),
}
}
}
fn read_fixed<const N: usize>(&self, addr: u64) -> PyResult<[u8; N]> {
let mut buf = [0u8; N];
self.inner
.target
.current_process()
.map_err(err)?
.memory()
.read_bytes(VirtAddr(addr), &mut buf)
.map_err(err)?;
Ok(buf)
}
}
#[pyclass(unsendable)]
pub struct Type {
dbg: Py<Debugger>,
name: String,
info: Arc<TypeInfo>,
}
#[pymethods]
impl Type {
#[getter]
fn name(&self) -> &str {
&self.name
}
#[getter]
fn size(&self) -> u64 {
self.info.size as u64
}
fn offset(&self, field: &str) -> PyResult<u64> {
self.info.field_offset(field).map_err(err)
}
#[getter]
fn fields(&self) -> Vec<(String, u64, u64, String)> {
let mut out: Vec<(String, u64, u64, String)> = self
.info
.fields
.iter()
.map(|(n, f)| {
(
n.clone(),
f.offset as u64,
f.size,
format!("{}", f.type_data),
)
})
.collect();
out.sort_by_key(|t| t.1);
out
}
fn at(&self, py: Python<'_>, addr: u64) -> Struct {
Struct {
dbg: self.dbg.clone_ref(py),
name: self.name.clone(),
info: self.info.clone(),
base: addr,
}
}
fn __getitem__(&self, field: &str) -> PyResult<(String, u64, u64, String)> {
let f = self
.info
.fields
.get(field)
.ok_or_else(|| pyo3::exceptions::PyKeyError::new_err(field.to_string()))?;
Ok((
field.to_string(),
f.offset as u64,
f.size,
format!("{}", f.type_data),
))
}
fn __repr__(&self) -> String {
format!("<Type {} size={:#x}>", self.name, self.info.size)
}
}
#[pyclass(unsendable)]
pub struct Struct {
dbg: Py<Debugger>,
name: String,
info: Arc<TypeInfo>,
base: u64,
}
impl Struct {
fn cursor_at(&self, py: Python<'_>, type_name: &str, base: u64) -> PyResult<Struct> {
let info = self.dbg.borrow(py).resolve_type(type_name)?;
Ok(Struct {
dbg: self.dbg.clone_ref(py),
name: type_name.to_string(),
info,
base,
})
}
fn decode_unicode_string_at(&self, py: Python<'_>, addr: u64) -> PyResult<String> {
self.dbg
.borrow(py)
.inner
.target
.read_unicode_string(VirtAddr(addr))
.map_err(err)
}
fn array_field(
&self,
py: Python<'_>,
element: &ParsedType,
count: u32,
addr: u64,
total_size: u64,
) -> PyResult<Py<PyAny>> {
let count = count as usize;
let stride = (total_size as usize) / count;
let list = PyList::empty(py);
if let ParsedType::Struct(sname) | ParsedType::Union(sname) = element {
let stride = if stride == 0 {
self.dbg.borrow(py).resolve_type(sname)?.size
} else {
stride
};
for index in 0..count {
let child = self.cursor_at(py, sname, addr + (index * stride) as u64)?;
list.append(Py::new(py, child)?)?;
}
return Ok(list.into_any().unbind());
}
if stride == 0 {
return Err(raise(format!(
"array field at {addr:#x} has zero-sized elements"
)));
}
let mut buf = vec![0u8; stride * count];
self.dbg
.borrow(py)
.inner
.read_masked(VirtAddr(addr), &mut buf)
.map_err(err)?;
let scalar = matches!(element, ParsedType::Pointer(_)) || matches!(stride, 1 | 2 | 4 | 8);
for chunk in buf.chunks_exact(stride) {
if scalar {
list.append(le_uint(chunk))?;
} else {
list.append(PyBytes::new(py, chunk))?;
}
}
Ok(list.into_any().unbind())
}
fn set_field(&self, py: Python<'_>, name: &str, value: &Bound<'_, PyAny>) -> PyResult<()> {
let field = self
.info
.fields
.get(name)
.ok_or_else(|| raise(format!("{} has no field '{}'", self.name, name)))?;
let addr = self.base + field.offset as u64;
let dbg = self.dbg.borrow(py);
let process = dbg.inner.target.current_process().map_err(err)?;
let mem = process.memory();
match &field.type_data {
ParsedType::Struct(_) | ParsedType::Union(_) => Err(raise(format!(
"cannot assign to nested struct field '{name}'; assign its scalar fields instead"
))),
ParsedType::Bitfield { pos, len, .. } => {
let v: u64 = value
.extract()
.map_err(|_| raise(format!("field '{name}' is a bitfield; expected int")))?;
let (pos, len) = (*pos as u32, *len as u32);
let sz = ((pos + len).div_ceil(8).clamp(1, 8)) as usize;
let mut buf = vec![0u8; sz];
mem.read_bytes(VirtAddr(addr), &mut buf).map_err(err)?;
let mask = if len >= 64 {
u64::MAX
} else {
(1u64 << len) - 1
};
let raw = (le_uint(&buf) & !(mask << pos)) | ((v & mask) << pos);
for (i, b) in buf.iter_mut().enumerate() {
*b = (raw >> (8 * i)) as u8;
}
mem.write_bytes(VirtAddr(addr), &buf).map_err(err)
}
ParsedType::Pointer(_) => {
let v: u64 = value
.extract()
.map_err(|_| raise(format!("field '{name}' is a pointer; expected int")))?;
mem.write_bytes(VirtAddr(addr), &v.to_le_bytes())
.map_err(err)
}
_ => {
let sz = field.size as usize;
if matches!(sz, 1 | 2 | 4 | 8)
&& let Ok(v) = value.extract::<u64>()
{
let bytes = v.to_le_bytes();
return mem.write_bytes(VirtAddr(addr), &bytes[..sz]).map_err(err);
}
let bytes: Vec<u8> = value.extract().map_err(|_| {
raise(format!(
"field '{name}' ({sz} bytes): expected int or bytes"
))
})?;
if bytes.len() != sz {
return Err(raise(format!(
"field '{name}' is {sz} bytes; got {} bytes",
bytes.len()
)));
}
mem.write_bytes(VirtAddr(addr), &bytes).map_err(err)
}
}
}
fn get_field(&self, py: Python<'_>, name: &str) -> PyResult<Py<PyAny>> {
let field = self
.info
.fields
.get(name)
.ok_or_else(|| raise(format!("{} has no field '{}'", self.name, name)))?;
let addr = self.base + field.offset as u64;
if matches!(&field.type_data, ParsedType::Struct(s) if s == "_UNICODE_STRING") {
let s = self.decode_unicode_string_at(py, addr)?;
return Ok(s.into_bound_py_any(py)?.unbind());
}
if let ParsedType::Struct(sname) | ParsedType::Union(sname) = &field.type_data {
let child = self.cursor_at(py, sname, addr)?;
return Ok(Py::new(py, child)?.into_any());
}
if let ParsedType::Array(element, count) = &field.type_data
&& field.type_data.c_string_len().is_none()
&& *count > 0
{
return self.array_field(py, element, *count, addr, field.size);
}
let sz = field.size as usize;
let mut buf = vec![0u8; sz];
self.dbg
.borrow(py)
.inner
.read_masked(VirtAddr(addr), &mut buf)
.map_err(err)?;
let obj = match &field.type_data {
ParsedType::Bitfield { pos, len, .. } => {
let raw = le_uint(&buf);
let mask = if *len >= 64 {
u64::MAX
} else {
(1u64 << len) - 1
};
((raw >> pos) & mask).into_bound_py_any(py)?
}
ParsedType::Pointer(_) => le_uint(&buf).into_bound_py_any(py)?,
t if t.c_string_len().is_some() => decode_c_string(&buf).into_bound_py_any(py)?,
_ => match sz {
1 | 2 | 4 | 8 => le_uint(&buf).into_bound_py_any(py)?,
_ => PyBytes::new(py, &buf).into_any(),
},
};
Ok(obj.unbind())
}
}
#[pymethods]
impl Struct {
#[getter]
fn addr(&self) -> u64 {
self.base
}
#[getter]
fn type_name(&self) -> &str {
&self.name
}
#[getter]
fn fields(&self) -> Vec<String> {
let mut v: Vec<String> = self.info.fields.keys().cloned().collect();
v.sort();
v
}
fn read_field(&self, py: Python<'_>, name: &str) -> PyResult<Py<PyAny>> {
self.get_field(py, name)
}
fn write_field(&self, py: Python<'_>, name: &str, value: Bound<'_, PyAny>) -> PyResult<()> {
self.set_field(py, name, &value)
}
fn __setattr__(&self, py: Python<'_>, name: &str, value: Bound<'_, PyAny>) -> PyResult<()> {
if !self.info.fields.contains_key(name) {
return Err(PyAttributeError::new_err(format!(
"'{}' has no settable field '{}'",
self.name, name
)));
}
self.set_field(py, name, &value)
}
fn follow(&self, py: Python<'_>, name: &str) -> PyResult<Struct> {
let field = self
.info
.fields
.get(name)
.ok_or_else(|| raise(format!("{} has no field '{}'", self.name, name)))?;
let sname = match &field.type_data {
ParsedType::Pointer(inner) => match inner.as_ref() {
ParsedType::Struct(s) | ParsedType::Union(s) => s.clone(),
_ => {
return Err(raise(format!(
"field '{name}' is not a pointer to a struct"
)));
}
},
_ => return Err(raise(format!("field '{name}' is not a pointer"))),
};
let addr = self.base + field.offset as u64;
let target = {
let dbg = self.dbg.borrow(py);
let mut b = [0u8; 8];
dbg.inner
.target
.current_process()
.map_err(err)?
.memory()
.read_bytes(VirtAddr(addr), &mut b)
.map_err(err)?;
u64::from_le_bytes(b)
};
self.cursor_at(py, &sname, target)
}
fn read<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let dbg = self.dbg.borrow(py);
dbg.read_struct(py, &self.name, self.base)
}
fn list(
&self,
py: Python<'_>,
head_field: &str,
record_type: &str,
link_field: &str,
) -> PyResult<Vec<Struct>> {
let head = self.base + self.info.field_offset(head_field).map_err(err)?;
let (record_ti, bases) = {
let dbg = self.dbg.borrow(py);
let record_ti = dbg.resolve_type(record_type)?;
let link_offset = record_ti.field_offset(link_field).map_err(err)?;
let bases = walk_list_bases(&dbg, head, link_offset)?;
(record_ti, bases)
};
Ok(bases
.into_iter()
.map(|base| Struct {
dbg: self.dbg.clone_ref(py),
name: record_type.to_string(),
info: Arc::clone(&record_ti),
base,
})
.collect())
}
fn threads(&self, py: Python<'_>) -> PyResult<Vec<Struct>> {
self.list(py, "ThreadListHead", "_ETHREAD", "ThreadListEntry")
}
fn unicode_string(&self, py: Python<'_>, name: &str) -> PyResult<String> {
let off = self.info.field_offset(name).map_err(err)?;
self.decode_unicode_string_at(py, self.base + off)
}
fn read_unicode_string(&self, py: Python<'_>) -> PyResult<String> {
self.decode_unicode_string_at(py, self.base)
}
fn __getattr__(&self, py: Python<'_>, name: &str) -> PyResult<Py<PyAny>> {
if name.starts_with("__") || !self.info.fields.contains_key(name) {
return Err(PyAttributeError::new_err(format!(
"'{}' has no field '{}'",
self.name, name
)));
}
self.get_field(py, name)
}
fn __getitem__(&self, py: Python<'_>, key: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
if let Ok(index) = key.extract::<i64>() {
let size = self.info.size as i64;
if size == 0 {
return Err(raise(format!("{} has no size to index by", self.name)));
}
let base = (self.base as i64).wrapping_add(index.wrapping_mul(size)) as u64;
let sibling = Struct {
dbg: self.dbg.clone_ref(py),
name: self.name.clone(),
info: Arc::clone(&self.info),
base,
};
return Ok(Py::new(py, sibling)?.into_any());
}
let name: String = key
.extract()
.map_err(|_| PyTypeError::new_err("Struct index must be a field name or an int"))?;
if !self.info.fields.contains_key(&name) {
return Err(pyo3::exceptions::PyKeyError::new_err(name));
}
self.get_field(py, &name)
}
#[getter]
fn size(&self) -> u64 {
self.info.size as u64
}
fn cast(&self, py: Python<'_>, type_name: &str) -> PyResult<Struct> {
self.cursor_at(py, type_name, self.base)
}
fn __dir__(&self) -> Vec<String> {
let mut v: Vec<String> = self.info.fields.keys().cloned().collect();
v.sort();
for m in [
"read",
"read_field",
"write_field",
"follow",
"list",
"threads",
"unicode_string",
"read_unicode_string",
"fields",
"addr",
"type_name",
"size",
"cast",
] {
v.push(m.to_string());
}
v
}
fn __repr__(&self) -> String {
format!("<{} @ {:#x}>", self.name, self.base)
}
}
#[pyfunction]
#[pyo3(signature = (backend="kd", connect=None, key=None, memory_source="auto"))]
fn attach(
backend: &str,
connect: Option<&str>,
key: Option<&str>,
memory_source: &str,
) -> PyResult<Debugger> {
let memory_source = memory_source
.parse::<KdMemorySource>()
.map_err(|error| err(Error::DebugInfo(error)))?;
let spec = if backend == "dmp" {
let path = connect.ok_or_else(|| {
err(Error::DebugInfo(
"dmp backend requires a dump file path via connect=".into(),
))
})?;
TargetSpec::Dump(path.into())
} else {
let backend = backend.parse::<Backend>().map_err(|error| {
err(Error::DebugInfo(format!(
"{error} (or 'dmp' with connect=<path>)"
)))
})?;
TargetSpec::Live {
backend,
connect: connect.map(str::to_string),
kdnet_key: key.map(str::to_string),
memory_source,
}
};
let inner = Session::open_with_progress(&spec, &mut |line| eprintln!("{line}")).map_err(err)?;
Ok(Debugger {
inner: SessionHandle::Owned(Box::new(inner)),
})
}
pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<Debugger>()?;
m.add_class::<Breakpoint>()?;
m.add_class::<StopOutcome>()?;
m.add_class::<AddressModule>()?;
m.add_class::<MemoryRegion>()?;
m.add_class::<MemorySearchMatch>()?;
m.add_class::<Type>()?;
m.add_class::<Struct>()?;
m.add_class::<Record>()?;
m.add_class::<Diagnostic>()?;
m.add_function(wrap_pyfunction!(attach, m)?)?;
m.add("NtoseyeError", m.py().get_type::<NtoseyeError>())?;
m.add("MemoryAccessError", m.py().get_type::<MemoryAccessError>())?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "must not be stashed")]
fn invalidated_borrow_panics_instead_of_dereferencing() {
let valid = Arc::new(AtomicBool::new(false));
let handle = SessionHandle::Borrowed {
ptr: NonNull::<Session>::dangling(),
valid,
};
let _ = &*handle;
}
#[test]
fn breakpoint_id_arg_rejects_foreign_handles() {
Python::attach(|py| {
let bp = Py::new(
py,
Breakpoint {
dbg: None,
session_id: 7,
snapshot: BreakpointSnapshot {
id: 42,
address: Some(0x1000),
enabled: true,
resolved: true,
deferred: false,
specification: None,
symbol: None,
scope: "global".to_string(),
thread: None,
processor: None,
condition: None,
pass_count: 0,
hit_count: 0,
remaining_pass_count: 0,
one_shot: false,
action: None,
temporary: false,
watch_access: None,
watch_length: None,
},
},
)
.unwrap();
let bp = bp.bind(py);
assert_eq!(breakpoint_id_arg(bp.as_any(), 7).unwrap(), 42);
let err = breakpoint_id_arg(bp.as_any(), 8).unwrap_err();
assert!(
err.to_string()
.contains("breakpoint handle belongs to a different debugger session")
);
});
}
}