Skip to content

Commit

Permalink
feat: Access embedded PortablePDB in DLL (#752)
Browse files Browse the repository at this point in the history
  • Loading branch information
vaind authored Feb 3, 2023
1 parent 5cc0026 commit 2fe64b8
Show file tree
Hide file tree
Showing 12 changed files with 240 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"editor.formatOnType": true,
"editor.tabSize": 4,
"editor.rulers": [100],
"files.autoSave": "onWindowChange",
"files.autoSave": "afterDelay",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true,

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Correctly read the `debug_id` of Deterministic PE files ([#658](https://github.com/getsentry/symbolic/pull/658))
- Gracefully handle invalid PPDBs ([#751](https://github.com/getsentry/symbolic/pull/751))
- Support embedded PortablePDB in DLLs ([#752](https://github.com/getsentry/symbolic/pull/752))

## 11.0.0

Expand Down
1 change: 1 addition & 0 deletions symbolic-debuginfo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ macho = [
# PDB/PE processing
ms = [
"elsa",
"flate2",
"goblin/pe32",
"goblin/pe64",
"goblin/std",
Expand Down
100 changes: 99 additions & 1 deletion symbolic-debuginfo/src/pe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
use std::borrow::Cow;
use std::error::Error;
use std::fmt;
use std::io::Read;

use flate2::read::DeflateDecoder;
use gimli::RunTimeEndian;
use goblin::pe;
use scroll::{Pread, LE};
use thiserror::Error;

use symbolic_common::{Arch, AsSelf, CodeId, DebugId};
Expand Down Expand Up @@ -73,7 +76,6 @@ pub struct PeObject<'data> {
impl<'data> PeObject<'data> {
/// Tests whether the buffer could contain an PE object.
pub fn test(data: &[u8]) -> bool {
use scroll::{Pread, LE};
matches!(
data.get(0..2)
.and_then(|data| data.pread_with::<u16>(0, LE).ok()),
Expand Down Expand Up @@ -276,6 +278,75 @@ impl<'data> PeObject<'data> {
self.pe.exception_data.as_ref()
}
}

/// Returns the raw buffer of Embedded Portable PDB Debug directory entry, if any.
pub fn embedded_ppdb(&self) -> Result<Option<PeEmbeddedPortablePDB<'data>>, PeError> {
// Note: This is currently not supported by goblin, see https://github.com/m4b/goblin/issues/314
let Some(opt_header) = self.pe.header.optional_header else { return Ok(None) };
let Some(debug_directory) = opt_header.data_directories.get_debug_table().as_ref() else { return Ok(None) };
let file_alignment = opt_header.windows_fields.file_alignment;
let parse_options = &pe::options::ParseOptions::default();
let Some(offset) = pe::utils::find_offset(
debug_directory.virtual_address as usize,
&self.pe.sections,
file_alignment,
parse_options,
) else { return Ok(None) };

use pe::debug::ImageDebugDirectory;
let entries = debug_directory.size as usize / std::mem::size_of::<ImageDebugDirectory>();
for i in 0..entries {
let entry = offset + i * std::mem::size_of::<ImageDebugDirectory>();
let idd: ImageDebugDirectory = self.data.pread_with(entry, LE).map_err(PeError::new)?;

// We're only looking for Embedded Portable PDB Debug Directory Entry (type 17).
if idd.data_type == 17 {
// See data specification:
// https://github.com/dotnet/runtime/blob/97ddb55e3adde20ceac579d935cef83cfe996169/docs/design/specs/PE-COFF.md#embedded-portable-pdb-debug-directory-entry-type-17
if idd.size_of_data < 8 {
return Err(PeError::new(symbolic_ppdb::FormatError::from(
symbolic_ppdb::FormatErrorKind::InvalidLength,
)));
}

// ImageDebugDirectory.pointer_to_raw_data stores a raw offset -- not a virtual offset -- which we can use directly
let mut offset: usize = match parse_options.resolve_rva {
true => idd.pointer_to_raw_data as usize,
false => idd.address_of_raw_data as usize,
};

let mut signature: [u8; 4] = [0; 4];
self.data
.gread_inout(&mut offset, &mut signature)
.map_err(PeError::new)?;
if signature != "MPDB".as_bytes() {
return Err(PeError::new(symbolic_ppdb::FormatError::from(
symbolic_ppdb::FormatErrorKind::InvalidSignature,
)));
}
let uncompressed_size: u32 = self
.data
.gread_with(&mut offset, LE)
.map_err(PeError::new)?;

// 8 == the number bytes we have just read.
let compressed_size = idd.size_of_data as usize - 8;

return Ok(Some(PeEmbeddedPortablePDB {
compressed_data: self
.data
.get(offset..(offset + compressed_size))
.ok_or_else(|| {
PeError::new(symbolic_ppdb::FormatError::from(
symbolic_ppdb::FormatErrorKind::InvalidBlobOffset,
))
})?,
uncompressed_size: uncompressed_size as usize,
}));
}
}
Ok(None)
}
}

impl fmt::Debug for PeObject<'_> {
Expand Down Expand Up @@ -419,3 +490,30 @@ impl<'data> Dwarf<'data> for PeObject<'data> {
Some(dwarf_sect)
}
}

/// Embedded Portable PDB data wrapper that can be decompressed when needed.
#[derive(Debug, Clone)]
pub struct PeEmbeddedPortablePDB<'data> {
compressed_data: &'data [u8],
uncompressed_size: usize,
}

impl<'data, 'object> PeEmbeddedPortablePDB<'data> {
/// Returns the uncompressed size of the Portable PDB buffer.
pub fn get_size(&'object self) -> usize {
self.uncompressed_size
}

/// Reads the Portable PDB contents into the provided vector.
pub fn decompress(&self) -> Result<Vec<u8>, PeError> {
let mut decoder = DeflateDecoder::new(self.compressed_data);
let mut output: Vec<u8> = vec![0; self.uncompressed_size];
let read_size = decoder.read(&mut output).map_err(PeError::new)?;
if read_size != self.uncompressed_size {
return Err(PeError::new(symbolic_ppdb::FormatError::from(
symbolic_ppdb::FormatErrorKind::InvalidLength,
)));
}
Ok(output)
}
}
27 changes: 26 additions & 1 deletion symbolic-debuginfo/tests/test_objects.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use std::{ffi::CString, fmt};

use symbolic_common::ByteView;
use symbolic_debuginfo::{elf::ElfObject, FileEntry, Function, LineInfo, Object, SymbolMap};
use symbolic_debuginfo::{
elf::ElfObject, pe::PeObject, FileEntry, Function, LineInfo, Object, SymbolMap,
};
use symbolic_testutils::fixture;

use similar_asserts::assert_eq;
Expand Down Expand Up @@ -546,6 +548,29 @@ fn test_pe_dwarf_functions() -> Result<(), Error> {
Ok(())
}

#[test]
fn test_pe_embedded_ppdb() -> Result<(), Error> {
{
let view = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.dll"))?;
let pe = PeObject::parse(&view).unwrap();
let embedded_ppdb = pe.embedded_ppdb()?;
assert!(embedded_ppdb.is_none());
}
{
let view = ByteView::open(fixture(
"windows/Sentry.Samples.Console.Basic-embedded-ppdb.dll",
))?;
let pe = PeObject::parse(&view).unwrap();

let embedded_ppdb = pe.embedded_ppdb().unwrap().unwrap();
assert_eq!(embedded_ppdb.get_size(), 10540);

let buf = embedded_ppdb.decompress()?;
assert_eq!(&buf[15..25], "\0PDB v1.0\0".as_bytes());
}
Ok(())
}

#[test]
fn test_pdb() -> Result<(), Error> {
let view = ByteView::open(fixture("windows/crash.pdb"))?;
Expand Down
16 changes: 16 additions & 0 deletions symbolic-ppdb/tests/fixtures/contents/Sentry.Attributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System;
using System.Reflection;

[assembly: System.Reflection.AssemblyMetadata("Sentry.ProjectDirectory", "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\")]

// Generated by the MSBuild WriteCodeFragment class.

13 changes: 0 additions & 13 deletions symbolic-ppdb/tests/test_caches.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,3 @@ fn test_integration() {
})
);
}

#[test]
fn test_matching_ids() {
let pdb_buf = std::fs::read(fixture("windows/portable.pdb")).unwrap();
let pdb = PortablePdb::parse(&pdb_buf).unwrap();
let pdb_debug_id = pdb.pdb_id().unwrap();

let pe_buf = std::fs::read("tests/fixtures/integration.dll").unwrap();
let pe = symbolic_debuginfo::pe::PeObject::parse(&pe_buf).unwrap();
let pe_debug_id = pe.debug_id();

assert_eq!(pe_debug_id, pdb_debug_id);
}
109 changes: 96 additions & 13 deletions symbolic-ppdb/tests/test_ppdb.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use symbolic_ppdb::PortablePdb;
use symbolic_debuginfo::pe::PeObject;
use symbolic_ppdb::{EmbeddedSource, PortablePdb};
use symbolic_testutils::fixture;

#[test]
Expand Down Expand Up @@ -39,6 +40,14 @@ fn test_embedded_sources() {
);
}

fn check_contents(item: &EmbeddedSource, length: usize, name: &str) {
let content = item.get_contents().unwrap();
assert_eq!(content.len(), length);

let expected = std::fs::read(format!("tests/fixtures/contents/{name}")).unwrap();
assert_eq!(content, expected);
}

#[test]
fn test_embedded_sources_contents() {
let buf = std::fs::read(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap();
Expand All @@ -48,18 +57,22 @@ fn test_embedded_sources_contents() {
let items = iter.collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(items.len(), 4);

let check_contents = |i: usize, length: usize, name: &str| {
let content = items[i].get_contents().unwrap();
assert_eq!(content.len(), length);

let expected = std::fs::read(format!("tests/fixtures/contents/{name}")).unwrap();
assert_eq!(content, expected);
};

check_contents(0, 204, "Program.cs");
check_contents(1, 295, "Sentry.Samples.Console.Basic.GlobalUsings.g.cs");
check_contents(2, 198, ".NETCoreApp,Version=v6.0.AssemblyAttributes.cs");
check_contents(3, 1019, "Sentry.Samples.Console.Basic.AssemblyInfo.cs");
check_contents(&items[0], 204, "Program.cs");
check_contents(
&items[1],
295,
"Sentry.Samples.Console.Basic.GlobalUsings.g.cs",
);
check_contents(
&items[2],
198,
".NETCoreApp,Version=v6.0.AssemblyAttributes.cs",
);
check_contents(
&items[3],
1019,
"Sentry.Samples.Console.Basic.AssemblyInfo.cs",
);
}

/// This is here to prevent regression. The following test PDB was built in sentry-dotnet MAUI
Expand All @@ -73,3 +86,73 @@ fn test_embedded_sources_with_metadata_broken() {
let items = iter.collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(items.len(), 0);
}

#[test]
fn test_matching_ids() {
let pdb_buf = std::fs::read(fixture("windows/portable.pdb")).unwrap();
let pdb = PortablePdb::parse(&pdb_buf).unwrap();
let pdb_debug_id = pdb.pdb_id().unwrap();

let pe_buf = std::fs::read(fixture("windows/integration.dll")).unwrap();
let pe = PeObject::parse(&pe_buf).unwrap();
let pe_debug_id = pe.debug_id();

assert_eq!(pe_debug_id, pdb_debug_id);
}

#[test]
fn test_pe_embedded_ppdb_without_sources() {
let pe_buf = std::fs::read(fixture(
"windows/Sentry.Samples.Console.Basic-embedded-ppdb.dll",
))
.unwrap();
let pe = PeObject::parse(&pe_buf).unwrap();

let embedded_ppdb = pe.embedded_ppdb().unwrap().unwrap();
let ppdb_buf = embedded_ppdb.decompress().unwrap();
let ppdb = PortablePdb::parse(&ppdb_buf).unwrap();

assert_eq!(ppdb.pdb_id().unwrap(), pe.debug_id());
assert!(ppdb.has_debug_info());

let mut iter = ppdb.get_embedded_sources().unwrap();
assert!(iter.next().is_none());
}

#[test]
fn test_pe_embedded_ppdb_with_sources() {
let pe_buf = std::fs::read(fixture(
"windows/Sentry.Samples.Console.Basic-embedded-ppdb-with-sources.dll",
))
.unwrap();
let pe = PeObject::parse(&pe_buf).unwrap();

let embedded_ppdb = pe.embedded_ppdb().unwrap().unwrap();
let ppdb_buf = embedded_ppdb.decompress().unwrap();
let ppdb = PortablePdb::parse(&ppdb_buf).unwrap();

assert_eq!(ppdb.pdb_id().unwrap(), pe.debug_id());
assert!(ppdb.has_debug_info());

let iter = ppdb.get_embedded_sources().unwrap();
let items = iter.collect::<Result<Vec<_>, _>>().unwrap();
assert_eq!(items.len(), 5);

check_contents(&items[0], 204, "Program.cs");
check_contents(
&items[1],
295,
"Sentry.Samples.Console.Basic.GlobalUsings.g.cs",
);
check_contents(
&items[2],
198,
".NETCoreApp,Version=v6.0.AssemblyAttributes.cs",
);
check_contents(&items[3], 610, "Sentry.Attributes.cs");
check_contents(
&items[4],
1019,
"Sentry.Samples.Console.Basic.AssemblyInfo.cs",
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.

0 comments on commit 2fe64b8

Please sign in to comment.