From 1a4df6b746384e37ffaaa807c18f4a8108229e64 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 26 Jan 2026 21:45:54 +0300 Subject: [PATCH 01/20] replace implementation with #[from] macro --- src/ext/search.rs | 32 ++++---------------------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/src/ext/search.rs b/src/ext/search.rs index b4a9fba..0b68e66 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -222,41 +222,17 @@ impl SearchExt for crate::prelude::Process { #[derive(Debug, Error)] pub enum SearchExtError { #[error("An IO error occured. {0:?} {0}")] - IO(io::Error), + IO(#[from] io::Error), #[error("Could not convert number. {0} {0:?}")] - TryFromIntError(num::TryFromIntError), + TryFromIntError(#[from] num::TryFromIntError), #[error("A *nix error occured. {0:?} {0}")] - Nix(nix::Error), + Nix(#[from] nix::Error), #[error("An error occured while checking process mapping. {0} {0:?}")] - MappingError(MappingExtError), + MappingError(#[from] MappingExtError), } pub type SearchExtResult = Result; -impl From for SearchExtError { - fn from(value: io::Error) -> Self { - Self::IO(value) - } -} - -impl From for SearchExtError { - fn from(value: nix::Error) -> Self { - Self::Nix(value) - } -} - -impl From for SearchExtError { - fn from(value: MappingExtError) -> Self { - Self::MappingError(value) - } -} - -impl From for SearchExtError { - fn from(value: num::TryFromIntError) -> Self { - Self::TryFromIntError(value) - } -} - #[cfg(target_os = "linux")] #[cfg(test)] mod linux_tests { From ff3ab1c5dcbc309295f7a2fda1ba094903531f2c Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 26 Jan 2026 23:56:31 +0300 Subject: [PATCH 02/20] install scopeguard --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + 2 files changed, 8 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ef8493c..2f2d6e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1055,6 +1055,7 @@ dependencies = [ "nix", "reqwest", "rstest", + "scopeguard", "thiserror 2.0.18", ] @@ -1076,6 +1077,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "security-framework" version = "3.5.1" diff --git a/Cargo.toml b/Cargo.toml index 71fa36e..98e0ef8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] nix = { version = "0.31.1", features = ["ptrace"] } +scopeguard = "1.2.0" thiserror = "2.0.18" [dev-dependencies] From c40bf65ac6e6d0bd14db3030d4479cc84e11633d Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Mon, 26 Jan 2026 23:57:20 +0300 Subject: [PATCH 03/20] globalize scopeguard macros --- src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index d9c81e1..79e9a18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,9 @@ extern crate assertables; #[macro_use] extern crate thiserror; +#[macro_use] +extern crate scopeguard; + mod ext; mod process; From 7efb644d546fab13a45e002a3e378b9383f06987 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:04:27 +0300 Subject: [PATCH 04/20] impl ReadExt::read_u8 --- src/ext/mod.rs | 1 + src/ext/read.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/ext/read.rs diff --git a/src/ext/mod.rs b/src/ext/mod.rs index 210446c..76def6c 100644 --- a/src/ext/mod.rs +++ b/src/ext/mod.rs @@ -5,4 +5,5 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. mod mapping; +pub mod read; pub mod search; diff --git a/src/ext/read.rs b/src/ext/read.rs new file mode 100644 index 0000000..7a923e8 --- /dev/null +++ b/src/ext/read.rs @@ -0,0 +1,151 @@ +// Copyright (c) 2026 Eray Erdin +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::{ + fs, + io::{self, Read, Seek}, + path, +}; + +use nix::{sys::ptrace, unistd::Pid}; + +pub trait ReadExt { + fn read_u8(&self, address: usize) -> ReadExtResult; +} + +impl ReadExt for crate::prelude::Process { + fn read_u8(&self, address: usize) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); + let mut file = fs::File::open(mempath)?; + + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(buffer[0]) + } +} + +#[derive(Debug, Error)] +pub enum ReadExtError { + #[error("A *nix error occured. {0} {0:?}")] + Nix(nix::Error), + #[error("An IO error occured. {0} {0:?}")] + IO(#[from] std::io::Error), +} + +impl From for ReadExtError { + fn from(value: nix::Error) -> Self { + Self::Nix(value) + } +} + +pub type ReadExtResult = Result; + +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use std::{env, io::BufRead, process}; + + use crate::process::Process; + + use super::*; + + struct DummyProcess { + child: process::Child, + pub pid: i32, + pub base_url: String, + } + + impl Drop for DummyProcess { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } + } + + #[fixture] + fn dummy_process() -> DummyProcess { + let cwd = env::current_dir().expect("Could not get the current working directory."); + let dummy_process_dir = cwd.join("dummy_process"); + + let build_status = process::Command::new("cargo") + .arg("build") + .current_dir(&dummy_process_dir) + .status() + .expect("Failed to run dummy_process compilation."); + assert!(build_status.success(), "Failed to build dummy_process"); + + let bin_path = dummy_process_dir.join("target/debug/dummy_process"); + let mut child = process::Command::new(bin_path) + .stdout(process::Stdio::piped()) + .spawn() + .expect("Could not spawn dummy_process."); + + let port = { + let stdout = child.stdout.as_mut().expect("Failed to get stdout."); + let reader = io::BufReader::new(stdout); + let port_line = reader + .lines() + .next() + .expect("Port line does not exist.") + .expect("Could not read port line of stdout."); + port_line + .trim() + .split(":") + .last() + .expect("Could not read port part from stdout.") + .parse::() + .expect("Could not parse port from stdout.") + }; + + let base_url = format!("http://127.0.0.1:{port}"); + + let pid = { + let body = reqwest::blocking::get(format!("{base_url}/pid")) + .expect("Could not request PID of dummy process."); + body.text() + .expect("Could not decode the charset of PID of dummy process.") + .parse::() + .expect("Could not parse PID of dummy process") + }; + + DummyProcess { + child, + pid, + base_url, + } + } + + #[rstest] + fn test_read_u8(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/u8val/address", dummy_process.base_url)) + .expect("Could not request u8val address."); + response + .text() + .expect("Could not decode the charset of u8val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_u8(target_address) + .expect("Could not read the value of u8 from the address."); + assert_eq!(value, 2); + } +} From 3f8433628bc1cd83653d7bc7de2217b4eb19848f Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:05:31 +0300 Subject: [PATCH 05/20] detach from process in SearchExt::search_bytes --- src/ext/search.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ext/search.rs b/src/ext/search.rs index 0b68e66..17927d1 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -44,6 +44,10 @@ impl SearchExt for crate::prelude::Process { ptrace::attach(Pid::from_raw(self.pid))?; + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + let mappings = self.mappings()?; let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); let mut file = fs::File::open(mempath)?; From 794b93f6ecc7eec9791cfd09ccbf0fbf79144a46 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:09:49 +0300 Subject: [PATCH 06/20] separate ByteOrder --- src/byte_order.rs | 12 ++++++++++++ src/ext/search.rs | 8 +------- src/lib.rs | 4 +++- 3 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 src/byte_order.rs diff --git a/src/byte_order.rs b/src/byte_order.rs new file mode 100644 index 0000000..14aac2b --- /dev/null +++ b/src/byte_order.rs @@ -0,0 +1,12 @@ +// Copyright (c) 2026 Eray Erdin +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +#[derive(Default)] +pub enum ByteOrder { + BigEndian, + #[default] + LittleEndian, +} diff --git a/src/ext/search.rs b/src/ext/search.rs index 17927d1..b899d5a 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -4,6 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use crate::byte_order::ByteOrder; use crate::ext::mapping::MappingExt; use core::num; use nix::{sys::ptrace, unistd::Pid}; @@ -13,13 +14,6 @@ use std::{fs, io::Read, path}; use crate::ext::mapping::MappingExtError; -#[derive(Default)] -pub enum ByteOrder { - BigEndian, - #[default] - LittleEndian, -} - /// An extension on to search for specific values in an app. pub trait SearchExt { /// Search for a byte pattern and return memory addresses. diff --git a/src/lib.rs b/src/lib.rs index 79e9a18..9e967b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,10 +12,12 @@ extern crate thiserror; #[macro_use] extern crate scopeguard; +mod byte_order; mod ext; mod process; pub mod prelude { - pub use crate::ext::search::{ByteOrder, SearchExt, SearchExtError, SearchExtResult}; + pub use crate::byte_order::ByteOrder; + pub use crate::ext::search::{SearchExt, SearchExtError, SearchExtResult}; pub use crate::process::{Process, ProcessError}; } From a34bde18a2774db682466371a17caebfbf0474de Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:18:29 +0300 Subject: [PATCH 07/20] remove default implementation for ByteOrder and choose native endianness in case none is provided --- src/byte_order.rs | 3 +-- src/ext/search.rs | 52 +++++++++++++++++++++++++++-------------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/byte_order.rs b/src/byte_order.rs index 14aac2b..4ad2615 100644 --- a/src/byte_order.rs +++ b/src/byte_order.rs @@ -4,9 +4,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -#[derive(Default)] +/// The byte order of the data. Default value is the native endianness of the system. pub enum ByteOrder { BigEndian, - #[default] LittleEndian, } diff --git a/src/ext/search.rs b/src/ext/search.rs index b899d5a..7a23534 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -23,7 +23,7 @@ pub trait SearchExt { fn search_u32(&self, value: u32, byte_order: Option) -> SearchExtResult>; #[cfg(target_pointer_width = "64")] fn search_u64(&self, value: u64, byte_order: Option) -> SearchExtResult>; - fn search_i8(&self, value: i8) -> SearchExtResult>; + fn search_i8(&self, value: i8, byte_order: Option) -> SearchExtResult>; fn search_i16(&self, value: i16, byte_order: Option) -> SearchExtResult>; fn search_i32(&self, value: i32, byte_order: Option) -> SearchExtResult>; fn search_i64(&self, value: i64, byte_order: Option) -> SearchExtResult>; @@ -151,67 +151,73 @@ impl SearchExt for crate::prelude::Process { } fn search_u16(&self, value: u16, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } fn search_u32(&self, value: u32, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } #[cfg(target_pointer_width = "64")] fn search_u64(&self, value: u64, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } - fn search_i8(&self, value: i8) -> SearchExtResult> { - self.search_bytes(&value.to_le_bytes()) + fn search_i8(&self, value: i8, byte_order: Option) -> SearchExtResult> { + self.search_bytes( + &(match byte_order { + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), + }), + ) } fn search_i16(&self, value: i16, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } fn search_i32(&self, value: i32, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } #[cfg(target_pointer_width = "64")] fn search_i64(&self, value: i64, byte_order: Option) -> SearchExtResult> { - let byte_order = byte_order.unwrap_or_default(); self.search_bytes( &(match byte_order { - ByteOrder::BigEndian => value.to_be_bytes(), - ByteOrder::LittleEndian => value.to_le_bytes(), + Some(ByteOrder::BigEndian) => value.to_be_bytes(), + Some(ByteOrder::LittleEndian) => value.to_le_bytes(), + None => value.to_ne_bytes(), }), ) } @@ -401,7 +407,9 @@ mod linux_tests { fn test_search_i8(dummy_process: DummyProcess) { let process = Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); - let addresses = process.search_i8(-1).expect("Could not search i8 value."); + let addresses = process + .search_i8(-1, None) + .expect("Could not search i8 value."); let target_address: usize = { let response = From c92e019a921b3bbb979710a81a78026e5928b0ae Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:20:56 +0300 Subject: [PATCH 08/20] impl ReadExt::read_u16 --- src/ext/read.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index 7a923e8..fafe590 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -12,10 +12,14 @@ use std::{ use nix::{sys::ptrace, unistd::Pid}; +use crate::prelude::ByteOrder; + pub trait ReadExt { fn read_u8(&self, address: usize) -> ReadExtResult; + fn read_u16(&self, address: usize, byte_order: Option) -> ReadExtResult; } +#[cfg(target_os = "linux")] impl ReadExt for crate::prelude::Process { fn read_u8(&self, address: usize) -> ReadExtResult { ptrace::attach(Pid::from_raw(self.pid))?; @@ -35,6 +39,29 @@ impl ReadExt for crate::prelude::Process { ptrace::detach(Pid::from_raw(self.pid), None)?; Ok(buffer[0]) } + + fn read_u16(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); + let mut file = fs::File::open(mempath)?; + + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 2]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => u16::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => u16::from_le_bytes(buffer), + None => u16::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -148,4 +175,25 @@ mod tests { .expect("Could not read the value of u8 from the address."); assert_eq!(value, 2); } + + #[rstest] + fn test_read_u16(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/u16val/address", dummy_process.base_url)) + .expect("Could not request u16val address."); + response + .text() + .expect("Could not decode the charset of u16val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_u16(target_address, None) + .expect("Could not read the value of u16 from the address."); + assert_eq!(value, 3); + } } From 366894c3b13318dd43a88f05e7cb59cd1d8ec48f Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:34:21 +0300 Subject: [PATCH 09/20] use nextest instead of cargo test --- .github/workflows/rust.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 90c0356..87f761d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,5 +14,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: test - run: cargo test + - uses: taiki-e/install-action@v2 + env: + CARGO_TERM_COLOR: always + with: + tool: nextest + - name: Test with latest nextest release + uses: actions-rs/cargo@v1 + with: + command: nextest + args: run --retries 3 From 44baf5dd82ce7865e267c16665e626a949d5e657 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:36:22 +0300 Subject: [PATCH 10/20] impl ReadExt::read_u64 --- src/ext/read.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index fafe590..07c217a 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -17,6 +17,8 @@ use crate::prelude::ByteOrder; pub trait ReadExt { fn read_u8(&self, address: usize) -> ReadExtResult; fn read_u16(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_u32(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -62,6 +64,50 @@ impl ReadExt for crate::prelude::Process { None => u16::from_ne_bytes(buffer), }) } + + fn read_u32(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); + let mut file = fs::File::open(mempath)?; + + file.seek(io::SeekFrom::Start(address as u64))?; + let mut buffer = [0u8; 4]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => u32::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => u32::from_le_bytes(buffer), + None => u32::from_ne_bytes(buffer), + }) + } + + fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); + let mut file = fs::File::open(mempath)?; + + file.seek(io::SeekFrom::Start(address as u64))?; + let mut buffer = [0u8; 8]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => u64::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => u64::from_le_bytes(buffer), + None => u64::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -196,4 +242,47 @@ mod tests { .expect("Could not read the value of u16 from the address."); assert_eq!(value, 3); } + + #[rstest] + fn test_read_u32(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/u32val/address", dummy_process.base_url)) + .expect("Could not request u32val address."); + response + .text() + .expect("Could not decode the charset of u32val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_u32(target_address, None) + .expect("Could not read the value of u32 from the address."); + assert_eq!(value, 4); + } + + #[cfg(target_pointer_width = "64")] + #[rstest] + fn test_read_u64(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/u64val/address", dummy_process.base_url)) + .expect("Could not request u64val address."); + response + .text() + .expect("Could not decode the charset of u64val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_u64(target_address, None) + .expect("Could not read the value of u64 from the address."); + assert_eq!(value, 5); + } } From e6e5f8e7725a08e3194a8d5a3b57a9960a3c147d Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 00:39:17 +0300 Subject: [PATCH 11/20] rename step --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 87f761d..79c32ff 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -19,7 +19,7 @@ jobs: CARGO_TERM_COLOR: always with: tool: nextest - - name: Test with latest nextest release + - name: test uses: actions-rs/cargo@v1 with: command: nextest From 3f6249feea4c99b2aa120ed829dec0d40de2c4d3 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 10:43:45 +0300 Subject: [PATCH 12/20] change retries to 10 --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 79c32ff..adb27c4 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,4 +23,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: nextest - args: run --retries 3 + args: run --retries 10 From bb822913d6998c3b83bb02e9fc6a520429016c6c Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 11:11:53 +0300 Subject: [PATCH 13/20] create and use Process::mem_file getter --- src/ext/read.rs | 17 +++++------------ src/ext/search.rs | 5 ++--- src/process.rs | 12 ++++++++++++ 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/ext/read.rs b/src/ext/read.rs index 07c217a..2a197b6 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -30,9 +30,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); - let mut file = fs::File::open(mempath)?; - + let mut file = self.mem_file()?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 1]; @@ -49,9 +47,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); - let mut file = fs::File::open(mempath)?; - + let mut file = self.mem_file()?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 2]; @@ -72,9 +68,7 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); - let mut file = fs::File::open(mempath)?; - + let mut file = self.mem_file()?; file.seek(io::SeekFrom::Start(address as u64))?; let mut buffer = [0u8; 4]; file.read_exact(&mut buffer)?; @@ -94,10 +88,9 @@ impl ReadExt for crate::prelude::Process { let _ = ptrace::detach(Pid::from_raw(self.pid), None); } - let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); - let mut file = fs::File::open(mempath)?; - + let mut file = self.mem_file()?; file.seek(io::SeekFrom::Start(address as u64))?; + let mut buffer = [0u8; 8]; file.read_exact(&mut buffer)?; diff --git a/src/ext/search.rs b/src/ext/search.rs index 7a23534..55a94fb 100644 --- a/src/ext/search.rs +++ b/src/ext/search.rs @@ -9,8 +9,8 @@ use crate::ext::mapping::MappingExt; use core::num; use nix::{sys::ptrace, unistd::Pid}; use std::io; +use std::io::Read; use std::io::Seek; -use std::{fs, io::Read, path}; use crate::ext::mapping::MappingExtError; @@ -43,8 +43,7 @@ impl SearchExt for crate::prelude::Process { } let mappings = self.mappings()?; - let mempath = path::PathBuf::from(format!("/proc/{}/mem", self.pid)); - let mut file = fs::File::open(mempath)?; + let mut file = self.mem_file()?; const BUFFER_SIZE: usize = 8192; let mut buffer = [0u8; BUFFER_SIZE]; diff --git a/src/process.rs b/src/process.rs index bbd642c..2979b4a 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,6 +5,8 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. use std::path; +#[cfg(target_os = "linux")] +use std::{fs, io}; /// Process spawned by OS. #[derive(Debug, PartialEq)] @@ -25,6 +27,16 @@ impl Process { Ok(Self { pid }) } + + #[cfg(target_os = "linux")] + fn mem_path(&self) -> path::PathBuf { + path::PathBuf::from(format!("/proc/{}/mem", self.pid)) + } + + #[cfg(target_os = "linux")] + pub(crate) fn mem_file(&self) -> io::Result { + fs::File::open(self.mem_path()) + } } /// Errors for [Process]. From 23e85959ffeaf562f35af6b8aa0f53d169940270 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 11:35:33 +0300 Subject: [PATCH 14/20] impl ReadExt::read_i8 --- src/ext/read.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index 2a197b6..ed66d40 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -19,6 +19,7 @@ pub trait ReadExt { fn read_u16(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_u32(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_i8(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -101,6 +102,27 @@ impl ReadExt for crate::prelude::Process { None => u64::from_ne_bytes(buffer), }) } + + fn read_i8(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 1]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => i8::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => i8::from_le_bytes(buffer), + None => i8::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -278,4 +300,25 @@ mod tests { .expect("Could not read the value of u64 from the address."); assert_eq!(value, 5); } + + #[rstest] + fn test_read_i8(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/i8val/address", dummy_process.base_url)) + .expect("Could not request i8val address."); + response + .text() + .expect("Could not decode the charset of i8val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_i8(target_address, None) + .expect("Could not read the value of i8 from the address."); + assert_eq!(value, -1); + } } From 72a1bf5451c21ccc2805dc376020ca309e172aa0 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 11:37:51 +0300 Subject: [PATCH 15/20] impl ReadExt::read_i16 --- src/ext/read.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index ed66d40..5609832 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -20,6 +20,7 @@ pub trait ReadExt { fn read_u32(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i8(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_i16(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -123,6 +124,27 @@ impl ReadExt for crate::prelude::Process { None => i8::from_ne_bytes(buffer), }) } + + fn read_i16(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 2]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => i16::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => i16::from_le_bytes(buffer), + None => i16::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -321,4 +343,25 @@ mod tests { .expect("Could not read the value of i8 from the address."); assert_eq!(value, -1); } + + #[rstest] + fn test_read_i16(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/i16val/address", dummy_process.base_url)) + .expect("Could not request i16val address."); + response + .text() + .expect("Could not decode the charset of i16val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_i16(target_address, None) + .expect("Could not read the value of i16 from the address."); + assert_eq!(value, -2); + } } From 93d3107845d7e5fbc346e646497b1eccd8174957 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 11:39:56 +0300 Subject: [PATCH 16/20] impl ReadExt::i32 --- src/ext/read.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index 5609832..0f3ebed 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -21,6 +21,7 @@ pub trait ReadExt { fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i8(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i16(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_i32(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -145,6 +146,27 @@ impl ReadExt for crate::prelude::Process { None => i16::from_ne_bytes(buffer), }) } + + fn read_i32(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 4]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => i32::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => i32::from_le_bytes(buffer), + None => i32::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -364,4 +386,25 @@ mod tests { .expect("Could not read the value of i16 from the address."); assert_eq!(value, -2); } + + #[rstest] + fn test_read_i32(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/i32val/address", dummy_process.base_url)) + .expect("Could not request i32val address."); + response + .text() + .expect("Could not decode the charset of i32val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_i32(target_address, None) + .expect("Could not read the value of i32 from the address."); + assert_eq!(value, -3); + } } From 9a9cb73f8adbe76a45df1af124252676e325058f Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 11:43:58 +0300 Subject: [PATCH 17/20] impl ReadExt::read_i64 --- src/ext/read.rs | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/ext/read.rs b/src/ext/read.rs index 0f3ebed..ed71715 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -18,10 +18,13 @@ pub trait ReadExt { fn read_u8(&self, address: usize) -> ReadExtResult; fn read_u16(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_u32(&self, address: usize, byte_order: Option) -> ReadExtResult; + #[cfg(target_pointer_width = "64")] fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i8(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i16(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_i32(&self, address: usize, byte_order: Option) -> ReadExtResult; + #[cfg(target_pointer_width = "64")] + fn read_i64(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -84,6 +87,7 @@ impl ReadExt for crate::prelude::Process { }) } + #[cfg(target_pointer_width = "64")] fn read_u64(&self, address: usize, byte_order: Option) -> ReadExtResult { ptrace::attach(Pid::from_raw(self.pid))?; @@ -167,6 +171,28 @@ impl ReadExt for crate::prelude::Process { None => i32::from_ne_bytes(buffer), }) } + + #[cfg(target_pointer_width = "64")] + fn read_i64(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 8]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => i64::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => i64::from_le_bytes(buffer), + None => i64::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -407,4 +433,26 @@ mod tests { .expect("Could not read the value of i32 from the address."); assert_eq!(value, -3); } + + #[cfg(target_pointer_width = "64")] + #[rstest] + fn test_read_i64(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/i64val/address", dummy_process.base_url)) + .expect("Could not request i64val address."); + response + .text() + .expect("Could not decode the charset of i64val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_i64(target_address, None) + .expect("Could not read the value of i64 from the address."); + assert_eq!(value, -4); + } } From 0aef14654d5ce595b90ee3e42f1a5ce64496c0a3 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 12:03:04 +0300 Subject: [PATCH 18/20] impl ReadExt::read_f32 --- dummy_process/src/main.rs | 15 +++++++++++++- src/ext/read.rs | 43 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/dummy_process/src/main.rs b/dummy_process/src/main.rs index 83a63c6..f834184 100644 --- a/dummy_process/src/main.rs +++ b/dummy_process/src/main.rs @@ -10,6 +10,7 @@ static I8VAL: atomic::AtomicI8 = atomic::AtomicI8::new(-1); static I16VAL: atomic::AtomicI16 = atomic::AtomicI16::new(-2); static I32VAL: atomic::AtomicI32 = atomic::AtomicI32::new(-3); static I64VAL: atomic::AtomicI64 = atomic::AtomicI64::new(-4); +static F32VAL: f32 = 1.5; #[tokio::main] async fn main() { @@ -135,6 +136,16 @@ async fn main() { "OK" }); + let f32val_endpoint = warp::path!("f32val").map(|| format!("{}", F32VAL)); + let f32val_address_endpoint = warp::path!("f32val" / "address").map(|| { + let hex_val = format!("{:?}", ptr::addr_of!(F32VAL)); + let cleaned = hex_val.trim_start_matches("0x"); + format!( + "{}", + usize::from_str_radix(cleaned, 16).expect("Failed to parse address hex value.") + ) + }); + let routes = warp::get().and( pid_endpoint .or(u8val_endpoint) @@ -160,7 +171,9 @@ async fn main() { .or(i32val_increment_endpoint) .or(i64val_endpoint) .or(i64val_address_endpoint) - .or(i64val_increment_endpoint), + .or(i64val_increment_endpoint) + .or(f32val_endpoint) + .or(f32val_address_endpoint), ); let port = rand::random_range(1000..u16::MAX); diff --git a/src/ext/read.rs b/src/ext/read.rs index ed71715..dfe9f61 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -25,6 +25,7 @@ pub trait ReadExt { fn read_i32(&self, address: usize, byte_order: Option) -> ReadExtResult; #[cfg(target_pointer_width = "64")] fn read_i64(&self, address: usize, byte_order: Option) -> ReadExtResult; + fn read_f32(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -193,6 +194,27 @@ impl ReadExt for crate::prelude::Process { None => i64::from_ne_bytes(buffer), }) } + + fn read_f32(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 4]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => f32::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => f32::from_le_bytes(buffer), + None => f32::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -455,4 +477,25 @@ mod tests { .expect("Could not read the value of i64 from the address."); assert_eq!(value, -4); } + + #[rstest] + fn test_read_f32(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/f32val/address", dummy_process.base_url)) + .expect("Could not request f32val address."); + response + .text() + .expect("Could not decode the charset of f32val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_f32(target_address, None) + .expect("Could not read the value of f32 from the address."); + assert_eq!(value, 1.5); + } } From fb5bd121dab66c67768bc269ff5dff27fd0b6e0f Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 12:05:43 +0300 Subject: [PATCH 19/20] serialize testing --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index adb27c4..d096f32 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -23,4 +23,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: nextest - args: run --retries 10 + args: run --test-threads 1 --retries 10 From 6c847148a4450bd3b4040c46d40da1df4454e842 Mon Sep 17 00:00:00 2001 From: Eray Erdin Date: Tue, 27 Jan 2026 12:10:17 +0300 Subject: [PATCH 20/20] impl ReadExt::read_f64 --- dummy_process/src/main.rs | 15 ++++++++++++- src/ext/read.rs | 46 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/dummy_process/src/main.rs b/dummy_process/src/main.rs index f834184..e939e91 100644 --- a/dummy_process/src/main.rs +++ b/dummy_process/src/main.rs @@ -11,6 +11,7 @@ static I16VAL: atomic::AtomicI16 = atomic::AtomicI16::new(-2); static I32VAL: atomic::AtomicI32 = atomic::AtomicI32::new(-3); static I64VAL: atomic::AtomicI64 = atomic::AtomicI64::new(-4); static F32VAL: f32 = 1.5; +static F64VAL: f64 = 2.5; #[tokio::main] async fn main() { @@ -146,6 +147,16 @@ async fn main() { ) }); + let f64val_endpoint = warp::path!("f64val").map(|| format!("{}", F64VAL)); + let f64val_address_endpoint = warp::path!("f64val" / "address").map(|| { + let hex_val = format!("{:?}", ptr::addr_of!(F64VAL)); + let cleaned = hex_val.trim_start_matches("0x"); + format!( + "{}", + usize::from_str_radix(cleaned, 16).expect("Failed to parse address hex value.") + ) + }); + let routes = warp::get().and( pid_endpoint .or(u8val_endpoint) @@ -173,7 +184,9 @@ async fn main() { .or(i64val_address_endpoint) .or(i64val_increment_endpoint) .or(f32val_endpoint) - .or(f32val_address_endpoint), + .or(f32val_address_endpoint) + .or(f64val_endpoint) + .or(f64val_address_endpoint), ); let port = rand::random_range(1000..u16::MAX); diff --git a/src/ext/read.rs b/src/ext/read.rs index dfe9f61..6898164 100644 --- a/src/ext/read.rs +++ b/src/ext/read.rs @@ -26,6 +26,8 @@ pub trait ReadExt { #[cfg(target_pointer_width = "64")] fn read_i64(&self, address: usize, byte_order: Option) -> ReadExtResult; fn read_f32(&self, address: usize, byte_order: Option) -> ReadExtResult; + #[cfg(target_pointer_width = "64")] + fn read_f64(&self, address: usize, byte_order: Option) -> ReadExtResult; } #[cfg(target_os = "linux")] @@ -215,6 +217,28 @@ impl ReadExt for crate::prelude::Process { None => f32::from_ne_bytes(buffer), }) } + + #[cfg(target_pointer_width = "64")] + fn read_f64(&self, address: usize, byte_order: Option) -> ReadExtResult { + ptrace::attach(Pid::from_raw(self.pid))?; + + defer! { + let _ = ptrace::detach(Pid::from_raw(self.pid), None); + } + + let mut file = self.mem_file()?; + file.seek(io::SeekFrom::Start(address as u64))?; + + let mut buffer = [0u8; 8]; + file.read_exact(&mut buffer)?; + + ptrace::detach(Pid::from_raw(self.pid), None)?; + Ok(match byte_order { + Some(ByteOrder::BigEndian) => f64::from_be_bytes(buffer), + Some(ByteOrder::LittleEndian) => f64::from_le_bytes(buffer), + None => f64::from_ne_bytes(buffer), + }) + } } #[derive(Debug, Error)] @@ -498,4 +522,26 @@ mod tests { .expect("Could not read the value of f32 from the address."); assert_eq!(value, 1.5); } + + #[cfg(target_pointer_width = "64")] + #[rstest] + fn test_read_f64(dummy_process: DummyProcess) { + let process = + Process::try_new(dummy_process.pid).expect("Could not get dummy process with id."); + let target_address: usize = { + let response = + reqwest::blocking::get(format!("{}/f64val/address", dummy_process.base_url)) + .expect("Could not request f64val address."); + response + .text() + .expect("Could not decode the charset of f64val address.") + .parse() + } + .expect("Could not parse address usize."); + + let value = process + .read_f64(target_address, None) + .expect("Could not read the value of f64 from the address."); + assert_eq!(value, 2.5); + } }