diff --git a/Cargo.toml b/Cargo.toml index c180951a..4960a0be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ categories = ["development-tools::debugging", "embedded", "emulators", "network- exclude = ["examples/**/*.elf", "examples/**/*.o"] [dependencies] +bitflags = "1.3" cfg-if = "0.1.10" log = "0.4" managed = { version = "0.8", default-features = false } diff --git a/README.md b/README.md index 573bc66e..f3858654 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,8 @@ Of course, most use-cases will want to support additional debugging features as - Get section/segment relocation offsets from the target - Custom `monitor` Commands - Extend the GDB protocol with custom debug commands using GDB's `monitor` command +- Get target memory map +- Perform Host I/O operations _Note:_ GDB features are implemented on an as-needed basis by `gdbstub`'s contributors. If there's a missing GDB feature that you'd like `gdbstub` to implement, please file an issue and/or open a PR! @@ -160,7 +162,7 @@ If you happen to stumble across this crate and end up using it to debug some bar - When the `paranoid_unsafe` feature is enabled, the following `unsafe` code is _removed_: - `src/protocol/packet.rs`: Swaps a couple slice-index methods in `PacketBuf` to use `get_unchecked_mut`. The public API of struct ensures that the bounds used to index into the array remain in-bounds. - - `src/protocol/common/hex`: Use an alternate implementation of `decode_hex_buf` which uses unsafe slice indexing. + - `src/protocol/common/hex`: Use an alternate implementation of `decode_hex_buf`/`decode_bin_buf` which uses unsafe slice indexing. - When the `std` feature is enabled: - `src/connection/impls/unixstream.rs`: An implementation of `UnixStream::peek` which uses `libc::recv`. This manual implementation will be removed once [rust-lang/rust#76923](https://github.com/rust-lang/rust/issues/76923) is stabilized. diff --git a/examples/armv4t/emu.rs b/examples/armv4t/emu.rs index e40cbb43..53b96edb 100644 --- a/examples/armv4t/emu.rs +++ b/examples/armv4t/emu.rs @@ -25,6 +25,7 @@ pub struct Emu { pub(crate) watchpoints: Vec, pub(crate) breakpoints: Vec, + pub(crate) files: Vec>, } impl Emu { @@ -72,6 +73,7 @@ impl Emu { watchpoints: Vec::new(), breakpoints: Vec::new(), + files: Vec::new(), }) } diff --git a/examples/armv4t/gdb/host_io.rs b/examples/armv4t/gdb/host_io.rs new file mode 100644 index 00000000..e4844ab7 --- /dev/null +++ b/examples/armv4t/gdb/host_io.rs @@ -0,0 +1,228 @@ +use std::io::{Read, Seek, Write}; + +use gdbstub::target; +use gdbstub::target::ext::host_io::{ + FsKind, HostIoErrno, HostIoError, HostIoOpenFlags, HostIoOpenMode, HostIoOutput, HostIoResult, + HostIoStat, HostIoToken, +}; + +use crate::emu::Emu; + +impl target::ext::host_io::HostIo for Emu { + #[inline(always)] + fn enable_open(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_close(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_pread(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_pwrite(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_fstat(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_unlink(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_readlink(&mut self) -> Option> { + Some(self) + } + + #[inline(always)] + fn enable_setfs(&mut self) -> Option> { + Some(self) + } +} + +impl target::ext::host_io::HostIoOpen for Emu { + fn open( + &mut self, + filename: &[u8], + flags: HostIoOpenFlags, + _mode: HostIoOpenMode, + ) -> HostIoResult { + if filename.starts_with(b"/proc") { + return Err(HostIoError::Errno(HostIoErrno::ENOENT)); + } + + let path = + std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?; + + let mut read = false; + let mut write = false; + if flags.contains(HostIoOpenFlags::O_RDWR) { + read = true; + write = true; + } else if flags.contains(HostIoOpenFlags::O_WRONLY) { + write = true; + } else { + read = true; + } + + let file = std::fs::OpenOptions::new() + .read(read) + .write(write) + .append(flags.contains(HostIoOpenFlags::O_APPEND)) + .create(flags.contains(HostIoOpenFlags::O_CREAT)) + .truncate(flags.contains(HostIoOpenFlags::O_TRUNC)) + .create_new(flags.contains(HostIoOpenFlags::O_EXCL)) + .open(path)?; + + let n = match self.files.iter_mut().enumerate().find(|(_, f)| f.is_none()) { + Some((n, free_file)) => { + *free_file = Some(file); + n + } + None => { + self.files.push(Some(file)); + self.files.len() - 1 + } + }; + + Ok(n as u32) + } +} + +impl target::ext::host_io::HostIoClose for Emu { + fn close(&mut self, fd: u32) -> HostIoResult<(), Self> { + let file = match self.files.get_mut(fd as usize) { + Some(file) => file, + _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)), + }; + + file.take().ok_or(HostIoError::Errno(HostIoErrno::EBADF))?; + while let Some(None) = self.files.last() { + self.files.pop(); + } + Ok(()) + } +} + +impl target::ext::host_io::HostIoPread for Emu { + fn pread<'a>( + &mut self, + fd: u32, + count: u32, + offset: u32, + output: HostIoOutput<'a>, + ) -> HostIoResult, Self> { + let file = match self.files.get_mut(fd as usize) { + Some(Some(file)) => file, + _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)), + }; + + let mut buffer = vec![0; count as usize]; + file.seek(std::io::SeekFrom::Start(offset as u64))?; + let n = file.read(&mut buffer)?; + Ok(output.write(&buffer[..n])) + } +} + +impl target::ext::host_io::HostIoPwrite for Emu { + fn pwrite(&mut self, fd: u32, offset: u32, data: &[u8]) -> HostIoResult { + let file = match self.files.get_mut(fd as usize) { + Some(Some(file)) => file, + _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)), + }; + + file.seek(std::io::SeekFrom::Start(offset as u64))?; + let n = file.write(data)?; + Ok(n as u32) + } +} + +impl target::ext::host_io::HostIoFstat for Emu { + fn fstat(&mut self, fd: u32) -> HostIoResult { + let metadata = match self.files.get(fd as usize) { + Some(Some(file)) => file.metadata()?, + _ => return Err(HostIoError::Errno(HostIoErrno::EBADF)), + }; + + macro_rules! time_to_secs { + ($time:expr) => { + $time + .map_err(|_| HostIoError::Errno(HostIoErrno::EACCES))? + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .map_err(|_| HostIoError::Errno(HostIoErrno::EACCES))? + .as_secs() as u32 + }; + } + let atime = time_to_secs!(metadata.accessed()); + let mtime = time_to_secs!(metadata.modified()); + let ctime = time_to_secs!(metadata.created()); + + Ok(HostIoStat { + st_dev: 0, + st_ino: 0, + st_mode: HostIoOpenMode::empty(), + st_nlink: 0, + st_uid: 0, + st_gid: 0, + st_rdev: 0, + st_size: metadata.len(), + st_blksize: 0, + st_blocks: 0, + st_atime: atime, + st_mtime: mtime, + st_ctime: ctime, + }) + } +} + +impl target::ext::host_io::HostIoUnlink for Emu { + fn unlink(&mut self, filename: &[u8]) -> HostIoResult<(), Self> { + let path = + std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?; + std::fs::remove_file(path)?; + Ok(()) + } +} + +impl target::ext::host_io::HostIoReadlink for Emu { + fn readlink<'a>( + &mut self, + filename: &[u8], + output: HostIoOutput<'a>, + ) -> HostIoResult, Self> { + if filename == b"/proc/1/exe" { + // Support `info proc exe` command + return Ok(output.write(b"/test.elf")); + } else if filename == b"/proc/1/cwd" { + // Support `info proc cwd` command + return Ok(output.write(b"/")); + } else if filename.starts_with(b"/proc") { + return Err(HostIoError::Errno(HostIoErrno::ENOENT)); + } + + let path = + std::str::from_utf8(filename).map_err(|_| HostIoError::Errno(HostIoErrno::ENOENT))?; + Ok(output.write( + std::fs::read_link(path)? + .to_str() + .ok_or(HostIoError::Errno(HostIoErrno::ENOENT))? + .as_bytes(), + )) + } +} + +impl target::ext::host_io::HostIoSetfs for Emu { + fn setfs(&mut self, _fs: FsKind) -> HostIoResult<(), Self> { + Ok(()) + } +} diff --git a/examples/armv4t/gdb/mod.rs b/examples/armv4t/gdb/mod.rs index 1d239894..6b8896a9 100644 --- a/examples/armv4t/gdb/mod.rs +++ b/examples/armv4t/gdb/mod.rs @@ -17,6 +17,7 @@ use crate::emu::{Emu, Event}; mod breakpoints; mod catch_syscalls; mod extended_mode; +mod host_io; mod memory_map; mod monitor_cmd; mod section_offsets; @@ -94,6 +95,11 @@ impl Target for Emu { fn catch_syscalls(&mut self) -> Option> { Some(self) } + + #[inline(always)] + fn host_io(&mut self) -> Option> { + Some(self) + } } impl Emu { diff --git a/src/gdbstub_impl/ext/host_io.rs b/src/gdbstub_impl/ext/host_io.rs new file mode 100644 index 00000000..911decda --- /dev/null +++ b/src/gdbstub_impl/ext/host_io.rs @@ -0,0 +1,166 @@ +use super::prelude::*; +use crate::protocol::commands::ext::HostIo; + +use crate::arch::Arch; +use crate::target::ext::host_io::{HostIoError, HostIoOutput, HostIoStat}; + +impl GdbStubImpl { + pub(crate) fn handle_host_io( + &mut self, + res: &mut ResponseWriter, + target: &mut T, + command: HostIo, + ) -> Result> { + let ops = match target.host_io() { + Some(ops) => ops, + None => return Ok(HandlerStatus::Handled), + }; + + crate::__dead_code_marker!("host_io", "impl"); + + macro_rules! handle_hostio_result { + ( if let Ok($val:pat) = $ret:expr => $callback:block ) => {{ + match $ret { + Ok($val) => $callback, + Err(HostIoError::Errno(errno)) => { + res.write_str("F-1,")?; + res.write_num(errno as u32)?; + } + Err(HostIoError::Fatal(e)) => return Err(Error::TargetError(e)), + } + }}; + } + + let handler_status = match command { + HostIo::vFileOpen(cmd) if ops.enable_open().is_some() => { + let ops = ops.enable_open().unwrap(); + handle_hostio_result! { + if let Ok(fd) = ops.open(cmd.filename, cmd.flags, cmd.mode) => { + res.write_str("F")?; + res.write_num(fd)?; + } + } + HandlerStatus::Handled + } + HostIo::vFileClose(cmd) if ops.enable_close().is_some() => { + let ops = ops.enable_close().unwrap(); + handle_hostio_result! { + if let Ok(()) = ops.close(cmd.fd) => { + res.write_str("F0")?; + } + } + HandlerStatus::Handled + } + HostIo::vFilePread(cmd) if ops.enable_pread().is_some() => { + let count = ::Usize::from_be_bytes(cmd.count) + .ok_or(Error::TargetMismatch)?; + let offset = ::Usize::from_be_bytes(cmd.offset) + .ok_or(Error::TargetMismatch)?; + let mut err: Result<_, Error> = Ok(()); + let mut callback = |data: &[u8]| { + let e = (|| { + res.write_str("F")?; + res.write_num(data.len())?; + res.write_str(";")?; + res.write_binary(data)?; + Ok(()) + })(); + + if let Err(e) = e { + err = Err(e) + } + }; + + let ops = ops.enable_pread().unwrap(); + handle_hostio_result! { + if let Ok(_) = ops.pread(cmd.fd, count, offset, HostIoOutput::new(&mut callback)) => {} + }; + err?; + + HandlerStatus::Handled + } + HostIo::vFilePwrite(cmd) if ops.enable_pwrite().is_some() => { + let offset = ::Usize::from_be_bytes(cmd.offset) + .ok_or(Error::TargetMismatch)?; + let ops = ops.enable_pwrite().unwrap(); + handle_hostio_result! { + if let Ok(ret) = ops.pwrite(cmd.fd, offset, cmd.data) => { + res.write_str("F")?; + res.write_num(ret)?; + } + }; + HandlerStatus::Handled + } + HostIo::vFileFstat(cmd) if ops.enable_fstat().is_some() => { + let ops = ops.enable_fstat().unwrap(); + handle_hostio_result! { + if let Ok(stat) = ops.fstat(cmd.fd) => { + let size = core::mem::size_of::(); + res.write_str("F")?; + res.write_num(size)?; + res.write_str(";")?; + res.write_binary(&stat.st_dev.to_be_bytes())?; + res.write_binary(&stat.st_ino.to_be_bytes())?; + res.write_binary(&(stat.st_mode.bits()).to_be_bytes())?; + res.write_binary(&stat.st_nlink.to_be_bytes())?; + res.write_binary(&stat.st_uid.to_be_bytes())?; + res.write_binary(&stat.st_gid.to_be_bytes())?; + res.write_binary(&stat.st_rdev.to_be_bytes())?; + res.write_binary(&stat.st_size.to_be_bytes())?; + res.write_binary(&stat.st_blksize.to_be_bytes())?; + res.write_binary(&stat.st_blocks.to_be_bytes())?; + res.write_binary(&stat.st_atime.to_be_bytes())?; + res.write_binary(&stat.st_mtime.to_be_bytes())?; + res.write_binary(&stat.st_ctime.to_be_bytes())?; + } + }; + HandlerStatus::Handled + } + HostIo::vFileUnlink(cmd) if ops.enable_unlink().is_some() => { + let ops = ops.enable_unlink().unwrap(); + handle_hostio_result! { + if let Ok(()) = ops.unlink(cmd.filename) => { + res.write_str("F0")?; + } + }; + HandlerStatus::Handled + } + HostIo::vFileReadlink(cmd) if ops.enable_readlink().is_some() => { + let mut err: Result<_, Error> = Ok(()); + let mut callback = |data: &[u8]| { + let e = (|| { + res.write_str("F")?; + res.write_num(data.len())?; + res.write_str(";")?; + res.write_binary(data)?; + Ok(()) + })(); + + if let Err(e) = e { + err = Err(e) + } + }; + + let ops = ops.enable_readlink().unwrap(); + handle_hostio_result! { + if let Ok(_) = ops.readlink(cmd.filename, HostIoOutput::new(&mut callback)) => {} + }; + err?; + + HandlerStatus::Handled + } + HostIo::vFileSetfs(cmd) if ops.enable_setfs().is_some() => { + let ops = ops.enable_setfs().unwrap(); + handle_hostio_result! { + if let Ok(()) = ops.setfs(cmd.fs) => { + res.write_str("F0")?; + } + }; + HandlerStatus::Handled + } + _ => HandlerStatus::Handled, + }; + + Ok(handler_status) + } +} diff --git a/src/gdbstub_impl/ext/mod.rs b/src/gdbstub_impl/ext/mod.rs index 227e3d8d..2aae2fdf 100644 --- a/src/gdbstub_impl/ext/mod.rs +++ b/src/gdbstub_impl/ext/mod.rs @@ -15,6 +15,7 @@ mod base; mod breakpoints; mod catch_syscalls; mod extended_mode; +mod host_io; mod memory_map; mod monitor_cmd; mod reverse_exec; diff --git a/src/gdbstub_impl/mod.rs b/src/gdbstub_impl/mod.rs index 221537b4..b27390ac 100644 --- a/src/gdbstub_impl/mod.rs +++ b/src/gdbstub_impl/mod.rs @@ -561,9 +561,7 @@ impl GdbStubImpl { ) -> Result> { match cmd { Command::Unknown(cmd) => { - // cmd must be ASCII, as the slice originated from a PacketBuf, which checks for - // ASCII as part of the initial validation. - info!("Unknown command: {}", core::str::from_utf8(cmd).unwrap()); + info!("Unknown command: {:?}", core::str::from_utf8(cmd)); Ok(HandlerStatus::Handled) } // `handle_X` methods are defined in the `ext` module @@ -579,6 +577,7 @@ impl GdbStubImpl { Command::ReverseCont(cmd) => self.handle_reverse_cont(res, target, cmd), Command::ReverseStep(cmd) => self.handle_reverse_step(res, target, cmd), Command::MemoryMap(cmd) => self.handle_memory_map(res, target, cmd), + Command::HostIo(cmd) => self.handle_host_io(res, target, cmd), } } } diff --git a/src/protocol/commands.rs b/src/protocol/commands.rs index e70e71a1..98538751 100644 --- a/src/protocol/commands.rs +++ b/src/protocol/commands.rs @@ -6,7 +6,9 @@ use crate::target::Target; pub(self) mod prelude { pub use super::ParseCommand; pub use crate::common::*; - pub use crate::protocol::common::hex::{decode_hex, decode_hex_buf, is_hex, HexString}; + pub use crate::protocol::common::hex::{ + decode_bin_buf, decode_hex, decode_hex_buf, is_hex, HexString, + }; pub use crate::protocol::common::lists; pub use crate::protocol::common::thread_id::{ IdKind, SpecificIdKind, SpecificThreadId, ThreadId, @@ -223,6 +225,17 @@ commands! { "qXfer:memory-map:read" => _qXfer_memory_map::qXferMemoryMapRead, } + host_io use 'a { + "vFile:open" => _vFile_open::vFileOpen<'a>, + "vFile:close" => _vFile_close::vFileClose, + "vFile:pread" => _vFile_pread::vFilePread<'a>, + "vFile:pwrite" => _vFile_pwrite::vFilePwrite<'a>, + "vFile:fstat" => _vFile_fstat::vFileFstat, + "vFile:unlink" => _vFile_unlink::vFileUnlink<'a>, + "vFile:readlink" => _vFile_readlink::vFileReadlink<'a>, + "vFile:setfs" => _vFile_setfs::vFileSetfs, + } + catch_syscalls use 'a { "QCatchSyscalls" => _QCatchSyscalls::QCatchSyscalls<'a>, } diff --git a/src/protocol/commands/_vFile_close.rs b/src/protocol/commands/_vFile_close.rs new file mode 100644 index 00000000..8f2e69ad --- /dev/null +++ b/src/protocol/commands/_vFile_close.rs @@ -0,0 +1,23 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFileClose { + pub fd: u32, +} + +impl<'a> ParseCommand<'a> for vFileClose { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let fd = decode_hex(body).ok()?; + Some(vFileClose{fd}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_fstat.rs b/src/protocol/commands/_vFile_fstat.rs new file mode 100644 index 00000000..9f8b02f3 --- /dev/null +++ b/src/protocol/commands/_vFile_fstat.rs @@ -0,0 +1,23 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFileFstat { + pub fd: u32, +} + +impl<'a> ParseCommand<'a> for vFileFstat { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let fd = decode_hex(body).ok()?; + Some(vFileFstat{fd}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_open.rs b/src/protocol/commands/_vFile_open.rs new file mode 100644 index 00000000..67db32fe --- /dev/null +++ b/src/protocol/commands/_vFile_open.rs @@ -0,0 +1,30 @@ +use super::prelude::*; + +use crate::target::ext::host_io::{HostIoOpenFlags, HostIoOpenMode}; + +#[derive(Debug)] +pub struct vFileOpen<'a> { + pub filename: &'a [u8], + pub flags: HostIoOpenFlags, + pub mode: HostIoOpenMode, +} + +impl<'a> ParseCommand<'a> for vFileOpen<'a> { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let mut body = body.splitn_mut_no_panic(3, |b| *b == b','); + let filename = decode_hex_buf(body.next()?).ok()?; + let flags = HostIoOpenFlags::from_bits(decode_hex(body.next()?).ok()?)?; + let mode = HostIoOpenMode::from_bits(decode_hex(body.next()?).ok()?)?; + Some(vFileOpen{filename, flags, mode}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_pread.rs b/src/protocol/commands/_vFile_pread.rs new file mode 100644 index 00000000..4b795952 --- /dev/null +++ b/src/protocol/commands/_vFile_pread.rs @@ -0,0 +1,28 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFilePread<'a> { + pub fd: u32, + pub count: &'a [u8], + pub offset: &'a [u8], +} + +impl<'a> ParseCommand<'a> for vFilePread<'a> { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let mut body = body.splitn_mut_no_panic(3, |b| *b == b','); + let fd = decode_hex(body.next()?).ok()?; + let count = decode_hex_buf(body.next()?).ok()?; + let offset = decode_hex_buf(body.next()?).ok()?; + Some(vFilePread{fd, count, offset}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_pwrite.rs b/src/protocol/commands/_vFile_pwrite.rs new file mode 100644 index 00000000..8e155d03 --- /dev/null +++ b/src/protocol/commands/_vFile_pwrite.rs @@ -0,0 +1,28 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFilePwrite<'a> { + pub fd: u32, + pub offset: &'a [u8], + pub data: &'a [u8], +} + +impl<'a> ParseCommand<'a> for vFilePwrite<'a> { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let mut body = body.splitn_mut_no_panic(3, |b| *b == b','); + let fd = decode_hex(body.next()?).ok()?; + let offset = decode_hex_buf(body.next()?).ok()?; + let data = decode_bin_buf(body.next()?).ok()?; + Some(vFilePwrite{fd, offset, data}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_readlink.rs b/src/protocol/commands/_vFile_readlink.rs new file mode 100644 index 00000000..1223bed5 --- /dev/null +++ b/src/protocol/commands/_vFile_readlink.rs @@ -0,0 +1,23 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFileReadlink<'a> { + pub filename: &'a [u8], +} + +impl<'a> ParseCommand<'a> for vFileReadlink<'a> { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let filename = decode_hex_buf(body).ok()?; + Some(vFileReadlink{filename}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_setfs.rs b/src/protocol/commands/_vFile_setfs.rs new file mode 100644 index 00000000..331948c5 --- /dev/null +++ b/src/protocol/commands/_vFile_setfs.rs @@ -0,0 +1,28 @@ +use super::prelude::*; + +use crate::target::ext::host_io::FsKind; + +#[derive(Debug)] +pub struct vFileSetfs { + pub fs: FsKind, +} + +impl<'a> ParseCommand<'a> for vFileSetfs { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let fs = match core::num::NonZeroUsize::new(decode_hex(body).ok()?) { + None => FsKind::Stub, + Some(pid) => FsKind::Pid(pid), + }; + Some(vFileSetfs{fs}) + }, + _ => None, + } + } +} diff --git a/src/protocol/commands/_vFile_unlink.rs b/src/protocol/commands/_vFile_unlink.rs new file mode 100644 index 00000000..a7137b5b --- /dev/null +++ b/src/protocol/commands/_vFile_unlink.rs @@ -0,0 +1,23 @@ +use super::prelude::*; + +#[derive(Debug)] +pub struct vFileUnlink<'a> { + pub filename: &'a [u8], +} + +impl<'a> ParseCommand<'a> for vFileUnlink<'a> { + fn from_packet(buf: PacketBuf<'a>) -> Option { + let body = buf.into_body(); + if body.is_empty() { + return None; + } + + match body { + [b':', body @ ..] => { + let filename = decode_hex_buf(body).ok()?; + Some(vFileUnlink{filename}) + }, + _ => None, + } + } +} diff --git a/src/protocol/common/hex.rs b/src/protocol/common/hex.rs index 2d13b232..e30d88c3 100644 --- a/src/protocol/common/hex.rs +++ b/src/protocol/common/hex.rs @@ -168,7 +168,43 @@ pub fn decode_hex_buf(base_buf: &mut [u8]) -> Result<&mut [u8], DecodeHexBufErro Ok(&mut base_buf[..decoded_len + odd_adust]) } -#[allow(dead_code)] +#[derive(Debug)] +pub enum DecodeBinBufError { + UnexpectedEnd, +} + +/// Decode GDB escaped binary bytes into origin bytes _in place_. +pub fn decode_bin_buf(buf: &mut [u8]) -> Result<&mut [u8], DecodeBinBufError> { + use DecodeBinBufError::*; + let mut i = 0; + let mut j = 0; + let len = buf.len(); + while i < len && j < len { + if buf[i] == b'}' { + if i + 1 >= len { + return Err(UnexpectedEnd); + } else { + buf[j] = buf[i + 1] ^ 0x20; + i += 1; + } + } else { + buf[j] = buf[i]; + } + i += 1; + j += 1; + } + + // SAFETY: by inspection, the value of j will never exceed buf.len(). + // Unfortunately, the LLVM optimizer isn't smart enough to see this, so + // we have to manually elide the bounds check... + if cfg!(feature = "paranoid_unsafe") { + Ok(&mut buf[..j]) + } else { + debug_assert!(j <= len); + unsafe { Ok(buf.get_unchecked_mut(..j)) } + } +} + #[derive(Debug)] pub enum EncodeHexBufError { SmallBuffer, @@ -268,4 +304,11 @@ mod tests { let res = decode_hex_buf(&mut payload).unwrap(); assert_eq!(res, [0x1]); } + + #[test] + fn decode_bin_buf_escaped() { + let mut payload = b"}\x03}\x04}]}\n".to_vec(); + let res = decode_bin_buf(&mut payload).unwrap(); + assert_eq!(res, [0x23, 0x24, 0x7d, 0x2a]); + } } diff --git a/src/protocol/packet.rs b/src/protocol/packet.rs index 61704249..84dd07b7 100644 --- a/src/protocol/packet.rs +++ b/src/protocol/packet.rs @@ -10,7 +10,6 @@ pub enum PacketParseError { MissingChecksum, MalformedChecksum, MalformedCommand, - NotAscii, UnexpectedHeader(u8), } @@ -41,7 +40,7 @@ pub struct PacketBuf<'a> { impl<'a> PacketBuf<'a> { /// Validate the contents of the raw packet buffer, checking for checksum - /// consistency, structural correctness, and ASCII validation. + /// consistency and structural correctness. pub fn new(pkt_buf: &'a mut [u8]) -> Result, PacketParseError> { if pkt_buf.is_empty() { return Err(PacketParseError::EmptyBuf); @@ -57,11 +56,6 @@ impl<'a> PacketBuf<'a> { .get(..2) .ok_or(PacketParseError::MalformedChecksum)?; - // validate that the body is valid ASCII - if !body.is_ascii() { - return Err(PacketParseError::NotAscii); - } - // validate the checksum let checksum = decode_hex(checksum).map_err(|_| PacketParseError::MalformedChecksum)?; let calculated = body.iter().fold(0u8, |a, x| a.wrapping_add(*x)); @@ -81,14 +75,9 @@ impl<'a> PacketBuf<'a> { } /// (used for tests) Create a packet buffer from a raw body buffer, skipping - /// the header/checksum trimming stage. ASCII validation is still performed. + /// the header/checksum trimming stage. #[cfg(test)] pub fn new_with_raw_body(body: &'a mut [u8]) -> Result, PacketParseError> { - // validate the packet is valid ASCII - if !body.is_ascii() { - return Err(PacketParseError::NotAscii); - } - let len = body.len(); Ok(PacketBuf { buf: body, diff --git a/src/protocol/response_writer.rs b/src/protocol/response_writer.rs index 28cca863..64c26d28 100644 --- a/src/protocol/response_writer.rs +++ b/src/protocol/response_writer.rs @@ -54,7 +54,7 @@ impl<'a, C: Connection + 'a> ResponseWriter<'a, C> { #[cfg(feature = "std")] trace!( "--> ${}#{:02x?}", - core::str::from_utf8(&self.msg).unwrap(), // buffers are always ascii + String::from_utf8_lossy(&self.msg), checksum ); diff --git a/src/target/ext/host_io.rs b/src/target/ext/host_io.rs new file mode 100644 index 00000000..36a60ee3 --- /dev/null +++ b/src/target/ext/host_io.rs @@ -0,0 +1,388 @@ +//! Provide Host I/O operations for the target. +use bitflags::bitflags; + +use crate::arch::Arch; +use crate::target::Target; + +bitflags! { + /// Host flags for opening files. + /// + /// Extracted from the GDB documentation at + /// [Open Flags](https://sourceware.org/gdb/current/onlinedocs/gdb/Open-Flags.html#Open-Flags) + pub struct HostIoOpenFlags: u32 { + /// A read-only file. + const O_RDONLY = 0x0; + /// A write-only file. + const O_WRONLY = 0x1; + /// A read-write file. + const O_RDWR = 0x2; + /// Append to an existing file. + const O_APPEND = 0x8; + /// Create a non-existent file. + const O_CREAT = 0x200; + /// Truncate an existing file. + const O_TRUNC = 0x400; + /// Exclusive access. + const O_EXCL = 0x800; + } +} + +bitflags! { + /// Host file permissions. + /// + /// Extracted from the GDB documentation at + /// [mode_t Values](https://sourceware.org/gdb/current/onlinedocs/gdb/mode_005ft-Values.html#mode_005ft-Values) + pub struct HostIoOpenMode: u32 { + /// A regular file. + const S_IFREG = 0o100000; + /// A directory. + const S_IFDIR = 0o40000; + /// User read permissions. + const S_IRUSR = 0o400; + /// User write permissions. + const S_IWUSR = 0o200; + /// User execute permissions. + const S_IXUSR = 0o100; + /// Group read permissions. + const S_IRGRP = 0o40; + /// Group write permissions + const S_IWGRP = 0o20; + /// Group execute permissions. + const S_IXGRP = 0o10; + /// World read permissions. + const S_IROTH = 0o4; + /// World write permissions + const S_IWOTH = 0o2; + /// World execute permissions. + const S_IXOTH = 0o1; + } +} + +/// Data returned by a host fstat request. +/// +/// Extracted from the GDB documentation at +/// [struct stat](https://sourceware.org/gdb/current/onlinedocs/gdb/struct-stat.html#struct-stat) +#[derive(Debug)] +pub struct HostIoStat { + /// The device. + pub st_dev: u32, + /// The inode. + pub st_ino: u32, + /// Protection bits. + pub st_mode: HostIoOpenMode, + /// The number of hard links. + pub st_nlink: u32, + /// The user id of the owner. + pub st_uid: u32, + /// The group id of the owner. + pub st_gid: u32, + /// The device type, if an inode device. + pub st_rdev: u32, + /// The size of the file in bytes. + pub st_size: u64, + /// The blocksize for the filesystem. + pub st_blksize: u64, + /// The number of blocks allocated. + pub st_blocks: u64, + /// The last time the file was accessed, in seconds since the epoch. + pub st_atime: u32, + /// The last time the file was modified, in seconds since the epoch. + pub st_mtime: u32, + /// The last time the file was changed, in seconds since the epoch. + pub st_ctime: u32, +} + +/// Select the filesystem vFile operations will operate on. Used by vFile setfs +/// command. +#[derive(Debug)] +pub enum FsKind { + /// Select the filesystem as seen by the remote stub. + Stub, + /// Select the filesystem as seen by process pid. + Pid(crate::common::Pid), +} + +/// Errno values for Host I/O operations. +/// +/// Extracted from the GDB documentation at +/// [Errno Values]: https://sourceware.org/gdb/onlinedocs/gdb/Errno-Values.html +#[derive(Debug)] +pub enum HostIoErrno { + /// Operation not permitted (POSIX.1-2001). + EPERM = 1, + /// No such file or directory (POSIX.1-2001). + /// + /// Typically, this error results when a specified pathname does not exist, + /// or one of the components in the directory prefix of a pathname does not + /// exist, or the specified pathname is a dangling symbolic link. + ENOENT = 2, + /// Interrupted function call (POSIX.1-2001); see signal(7). + EINTR = 4, + /// Bad file descriptor (POSIX.1-2001). + EBADF = 9, + /// Permission denied (POSIX.1-2001). + EACCES = 13, + /// Bad address (POSIX.1-2001). + EFAULT = 14, + /// Device or resource busy (POSIX.1-2001). + EBUSY = 16, + /// File exists (POSIX.1-2001). + EEXIST = 17, + /// No such device (POSIX.1-2001). + ENODEV = 19, + /// Not a directory (POSIX.1-2001). + ENOTDIR = 20, + /// Is a directory (POSIX.1-2001). + EISDIR = 21, + /// Invalid argument (POSIX.1-2001). + EINVAL = 22, + /// Too many open files in system (POSIX.1-2001). On Linux, this is probably + /// a result of encountering the /proc/sys/fs/file-max limit (see proc(5)). + ENFILE = 23, + /// Too many open files (POSIX.1-2001). Commonly caused by exceeding the + /// RLIMIT_NOFILE resource limit described in getrlimit(2). + EMFILE = 24, + /// File too large (POSIX.1-2001). + EFBIG = 27, + /// No space left on device (POSIX.1-2001). + ENOSPC = 28, + /// Invalid seek (POSIX.1-2001). + ESPIPE = 29, + /// Read-only filesystem (POSIX.1-2001). + EROFS = 30, + /// Filename too long (POSIX.1-2001). + ENAMETOOLONG = 91, + /// Unknown errno - there may not be a GDB mapping for this value + EUNKNOWN = 9999, +} + +/// The error type for Host I/O operations. +pub enum HostIoError { + /// An operation-specific non-fatal error code. + /// + /// See [`HostIoErrno`] for more details. + Errno(HostIoErrno), + /// A target-specific fatal error. + /// + /// **WARNING:** Returning this error will immediately halt the target's + /// execution and return a `GdbStubError::TargetError` from `GdbStub::run`! + /// + /// Note that the debugging session will will _not_ be terminated, and can + /// be resumed by calling `GdbStub::run` after resolving the error and/or + /// setting up a post-mortem debugging environment. + Fatal(E), +} + +/// When the `std` feature is enabled, `HostIoError` implements +/// `From`, mapping [`std::io::ErrorKind`] to the appropriate +/// [`HostIoErrno`] when possible, and falling back to [`HostIoErrno::EUNKNOWN`] +/// when no mapping exists. +#[cfg(feature = "std")] +impl From for HostIoError { + fn from(e: std::io::Error) -> HostIoError { + use std::io::ErrorKind::*; + let errno = match e.kind() { + PermissionDenied => HostIoErrno::EPERM, + NotFound => HostIoErrno::ENOENT, + Interrupted => HostIoErrno::EINTR, + AlreadyExists => HostIoErrno::EEXIST, + InvalidInput => HostIoErrno::EINVAL, + _ => HostIoErrno::EUNKNOWN, + }; + HostIoError::Errno(errno) + } +} + +/// A specialized `Result` type for Host I/O operations. Supports reporting +/// non-fatal errors back to the GDB client. +/// +/// See [`HostIoError`] for more details. +pub type HostIoResult = Result::Error>>; + +/// Zero-sized type token that ensures HostIoOutput::write is called. +pub struct HostIoToken<'a>(core::marker::PhantomData<&'a *mut ()>); + +/// An interface to send pread data back to the GDB client. +pub struct HostIoOutput<'a> { + cb: &'a mut dyn FnMut(&[u8]), + token: HostIoToken<'a>, +} + +impl<'a> HostIoOutput<'a> { + pub(crate) fn new(cb: &'a mut dyn FnMut(&[u8])) -> Self { + Self { + cb, + token: HostIoToken(core::marker::PhantomData), + } + } + + /// Write out raw file bytes to the GDB debugger. + pub fn write(self, buf: &[u8]) -> HostIoToken<'a> { + (self.cb)(buf); + self.token + } +} + +/// Target Extension - Perform I/O operations on host +pub trait HostIo: Target { + /// Enable open operation. + #[inline(always)] + fn enable_open(&mut self) -> Option> { + None + } + /// Enable close operation. + #[inline(always)] + fn enable_close(&mut self) -> Option> { + None + } + /// Enable pread operation. + #[inline(always)] + fn enable_pread(&mut self) -> Option> { + None + } + /// Enable pwrite operation. + #[inline(always)] + fn enable_pwrite(&mut self) -> Option> { + None + } + /// Enable fstat operation. + #[inline(always)] + fn enable_fstat(&mut self) -> Option> { + None + } + /// Enable unlink operation. + #[inline(always)] + fn enable_unlink(&mut self) -> Option> { + None + } + /// Enable readlink operation. + #[inline(always)] + fn enable_readlink(&mut self) -> Option> { + None + } + /// Enable setfs operation. + #[inline(always)] + fn enable_setfs(&mut self) -> Option> { + None + } +} + +define_ext!(HostIoOps, HostIo); + +/// Nested Target Extension - Host I/O open operation. +pub trait HostIoOpen: HostIo { + /// Open a file at `filename` and return a file descriptor for it, or return + /// [`HostIoError::Errno`] if an error occurs. + /// + /// `flags` are the flags used when opening the file (see + /// [`HostIoOpenFlags`]), and `mode` is the mode used if the file is + /// created (see [`HostIoOpenMode`]). + fn open( + &mut self, + filename: &[u8], + flags: HostIoOpenFlags, + mode: HostIoOpenMode, + ) -> HostIoResult; +} + +define_ext!(HostIoOpenOps, HostIoOpen); + +/// Nested Target Extension - Host I/O close operation. +pub trait HostIoClose: HostIo { + /// Close the open file corresponding to `fd`. + fn close(&mut self, fd: u32) -> HostIoResult<(), Self>; +} + +define_ext!(HostIoCloseOps, HostIoClose); + +/// Nested Target Extension - Host I/O pread operation. +pub trait HostIoPread: HostIo { + /// Read data from the open file corresponding to `fd`. + /// + /// Up to `count` bytes will be read from the file, starting at `offset` + /// relative to the start of the file. + /// + /// The data read _must_ be sent by calling [`HostIoOutput::write`], which + /// will consume the `output` object and return a [`HostIoToken`]. This + /// token ensures that the implementer of this method calls + /// [`HostIoOutput::write`]. + fn pread<'a>( + &mut self, + fd: u32, + count: ::Usize, + offset: ::Usize, + output: HostIoOutput<'a>, + ) -> HostIoResult, Self>; +} + +define_ext!(HostIoPreadOps, HostIoPread); + +/// Nested Target Extension - Host I/O pwrite operation. +pub trait HostIoPwrite: HostIo { + /// Write `data` to the open file corresponding to `fd`. + /// + /// Start the write at `offset` from the start of the file. + /// + /// Return the number of bytes written, which may be shorter + /// than the length of data, or [`HostIoError::Errno`] if an error occurred. + fn pwrite( + &mut self, + fd: u32, + offset: ::Usize, + data: &[u8], + ) -> HostIoResult<::Usize, Self>; +} + +define_ext!(HostIoPwriteOps, HostIoPwrite); + +/// Nested Target Extension - Host I/O fstat operation. +pub trait HostIoFstat: HostIo { + /// Get information about the open file corresponding to `fd`. + /// + /// On success return a [`HostIoStat`] struct. + /// Return [`HostIoError::Errno`] if an error occurs. + fn fstat(&mut self, fd: u32) -> HostIoResult; +} + +define_ext!(HostIoFstatOps, HostIoFstat); + +/// Nested Target Extension - Host I/O unlink operation. +pub trait HostIoUnlink: HostIo { + /// Delete the file at `filename` on the target. + fn unlink(&mut self, filename: &[u8]) -> HostIoResult<(), Self>; +} + +define_ext!(HostIoUnlinkOps, HostIoUnlink); + +/// Nested Target Extension - Host I/O readlink operation. +pub trait HostIoReadlink: HostIo { + /// Read value of symbolic link `filename` on the target. + /// + /// The data read _must_ be sent by calling [`HostIoOutput::write`], which + /// will consume the `output` object and return a [`HostIoToken`]. This + /// token ensures that the implementer of this method calls + /// [`HostIoOutput::write`]. + fn readlink<'a>( + &mut self, + filename: &[u8], + output: HostIoOutput<'a>, + ) -> HostIoResult, Self>; +} + +define_ext!(HostIoReadlinkOps, HostIoReadlink); + +/// Nested Target Extension - Host I/O setfs operation. +pub trait HostIoSetfs: HostIo { + /// Select the filesystem on which vFile operations with filename arguments + /// will operate. This is required for GDB to be able to access files on + /// remote targets where the remote stub does not share a common filesystem + /// with the inferior(s). + /// + /// See [`FsKind`] for the meaning of `fs`. + /// + /// If setfs indicates success, the selected filesystem remains selected + /// until the next successful setfs operation. + fn setfs(&mut self, fs: FsKind) -> HostIoResult<(), Self>; +} + +define_ext!(HostIoSetfsOps, HostIoSetfs); diff --git a/src/target/ext/mod.rs b/src/target/ext/mod.rs index 5bcd2163..99f02809 100644 --- a/src/target/ext/mod.rs +++ b/src/target/ext/mod.rs @@ -260,6 +260,7 @@ pub mod base; pub mod breakpoints; pub mod catch_syscalls; pub mod extended_mode; +pub mod host_io; pub mod memory_map; pub mod monitor_cmd; pub mod section_offsets; diff --git a/src/target/mod.rs b/src/target/mod.rs index 02ca29cb..0f262bee 100644 --- a/src/target/mod.rs +++ b/src/target/mod.rs @@ -358,6 +358,12 @@ pub trait Target { fn catch_syscalls(&mut self) -> Option> { None } + + /// Support Host I/O operations. + #[inline(always)] + fn host_io(&mut self) -> Option> { + None + } } macro_rules! impl_dyn_target { @@ -394,6 +400,16 @@ macro_rules! impl_dyn_target { (**self).extended_mode() } + #[inline(always)] + fn host_io(&mut self) -> Option> { + (**self).host_io() + } + + #[inline(always)] + fn memory_map(&mut self) -> Option> { + (**self).memory_map() + } + #[inline(always)] fn section_offsets(&mut self) -> Option> { (**self).section_offsets()