Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/protocol/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ commands! {
"qXfer:libraries-svr4:read" => _qXfer_libraries_svr4_read::qXferLibrariesSvr4Read<'a>,
}

libraries use 'a {
"qXfer:libraries:read" => _qXfer_libraries_read::qXferLibrariesRead<'a>,
}

tracepoints use 'a {
"QTDPsrc" => _QTDPsrc::QTDPsrc<'a>,
"QTDP" => _QTDP::QTDP<'a>,
Expand Down
18 changes: 18 additions & 0 deletions src/protocol/commands/_qXfer_libraries_read.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::protocol::common::qxfer::ParseAnnex;
use crate::protocol::common::qxfer::QXferReadBase;

pub type qXferLibrariesRead<'a> = QXferReadBase<'a, LibrariesAnnex>;

#[derive(Debug)]
pub struct LibrariesAnnex;

impl ParseAnnex<'_> for LibrariesAnnex {
#[inline(always)]
fn from_buf(buf: &[u8]) -> Option<Self> {
if buf != b"" {
return None;
}

Some(LibrariesAnnex)
}
}
167 changes: 166 additions & 1 deletion src/protocol/response_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl<'a, C: Connection + 'a> ResponseWriter<'a, C> {
}
}
// RLE would output an invalid char ('#' or '$')
6 | 7 => {
// repeat=7 gives 28+7=35='#', repeat=8 gives 28+8=36='$'
7 | 8 => {
self.inner_write(self.rle_char)?;
self.rle_repeat -= 1;
continue;
Expand Down Expand Up @@ -287,3 +288,167 @@ impl<'a, C: Connection + 'a> ResponseWriter<'a, C> {
Ok(())
}
}

#[cfg(test)]
mod tests {
use super::*;
use alloc::vec::Vec;

/// A mock connection that captures all written bytes
struct MockConnection {
data: Vec<u8>,
}

impl MockConnection {
fn new() -> Self {
Self { data: Vec::new() }
}
}

impl Connection for MockConnection {
type Error = ();

fn write(&mut self, byte: u8) -> Result<(), Self::Error> {
self.data.push(byte);
Ok(())
}

fn write_all(&mut self, buf: &[u8]) -> Result<(), Self::Error> {
self.data.extend_from_slice(buf);
Ok(())
}

fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}

/// Helper to decode RLE-encoded data for verification
///
/// GDB RLE format: X*Y means output character X a total of (Y - 29 + 1) times
/// The +1 accounts for the base character X itself.
fn decode_rle(data: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
let mut i = 0;
while i < data.len() {
if i + 2 < data.len() && data[i + 1] == b'*' {
// RLE sequence: char * count
// Output 1 (base) + (count_byte - 29) additional copies
let ch = data[i];
let additional = data[i + 2].wrapping_sub(29);
result.push(ch); // base character
for _ in 0..additional {
result.push(ch); // additional copies
}
i += 3;
} else {
result.push(data[i]);
i += 1;
}
}
result
}

/// Regression test: RLE encoding must not produce '#' or '$' as repeat count.
///
/// The RLE count byte formula is: 28 + repeat_count
/// - repeat=7 gives 35 = '#' (packet terminator)
/// - repeat=8 gives 36 = '$' (packet start)
///
/// Both must be avoided by writing characters directly instead of using RLE.
#[test]
fn rle_avoids_hash_and_dollar() {
let mut conn = MockConnection::new();

{
let mut writer = ResponseWriter::new(&mut conn, true);
// Write exactly 8 identical characters - this would produce '$' as count
// if the bug exists (repeat=8 -> 28+8=36='$')
writer.write_str("00000000").unwrap();
writer.flush().unwrap();
}

// The encoded data should NOT contain '$' except at position 0 (packet start)
// and should NOT contain '#' except at the checksum delimiter
let data = &conn.data;

// Find the checksum delimiter position
let hash_pos = data.iter().rposition(|&b| b == b'#');
assert!(hash_pos.is_some(), "packet should have checksum delimiter");
let hash_pos = hash_pos.unwrap();

// Check that '$' only appears at position 0
for (i, &byte) in data.iter().enumerate() {
if byte == b'$' {
assert_eq!(i, 0, "found '$' at position {} in packet body (should only be at 0)", i);
}
}

// Check that '#' only appears at the checksum position
for (i, &byte) in data.iter().enumerate() {
if byte == b'#' {
assert_eq!(i, hash_pos, "found '#' at position {} (should only be at {})", i, hash_pos);
}
}

// Verify the decoded content is correct
let body = &data[1..hash_pos]; // skip '$' and stop before '#'
let decoded = decode_rle(body);
assert_eq!(decoded, b"00000000", "decoded content should be 8 zeros");
}

/// Test that 7 identical characters (which would produce '#') is also handled
#[test]
fn rle_avoids_hash_with_7_chars() {
let mut conn = MockConnection::new();

{
let mut writer = ResponseWriter::new(&mut conn, true);
// Write exactly 7 identical characters - this would produce '#' as count
// if the bug exists (repeat=7 -> 28+7=35='#')
writer.write_str("1111111").unwrap();
writer.flush().unwrap();
}

let data = &conn.data;
let hash_pos = data.iter().rposition(|&b| b == b'#').unwrap();

// Check that '#' only appears at the checksum position
for (i, &byte) in data.iter().enumerate() {
if byte == b'#' {
assert_eq!(i, hash_pos, "found '#' at position {} (should only be at {})", i, hash_pos);
}
}

let body = &data[1..hash_pos];
let decoded = decode_rle(body);
assert_eq!(decoded, b"1111111", "decoded content should be 7 ones");
}

/// Test that longer runs work correctly (more than 8)
#[test]
fn rle_handles_long_runs() {
let mut conn = MockConnection::new();

{
let mut writer = ResponseWriter::new(&mut conn, true);
// Write 20 identical characters
writer.write_str("aaaaaaaaaaaaaaaaaaaa").unwrap();
writer.flush().unwrap();
}

let data = &conn.data;
let hash_pos = data.iter().rposition(|&b| b == b'#').unwrap();

// Verify no invalid characters in packet body
for (i, &byte) in data[1..hash_pos].iter().enumerate() {
assert_ne!(byte, b'$', "found '$' at body position {}", i);
// '#' in body would break packet framing
assert_ne!(byte, b'#', "found '#' at body position {}", i);
}

let body = &data[1..hash_pos];
let decoded = decode_rle(body);
assert_eq!(decoded, b"aaaaaaaaaaaaaaaaaaaa", "decoded content should be 20 a's");
}
}
1 change: 1 addition & 0 deletions src/stub/core_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
Command::ThreadExtraInfo(cmd) => self.handle_thread_extra_info(res, target, cmd),
Command::LldbRegisterInfo(cmd) => self.handle_lldb_register_info(res, target, cmd),
Command::LibrariesSvr4(cmd) => self.handle_libraries_svr4(res, target, cmd),
Command::Libraries(cmd) => self.handle_libraries(res, target, cmd),
Command::Tracepoints(cmd) => self.handle_tracepoints(res, target, cmd),
// in the worst case, the command could not be parsed...
Command::Unknown(cmd) => {
Expand Down
4 changes: 4 additions & 0 deletions src/stub/core_impl/base.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,10 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {
res.write_str(";qXfer:libraries-svr4:read+")?;
}

if target.support_libraries().is_some() {
res.write_str(";qXfer:libraries:read+")?;
}

HandlerStatus::Handled
}

Expand Down
32 changes: 32 additions & 0 deletions src/stub/core_impl/libraries.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::prelude::*;
use crate::protocol::commands::ext::Libraries;
use crate::protocol::commands::ext::LibrariesSvr4;

impl<T: Target, C: Connection> GdbStubImpl<T, C> {
Expand Down Expand Up @@ -33,4 +34,35 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {

Ok(handler_status)
}

pub(crate) fn handle_libraries(
&mut self,
res: &mut ResponseWriter<'_, C>,
target: &mut T,
command: Libraries<'_>,
) -> Result<HandlerStatus, Error<T::Error, C::Error>> {
let ops = match target.support_libraries() {
Some(ops) => ops,
None => return Ok(HandlerStatus::Handled),
};

crate::__dead_code_marker!("libraries", "impl");

let handler_status = match command {
Libraries::qXferLibrariesRead(cmd) => {
let ret = ops
.get_libraries(cmd.offset, cmd.length, cmd.buf)
.handle_error()?;
if ret == 0 {
res.write_str("l")?;
} else {
res.write_str("m")?;
res.write_binary(cmd.buf.get(..ret).ok_or(Error::PacketBufferOverflow)?)?;
}
HandlerStatus::Handled
}
};

Ok(handler_status)
}
}
13 changes: 12 additions & 1 deletion src/stub/core_impl/resume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,13 +442,24 @@ impl<T: Target, C: Connection> GdbStubImpl<T, C> {

FinishExecStatus::Handled
}
MultiThreadStopReason::Library(tid)
if target.support_libraries().is_some()
|| target.support_libraries_svr4().is_some() =>
{
crate::__dead_code_marker!("libraries", "stop_reason");

self.write_stop_common(res, target, Some(tid), Signal::SIGTRAP)?;
res.write_str("library:;")?;
FinishExecStatus::Handled
}
// Explicitly avoid using `_ =>` to handle the "unguarded" variants, as doing so would
// squelch the useful compiler error that crops up whenever stop reasons are added.
MultiThreadStopReason::SwBreak(_)
| MultiThreadStopReason::HwBreak(_)
| MultiThreadStopReason::Watch { .. }
| MultiThreadStopReason::ReplayLog { .. }
| MultiThreadStopReason::CatchSyscall { .. } => {
| MultiThreadStopReason::CatchSyscall { .. }
| MultiThreadStopReason::Library(_) => {
return Err(Error::UnsupportedStopReason);
}
};
Expand Down
13 changes: 13 additions & 0 deletions src/stub/stop_reason.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ pub enum BaseStopReason<Tid, U> {
/// The location the event occurred at.
position: CatchSyscallPosition,
},
/// The target's library list has changed.
///
/// This stop reason is used to notify the debugger that the list of loaded
/// libraries has changed (e.g., a new shared library was loaded). The
/// debugger can then request the updated library list via
/// `qXfer:libraries:read` or `qXfer:libraries-svr4:read`.
///
/// Requires: [`Libraries`] or [`LibrariesSvr4`].
///
/// [`Libraries`]: crate::target::ext::libraries::Libraries
/// [`LibrariesSvr4`]: crate::target::ext::libraries::LibrariesSvr4
Library(Tid),
}

/// A stop reason for a single threaded target.
Expand Down Expand Up @@ -140,6 +152,7 @@ impl<U> From<BaseStopReason<(), U>> for BaseStopReason<Tid, U> {
number,
position,
},
BaseStopReason::Library(_) => BaseStopReason::Library(crate::SINGLE_THREAD_TID),
}
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/target/ext/libraries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,39 @@ pub trait LibrariesSvr4: Target {
}

define_ext!(LibrariesSvr4Ops, LibrariesSvr4);

/// Target Extension - List a target's libraries (Windows/generic format).
///
/// This is used for targets where library offsets are maintained externally
/// (e.g., Windows PE targets). Unlike SVR4 format, this uses a simpler XML
/// structure with segment addresses.
pub trait Libraries: Target {
/// Get library list XML for this target.
///
/// The expected XML format is:
/// ```xml
/// <library-list>
/// <library name="path/to/library.dll">
/// <segment address="0x10000000"/>
/// </library>
/// </library-list>
/// ```
///
/// See the [GDB Documentation] for more details.
///
/// [GDB Documentation]: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Library-List-Format.html
///
/// Return the number of bytes written into `buf` (which may be less than
/// `length`).
///
/// If `offset` is greater than the length of the underlying data, return
/// `Ok(0)`.
fn get_libraries(
&self,
offset: u64,
length: usize,
buf: &mut [u8],
) -> TargetResult<usize, Self>;
}

define_ext!(LibrariesOps, Libraries);
9 changes: 9 additions & 0 deletions src/target/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,15 @@ pub trait Target {
fn support_libraries_svr4(&mut self) -> Option<ext::libraries::LibrariesSvr4Ops<'_, Self>> {
None
}

/// Support for reading a list of libraries (Windows/generic format).
///
/// This is used for targets where library offsets are maintained externally
/// (e.g., Windows PE targets).
#[inline(always)]
fn support_libraries(&mut self) -> Option<ext::libraries::LibrariesOps<'_, Self>> {
None
}
}

macro_rules! __delegate {
Expand Down
Loading