Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the MINIDUMP_HANDLE_DATA_STREAM to Linux #94

Merged
Merged
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
16 changes: 8 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ byteorder = "1.4"
cfg-if = "1.0"
crash-context = "0.6"
memoffset = "0.9"
minidump-common = "0.18"
minidump-common = "0.19"
scroll = "0.11"
tempfile = "3.8"
thiserror = "1.0"

[target.'cfg(unix)'.dependencies]
libc = "0.2"
goblin = "0.7"
memmap2 = "0.5"
goblin = "0.7.1"
memmap2 = "0.8"

[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies]
nix = { version = "0.27", default-features = false, features = [
Expand All @@ -33,7 +33,7 @@ nix = { version = "0.27", default-features = false, features = [
] }
# Used for parsing procfs info.
# default-features is disabled since it pulls in chrono
procfs-core = { version = "0.16.0-RC1", default-features = false }
procfs-core = { version = "0.16", default-features = false }

[target.'cfg(target_os = "windows")'.dependencies]
bitflags = "2.4"
Expand All @@ -45,14 +45,14 @@ mach2 = "0.4"
[dev-dependencies]
# Minidump-processor is async so we need an executor
futures = { version = "0.3", features = ["executor"] }
minidump = "0.18"
memmap2 = "0.5"
minidump = "0.19"
memmap2 = "0.8"

[target.'cfg(target_os = "macos")'.dev-dependencies]
# We dump symbols for the `test` executable so that we can validate that minidumps
# created by this crate can be processed by minidump-processor
dump_syms = { version = "2.2", default-features = false }
minidump-processor = { version = "0.18", default-features = false }
minidump-unwind = { version = "0.18", features = ["debuginfo"] }
minidump-processor = { version = "0.19", default-features = false }
minidump-unwind = { version = "0.19", features = ["debuginfo"] }
similar-asserts = "1.5"
uuid = "1.4"
24 changes: 24 additions & 0 deletions src/bin/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,26 @@ mod linux {
}
}

fn create_files_wait(num: usize) -> Result<()> {
let mut file_array = Vec::<tempfile::NamedTempFile>::with_capacity(num);
for id in 0..num {
let file = tempfile::Builder::new()
.prefix("test_file")
.suffix::<str>(id.to_string().as_ref())
.tempfile()
.unwrap();
file_array.push(file);
println!("1");
}
println!("1");
loop {
std::thread::park();
// This shouldn't be executed, but we put it here to ensure that
// all the files within the array are kept open.
println!("{}", file_array.len());
}
}

pub(super) fn real_main(args: Vec<String>) -> Result<()> {
match args.len() {
1 => match args[0].as_ref() {
Expand All @@ -268,6 +288,10 @@ mod linux {
let num_of_threads: usize = args[1].parse().unwrap();
spawn_name_wait(num_of_threads)
}
"create_files_wait" => {
let num_of_files: usize = args[1].parse().unwrap();
create_files_wait(num_of_files)
}
_ => Err(format!("Len 2: Unknown test option: {}", args[0]).into()),
},
3 => {
Expand Down
12 changes: 12 additions & 0 deletions src/linux/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ pub enum SectionExceptionStreamError {
MemoryWriterError(#[from] MemoryWriterError),
}

#[derive(Debug, Error)]
pub enum SectionHandleDataStreamError {
#[error("Failed to access file")]
IOError(#[from] std::io::Error),
#[error("Failed to write to memory")]
MemoryWriterError(#[from] MemoryWriterError),
#[error("Failed integer conversion")]
TryFromIntError(#[from] std::num::TryFromIntError),
}

#[derive(Debug, Error)]
pub enum SectionMappingsError {
#[error("Failed to write to memory")]
Expand Down Expand Up @@ -218,6 +228,8 @@ pub enum WriterError {
SectionAppMemoryError(#[from] SectionAppMemoryError),
#[error("Failed when writing section ExceptionStream")]
SectionExceptionStreamError(#[from] SectionExceptionStreamError),
#[error("Failed when writing section HandleDataStream")]
SectionHandleDataStreamError(#[from] SectionHandleDataStreamError),
#[error("Failed when writing section MappingsError")]
SectionMappingsError(#[from] SectionMappingsError),
#[error("Failed when writing section MemList")]
Expand Down
7 changes: 6 additions & 1 deletion src/linux/minidump_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ impl MinidumpWriter {
) -> Result<()> {
// A minidump file contains a number of tagged streams. This is the number
// of streams which we write.
let num_writers = 15u32;
let num_writers = 16u32;

let mut header_section = MemoryWriter::<MDRawHeader>::alloc(buffer)?;

Expand Down Expand Up @@ -327,6 +327,11 @@ impl MinidumpWriter {
// Write section to file
dir_section.write_to_file(buffer, Some(dirent))?;

// This section is optional, so we ignore errors when writing it
if let Ok(dirent) = handle_data_stream::write(self, buffer) {
let _ = dir_section.write_to_file(buffer, Some(dirent));
}

// If you add more directory entries, don't forget to update num_writers, above.
Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions src/linux/sections.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod app_memory;
pub mod exception_stream;
pub mod handle_data_stream;
pub mod mappings;
pub mod memory_info_list_stream;
pub mod memory_list_stream;
Expand Down
84 changes: 84 additions & 0 deletions src/linux/sections/handle_data_stream.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use std::{
ffi::{CString, OsString},
fs::{self, DirEntry},
mem::{self},
os::unix::prelude::OsStrExt,
path::{Path, PathBuf},
};

use crate::mem_writer::MemoryWriter;

use super::*;

fn file_stat(path: &Path) -> Option<libc::stat> {
let c_path = CString::new(path.as_os_str().as_bytes()).ok()?;
let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
let result = unsafe { libc::stat(c_path.as_ptr(), &mut stat) };

if result == 0 {
Some(stat)
} else {
None
}
}

fn direntry_to_descriptor(buffer: &mut DumpBuf, entry: &DirEntry) -> Option<MDRawHandleDescriptor> {
let handle = filename_to_fd(&entry.file_name())?;
let realpath = fs::read_link(entry.path()).ok()?;
let path_rva = write_string_to_location(buffer, realpath.to_string_lossy().as_ref()).ok()?;
let stat = file_stat(&entry.path())?;

// TODO: We store the contents of `st_mode` into the `attributes` field, but
// we could also store a human-readable string of the file type inside
// `type_name_rva`. We might move this missing information (and
// more) inside a custom `MINIDUMP_HANDLE_OBJECT_INFORMATION_TYPE` blob.
// That would make this conversion loss-less.
Some(MDRawHandleDescriptor {
handle,
type_name_rva: 0,
object_name_rva: path_rva.rva,
attributes: stat.st_mode,
granted_access: 0,
handle_count: 0,
pointer_count: 0,
})
}

fn filename_to_fd(filename: &OsString) -> Option<u64> {
let filename = filename.to_string_lossy();
filename.parse::<u64>().ok()
}

pub fn write(
config: &mut MinidumpWriter,
buffer: &mut DumpBuf,
) -> Result<MDRawDirectory, errors::SectionHandleDataStreamError> {
let proc_fd_path = PathBuf::from(format!("/proc/{}/fd", config.process_id));
let proc_fd_iter = fs::read_dir(proc_fd_path)?;
let descriptors: Vec<_> = proc_fd_iter
.filter_map(|entry| entry.ok())
.filter_map(|entry| direntry_to_descriptor(buffer, &entry))
.collect();
let number_of_descriptors = descriptors.len() as u32;

let stream_header = MemoryWriter::<MDRawHandleDataStream>::alloc_with_val(
buffer,
MDRawHandleDataStream {
size_of_header: mem::size_of::<MDRawHandleDataStream>() as u32,
size_of_descriptor: mem::size_of::<MDRawHandleDescriptor>() as u32,
number_of_descriptors,
reserved: 0,
},
)?;

let mut dirent = MDRawDirectory {
stream_type: MDStreamType::HandleDataStream as u32,
location: stream_header.location(),
};

let descriptor_list =
MemoryArrayWriter::<MDRawHandleDescriptor>::alloc_from_iter(buffer, descriptors)?;

dirent.location.data_size += descriptor_list.location().data_size;
Ok(dirent)
}
4 changes: 3 additions & 1 deletion src/minidump_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ pub use minidump_common::format::{
self, ArmElfHwCaps as MDCPUInformationARMElfHwCaps, PlatformId,
ProcessorArchitecture as MDCPUArchitecture, GUID, MINIDUMP_DIRECTORY as MDRawDirectory,
MINIDUMP_EXCEPTION as MDException, MINIDUMP_EXCEPTION_STREAM as MDRawExceptionStream,
MINIDUMP_HEADER as MDRawHeader, MINIDUMP_LOCATION_DESCRIPTOR as MDLocationDescriptor,
MINIDUMP_HANDLE_DATA_STREAM as MDRawHandleDataStream,
MINIDUMP_HANDLE_DESCRIPTOR as MDRawHandleDescriptor, MINIDUMP_HEADER as MDRawHeader,
MINIDUMP_LOCATION_DESCRIPTOR as MDLocationDescriptor,
MINIDUMP_MEMORY_DESCRIPTOR as MDMemoryDescriptor, MINIDUMP_MEMORY_INFO as MDMemoryInfo,
MINIDUMP_MEMORY_INFO_LIST as MDMemoryInfoList, MINIDUMP_MODULE as MDRawModule,
MINIDUMP_SIGNATURE as MD_HEADER_SIGNATURE, MINIDUMP_STREAM_TYPE as MDStreamType,
Expand Down
5 changes: 5 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ pub fn start_child_and_wait_for_named_threads(num: usize) -> Child {
start_child_and_wait_for_threads_helper("spawn_name_wait", num)
}

#[allow(unused)]
pub fn start_child_and_wait_for_create_files(num: usize) -> Child {
start_child_and_wait_for_threads_helper("create_files_wait", num)
}

#[allow(unused)]
pub fn wait_for_threads(child: &mut Child, num: usize) {
let mut f = BufReader::new(child.stdout.as_mut().expect("Can't open stdout"));
Expand Down
40 changes: 40 additions & 0 deletions tests/linux_minidump_writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,46 @@ contextual_tests! {
expected.insert(format!("thread_{}", id));
}
assert_eq!(expected, names);

}

fn test_file_descriptors(context: Context) {
let num_of_files = 5;
let mut child = start_child_and_wait_for_create_files(num_of_files);
let pid = child.id() as i32;

let mut tmpfile = tempfile::Builder::new()
.prefix("testfiles")
.tempfile()
.unwrap();

let mut tmp = context.minidump_writer(pid);
let _ = tmp.dump(&mut tmpfile).expect("Could not write minidump");
child.kill().expect("Failed to kill process");

// Reap child
let waitres = child.wait().expect("Failed to wait for child");
let status = waitres.signal().expect("Child did not die due to signal");
assert_eq!(waitres.code(), None);
assert_eq!(status, Signal::SIGKILL as i32);

// Read dump file and check its contents. There should be a truncated minidump available
let dump = Minidump::read_path(tmpfile.path()).expect("Failed to read minidump");
let fds: MinidumpHandleDataStream = dump.get_stream().expect("Couldn't find MinidumpHandleDataStream");
// We check that we create num_of_files plus stdin, stdout and stderr
for i in 0..2 {
let descriptor = fds.handles.get(i).expect("Descriptor should be present");
let fd = *descriptor.raw.handle().expect("Handle should be populated");
assert_eq!(fd, i as u64);
}

for i in 3..num_of_files {
let descriptor = fds.handles.get(i).expect("Descriptor should be present");
let object_name = descriptor.object_name.as_ref().expect("The path should be populated");
let file_name = object_name.split('/').last().expect("The filename should be present");
assert!(file_name.starts_with("test_file"));
assert!(file_name.ends_with(&(i - 3).to_string()));
}
}
}

Expand Down
Loading