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::class::basic::CompareOp;
use pyo3::IntoPyObjectExt;
use pyo3::exceptions::{PyAttributeError, PyKeyError, PyTypeError};
use pyo3::prelude::*;
use pyo3::types::{PyAny, PyBytes, PyDict, PyList};
use crate::backend::MemoryOps;
use crate::bugchecks::{analyze_bugcheck, current_bugcheck};
use crate::dbg_backend::DebugBackend;
use crate::error::Error;
use crate::expr::Expr;
use crate::gdb::breakpoints::Breakpoint as CoreBreakpoint;
use crate::gdb::GdbClient;
use crate::kd::KdBackend;
use crate::memory_backend::MemoryBackend;
use crate::repl::ReplState;
use crate::session::{ContinueOutcome, Session};
use crate::symbols::{FieldValue, ParsedType, TypeInfo, le_uint};
use crate::types::VirtAddr;
use crate::view;
pub mod embed;
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::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_dict<'py>(py: Python<'py>, v: &view::View) -> PyResult<Bound<'py, PyDict>> {
view::to_py(py, v)?
.cast_into::<PyDict>()
.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",
))
}
#[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: u64,
enabled: bool,
symbol: Option<String>,
scope: String,
condition: Option<String>,
temporary: bool,
}
impl BreakpointSnapshot {
fn from_core(bp: &CoreBreakpoint) -> Self {
Self {
id: bp.id,
address: bp.address.0,
enabled: bp.enabled,
symbol: bp.symbol.clone(),
scope: bp.scope.label(),
condition: bp.condition.clone(),
temporary: bp.temporary,
}
}
}
#[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 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) -> u64 {
self.snapshot.address
}
#[getter]
fn symbol(&self) -> Option<String> {
self.snapshot.symbol.clone()
}
#[getter]
fn scope(&self) -> String {
self.snapshot.scope.clone()
}
#[getter]
fn condition(&self) -> Option<String> {
self.snapshot.condition.clone()
}
#[getter]
fn temporary(&self) -> bool {
self.snapshot.temporary
}
#[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)
}
}
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 d = PyDict::new(py);
d.set_item("id", self.snapshot.id)?;
d.set_item("address", self.snapshot.address)?;
d.set_item("enabled", self.enabled(py))?;
d.set_item("symbol", self.snapshot.symbol.clone())?;
d.set_item("scope", self.snapshot.scope.clone())?;
d.set_item("condition", self.snapshot.condition.clone())?;
d.set_item("temporary", self.snapshot.temporary)?;
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" };
let symbol = self
.snapshot
.symbol
.as_ref()
.map(|s| format!(" {s}"))
.unwrap_or_default();
format!(
"<Breakpoint #{} at {:#x}{} {}>",
self.snapshot.id, self.snapshot.address, symbol, state
)
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StopKind {
Breakpoint,
Bugcheck,
Exception,
Step,
TargetReloaded,
Running,
Halted,
}
impl StopKind {
fn as_str(self) -> &'static str {
match self {
Self::Breakpoint => "breakpoint",
Self::Bugcheck => "bugcheck",
Self::Exception => "exception",
Self::Step => "step",
Self::TargetReloaded => "target_reloaded",
Self::Running => "running",
Self::Halted => "halted",
}
}
}
struct StopOutcomeData {
kind: StopKind,
rip: Option<u64>,
symbol: Option<String>,
process: Option<(u64, String)>,
breakpoints: Vec<BreakpointSnapshot>,
address: Option<u64>,
temporary: Option<bool>,
exception_code: Option<u32>,
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()
}
}
#[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 timed_out(&self) -> bool {
self.running()
}
#[getter]
fn breakpoint_stop(&self) -> bool {
self.data.kind == StopKind::Breakpoint
}
#[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 reload(&self) -> bool {
self.target_reloaded()
}
#[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 process(&self) -> Option<(u64, String)> {
self.data.process.clone()
}
#[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 exception_code(&self) -> Option<u32> {
self.data.exception_code
}
#[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 to_dict<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let d = PyDict::new(py);
d.set_item("stop", self.reason())?;
match self.data.kind {
StopKind::Breakpoint => {
d.set_item("rip", self.data.rip)?;
if let Some(bp) = self.data.breakpoints.first() {
d.set_item("id", bp.id)?;
d.set_item("breakpoint", bp.id)?;
d.set_item("address", self.data.address.unwrap_or(bp.address))?;
d.set_item("symbol", self.data.symbol.clone())?;
d.set_item("temporary", self.data.temporary.unwrap_or(bp.temporary))?;
}
d.set_item("process", self.data.process.clone())?;
}
StopKind::Bugcheck => {
d.set_item("rip", self.data.rip)?;
match &self.data.bugcheck_info {
Some(info) => d.set_item("bugcheck", info.clone_ref(py))?,
None => d.set_item("bugcheck", true)?,
}
}
StopKind::Exception => {
d.set_item("rip", self.data.rip)?;
d.set_item("exception_code", self.data.exception_code)?;
d.set_item("symbol", self.data.symbol.clone())?;
d.set_item("process", self.data.process.clone())?;
}
StopKind::Step => {
d.set_item("rip", self.data.rip)?;
d.set_item("symbol", self.data.symbol.clone())?;
d.set_item("process", self.data.process.clone())?;
}
StopKind::TargetReloaded => {
d.set_item("target_reloaded", true)?;
d.set_item("kernel_base", self.data.kernel_base)?;
d.set_item("coherent", self.data.coherent)?;
}
StopKind::Running => {}
StopKind::Halted => {
d.set_item("rip", self.data.rip)?;
d.set_item("symbol", self.data.symbol.clone())?;
}
}
Ok(d)
}
#[pyo3(signature = (key, default=None))]
fn get(&self, py: Python<'_>, key: &str, default: Option<Py<PyAny>>) -> PyResult<Py<PyAny>> {
let d = self.to_dict(py)?;
match d.get_item(key)? {
Some(value) => Ok(value.unbind()),
None => Ok(default.unwrap_or_else(|| py.None())),
}
}
fn __getitem__(&self, py: Python<'_>, key: &str) -> PyResult<Py<PyAny>> {
let d = self.to_dict(py)?;
d.get_item(key)?
.map(|value| value.unbind())
.ok_or_else(|| PyKeyError::new_err(key.to_string()))
}
fn __contains__(&self, py: Python<'_>, key: &str) -> PyResult<bool> {
self.to_dict(py)?.contains(key)
}
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];
let process = self.inner.target.current_process();
process
.memory()
.read_bytes(VirtAddr(addr), &mut buf)
.map_err(err)?;
self.inner
.breakpoints
.mask_breakpoint_bytes(VirtAddr(addr), &mut buf, process.dtb());
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 write(&self, addr: u64, data: &[u8]) -> PyResult<()> {
self.inner
.target
.current_process()
.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())
}
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.backend.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.backend.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)
}
fn cont(&mut self) -> PyResult<()> {
self.inner.resume().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> {
let never = std::sync::atomic::AtomicBool::new(false);
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
let outcome = match timeout_ms {
Some(ms) => dbg
.inner
.wait_for_stop_bounded(Some(Duration::from_millis(ms)), &never)
.map_err(err)?,
None => loop {
match dbg
.inner
.wait_for_stop_bounded(Some(Duration::from_secs(1)), &never)
.map_err(err)?
{
ContinueOutcome::Running => {
py.check_signals()?;
continue;
}
other => break other,
}
},
};
(dbg.session_id(), dbg.continue_outcome_data(py, outcome)?)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
#[pyo3(signature = (timeout_ms=None))]
fn run<'py>(
slf: Bound<'py, Self>,
py: Python<'py>,
timeout_ms: Option<u64>,
) -> PyResult<StopOutcome> {
let never = std::sync::atomic::AtomicBool::new(false);
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
let outcome = match timeout_ms {
Some(ms) => dbg
.inner
.continue_until_break(Some(Duration::from_millis(ms)), &never)
.map_err(err)?,
None => loop {
match dbg
.inner
.continue_until_break(Some(Duration::from_secs(1)), &never)
.map_err(err)?
{
ContinueOutcome::Running => {
py.check_signals()?; continue;
}
other => break other,
}
},
};
(dbg.session_id(), dbg.continue_outcome_data(py, outcome)?)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
fn step<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("step")?;
dbg.inner.step().map_err(err)?;
let regs = dbg.inner.backend.read_registers().map_err(err)?;
let rip = dbg.inner.register_map.read_u64("rip", ®s).map_err(err)?;
let data = dbg.continue_outcome_data(py, ContinueOutcome::Step { rip })?;
(dbg.session_id(), data)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
fn step_over<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
let never = std::sync::atomic::AtomicBool::new(false);
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("step_over")?;
let outcome = dbg.inner.step_over(&never).map_err(err)?;
(dbg.session_id(), dbg.continue_outcome_data(py, outcome)?)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
fn step_out<'py>(slf: Bound<'py, Self>, py: Python<'py>) -> PyResult<StopOutcome> {
let never = std::sync::atomic::AtomicBool::new(false);
let (session_id, data) = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("step_out")?;
let outcome = dbg.inner.step_out(&never).map_err(err)?;
(dbg.session_id(), dbg.continue_outcome_data(py, outcome)?)
};
Ok(StopOutcome {
dbg: slf.unbind(),
session_id,
data,
})
}
fn interrupt(&mut self) -> PyResult<()> {
self.inner.interrupt().map(|_| ()).map_err(err)
}
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()
}
fn is_running(&self) -> bool {
self.inner.backend.is_running()
}
fn status<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
let s = self.inner.run_status();
let d = PyDict::new(py);
d.set_item("running", s.running)?;
d.set_item("current_thread", s.current_thread)?;
d.set_item("rip", s.rip)?;
d.set_item("symbol", s.symbol)?;
d.set_item("process", s.process)?;
d.set_item("coherent", s.coherent)?;
d.set_item("kernel_base", s.kernel_base)?;
Ok(d)
}
fn bugcheck<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyDict>>> {
match current_bugcheck(&self.inner.target) {
Some(analysis) => Ok(Some(view_dict(py, &view::bugcheck(&analysis))?)),
None => Ok(None),
}
}
fn reload(&mut self) -> PyResult<()> {
self.inner.reload().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 dtb = dbg.inner.target.current_dtb();
let info = dbg
.inner
.target
.symbols
.find_type_across_modules(dtb, "_EPROCESS")
.ok_or_else(|| raise("unknown 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: info.clone(),
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 type_size(&self, ty: &str) -> PyResult<u64> {
let dtb = self.inner.target.current_dtb();
self.inner
.target
.symbols
.find_type_across_modules(dtb, ty)
.map(|t| t.size as u64)
.ok_or_else(|| raise(self.inner.target.symbols.unresolved_type_message(dtb, ty)))
}
fn offset_of(&self, ty: &str, field: &str) -> PyResult<u64> {
let dtb = self.inner.target.current_dtb();
let info = self
.inner
.target
.symbols
.find_type_across_modules(dtb, ty)
.ok_or_else(|| raise(self.inner.target.symbols.unresolved_type_message(dtb, ty)))?;
info.field_offset(field).map_err(err)
}
fn fields(&self, ty: &str) -> PyResult<Vec<(String, u64, u64, String)>> {
let dtb = self.inner.target.current_dtb();
let info = self
.inner
.target
.symbols
.find_type_across_modules(dtb, ty)
.ok_or_else(|| raise(self.inner.target.symbols.unresolved_type_message(dtb, ty)))?;
let mut out: Vec<(String, u64, u64, String)> = 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);
Ok(out)
}
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 dtb = self.inner.target.current_dtb();
let info = self
.inner
.target
.symbols
.find_type_across_modules(dtb, ty)
.ok_or_else(|| raise(self.inner.target.symbols.unresolved_type_message(dtb, ty)))?;
let mut buf = vec![0u8; info.size];
self.inner
.target
.current_process()
.memory()
.read_bytes(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 = {
let d = slf.borrow();
let dtb = d.inner.target.current_dtb();
d.inner
.target
.symbols
.find_type_across_modules(dtb, name)
.ok_or_else(|| raise(d.inner.target.symbols.unresolved_type_message(dtb, 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 dtb = dbg.inner.target.current_dtb();
let record_ti = dbg
.inner
.target
.symbols
.find_type_across_modules(dtb, record_type)
.ok_or_else(|| raise(format!("unknown 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: record_ti.clone(),
base,
})
.collect())
}
fn disassemble(
&self,
addr: u64,
count: usize,
) -> PyResult<Vec<(u64, String, String, Option<String>)>> {
let rows = self.inner.disassemble(VirtAddr(addr), count).map_err(err)?;
Ok(rows
.into_iter()
.map(|r| (r.ip, r.hex, r.asm, r.comment))
.collect())
}
fn backtrace(
&mut self,
limit: Option<usize>,
) -> PyResult<Vec<(u64, u64, String, &'static str)>> {
self.require_halted("backtrace")?;
let trace = self.inner.backtrace(limit.unwrap_or(64)).map_err(err)?;
Ok(trace
.frames
.into_iter()
.map(|f| (f.ip, f.sp, f.symbol, f.source.as_str()))
.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, PyDict>> {
let t = self
.inner
.target
.pte_traverse(VirtAddr(addr))
.map_err(err)?;
let levels = PyList::empty(py);
levels.append(view::to_py(py, &view::pte_level(&t.pxe))?)?;
levels.append(view::to_py(py, &view::pte_level(&t.ppe))?)?;
if let Some(pde) = &t.pde {
levels.append(view::to_py(py, &view::pte_level(pde))?)?;
}
if let Some(pte) = &t.pte {
levels.append(view::to_py(py, &view::pte_level(pte))?)?;
}
let out = PyDict::new(py);
out.set_item("address", t.address.0)?;
out.set_item("dtb", t.dtb)?;
out.set_item("levels", levels)?;
Ok(out)
}
fn describe_address<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, PyDict>> {
let d = self
.inner
.target
.describe_address(VirtAddr(addr))
.map_err(err)?;
view_dict(py, &view::address_description(&d))
}
fn inspect_irp<'py>(&self, py: Python<'py>, addr: u64) -> PyResult<Bound<'py, PyDict>> {
let irp = self.inner.target.inspect_irp(VirtAddr(addr)).map_err(err)?;
view_dict(py, &view::irp(&irp))
}
fn inspect_driver_object<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, PyDict>> {
let d = self
.inner
.target
.inspect_driver_object(VirtAddr(addr))
.map_err(err)?;
view_dict(py, &view::driver_object(&self.inner.target, &d))
}
fn inspect_device_object<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, PyDict>> {
let d = self
.inner
.target
.inspect_device_object(VirtAddr(addr))
.map_err(err)?;
view_dict(py, &view::device_object(&d))
}
fn inspect_object_header<'py>(
&self,
py: Python<'py>,
addr: u64,
) -> PyResult<Bound<'py, PyDict>> {
let o = self
.inner
.target
.inspect_object_header(VirtAddr(addr))
.map_err(err)?;
view_dict(py, &view::object_header(&o))
}
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.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> {
self.inner.target.attach(pid).map(|r| r.name).map_err(err)
}
fn detach(&mut self) {
self.inner.target.detach();
}
fn current_process(&self) -> Option<(u64, String, u64)> {
self.inner
.target
.current_process_info
.as_ref()
.map(|p| (p.pid, p.name.clone(), p.eprocess_va.0))
}
fn memory_map<'py>(&self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
let process = self
.inner
.target
.current_process_info
.clone()
.ok_or_else(|| raise("not attached to a process; call attach_process(pid) first"))?;
let regions = self
.inner
.target
.enumerate_vad_regions_for_process_info(&process)
.map_err(err)?;
let mut out = Vec::with_capacity(regions.len());
for r in regions {
let d = PyDict::new(py);
d.set_item("start", r.start.0)?;
d.set_item("end", r.end.0)?;
d.set_item("size", r.size())?;
d.set_item("protection", r.protection)?;
d.set_item("vad_type", r.vad_type)?;
d.set_item("private", r.private_memory)?;
d.set_item("commit", r.commit_charge)?;
d.set_item("details", r.details)?;
out.push(d);
}
Ok(out)
}
fn kernel_modules(&self) -> PyResult<Vec<(String, u64, u32)>> {
let mods = self.inner.target.guest.kernel_modules().map_err(err)?;
Ok(mods
.into_iter()
.map(|m| (m.name, m.base_address.0, m.size))
.collect())
}
fn modules(&self) -> PyResult<Vec<(String, u64, u32)>> {
let mods = self.inner.target.modules().map_err(err)?;
Ok(mods
.into_iter()
.map(|m| (m.name, m.base_address.0, m.size))
.collect())
}
fn driver_objects(&self) -> PyResult<Vec<(String, u64, u64, u64)>> {
let drivers = self.inner.target.enumerate_driver_objects().map_err(err)?;
Ok(drivers
.into_iter()
.map(|d| (d.name, d.object.0, d.driver_start.0, d.driver_size))
.collect())
}
fn threads<'py>(&mut self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
let (threads, active) = self.inner.windows_threads().map_err(err)?;
let mut out = Vec::with_capacity(threads.len());
for t in threads {
let d = PyDict::new(py);
d.set_item("tid", t.tid)?;
d.set_item("pid", t.pid)?;
d.set_item("process_name", t.process_name)?;
d.set_item("ethread", t.ethread.0)?;
d.set_item("kthread", t.kthread.0)?;
d.set_item("eprocess", t.eprocess.map(|a| a.0))?;
d.set_item("state", t.state)?;
d.set_item("wait_reason", t.wait_reason)?;
d.set_item("active", active.get(&t.ethread.0).cloned())?;
out.push(d);
}
Ok(out)
}
fn vcpus<'py>(&mut self, py: Python<'py>) -> PyResult<Vec<Bound<'py, PyDict>>> {
self.require_halted("vcpus")?;
let vcpus = self.inner.vcpus().map_err(err)?;
let mut out = Vec::with_capacity(vcpus.len());
for v in vcpus {
let d = PyDict::new(py);
d.set_item("id", v.id)?;
d.set_item("rip", v.rip)?;
d.set_item("context", v.context)?;
d.set_item("symbol", v.symbol)?;
d.set_item("error", v.error)?;
out.push(d);
}
Ok(out)
}
fn capabilities(&self) -> Vec<(String, bool)> {
self.inner
.capabilities()
.into_iter()
.map(|c| (c.capability.label().to_string(), c.supported))
.collect()
}
#[pyo3(signature = (since_seq=0))]
fn debug_log<'py>(&self, py: Python<'py>, since_seq: u64) -> PyResult<Bound<'py, PyDict>> {
let page = self.inner.read_debug_output(since_seq);
let lines = pyo3::types::PyList::empty(py);
for line in &page.lines {
let d = PyDict::new(py);
d.set_item("seq", line.seq)?;
d.set_item("timestamp_ms", line.timestamp_ms)?;
d.set_item("text", &line.text)?;
lines.append(d)?;
}
let out = PyDict::new(py);
out.set_item("lines", lines)?;
out.set_item("next_seq", page.next_seq)?;
out.set_item("dropped", page.dropped)?;
Ok(out)
}
#[pyo3(signature = (addr, condition=None))]
fn set_breakpoint(&mut self, addr: u64, condition: Option<String>) -> PyResult<u32> {
self.require_halted("set_breakpoint")?;
self.inner
.add_breakpoint_with_condition(VirtAddr(addr), condition)
.map_err(err)
}
#[pyo3(signature = (target, condition=None))]
fn breakpoint(
slf: Bound<'_, Self>,
target: &Bound<'_, PyAny>,
condition: Option<String>,
) -> PyResult<Breakpoint> {
let (session_id, snapshot) = {
let mut dbg = slf.borrow_mut();
dbg.require_halted("breakpoint")?;
let (addr, symbol) = breakpoint_target_arg(&dbg, target)?;
let id = dbg
.inner
.add_breakpoint_with_symbol_condition(VirtAddr(addr), symbol, condition)
.map_err(err)?;
let snapshot = dbg
.inner
.breakpoint(id)
.map(BreakpointSnapshot::from_core)
.ok_or_else(|| raise(format!("breakpoint {id} disappeared after install")))?;
(dbg.session_id(), snapshot)
};
Ok(Breakpoint {
dbg: Some(slf.unbind()),
session_id,
snapshot,
})
}
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()
}
fn run_command(&mut self, line: &str) -> PyResult<()> {
if line.trim().is_empty() {
return Ok(());
}
let mut state = ReplState::for_oneshot(&mut self.inner);
state.line = line.trim().to_string();
state.dispatch_line(line).map(|_| ()).map_err(err)
}
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() {
Err(raise(format!(
"{operation} requires the VM to be halted; call interrupt() first"
)))
} else {
Ok(())
}
}
fn continue_outcome_data(
&self,
py: Python<'_>,
outcome: ContinueOutcome,
) -> PyResult<StopOutcomeData> {
let symbol_at = |rip: u64| {
self.inner
.target
.closest_symbol_current_context(VirtAddr(rip))
};
let process = self
.inner
.target
.current_process_info
.as_ref()
.map(|p| (p.pid, p.name.clone()));
let data = match outcome {
ContinueOutcome::Breakpoint {
id,
address,
symbol,
temporary,
rip,
} => {
let snapshot = self
.inner
.breakpoint(id)
.map(BreakpointSnapshot::from_core)
.unwrap_or(BreakpointSnapshot {
id,
address,
enabled: true,
symbol: symbol.clone(),
scope: "unknown".to_string(),
condition: None,
temporary,
});
StopOutcomeData {
kind: StopKind::Breakpoint,
rip: Some(rip),
symbol: snapshot
.symbol
.clone()
.or_else(|| symbol.or_else(|| symbol_at(rip))),
process,
breakpoints: vec![snapshot],
address: Some(address),
temporary: Some(temporary),
exception_code: None,
bugcheck_info: None,
kernel_base: None,
coherent: None,
}
}
ContinueOutcome::Bugcheck { rip, info } => {
let analysis = info
.map(|i| analyze_bugcheck(&self.inner.target, &i))
.or_else(|| current_bugcheck(&self.inner.target));
let bugcheck_info = match analysis {
Some(a) => Some(view_dict(py, &view::bugcheck(&a))?.into_any().unbind()),
None => None,
};
StopOutcomeData {
kind: StopKind::Bugcheck,
rip,
symbol: rip.and_then(symbol_at),
process: None,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code: None,
bugcheck_info,
kernel_base: None,
coherent: None,
}
}
ContinueOutcome::Stopped {
rip,
exception_code,
} => StopOutcomeData {
kind: StopKind::Exception,
rip: Some(rip),
symbol: symbol_at(rip),
process,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code,
bugcheck_info: None,
kernel_base: None,
coherent: None,
},
ContinueOutcome::Step { rip } => StopOutcomeData {
kind: StopKind::Step,
rip: Some(rip),
symbol: symbol_at(rip),
process,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code: None,
bugcheck_info: None,
kernel_base: None,
coherent: None,
},
ContinueOutcome::TargetReloaded {
kernel_base,
coherent,
} => StopOutcomeData {
kind: StopKind::TargetReloaded,
rip: None,
symbol: None,
process: None,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code: None,
bugcheck_info: None,
kernel_base,
coherent: Some(coherent),
},
ContinueOutcome::Running => StopOutcomeData {
kind: StopKind::Running,
rip: None,
symbol: None,
process: None,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code: None,
bugcheck_info: None,
kernel_base: None,
coherent: None,
},
ContinueOutcome::Halted { rip } => StopOutcomeData {
kind: StopKind::Halted,
rip: Some(rip),
symbol: symbol_at(rip),
process: None,
breakpoints: Vec::new(),
address: None,
temporary: None,
exception_code: None,
bugcheck_info: None,
kernel_base: None,
coherent: None,
},
};
Ok(data)
}
fn read_fixed<const N: usize>(&self, addr: u64) -> PyResult<[u8; N]> {
let mut buf = [0u8; N];
self.inner
.target
.current_process()
.memory()
.read_bytes(VirtAddr(addr), &mut buf)
.map_err(err)?;
Ok(buf)
}
}
#[pyclass(unsendable)]
pub struct Type {
dbg: Py<Debugger>,
name: String,
info: 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: TypeInfo,
base: u64,
}
impl Struct {
fn cursor_at(&self, py: Python<'_>, type_name: &str, base: u64) -> PyResult<Struct> {
let info = {
let dbg = self.dbg.borrow(py);
let dtb = dbg.inner.target.current_dtb();
dbg.inner
.target
.symbols
.find_type_across_modules(dtb, type_name)
.ok_or_else(|| {
raise(
dbg.inner
.target
.symbols
.unresolved_type_message(dtb, 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> {
let dbg = self.dbg.borrow(py);
let dtb = dbg.inner.target.current_dtb();
let us = dbg
.inner
.target
.symbols
.find_type_across_modules(dtb, "_UNICODE_STRING")
.ok_or_else(|| raise("unknown type: _UNICODE_STRING"))?;
let len_off = us.field_offset("Length").map_err(err)?;
let buf_off = us.field_offset("Buffer").map_err(err)?;
let process = dbg.inner.target.current_process();
let mem = process.memory();
let mut b2 = [0u8; 2];
mem.read_bytes(VirtAddr(addr + len_off), &mut b2)
.map_err(err)?;
let length = u16::from_le_bytes(b2) as usize;
let mut b8 = [0u8; 8];
mem.read_bytes(VirtAddr(addr + buf_off), &mut b8)
.map_err(err)?;
let buffer = u64::from_le_bytes(b8);
if length == 0 || buffer == 0 {
return Ok(String::new());
}
let mut raw = vec![0u8; length];
mem.read_bytes(VirtAddr(buffer), &mut raw).map_err(err)?;
let u16s: Vec<u16> = raw
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
Ok(String::from_utf16_lossy(&u16s))
}
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();
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 + 7) / 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) {
if 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());
}
let sz = field.size as usize;
let mut buf = vec![0u8; sz];
{
let dbg = self.dbg.borrow(py);
dbg.inner
.target
.current_process()
.memory()
.read_bytes(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()
.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 dtb = dbg.inner.target.current_dtb();
let record_ti = dbg
.inner
.target
.symbols
.find_type_across_modules(dtb, record_type)
.ok_or_else(|| raise(format!("unknown 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: record_ti.clone(),
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<'_>, name: &str) -> PyResult<Py<PyAny>> {
if !self.info.fields.contains_key(name) {
return Err(pyo3::exceptions::PyKeyError::new_err(name.to_string()));
}
self.get_field(py, name)
}
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",
] {
v.push(m.to_string());
}
v
}
fn __repr__(&self) -> String {
format!("<{} @ {:#x}>", self.name, self.base)
}
}
#[pyfunction]
#[pyo3(signature = (backend="kd", connect=None))]
fn attach(backend: &str, connect: Option<&str>) -> PyResult<Debugger> {
let inner = Session::connect(|| {
let be: Box<dyn DebugBackend> = match backend {
"gdb" => Box::new(GdbClient::connect(connect.unwrap_or("127.0.0.1:1234"))?),
"kd" => Box::new(KdBackend::connect(
connect.unwrap_or("/tmp/ntoseye-kd.sock"),
)?),
"memory" => Box::new(MemoryBackend::new()),
other => {
return Err(Error::DebugInfo(format!(
"unknown backend '{other}': expected 'kd', 'gdb', or 'memory'"
)));
}
};
Ok(be)
})
.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::<Type>()?;
m.add_class::<Struct>()?;
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: 0x1000,
enabled: true,
symbol: None,
scope: "global".to_string(),
condition: None,
temporary: false,
},
},
)
.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")
);
});
}
}