Skip to content

Commit b2cec81

Browse files
committed
Also parse permissions and memory mapped filename
1 parent 6d3b603 commit b2cec81

File tree

3 files changed

+184
-44
lines changed

3 files changed

+184
-44
lines changed

crates/rrg/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ features = [
149149
"Win32_System_Threading",
150150
"Win32_Storage_FileSystem",
151151
"Win32_System_Diagnostics_Debug",
152+
"Win32_System_ProcessStatus",
152153
]
153154

154155
[dependencies.ed25519-dalek]

crates/rrg/src/action/dump_process_memory.rs

Lines changed: 182 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
// Use of this source code is governed by an MIT-style license that can be found
44
// in the LICENSE file or at https://opensource.org/licenses/MIT.
55
use std::collections::VecDeque;
6-
use std::fs::File;
7-
use std::io::{Read as _, Seek as _, SeekFrom};
86
use std::path::PathBuf;
97

108
/// A memory mapping in a running process' virtual address space.
@@ -18,6 +16,7 @@ pub struct MappedRegion {
1816
/// Permissions associated with this mapping.
1917
pub permissions: Permissions,
2018
/// If this mapping is backed by a file, this field will contain said file's inode.
19+
/// Only valid on Linux.
2120
pub inode: Option<u64>,
2221
/// If this mapping is backed by a file, this field will contain the path to said file.
2322
/// It can also contain a pseudo-path that indicates the type of mapping, otherwise.
@@ -71,7 +70,8 @@ pub use windows::*;
7170
#[cfg(target_os = "linux")]
7271
mod linux {
7372
use super::*;
74-
use std::io::BufRead;
73+
use std::fs::File;
74+
use std::io::{BufRead, Read as _, Seek as _, SeekFrom};
7575

7676
/// An error that occurred during parsing of a process' memory mappings file.
7777
#[derive(Debug)]
@@ -332,7 +332,7 @@ mod windows {
332332
use std::mem::MaybeUninit;
333333
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
334334
use windows_sys::Win32::System::Diagnostics::Debug::ReadProcessMemory;
335-
use windows_sys::Win32::System::Memory::{MEMORY_BASIC_INFORMATION, VirtualQueryEx};
335+
use windows_sys::Win32::System::Memory::{MEM_FREE, MEMORY_BASIC_INFORMATION, VirtualQueryEx};
336336
use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_ALL_ACCESS};
337337

338338
struct ProcessHandle(HANDLE);
@@ -355,33 +355,9 @@ mod windows {
355355
}
356356
}
357357

358-
impl MappedRegion {
359-
fn query(handle: &ProcessHandle, address_start: u64) -> std::io::Result<Self> {
360-
let mut mem: std::mem::MaybeUninit<MEMORY_BASIC_INFORMATION> = MaybeUninit::zeroed();
361-
if unsafe {
362-
VirtualQueryEx(
363-
handle.0,
364-
address_start as *const c_void,
365-
mem.as_mut_ptr(),
366-
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
367-
) == 0
368-
} {
369-
return Err(std::io::Error::last_os_error());
370-
}
371-
372-
let mem = unsafe { mem.assume_init() };
373-
// let permissions = ...;
374-
Ok(Self {
375-
address_start,
376-
size: mem.RegionSize as u64,
377-
..Default::default()
378-
})
379-
}
380-
}
381-
382358
pub struct MappedRegionIter {
383359
process: ProcessHandle,
384-
cur_addr: u64,
360+
cur_addr: *mut c_void,
385361
}
386362

387363
impl MappedRegionIter {
@@ -390,25 +366,106 @@ mod windows {
390366
let process = ProcessHandle::open(pid)?;
391367
Ok(Self {
392368
process,
393-
cur_addr: 0,
369+
cur_addr: std::ptr::null_mut(),
394370
})
395371
}
396372
}
397373

374+
fn parse_permissions(mem_info: &MEMORY_BASIC_INFORMATION) -> Permissions {
375+
use windows_sys::Win32::System::Memory::*;
376+
377+
// Info obtained from https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants
378+
// and https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-memory_basic_information
379+
380+
const EXECUTE_FLAGS: u32 =
381+
PAGE_EXECUTE | PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY;
382+
const WRITE_FLAGS: u32 =
383+
PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY | PAGE_READWRITE | PAGE_WRITECOPY;
384+
385+
// Use the protection information of the actual committed memory if available,
386+
// otherwise the flags with which the memory mapping was created.
387+
let flags = if mem_info.State == MEM_COMMIT {
388+
mem_info.Protect
389+
} else {
390+
mem_info.AllocationProtect
391+
};
392+
393+
let execute = flags & EXECUTE_FLAGS != 0;
394+
let write = flags & WRITE_FLAGS != 0;
395+
// The only flag which disables write access is PAGE_NOACCESS
396+
let read = flags & PAGE_NOACCESS == 0;
397+
let private = mem_info.Type == MEM_PRIVATE;
398+
Permissions {
399+
read,
400+
write,
401+
execute,
402+
private,
403+
shared: false,
404+
}
405+
}
406+
407+
fn get_mapped_filename(addr: *const c_void, process: &ProcessHandle) -> Option<PathBuf> {
408+
use windows_sys::Win32::Foundation::MAX_PATH;
409+
use windows_sys::Win32::System::ProcessStatus::GetMappedFileNameW;
410+
411+
use std::ffi::OsString;
412+
use std::os::windows::ffi::OsStringExt;
413+
414+
let mut buf = [0u16; (MAX_PATH + 1) as usize];
415+
let len = unsafe { GetMappedFileNameW(process.0, addr, buf.as_mut_ptr(), buf.len() as u32) }
416+
as usize;
417+
// A return value of 0 indicates an error, and nSize indicates that the path was
418+
// truncated. We treat both cases as errors and just return None.
419+
if len == 0 || len == buf.len() {
420+
return None;
421+
}
422+
Some(PathBuf::from(OsString::from_wide(&buf[..len])))
423+
}
424+
398425
impl Iterator for MappedRegionIter {
399-
type Item = Result<MappedRegion, crate::session::Error>;
426+
type Item = std::io::Result<MappedRegion>;
400427
fn next(&mut self) -> Option<Self::Item> {
401-
let result = MappedRegion::query(&self.process, self.cur_addr);
402-
match &result {
403-
Err(e) if e.kind() == std::io::ErrorKind::InvalidInput => {
404-
// Invalid input is returned when the address falls outside of the accessible
405-
// memory bounds of the process, so we can stop iteration here
406-
return None;
428+
loop {
429+
let mem_info: MEMORY_BASIC_INFORMATION = {
430+
let mut mem: std::mem::MaybeUninit<MEMORY_BASIC_INFORMATION> =
431+
MaybeUninit::zeroed();
432+
if unsafe {
433+
VirtualQueryEx(
434+
self.process.0,
435+
self.cur_addr,
436+
mem.as_mut_ptr(),
437+
std::mem::size_of::<MEMORY_BASIC_INFORMATION>(),
438+
) == 0
439+
} {
440+
let err = std::io::Error::last_os_error();
441+
// InvalidInput is returned when the given address
442+
// falls beyond the last page accessible by the process,
443+
// so we know we are done iterating when we receive that.
444+
if err.kind() == std::io::ErrorKind::InvalidInput {
445+
return None;
446+
}
447+
break Some(Err(err));
448+
}
449+
unsafe { mem.assume_init() }
450+
};
451+
452+
let address_start = mem_info.BaseAddress;
453+
let address_end = address_start.wrapping_byte_add(mem_info.RegionSize);
454+
self.cur_addr = address_end;
455+
456+
if mem_info.State == MEM_FREE {
457+
// Skip over chunks of free memory
458+
continue;
407459
}
408-
Ok(region) => self.cur_addr = region.end_address(),
409-
_ => (),
460+
461+
break Some(Ok(MappedRegion {
462+
address_start: address_start as u64,
463+
size: mem_info.RegionSize as u64,
464+
permissions: parse_permissions(&mem_info),
465+
inode: None,
466+
path: get_mapped_filename(address_start, &self.process),
467+
}));
410468
}
411-
Some(result.map_err(crate::session::Error::action))
412469
}
413470
}
414471

@@ -450,6 +507,88 @@ mod windows {
450507
Ok(buf)
451508
}
452509
}
510+
511+
#[cfg(test)]
512+
mod test {
513+
use super::*;
514+
515+
#[test]
516+
fn region_iter_detects_mmap() {
517+
// `mmap` a file and check that the mapping is detected among those returned by
518+
// `MappedRegionIter`.
519+
use std::io::Write as _;
520+
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
521+
use windows_sys::Win32::System::Memory::{
522+
CreateFileMappingW, FILE_MAP_ALL_ACCESS, MEMORY_MAPPED_VIEW_ADDRESS, MapViewOfFile,
523+
PAGE_READWRITE, UnmapViewOfFile,
524+
};
525+
526+
let mut file = tempfile::tempfile().unwrap();
527+
writeln!(file, "hello there").unwrap();
528+
529+
let meta = file.metadata().unwrap();
530+
let length = meta.len() as usize;
531+
532+
// Create a file mapping object for the file
533+
let mapping = unsafe {
534+
CreateFileMappingW(
535+
file.as_raw_handle(),
536+
std::ptr::null(), // default security
537+
PAGE_READWRITE, // read/write permission
538+
0, // size of mapping object, high
539+
length as u32, // size of mapping object, low
540+
std::ptr::null(),
541+
)
542+
};
543+
544+
if mapping.is_null() {
545+
panic!("Could not create file mapping");
546+
}
547+
548+
let mapping = unsafe { OwnedHandle::from_raw_handle(mapping) };
549+
550+
/// RAII wrapper around a `mmap`ed pointer that will `munmap` on drop.
551+
struct MappedView {
552+
addr: MEMORY_MAPPED_VIEW_ADDRESS,
553+
}
554+
555+
impl Drop for MappedView {
556+
fn drop(&mut self) {
557+
unsafe {
558+
UnmapViewOfFile(self.addr);
559+
}
560+
}
561+
}
562+
563+
// Map the view and test the results.
564+
565+
let view = unsafe {
566+
let addr = MapViewOfFile(
567+
mapping.as_raw_handle(), // handle to mapping object
568+
FILE_MAP_ALL_ACCESS, // read/write
569+
0, // high-order 32 bits of file offset
570+
0, // low-order 32 bits of file offset
571+
length,
572+
);
573+
assert!(!addr.Value.is_null());
574+
MappedView { addr }
575+
};
576+
577+
let regions: Vec<MappedRegion> = MappedRegionIter::from_pid(std::process::id())
578+
.expect("Could not read memory regions of current process")
579+
.collect::<Result<_, _>>()
580+
.expect("Reading maps");
581+
582+
assert!(regions.into_iter().any(|r| {
583+
r.address_start == view.addr.Value as u64
584+
&& r.permissions.read
585+
&& r.permissions.write
586+
&& r.path.is_some()
587+
}));
588+
589+
drop(view);
590+
}
591+
}
453592
}
454593

455594
/// A set of permissions associated with a [`MappedRegion`].
@@ -464,10 +603,11 @@ pub struct Permissions {
464603
/// Whether this mapping was created as `shared`.
465604
/// Writes to a shared mapping (usually backed by a file)
466605
/// can be observed by other processes which map the same file.
606+
/// Currently only supported on Linux.
467607
shared: bool,
468608
/// Whether this mapping was created as 'private'.
469609
/// Writes to a private mapping (usually backed by a file)
470-
/// are private to a proess and cannot be observed by other processes which map the same file.
610+
/// are private to a process and cannot be observed by other processes which map the same file.
471611
private: bool,
472612
}
473613

@@ -560,7 +700,7 @@ impl Args {
560700
if self.skip_executable_regions && region.permissions.execute {
561701
return false;
562702
}
563-
if self.skip_mapped_files && region.inode.is_some() {
703+
if self.skip_mapped_files && region.inode.is_some() || region.path.is_some() {
564704
return false;
565705
}
566706
if self.skip_readonly_regions

proto/rrg/action/dump_process_memory.proto

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ message Args {
2323
// remaining memory pages will be dumped up to size_limit.
2424
repeated uint64 priority_offsets = 3;
2525

26-
// Set this flag to avoid dumping mapped files. Applies to Linux only.
26+
// Set this flag to avoid dumping mapped files.
2727
bool skip_mapped_files = 4;
2828
// Set this flag to avoid dumping shared memory regions. Applies to Linux only.
2929
bool skip_shared_regions = 5;
@@ -45,7 +45,6 @@ message Permissions {
4545
// Applies to Linux only.
4646
bool shared = 4;
4747
// Indicates a region of memory that was mapped in private mode.
48-
// Applies to Linux only.
4948
bool private = 5;
5049
}
5150

0 commit comments

Comments
 (0)