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

Better addr2line #2989

Merged
merged 9 commits into from
Feb 16, 2025
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
138 changes: 6 additions & 132 deletions libafl_qemu/src/modules/usermode/asan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@

use core::{fmt, slice};
use std::{
borrow::Cow,
env,
fmt::{Debug, Display, Write},
fmt::{Debug, Display},
fs,
path::PathBuf,
pin::Pin,
sync::Mutex,
};
Expand All @@ -21,19 +19,13 @@ use libc::{
};
use meminterval::{Interval, IntervalTree};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use object::Object;
use rangemap::RangeMap;

use crate::{
emu::EmulatorModules,
modules::{
calls::FullBacktraceCollector,
snapshot::SnapshotModule,
utils::{
addr2line_legacy,
filters::{HasAddressFilter, StdAddressFilter},
load_file_section,
},
utils::filters::{HasAddressFilter, StdAddressFilter},
AddressFilter, EmulatorModule, EmulatorModuleTuple,
},
qemu::{Hook, MemAccessInfo, QemuHooks, SyscallHookResult},
Expand Down Expand Up @@ -1348,126 +1340,8 @@ where
/// # Safety
/// Will access the global [`FullBacktraceCollector`].
/// Calling this function concurrently might be racey.
#[expect(clippy::too_many_lines, clippy::unnecessary_cast)]
pub unsafe fn asan_report(rt: &AsanGiovese, qemu: Qemu, pc: GuestAddr, err: &AsanError) {
let mut regions = HashMap::new();
for region in qemu.mappings() {
if let Some(path) = region.path() {
let start = region.start();
let end = region.end();
let entry = regions.entry(path.to_owned()).or_insert(start..end);
if start < entry.start {
*entry = start..entry.end;
}
if end > entry.end {
*entry = entry.start..end;
}
}
}

let mut resolvers = vec![];
let mut images = vec![];
let mut ranges = RangeMap::new();

for (path, rng) in regions {
let data = fs::read(&path);
if data.is_err() {
continue;
}
let data = data.unwrap();
let idx = images.len();
images.push((path, data));
ranges.insert(rng, idx);
}

let arena_data = typed_arena::Arena::new();

for img in &images {
if let Ok(obj) = object::read::File::parse(&*img.1) {
let endian = if obj.is_little_endian() {
addr2line::gimli::RunTimeEndian::Little
} else {
addr2line::gimli::RunTimeEndian::Big
};

let mut load_section = |id: addr2line::gimli::SectionId| -> Result<_, _> {
load_file_section(id, &obj, endian, &arena_data)
};

let dwarf = addr2line::gimli::Dwarf::load(&mut load_section).unwrap();
let ctx = addr2line::Context::from_dwarf(dwarf)
.expect("Failed to create an addr2line context");

//let ctx = addr2line::Context::new(&obj).expect("Failed to create an addr2line context");
resolvers.push(Some((obj, ctx)));
} else {
resolvers.push(None);
}
}

let resolve_addr = |addr: GuestAddr| -> String {
let mut info = String::new();
if let Some((rng, idx)) = ranges.get_key_value(&addr) {
let raddr = (addr - rng.start) as u64;
if let Some((obj, ctx)) = resolvers[*idx].as_ref() {
let symbols = obj.symbol_map();
let mut func = symbols.get(raddr).map(|x| x.name().to_string());

if func.is_none() {
let pathname = PathBuf::from(images[*idx].0.clone());
let mut split_dwarf_loader = addr2line_legacy::SplitDwarfLoader::new(
|data, endian| {
addr2line::gimli::EndianSlice::new(
arena_data.alloc(Cow::Owned(data.into_owned())),
endian,
)
},
Some(pathname),
);

let frames = ctx.find_frames(raddr);
if let Ok(mut frames) = split_dwarf_loader.run(frames) {
if let Some(frame) = frames.next().unwrap_or(None) {
if let Some(function) = frame.function {
if let Ok(name) = function.raw_name() {
let demangled =
addr2line::demangle_auto(name, function.language);
func = Some(demangled.to_string());
}
}
}
}
}

if let Some(name) = func {
info += " in ";
info += &name;
}

if let Some(loc) = ctx.find_location(raddr).unwrap_or(None) {
if info.is_empty() {
info += " in";
}
info += " ";
if let Some(file) = loc.file {
info += file;
}
if let Some(line) = loc.line {
info += ":";
info += &line.to_string();
}
} else {
let _ = write!(&mut info, " ({}+{raddr:#x})", images[*idx].0);
}
}
if info.is_empty() {
let _ = write!(&mut info, " ({}+{raddr:#x})", images[*idx].0);
}
}
info
};

// TODO, make a class Resolver for resolving the addresses??
let resolver = crate::modules::utils::addr2line::AddressResolver::new(&qemu);
eprintln!("=================================================================");
let backtrace = FullBacktraceCollector::backtrace()
.map(|r| {
Expand All @@ -1478,7 +1352,7 @@ pub unsafe fn asan_report(rt: &AsanGiovese, qemu: Qemu, pc: GuestAddr, err: &Asa
.unwrap_or(vec![pc]);
eprintln!("AddressSanitizer Error: {err}");
for (i, addr) in backtrace.iter().rev().enumerate() {
eprintln!("\t#{i} {addr:#x}{}", resolve_addr(*addr));
eprintln!("\t#{i} {addr:#x}{}", resolver.resolve(*addr));
}
let addr = match err {
AsanError::Read(addr, _) | AsanError::Write(addr, _) | AsanError::BadFree(addr, _) => {
Expand All @@ -1493,13 +1367,13 @@ pub unsafe fn asan_report(rt: &AsanGiovese, qemu: Qemu, pc: GuestAddr, err: &Asa
} else {
eprintln!("Freed at:");
for (i, addr) in item.free_backtrace.iter().rev().enumerate() {
eprintln!("\t#{i} {addr:#x}{}", resolve_addr(*addr));
eprintln!("\t#{i} {addr:#x}{}", resolver.resolve(*addr));
}
eprintln!("And previously allocated at:");
}

for (i, addr) in item.backtrace.iter().rev().enumerate() {
eprintln!("\t#{i} {addr:#x}{}", resolve_addr(*addr));
eprintln!("\t#{i} {addr:#x}{}", resolver.resolve(*addr));
}
};

Expand Down
166 changes: 166 additions & 0 deletions libafl_qemu/src/modules/utils/addr2line.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
//! Utils for addr2line

use std::{borrow::Cow, fmt::Write, fs};

use addr2line::{fallible_iterator::FallibleIterator, Loader};
use goblin::elf::dynamic::{DF_1_PIE, DT_FLAGS_1};
use hashbrown::HashMap;
use libafl_qemu_sys::GuestAddr;
use rangemap::RangeMap;

use crate::Qemu;
// (almost) Copy paste from addr2line/src/bin/addr2line.rs
fn print_function(name: Option<&str>, language: Option<addr2line::gimli::DwLang>) -> String {
let ret = if let Some(name) = name {
addr2line::demangle_auto(Cow::from(name), language).to_string()
} else {
"??".to_string()
};
// println!("{ret:?}");
ret
}

/// check if this binary is pie (for 64bit binary only)
#[must_use]
pub fn is_pie(file: object::File<'_>) -> bool {
let is_pie = match file {
object::File::Elf64(elf) => {
let mut is_pie = false;
let table = elf.elf_section_table();
let dyn_sec = table.dynamic(elf.endian(), elf.data());
if let Ok(Some(d)) = dyn_sec {
let arr = d.0;
for v in arr {
if v.d_tag.get(elf.endian()) == DT_FLAGS_1
&& v.d_val.get(elf.endian()) & DF_1_PIE == DF_1_PIE
{
is_pie = true;
}
}
}
is_pie
}
_ => false,
};

is_pie
}

pub struct AddressResolver {
ranges: RangeMap<GuestAddr, usize>,
images: Vec<(String, Vec<u8>)>,
resolvers: Vec<Option<(Loader, bool)>>,
}

impl AddressResolver {
#[must_use]
pub fn new(qemu: &Qemu) -> Self {
let mut regions = HashMap::new();
for region in qemu.mappings() {
if let Some(path) = region.path() {
let start = region.start();
let end = region.end();
let entry = regions.entry(path.to_owned()).or_insert(start..end);
if start < entry.start {
*entry = start..entry.end;
}
if end > entry.end {
*entry = entry.start..end;
}
}
}

let mut resolvers = vec![];
let mut images = vec![];
let mut ranges: RangeMap<GuestAddr, usize> = RangeMap::new();

for (path, rng) in regions {
let data = fs::read(&path);
if data.is_err() {
continue;
}
let data = data.unwrap();
let idx = images.len();
images.push((path, data));
ranges.insert(rng, idx);
}

for img in &images {
if let Ok(obj) = object::read::File::parse(&*img.1) {
let is_pie = is_pie(obj);

let ctx = Loader::new(img.0.clone()).unwrap();
resolvers.push(Some((ctx, is_pie)));
} else {
resolvers.push(None);
}
}
Self {
ranges,
images,
resolvers,
}
}

#[must_use]
pub fn resolve(&self, pc: GuestAddr) -> String {
let resolve_addr = |addr: GuestAddr| -> String {
let mut info = String::new();
if let Some((range, idx)) = self.ranges.get_key_value(&addr) {
if let Some((ctx, is_pie)) = self.resolvers[*idx].as_ref() {
let raddr = if *is_pie { addr - range.start } else { addr };
let mut frames = ctx.find_frames(raddr.into()).unwrap().peekable();
let mut fname = None;
while let Some(frame) = frames.next().unwrap() {
// Only use the symbol table if this isn't an inlined function.
let symbol = if matches!(frames.peek(), Ok(None)) {
ctx.find_symbol(raddr.into())
} else {
None
};
if symbol.is_some() {
// Prefer the symbol table over the DWARF name because:
// - the symbol can include a clone suffix
// - llvm may omit the linkage name in the DWARF with -g1
fname = Some(print_function(symbol, None));
} else if let Some(func) = frame.function {
fname = Some(print_function(
func.raw_name().ok().as_deref(),
func.language,
));
} else {
fname = Some(print_function(None, None));
}
}

if let Some(name) = fname {
info += " in ";
info += &name;
}

if let Some(loc) = ctx.find_location(raddr.into()).unwrap_or(None) {
if info.is_empty() {
info += " in";
}
info += " ";
if let Some(file) = loc.file {
info += file;
}
if let Some(line) = loc.line {
info += ":";
info += &line.to_string();
}
} else {
let _ = write!(&mut info, " ({}+{addr:#x})", self.images[*idx].0);
}
}
if info.is_empty() {
let _ = write!(&mut info, " ({}+{addr:#x})", self.images[*idx].0);
}
}
info
};

resolve_addr(pc)
}
}
Loading
Loading