diff --git a/one_collect/Cargo.toml b/one_collect/Cargo.toml index 56329359..c5a86e94 100644 --- a/one_collect/Cargo.toml +++ b/one_collect/Cargo.toml @@ -92,6 +92,10 @@ harness = false name = "kallsyms" harness = false +[[bench]] +name = "symbols" +harness = false + [lints.rust] unreachable_pub = "deny" diff --git a/one_collect/benches/symbols.rs b/one_collect/benches/symbols.rs new file mode 100644 index 00000000..8b760242 --- /dev/null +++ b/one_collect/benches/symbols.rs @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use std::collections::HashMap; +use std::fs::File; +use std::path::{Path, PathBuf}; + +use criterion::{ + black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput, +}; +use one_collect::helpers::exporting::symbols::{ElfSymbolReader, GoPclnTabSymbolReader}; +use one_collect::helpers::exporting::{ExportMachine, ExportMapping, ExportSymbolReader}; +use one_collect::intern::InternedStrings; +use one_collect::unwind::{ElfLoadHeader, UnwindType}; + +#[cfg(target_os = "linux")] +use one_collect::helpers::exporting::process::MetricValue; +#[cfg(target_os = "linux")] +use one_collect::helpers::exporting::{ExportProcessSample, ExportSettings}; + +const LOAD_VADDR: u64 = 0x400000; +const MAPPING_START: u64 = 0x10000000; +const MAPPING_LEN: u64 = 0x200000; + +#[derive(Clone)] +struct Symbol { + start: u64, + end: u64, + name: String, +} + +fn fixture(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../test/assets/go") + .join(name) +} + +fn load_header() -> ElfLoadHeader { + ElfLoadHeader::new(0, LOAD_VADDR) +} + +fn collect_symbols(reader: &mut impl ExportSymbolReader) -> Vec { + let mut symbols = Vec::new(); + reader.reset(); + while reader.next() { + symbols.push(Symbol { + start: reader.start(), + end: reader.end(), + name: reader.name().to_string(), + }); + } + symbols +} + +fn elf_symbols(path: &Path, page_size: u64) -> Vec { + let mut reader = ElfSymbolReader::new( + File::open(path).expect("open ELF fixture"), + load_header(), + page_size, + ); + collect_symbols(&mut reader) +} + +fn go_symbols(path: &Path, page_size: u64) -> Vec { + let mut reader = GoPclnTabSymbolReader::new( + File::open(path).expect("open Go fixture"), + load_header(), + page_size, + ) + .expect("parse Go fixture"); + collect_symbols(&mut reader) +} + +fn shared_symbols(elf: &[Symbol], go: &[Symbol]) -> Vec { + let elf_by_name: HashMap<&str, &Symbol> = elf + .iter() + .map(|symbol| (symbol.name.as_str(), symbol)) + .collect(); + let mut shared: Vec = go + .iter() + .filter(|symbol| { + elf_by_name + .get(symbol.name.as_str()) + .is_some_and(|elf_symbol| elf_symbol.start == symbol.start) + }) + .cloned() + .collect(); + shared.sort_by_key(|symbol| symbol.start); + shared +} + +fn new_mapping() -> ExportMapping { + ExportMapping::new( + 0, + 0, + MAPPING_START, + MAPPING_START + MAPPING_LEN - 1, + 0, + false, + 0, + UnwindType::DWARF, + ) +} + +fn sampled_ips(shared: &[Symbol]) -> Vec { + shared + .iter() + .map(|symbol| MAPPING_START + symbol.start) + .collect() +} + +fn benchmark_readers( + c: &mut Criterion, + elf_path: &Path, + go_path: &Path, + page_size: u64, + elf_count: usize, + go_count: usize, +) { + let mut setup = c.benchmark_group("symbol_readers/setup"); + setup.sample_size(20); + + setup.bench_function("elf", |b| { + b.iter_batched( + || File::open(elf_path).expect("open ELF fixture"), + |file| { + let mut reader = ElfSymbolReader::new(file, load_header(), page_size); + reader.reset(); + black_box(reader); + }, + BatchSize::SmallInput, + ) + }); + + setup.bench_function("go_pclntab", |b| { + b.iter_batched( + || File::open(go_path).expect("open Go fixture"), + |file| { + let mut reader = GoPclnTabSymbolReader::new(file, load_header(), page_size) + .expect("parse Go fixture"); + reader.reset(); + black_box(reader); + }, + BatchSize::SmallInput, + ) + }); + setup.finish(); + + // This includes each reader's real initialization path as well as iteration. + // The setup group above isolates initialization so its contribution remains visible. + let mut scan = c.benchmark_group("symbol_readers/full_lifecycle"); + scan.sample_size(20); + + scan.throughput(Throughput::Elements(elf_count as u64)); + scan.bench_function(BenchmarkId::new("elf", elf_count), |b| { + b.iter_batched( + || File::open(elf_path).expect("open ELF fixture"), + |file| { + let mut reader = ElfSymbolReader::new(file, load_header(), page_size); + reader.reset(); + while reader.next() { + black_box((reader.start(), reader.end(), reader.name())); + } + }, + BatchSize::SmallInput, + ) + }); + + scan.throughput(Throughput::Elements(go_count as u64)); + scan.bench_function(BenchmarkId::new("go_pclntab", go_count), |b| { + b.iter_batched( + || File::open(go_path).expect("open Go fixture"), + |file| { + let mut reader = GoPclnTabSymbolReader::new(file, load_header(), page_size) + .expect("parse Go fixture"); + reader.reset(); + while reader.next() { + black_box((reader.start(), reader.end(), reader.name())); + } + }, + BatchSize::SmallInput, + ) + }); + scan.finish(); +} + +fn benchmark_mapping_resolution( + c: &mut Criterion, + elf_path: &Path, + go_path: &Path, + page_size: u64, + ips: &[u64], +) { + let mut group = c.benchmark_group("symbol_resolution/mapping"); + group.sample_size(20); + group.throughput(Throughput::Elements(ips.len() as u64)); + + group.bench_function("elf", |b| { + b.iter_batched( + || { + ( + File::open(elf_path).expect("open ELF fixture"), + new_mapping(), + InternedStrings::new(2048), + ips.to_vec(), + ) + }, + |(file, mut mapping, mut strings, mut sampled_ips)| { + let mut reader = ElfSymbolReader::new(file, load_header(), page_size); + mapping.add_matching_symbols(&mut sampled_ips, &mut reader, &mut strings); + black_box(mapping); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("go_pclntab", |b| { + b.iter_batched( + || { + ( + File::open(go_path).expect("open Go fixture"), + new_mapping(), + InternedStrings::new(2048), + ips.to_vec(), + ) + }, + |(file, mut mapping, mut strings, mut sampled_ips)| { + let mut reader = GoPclnTabSymbolReader::new(file, load_header(), page_size) + .expect("parse Go fixture"); + mapping.add_matching_symbols(&mut sampled_ips, &mut reader, &mut strings); + black_box(mapping); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("elf_plus_go_union", |b| { + b.iter_batched( + || { + ( + File::open(elf_path).expect("open ELF fixture"), + new_mapping(), + InternedStrings::new(2048), + ips.to_vec(), + ) + }, + |(file, mut mapping, mut strings, mut sampled_ips)| { + let go_file = file.try_clone().expect("clone fixture"); + let mut elf_reader = ElfSymbolReader::new(file, load_header(), page_size); + mapping.add_matching_symbols(&mut sampled_ips, &mut elf_reader, &mut strings); + + let mut go_reader = GoPclnTabSymbolReader::new(go_file, load_header(), page_size) + .expect("parse Go fixture"); + mapping.add_matching_symbols(&mut sampled_ips, &mut go_reader, &mut strings); + black_box(mapping); + }, + BatchSize::SmallInput, + ) + }); + group.finish(); +} + +#[cfg(target_os = "linux")] +fn build_machine(path: &Path, symbol_vas: &[u64]) -> ExportMachine { + use std::os::unix::fs::MetadataExt; + + let metadata = std::fs::metadata(path).expect("read fixture metadata"); + let pid = std::process::id(); + let mut machine = ExportMachine::new(ExportSettings::default()); + machine + .add_comm_exec(pid, "symbol-benchmark", 0) + .expect("add benchmark process"); + machine + .add_mmap_exec( + 0, + pid, + LOAD_VADDR, + MAPPING_LEN, + 0, + libc::major(metadata.dev()) as u32, + libc::minor(metadata.dev()) as u32, + metadata.ino(), + path.to_str().expect("UTF-8 fixture path"), + ) + .expect("add fixture mapping"); + + for (index, ip) in symbol_vas.iter().enumerate() { + machine + .add_process_sample( + pid, + ExportProcessSample::new(index as u64, MetricValue::Count(1), 0, 0, pid, *ip, 0), + ) + .expect("add fixture sample"); + } + machine +} + +#[cfg(target_os = "linux")] +fn resolved_symbol_count(machine: &ExportMachine, ip: u64) -> usize { + machine + .find_process(std::process::id()) + .and_then(|process| process.find_mapping(ip, None)) + .map_or(0, |mapping| mapping.symbols().len()) +} + +#[cfg(target_os = "linux")] +fn benchmark_export_machine( + c: &mut Criterion, + elf_path: &Path, + go_path: &Path, + symbol_vas: &[u64], +) { + for path in [elf_path, go_path] { + let mut machine = build_machine(path, symbol_vas); + machine.capture_file_symbol_metadata(); + machine.resolve_local_file_symbols(); + assert_eq!( + resolved_symbol_count(&machine, symbol_vas[0]), + symbol_vas.len() + ); + } + + let mut group = c.benchmark_group("symbol_resolution/export_machine"); + group.sample_size(10); + group.throughput(Throughput::Elements(symbol_vas.len() as u64)); + + group.bench_function("unstripped_elf_plus_go", |b| { + b.iter_batched( + || build_machine(elf_path, symbol_vas), + |mut machine| { + machine.capture_file_symbol_metadata(); + machine.resolve_local_file_symbols(); + black_box(machine); + }, + BatchSize::SmallInput, + ) + }); + + group.bench_function("stripped_go", |b| { + b.iter_batched( + || build_machine(go_path, symbol_vas), + |mut machine| { + machine.capture_file_symbol_metadata(); + machine.resolve_local_file_symbols(); + black_box(machine); + }, + BatchSize::SmallInput, + ) + }); + group.finish(); +} + +pub fn criterion_benchmark(c: &mut Criterion) { + let elf_path = fixture("symbol_benchmark_elf"); + let go_path = fixture("symbol_benchmark_go"); + let page_size = ExportMachine::system_page_size(); + let elf = elf_symbols(&elf_path, page_size); + let go = go_symbols(&go_path, page_size); + let shared = shared_symbols(&elf, &go); + + assert!(elf.len() >= 1000, "expected at least 1000 ELF symbols"); + assert!(go.len() >= 1000, "expected at least 1000 Go symbols"); + assert!( + shared.len() >= 1000, + "expected at least 1000 shared symbols" + ); + assert!( + shared + .iter() + .any(|symbol| { symbol.name == "main.benchmarkTarget" && symbol.end >= symbol.start }), + "benchmark target should be present" + ); + + let ips = sampled_ips(&shared); + benchmark_readers(c, &elf_path, &go_path, page_size, elf.len(), go.len()); + benchmark_mapping_resolution(c, &elf_path, &go_path, page_size, &ips); + + #[cfg(target_os = "linux")] + { + let symbol_vas: Vec = shared + .iter() + .map(|symbol| LOAD_VADDR + symbol.start) + .collect(); + benchmark_export_machine(c, &elf_path, &go_path, &symbol_vas); + } +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/one_collect/src/helpers/exporting/symbols.rs b/one_collect/src/helpers/exporting/symbols.rs index b43945f4..20a88c5b 100644 --- a/one_collect/src/helpers/exporting/symbols.rs +++ b/one_collect/src/helpers/exporting/symbols.rs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -use std::{fs::File, io::{BufRead, BufReader, Seek, SeekFrom}}; +use crate::ruwind::elf::{symbol_rva, ElfLoadHeader, ElfSymbol, ElfSymbolIterator}; +use crate::ruwind::go_pclntab::{GoPclnTab, GoPclnTabFileReader}; use std::collections::HashSet; -use crate::ruwind::elf::{ElfLoadHeader, ElfSymbol, ElfSymbolIterator, symbol_rva}; -use crate::ruwind::go_pclntab::GoPclnTab; +use std::{ + fs::File, + io::{BufRead, BufReader, Seek, SeekFrom}, +}; use tracing::{info, trace, warn}; use crate::helpers::exporting::ExportMachine; @@ -418,14 +421,13 @@ pub struct PerfMapSymbolReader { /// by Kubernetes). Function virtual addresses from the table are translated to /// file RVAs via [`symbol_rva`], identical to [`ElfSymbolReader`], so the two /// can be layered (unioned) for the same mapping. -pub(crate) struct GoPclnTabSymbolReader { - tab: GoPclnTab, +#[doc(hidden)] +pub struct GoPclnTabSymbolReader { + tab: GoPclnTab, load_header: ElfLoadHeader, page_mask: u64, - index: usize, current_start: u64, current_end: u64, - current_name: String, valid: bool, } @@ -433,24 +435,22 @@ impl GoPclnTabSymbolReader { /// Attempts to build a reader from `file`. Returns `None` if the file has no /// usable `.gopclntab` section or the table cannot be parsed (e.g. an /// unsupported/older Go layout), so callers fall back to ELF symbols. - pub(crate) fn new( - mut file: File, - load_header: ElfLoadHeader, - system_page_size: u64) -> Option { + pub fn new(mut file: File, load_header: ElfLoadHeader, system_page_size: u64) -> Option { // Prefer the .gopclntab section; fall back to scanning for a stripped // (section-header-removed) line table. - let (data, text_start) = crate::ruwind::elf::read_go_pclntab(&mut file) + let location = crate::ruwind::elf::read_go_pclntab(&mut file) .or_else(|| crate::ruwind::elf::recover_go_pclntab(&mut file))?; - let tab = GoPclnTab::parse(data, text_start)?; + let tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + )?; Some(Self { tab, load_header, page_mask: crate::page_size_to_mask(system_page_size), - index: 0, current_start: 0, current_end: 0, - current_name: String::with_capacity(256), valid: false, }) } @@ -458,41 +458,41 @@ impl GoPclnTabSymbolReader { impl ExportSymbolReader for GoPclnTabSymbolReader { fn reset(&mut self) { - self.index = 0; + self.tab.reset(); self.valid = false; } fn next(&mut self) -> bool { let p_vaddr = self.load_header.p_vaddr(); - while self.index < self.tab.len() { - let i = self.index; - self.index += 1; + while self.tab.next() { + let start_va = self.tab.start(); + let end_va = self.tab.end(); - if let Some((start_va, end_va, name)) = self.tab.func(i) { - // Guard against entries below the loaded segment (would - // underflow the RVA translation). - if start_va < p_vaddr { - continue; - } - - let start = symbol_rva(start_va, &self.load_header, self.page_mask); - - // pclntab end is the next function's entry (exclusive); make it - // inclusive to match ElfSymbol semantics and avoid overlapping - // the following function's start. - let end_exclusive = if end_va > start_va { end_va } else { start_va + 1 }; - let end = symbol_rva(end_exclusive, &self.load_header, self.page_mask) - .saturating_sub(1) - .max(start); - - self.current_start = start; - self.current_end = end; - self.current_name.clear(); - self.current_name.push_str(name); - self.valid = true; - return true; + // Guard against entries below the loaded segment (would + // underflow the RVA translation). + if start_va < p_vaddr { + continue; } + + let start = symbol_rva(start_va, &self.load_header, self.page_mask); + + // pclntab end is the next function's entry (exclusive); make it + // inclusive to match ElfSymbol semantics and avoid overlapping + // the following function's start. + let end_exclusive = if end_va > start_va { + end_va + } else { + start_va + 1 + }; + let end = symbol_rva(end_exclusive, &self.load_header, self.page_mask) + .saturating_sub(1) + .max(start); + + self.current_start = start; + self.current_end = end; + self.valid = true; + return true; } self.valid = false; @@ -500,15 +500,27 @@ impl ExportSymbolReader for GoPclnTabSymbolReader { } fn start(&self) -> u64 { - if self.valid { self.current_start } else { 0 } + if self.valid { + self.current_start + } else { + 0 + } } fn end(&self) -> u64 { - if self.valid { self.current_end } else { 0 } + if self.valid { + self.current_end + } else { + 0 + } } fn name(&self) -> &str { - if self.valid { &self.current_name } else { "" } + if self.valid { + self.tab.name() + } else { + "" + } } fn demangle(&mut self) -> Option { @@ -1189,19 +1201,82 @@ mod tests { count += 1; assert!(reader.start() <= reader.end()); assert!(!reader.name().is_empty()); - assert!(reader.demangle().is_none(), "Go names must not be demangled"); + assert!( + reader.demangle().is_none(), + "Go names must not be demangled" + ); if reader.name() == "main.main" { found_main = true; } } - assert!(count > 100, "{}: expected many symbols, got {}", name, count); + assert!( + count > 100, + "{}: expected many symbols, got {}", + name, + count + ); assert!(found_main, "{}: main.main should resolve", name); } } + #[test] + #[cfg(target_os = "linux")] + fn symbol_benchmark_fixtures_match() { + use std::collections::HashMap; + + let fixture_dir = std::env::current_dir().unwrap().join("../test/assets/go"); + let elf_path = fixture_dir.join("symbol_benchmark_elf"); + let go_path = fixture_dir.join("symbol_benchmark_go"); + let page = system_page_size(); + + let mut elf_symbols = HashMap::new(); + let mut elf = ElfSymbolReader::new( + File::open(&elf_path).expect("open ELF benchmark fixture"), + ElfLoadHeader::new(0, 0x400000), + page, + ); + elf.reset(); + while elf.next() { + elf_symbols.insert(elf.name().to_string(), elf.start()); + } + + let mut go = GoPclnTabSymbolReader::new( + File::open(&go_path).expect("open Go benchmark fixture"), + ElfLoadHeader::new(0, 0x400000), + page, + ) + .expect("parse Go benchmark fixture"); + go.reset(); + + let mut go_count = 0; + let mut shared_count = 0; + let mut found_target = false; + while go.next() { + go_count += 1; + if elf_symbols.get(go.name()) == Some(&go.start()) { + shared_count += 1; + } + if go.name() == "main.benchmarkTarget" { + found_target = true; + } + } + + assert!( + elf_symbols.len() >= 1000, + "expected at least 1000 ELF symbols" + ); + assert!(go_count >= 1000, "expected at least 1000 Go symbols"); + assert!( + shared_count >= 1000, + "expected at least 1000 shared symbols" + ); + assert!(found_target, "benchmark target should be present"); + } + #[test] fn symbol_page_map() { - let mut map = SymbolPageMap::new(256); map.mark_ip(0); + let mut map = SymbolPageMap::new(256); + map.mark_ip(0); map.mark_ip(257); map.mark_ip(1024); diff --git a/one_collect/src/ruwind/elf.rs b/one_collect/src/ruwind/elf.rs index bfe2fcc7..5576f1c5 100644 --- a/one_collect/src/ruwind/elf.rs +++ b/one_collect/src/ruwind/elf.rs @@ -717,414 +717,596 @@ pub fn read_debug_link<'a>( Ok(None) } -/// The ELF section name that holds the Go line table when not stripped. -const GO_PCLNTAB_SECTION: &str = ".gopclntab"; - -/// Returns true if the ELF file contains a `.gopclntab` section. -/// -/// Used as a cheap, one-time probe to decide whether a binary carries a Go -/// line table without reading the (potentially large) section contents. -pub(crate) fn has_go_pclntab( - reader: &mut (impl Read + Seek)) -> bool { - let mut sections = Vec::new(); - let mut section_offsets = Vec::new(); +mod go { + use super::*; - if enum_section_metadata(reader, None, None, &mut sections).is_err() { - return false; - } - if get_section_offsets(reader, None, &mut section_offsets).is_err() { - return false; - } + /// The ELF section name that holds the Go line table when not stripped. + const GO_PCLNTAB_SECTION: &str = ".gopclntab"; + pub(super) const GO_ELF_SECTION_LIMIT: usize = 2048; - find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION).is_some() -} + pub(super) fn go_elf_section_count_allowed(reader: &mut (impl Read + Seek)) -> bool { + if reader.seek(SeekFrom::Start(0)).is_err() { + return false; + } + let ident = match get_ident(reader) { + Ok(ident) => ident, + Err(_) => return false, + }; + if reader.seek(SeekFrom::Start(16)).is_err() { + return false; + } -/// Finds the metadata for a section by name, if present. -fn find_section_by_name<'a>( - reader: &mut (impl Read + Seek), - sections: &'a [SectionMetadata], - section_offsets: &[u64], - name: &str) -> Option<&'a SectionMetadata> { - for section in sections { - let mut name_buf: [u8; 256] = [0; 256]; - if let Ok(sec_name) = read_section_name(reader, section, section_offsets, &mut name_buf) { - if sec_name == name { - return Some(section); + let section_count = match ident[EI_CLASS] { + ELFCLASS32 => { + let mut header = ElfHeader32::default(); + unsafe { + if reader + .read_exact(slice::from_raw_parts_mut( + &mut header as *mut _ as *mut u8, + size_of::(), + )) + .is_err() + { + return false; + } + } + if header.e_shnum != 0 || header.e_shoff == 0 { + header.e_shnum as usize + } else { + if reader.seek(SeekFrom::Start(header.e_shoff as u64)).is_err() { + return false; + } + let mut section = ElfSectionHeader32::default(); + if get_section_header32(reader, &mut section).is_err() { + return false; + } + section.sh_size as usize + } } - } + ELFCLASS64 => { + let mut header = ElfHeader64::default(); + unsafe { + if reader + .read_exact(slice::from_raw_parts_mut( + &mut header as *mut _ as *mut u8, + size_of::(), + )) + .is_err() + { + return false; + } + } + if header.e_shnum != 0 || header.e_shoff == 0 { + header.e_shnum as usize + } else { + if reader.seek(SeekFrom::Start(header.e_shoff)).is_err() { + return false; + } + let mut section = ElfSectionHeader64::default(); + if get_section_header64(reader, &mut section).is_err() { + return false; + } + match usize::try_from(section.sh_size) { + Ok(count) => count, + Err(_) => return false, + } + } + } + _ => return false, + }; + + section_count <= GO_ELF_SECTION_LIMIT } - None -} -/// Reads the `.gopclntab` section bytes and resolves the Go runtime text start. -/// -/// Returns `(pclntab_bytes, text_start)` when the `.gopclntab` section is -/// present, or `None` otherwise. `text_start` is the virtual address of -/// `runtime.text` (the base the line table's PC offsets are relative to), which -/// is **not** always the `.text` section address — for cgo / externally linked -/// binaries the first part of `.text` holds C startup stubs and `runtime.text` -/// is offset past them. It is resolved by priority: -/// -/// 1. the `runtime.text` symbol (authoritative; present unless stripped), -/// 2. the pcHeader `textStart` field in the table (set for internally linked -/// binaries; zero/relocated otherwise), -/// 3. the `.text` section address (best-effort fallback). -/// -/// The returned function virtual addresses are in the same space as ELF symbol -/// `st_value`, so the caller applies the usual load-header relocation. -pub(crate) fn read_go_pclntab( - reader: &mut (impl Read + Seek)) -> Option<(Vec, u64)> { - let mut sections = Vec::new(); - let mut section_offsets = Vec::new(); + /// Returns true if the ELF file contains a `.gopclntab` section. + /// + /// Used as a cheap, one-time probe to decide whether a binary carries a Go + /// line table without reading the (potentially large) section contents. + pub(crate) fn has_go_pclntab(reader: &mut (impl Read + Seek)) -> bool { + if !go_elf_section_count_allowed(reader) { + return false; + } + + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + + if enum_section_metadata(reader, None, None, &mut sections).is_err() { + return false; + } + if get_section_offsets(reader, None, &mut section_offsets).is_err() { + return false; + } - enum_section_metadata(reader, None, None, &mut sections).ok()?; - get_section_offsets(reader, None, &mut section_offsets).ok()?; + find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION).is_some() + } - let pclntab = find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION)?; - let pclntab_offset = pclntab.offset; - let pclntab_size = pclntab.size as usize; - let pclntab_vaddr = pclntab.address; + /// Finds the metadata for a section by name, if present. + fn find_section_by_name<'a>( + reader: &mut (impl Read + Seek), + sections: &'a [SectionMetadata], + section_offsets: &[u64], + name: &str, + ) -> Option<&'a SectionMetadata> { + for section in sections { + let mut name_buf: [u8; 256] = [0; 256]; + if let Ok(sec_name) = read_section_name(reader, section, section_offsets, &mut name_buf) + { + if sec_name == name { + return Some(section); + } + } + } + None + } + + /// Locates the `.gopclntab` section and resolves the Go runtime text start. + /// + /// Returns bounded file metadata when the `.gopclntab` section is present, or + /// `None` otherwise. `text_start` is the virtual address of + /// `runtime.text` (the base the line table's PC offsets are relative to), which + /// is **not** always the `.text` section address — for cgo / externally linked + /// binaries the first part of `.text` holds C startup stubs and `runtime.text` + /// is offset past them. It is resolved by priority: + /// + /// 1. the `runtime.text` symbol (authoritative; present unless stripped), + /// 2. the pcHeader `textStart` field in the table (set for internally linked + /// binaries; zero/relocated otherwise), + /// 3. the `.text` section address (best-effort fallback). + /// + /// The returned function virtual addresses are in the same space as ELF symbol + /// `st_value`, so the caller applies the usual load-header relocation. + pub(crate) fn read_go_pclntab( + reader: &mut (impl Read + Seek), + ) -> Option { + if !go_elf_section_count_allowed(reader) { + return None; + } + + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); - // Optional .text address for the fallback. - let text_vaddr = find_section_by_name(reader, §ions, §ion_offsets, ".text") - .map(|s| s.address); + enum_section_metadata(reader, None, None, &mut sections).ok()?; + get_section_offsets(reader, None, &mut section_offsets).ok()?; - let mut data = vec![0u8; pclntab_size]; - reader.seek(SeekFrom::Start(pclntab_offset)).ok()?; - reader.read_exact(&mut data).ok()?; + let pclntab = + find_section_by_name(reader, §ions, §ion_offsets, GO_PCLNTAB_SECTION)?; + let pclntab_offset = pclntab.offset; + let pclntab_size = pclntab.size; + let pclntab_vaddr = pclntab.address; - let text_start = resolve_go_text_start(reader, &data, Some(pclntab_vaddr), text_vaddr)?; + // Optional .text address for the fallback. + let text_vaddr = + find_section_by_name(reader, §ions, §ion_offsets, ".text").map(|s| s.address); - debug!( - "read_go_pclntab: pclntab size={} text_start={:#x}", - pclntab_size, text_start); + let header_len = usize::try_from( + pclntab_size.min(crate::ruwind::go_pclntab::GO_PCLNTAB_HEADER_MAX as u64), + ) + .ok()?; + let mut header = [0u8; crate::ruwind::go_pclntab::GO_PCLNTAB_HEADER_MAX]; + reader.seek(SeekFrom::Start(pclntab_offset)).ok()?; + reader.read_exact(&mut header[..header_len]).ok()?; - Some((data, text_start)) -} + let text_start = resolve_go_text_start( + reader, + &header[..header_len], + Some(pclntab_vaddr), + text_vaddr, + )?; + + debug!( + "read_go_pclntab: pclntab size={} text_start={:#x}", + pclntab_size, text_start + ); -/// Resolves the Go `runtime.text` virtual address (the line table PC base). -/// -/// `pclntab_vaddr`, when known, is the virtual address at which the line table -/// is mapped; it is used to look up the PIE relocation that supplies -/// `runtime.text` when the in-table field is unrelocated (zero). -fn resolve_go_text_start( - reader: &mut (impl Read + Seek), - pclntab: &[u8], - pclntab_vaddr: Option, - text_vaddr: Option) -> Option { - // 1. The runtime.text symbol is authoritative when present. - if let Some(value) = find_symbol_value(reader, "runtime.text") { - if value != 0 { - return Some(value); - } + Some(crate::ruwind::go_pclntab::GoPclnTabLocation::new( + pclntab_offset, + pclntab_size, + text_start, + )) } - let ptr_size = if pclntab.len() >= 8 { pclntab[7] as usize } else { 0 }; + /// Resolves the Go `runtime.text` virtual address (the line table PC base). + /// + /// `pclntab_vaddr`, when known, is the virtual address at which the line table + /// is mapped; it is used to look up the PIE relocation that supplies + /// `runtime.text` when the in-table field is unrelocated (zero). + fn resolve_go_text_start( + reader: &mut (impl Read + Seek), + pclntab: &[u8], + pclntab_vaddr: Option, + text_vaddr: Option, + ) -> Option { + // 1. The runtime.text symbol is authoritative when present. + if let Some(value) = find_symbol_value(reader, "runtime.text") { + if value != 0 { + return Some(value); + } + } - // 2. The pcHeader textStart field (offset 8 + 2*ptrSize). Non-zero for - // internally linked binaries; zero/relocated for externally linked ones. - if ptr_size == 4 || ptr_size == 8 { - let off = 8 + 2 * ptr_size; - let header_text_start = match ptr_size { - 8 if pclntab.len() >= off + 8 => Some(u64::from_le_bytes( - pclntab[off..off + 8].try_into().unwrap())), - 4 if pclntab.len() >= off + 4 => Some(u32::from_le_bytes( - pclntab[off..off + 4].try_into().unwrap()) as u64), - _ => None, + let ptr_size = if pclntab.len() >= 8 { + pclntab[7] as usize + } else { + 0 }; - if let Some(ts) = header_text_start { - if ts != 0 { - return Some(ts); + + // 2. The pcHeader textStart field (offset 8 + 2*ptrSize). Non-zero for + // internally linked binaries; zero/relocated for externally linked ones. + if ptr_size == 4 || ptr_size == 8 { + let off = 8 + 2 * ptr_size; + let header_text_start = match ptr_size { + 8 if pclntab.len() >= off + 8 => Some(u64::from_le_bytes( + pclntab[off..off + 8].try_into().unwrap(), + )), + 4 if pclntab.len() >= off + 4 => { + Some(u32::from_le_bytes(pclntab[off..off + 4].try_into().unwrap()) as u64) + } + _ => None, + }; + if let Some(ts) = header_text_start { + if ts != 0 { + return Some(ts); + } } - } - // 3. For PIE binaries the field above is zero and relocated at load. The - // relocation that targets the field carries runtime.text as its addend. - if let Some(base) = pclntab_vaddr { - let field_vaddr = base + off as u64; - if let Some(addend) = find_relative_reloc_addend(reader, field_vaddr) { - if addend != 0 { - return Some(addend); + // 3. For PIE binaries the field above is zero and relocated at load. The + // relocation that targets the field carries runtime.text as its addend. + if let Some(base) = pclntab_vaddr { + let field_vaddr = base + off as u64; + if let Some(addend) = find_relative_reloc_addend(reader, field_vaddr) { + if addend != 0 { + return Some(addend); + } } } } - } - // 4. Fall back to the .text section address. - text_vaddr -} - -/// Finds the addend of an `R_*_RELATIVE` relocation targeting `target_vaddr`. -/// -/// Used to recover `runtime.text` for position-independent Go binaries, where -/// the pcHeader `textStart` field is zero on disk and supplied by this -/// relocation at load time. -fn find_relative_reloc_addend( - reader: &mut (impl Read + Seek), - target_vaddr: u64) -> Option { - let mut sections = Vec::new(); - if get_section_metadata(reader, None, SHT_RELA, &mut sections).is_err() { - return None; + // 4. Fall back to the .text section address. + text_vaddr } - // Elf64_Rela: r_offset (u64), r_info (u64), r_addend (i64) = 24 bytes. - const RELA_SIZE: usize = 24; - let mut buf: Vec = Vec::new(); - - for section in §ions { - // Only 64-bit RELA is handled (sufficient for the supported targets). - if section.class != ELFCLASS64 || section.entry_size as usize != RELA_SIZE { - continue; - } - if section.size == 0 { - continue; - } - - buf.resize(section.size as usize, 0); - if reader.seek(SeekFrom::Start(section.offset)).is_err() { - continue; - } - if reader.read_exact(&mut buf).is_err() { - continue; + /// Finds the addend of an `R_*_RELATIVE` relocation targeting `target_vaddr`. + /// + /// Used to recover `runtime.text` for position-independent Go binaries, where + /// the pcHeader `textStart` field is zero on disk and supplied by this + /// relocation at load time. + fn find_relative_reloc_addend( + reader: &mut (impl Read + Seek), + target_vaddr: u64, + ) -> Option { + let mut sections = Vec::new(); + if get_section_metadata(reader, None, SHT_RELA, &mut sections).is_err() { + return None; } - let count = buf.len() / RELA_SIZE; - for i in 0..count { - let base = i * RELA_SIZE; - let r_offset = u64::from_le_bytes(buf[base..base + 8].try_into().unwrap()); - if r_offset != target_vaddr { + // Elf64_Rela: r_offset (u64), r_info (u64), r_addend (i64) = 24 bytes. + const RELA_SIZE: usize = 24; + for section in §ions { + // Only 64-bit RELA is handled (sufficient for the supported targets). + if section.class != ELFCLASS64 || section.entry_size as usize != RELA_SIZE { continue; } - let r_info = u64::from_le_bytes(buf[base + 8..base + 16].try_into().unwrap()); - let r_type = (r_info & 0xffff_ffff) as u32; - if r_type == R_X86_64_RELATIVE || r_type == R_AARCH64_RELATIVE { - let r_addend = i64::from_le_bytes(buf[base + 16..base + 24].try_into().unwrap()); - return Some(r_addend as u64); + if section.size == 0 { + continue; } - } - } - None -} + let count = section.size as usize / RELA_SIZE; + let mut entry = [0u8; RELA_SIZE]; + for i in 0..count { + let entry_offset = match section.offset.checked_add((i * RELA_SIZE) as u64) { + Some(offset) => offset, + None => break, + }; + if reader.seek(SeekFrom::Start(entry_offset)).is_err() + || reader.read_exact(&mut entry).is_err() + { + break; + } + let r_offset = u64::from_le_bytes(entry[..8].try_into().unwrap()); + if r_offset != target_vaddr { + continue; + } + let r_info = u64::from_le_bytes(entry[8..16].try_into().unwrap()); + let r_type = (r_info & 0xffff_ffff) as u32; + if r_type == R_X86_64_RELATIVE || r_type == R_AARCH64_RELATIVE { + let r_addend = i64::from_le_bytes(entry[16..24].try_into().unwrap()); + return Some(r_addend as u64); + } + } + } -/// Finds the value (`st_value`) of a named ELF symbol in `.symtab` or `.dynsym`. -/// -/// Unlike the function-symbol iterator this matches symbols of any type (e.g. -/// `runtime.text`, which is not `STT_FUNC`). Returns the first match. -fn find_symbol_value( - reader: &mut (impl Read + Seek), - name: &str) -> Option { - let mut sections = Vec::new(); - let mut section_offsets = Vec::new(); + None + } - get_section_offsets(reader, None, &mut section_offsets).ok()?; - get_section_metadata(reader, None, SHT_SYMTAB, &mut sections).ok()?; - get_section_metadata(reader, None, SHT_DYNSYM, &mut sections).ok()?; + /// Finds the value (`st_value`) of a named ELF symbol in `.symtab` or `.dynsym`. + /// + /// Unlike the function-symbol iterator this matches symbols of any type (e.g. + /// `runtime.text`, which is not `STT_FUNC`). Returns the first match. + fn find_symbol_value(reader: &mut (impl Read + Seek), name: &str) -> Option { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); - let target = name.as_bytes(); - let mut name_buf = vec![0u8; target.len() + 1]; + get_section_offsets(reader, None, &mut section_offsets).ok()?; + get_section_metadata(reader, None, SHT_SYMTAB, &mut sections).ok()?; + get_section_metadata(reader, None, SHT_DYNSYM, &mut sections).ok()?; - for section in §ions { - if section.entry_size == 0 { - continue; - } - let strtab_off = match section_offsets.get(section.link as usize) { - Some(off) => *off, - None => continue, - }; + let target = name.as_bytes(); + let mut name_buf = vec![0u8; target.len() + 1]; - let count = section.size / section.entry_size; - for i in 0..count { - let entry_pos = section.offset + i * section.entry_size; - if reader.seek(SeekFrom::Start(entry_pos)).is_err() { - break; + for section in §ions { + if section.entry_size == 0 { + continue; } + let strtab_off = match section_offsets.get(section.link as usize) { + Some(off) => *off, + None => continue, + }; - // st_name is the first 4 bytes in both ELF32 and ELF64. - let mut name_off_buf = [0u8; 4]; - if reader.read_exact(&mut name_off_buf).is_err() { - break; - } - let st_name = u32::from_le_bytes(name_off_buf) as u64; + let count = section.size / section.entry_size; + for i in 0..count { + let entry_pos = section.offset + i * section.entry_size; + if reader.seek(SeekFrom::Start(entry_pos)).is_err() { + break; + } - // st_value: ELF64 at entry+8 (u64); ELF32 at entry+4 (u32). - let st_value = match section.class { - ELFCLASS64 => { - if reader.seek(SeekFrom::Start(entry_pos + 8)).is_err() { - break; - } - let mut v = [0u8; 8]; - if reader.read_exact(&mut v).is_err() { - break; - } - u64::from_le_bytes(v) + // st_name is the first 4 bytes in both ELF32 and ELF64. + let mut name_off_buf = [0u8; 4]; + if reader.read_exact(&mut name_off_buf).is_err() { + break; } - _ => { - if reader.seek(SeekFrom::Start(entry_pos + 4)).is_err() { - break; + let st_name = u32::from_le_bytes(name_off_buf) as u64; + + // st_value: ELF64 at entry+8 (u64); ELF32 at entry+4 (u32). + let st_value = match section.class { + ELFCLASS64 => { + if reader.seek(SeekFrom::Start(entry_pos + 8)).is_err() { + break; + } + let mut v = [0u8; 8]; + if reader.read_exact(&mut v).is_err() { + break; + } + u64::from_le_bytes(v) } - let mut v = [0u8; 4]; - if reader.read_exact(&mut v).is_err() { - break; + _ => { + if reader.seek(SeekFrom::Start(entry_pos + 4)).is_err() { + break; + } + let mut v = [0u8; 4]; + if reader.read_exact(&mut v).is_err() { + break; + } + u32::from_le_bytes(v) as u64 } - u32::from_le_bytes(v) as u64 - } - }; + }; - // Compare the name (exact length, NUL-terminated). - if reader.seek(SeekFrom::Start(strtab_off + st_name)).is_err() { - continue; - } - if reader.read_exact(&mut name_buf).is_err() { - continue; - } - if name_buf[target.len()] == 0 && &name_buf[..target.len()] == target { - return Some(st_value); + // Compare the name (exact length, NUL-terminated). + if reader.seek(SeekFrom::Start(strtab_off + st_name)).is_err() { + continue; + } + if reader.read_exact(&mut name_buf).is_err() { + continue; + } + if name_buf[target.len()] == 0 && &name_buf[..target.len()] == target { + return Some(st_value); + } } } - } - None -} - -/// Returns true if the ELF file carries a Go build-info marker section. -/// -/// `.go.buildinfo` (and `.note.go.buildid`) are allocated and survive `strip`, -/// so they remain even when the `.gopclntab` section header has been removed. -/// This is used as a cheap gate before attempting the (more expensive) scan to -/// recover a stripped line table: non-Go binaries lack these markers and never -/// pay for the scan. -pub(crate) fn has_go_build_info( - reader: &mut (impl Read + Seek)) -> bool { - let mut sections = Vec::new(); - let mut section_offsets = Vec::new(); - - if enum_section_metadata(reader, None, None, &mut sections).is_err() { - return false; + None } - if get_section_offsets(reader, None, &mut section_offsets).is_err() { - return false; - } - - find_section_by_name(reader, §ions, §ion_offsets, ".go.buildinfo").is_some() - || find_section_by_name(reader, §ions, §ion_offsets, ".note.go.buildid").is_some() -} -/// Magic prefix bytes shared by the supported pclntab versions: the low byte -/// (`0xf0`/`0xf1`) varies, the upper three bytes are `0xff`. -const PCLNTAB_MAGIC_GO118: [u8; 4] = [0xf0, 0xff, 0xff, 0xff]; -const PCLNTAB_MAGIC_GO120: [u8; 4] = [0xf1, 0xff, 0xff, 0xff]; - -/// Recovers a Go line table whose `.gopclntab` section header has been stripped. -/// -/// Scans the read-only `PT_LOAD` segments for a supported pclntab magic, -/// validates and parses each candidate, and returns the first table that parses -/// cleanly together with its resolved `runtime.text`. Only 64-bit ELF is -/// supported; returns `None` otherwise (callers fall back to ELF symbols). -/// -/// Callers should gate this behind [`has_go_build_info`] and the absence of a -/// `.gopclntab` section so non-Go binaries never incur the scan. -pub(crate) fn recover_go_pclntab( - reader: &mut (impl Read + Seek)) -> Option<(Vec, u64)> { - use crate::ruwind::go_pclntab::GoPclnTab; - - // Determine class; only 64-bit recovery is supported. - reader.seek(SeekFrom::Start(0)).ok()?; - let ident = get_ident(reader).ok()?; - if ident[..4] != ELF_MAGIC || ident[EI_CLASS] != ELFCLASS64 { - return None; - } - - // Read the ELF header for program-header table location. The ElfHeader64 - // struct begins after the 16-byte e_ident, so seek past it first. - reader.seek(SeekFrom::Start(16)).ok()?; - let mut header = ElfHeader64::default(); - unsafe { - reader.read_exact( - slice::from_raw_parts_mut( - &mut header as *mut _ as *mut u8, - size_of::())).ok()?; - } + /// Returns true if the ELF file carries a Go build-info marker section. + /// + /// `.go.buildinfo` (and `.note.go.buildid`) are allocated and survive `strip`, + /// so they remain even when the `.gopclntab` section header has been removed. + /// This is used as a cheap gate before attempting the (more expensive) scan to + /// recover a stripped line table: non-Go binaries lack these markers and never + /// pay for the scan. + pub(crate) fn has_go_build_info(reader: &mut (impl Read + Seek)) -> bool { + if !go_elf_section_count_allowed(reader) { + return false; + } - // Optional .text address (fallback for text_start resolution). - let text_vaddr = { let mut sections = Vec::new(); let mut section_offsets = Vec::new(); - enum_section_metadata(reader, None, None, &mut sections).ok(); - get_section_offsets(reader, None, &mut section_offsets).ok(); - find_section_by_name(reader, §ions, §ion_offsets, ".text").map(|s| s.address) - }; - - // Collect read-only PT_LOAD segments (where the line table lives). - let mut segments: Vec<(u64, u64, u64)> = Vec::new(); // (p_offset, p_filesz, p_vaddr) - let mut ph_offset = header.e_phoff; - for _ in 0..header.e_phnum { - reader.seek(SeekFrom::Start(ph_offset)).ok()?; - let mut ph = ElfProgramHeader64::default(); - if get_program_header64(reader, &mut ph).is_err() { - break; + + if enum_section_metadata(reader, None, None, &mut sections).is_err() { + return false; } - ph_offset += header.e_phentsize as u64; - - // Loadable, non-executable segment (the line table is read-only data, - // but on PIE binaries it lives in a writable data-rel-ro segment, so we - // only exclude executable segments here). - if ph.p_type == PT_LOAD - && (ph.p_flags & PF_X) == 0 - && ph.p_filesz > 0 { - segments.push((ph.p_offset, ph.p_filesz, ph.p_vaddr)); + if get_section_offsets(reader, None, &mut section_offsets).is_err() { + return false; } + + find_section_by_name(reader, §ions, §ion_offsets, ".go.buildinfo").is_some() + || find_section_by_name(reader, §ions, §ion_offsets, ".note.go.buildid") + .is_some() + } + + /// Magic prefix bytes shared by the supported pclntab versions: the low byte + /// (`0xf0`/`0xf1`) varies, the upper three bytes are `0xff`. + const PCLNTAB_MAGIC_GO118: [u8; 4] = [0xf0, 0xff, 0xff, 0xff]; + const PCLNTAB_MAGIC_GO120: [u8; 4] = [0xf1, 0xff, 0xff, 0xff]; + + /// Recovers a Go line table whose `.gopclntab` section header has been stripped. + /// + /// Scans the read-only `PT_LOAD` segments for a supported pclntab magic, + /// validates and parses each candidate, and returns the first table that parses + /// cleanly together with its resolved `runtime.text`. Only 64-bit ELF is + /// supported; returns `None` otherwise (callers fall back to ELF symbols). + /// + /// Callers should gate this behind [`has_go_build_info`] and the absence of a + /// `.gopclntab` section so non-Go binaries never incur the scan. + pub(crate) fn recover_go_pclntab( + reader: &mut (impl Read + Seek), + ) -> Option { + recover_go_pclntab_with_chunk(reader, 64 * 1024) } - for (seg_offset, seg_filesz, seg_vaddr) in segments { - let mut bytes = vec![0u8; seg_filesz as usize]; - if reader.seek(SeekFrom::Start(seg_offset)).is_err() { - continue; + pub(crate) fn recover_go_pclntab_with_chunk( + reader: &mut (impl Read + Seek), + scan_chunk_size: usize, + ) -> Option { + if scan_chunk_size < 8 { + return None; + } + + // Determine class; only 64-bit recovery is supported. + reader.seek(SeekFrom::Start(0)).ok()?; + let ident = get_ident(reader).ok()?; + if ident[..4] != ELF_MAGIC || ident[EI_CLASS] != ELFCLASS64 { + return None; + } + + // Read the ELF header for program-header table location. The ElfHeader64 + // struct begins after the 16-byte e_ident, so seek past it first. + reader.seek(SeekFrom::Start(16)).ok()?; + let mut header = ElfHeader64::default(); + unsafe { + reader + .read_exact(slice::from_raw_parts_mut( + &mut header as *mut _ as *mut u8, + size_of::(), + )) + .ok()?; } - if reader.read_exact(&mut bytes).is_err() { - continue; + + // Optional .text address (fallback for text_start resolution). + let text_vaddr = if go_elf_section_count_allowed(reader) { + let mut sections = Vec::new(); + let mut section_offsets = Vec::new(); + enum_section_metadata(reader, None, None, &mut sections).ok(); + get_section_offsets(reader, None, &mut section_offsets).ok(); + find_section_by_name(reader, §ions, §ion_offsets, ".text").map(|s| s.address) + } else { + None + }; + + let mut ph_offset = header.e_phoff; + for _ in 0..header.e_phnum { + reader.seek(SeekFrom::Start(ph_offset)).ok()?; + let mut ph = ElfProgramHeader64::default(); + if get_program_header64(reader, &mut ph).is_err() { + break; + } + ph_offset += header.e_phentsize as u64; + + // Loadable, non-executable segment (the line table is read-only data, + // but on PIE binaries it lives in a writable data-rel-ro segment, so we + // only exclude executable segments here). + if ph.p_type == PT_LOAD && (ph.p_flags & PF_X) == 0 && ph.p_filesz > 0 { + if let Some(location) = scan_go_pclntab_segment( + reader, + ph.p_offset, + ph.p_filesz, + ph.p_vaddr, + text_vaddr, + scan_chunk_size, + ) { + return Some(location); + } + } } - // Scan for a candidate magic. - let mut i = 0usize; - while i + 8 <= bytes.len() { - let b = bytes[i]; - if (b == 0xf0 || b == 0xf1) - && bytes[i..i + 4] == (if b == 0xf0 { PCLNTAB_MAGIC_GO118 } else { PCLNTAB_MAGIC_GO120 }) - // Header bytes 4,5 must be zero; minLC and ptrSize must be sane. - && bytes[i + 4] == 0 - && bytes[i + 5] == 0 - && matches!(bytes[i + 6], 1 | 2 | 4) - && matches!(bytes[i + 7], 4 | 8) { - - let candidate_vaddr = seg_vaddr + i as u64; - let slice = bytes[i..].to_vec(); - let text_start = resolve_go_text_start(reader, &slice, Some(candidate_vaddr), text_vaddr) + None + } + + fn scan_go_pclntab_segment( + reader: &mut (impl Read + Seek), + seg_offset: u64, + seg_filesz: u64, + seg_vaddr: u64, + text_vaddr: Option, + scan_chunk_size: usize, + ) -> Option { + const SCAN_OVERLAP: usize = 7; + + let seg_end = seg_offset.checked_add(seg_filesz)?; + let mut bytes = vec![0u8; scan_chunk_size.checked_add(SCAN_OVERLAP)?]; + let mut read_offset = seg_offset; + let mut carry = 0usize; + + while read_offset < seg_end { + let read_len = + usize::try_from((seg_end - read_offset).min(scan_chunk_size as u64)).ok()?; + if reader.seek(SeekFrom::Start(read_offset)).is_err() + || reader + .read_exact(&mut bytes[carry..carry + read_len]) + .is_err() + { + return None; + } + + let buffer_start = read_offset.checked_sub(carry as u64)?; + let buffer_len = carry + read_len; + let mut i = 0usize; + while i + 8 <= buffer_len { + let b = bytes[i]; + if (b == 0xf0 || b == 0xf1) + && bytes[i..i + 4] + == (if b == 0xf0 { + PCLNTAB_MAGIC_GO118 + } else { + PCLNTAB_MAGIC_GO120 + }) + && bytes[i + 4] == 0 + && bytes[i + 5] == 0 + && matches!(bytes[i + 6], 1 | 2 | 4) + && matches!(bytes[i + 7], 4 | 8) + { + let candidate_offset = buffer_start.checked_add(i as u64)?; + let candidate_delta = candidate_offset.checked_sub(seg_offset)?; + let candidate_vaddr = seg_vaddr.checked_add(candidate_delta)?; + let max_len = seg_end.checked_sub(candidate_offset)?; + let header_len = usize::try_from( + max_len.min(crate::ruwind::go_pclntab::GO_PCLNTAB_HEADER_MAX as u64), + ) + .ok()?; + let mut header = [0u8; crate::ruwind::go_pclntab::GO_PCLNTAB_HEADER_MAX]; + reader.seek(SeekFrom::Start(candidate_offset)).ok()?; + reader.read_exact(&mut header[..header_len]).ok()?; + + let text_start = resolve_go_text_start( + reader, + &header[..header_len], + Some(candidate_vaddr), + text_vaddr, + ) .unwrap_or(0); + let location = crate::ruwind::go_pclntab::GoPclnTabLocation::new( + candidate_offset, + max_len, + text_start, + ); - if let Some(tab) = GoPclnTab::parse(slice, text_start) { - // Sanity-check: a real table has many functions whose first - // and last entries decode to non-empty names. - if tab.len() > 16 - && tab.func(0).map(|(_, _, n)| !n.is_empty()).unwrap_or(false) - && tab.func(tab.len() - 1).map(|(_, _, n)| !n.is_empty()).unwrap_or(false) { + if crate::ruwind::go_pclntab::validate_go_pclntab_location(reader, &location) { debug!( - "recover_go_pclntab: recovered {} funcs at vaddr={:#x} text_start={:#x}", - tab.len(), candidate_vaddr, text_start); - // Return the recovered table bytes; parse consumed the - // first copy, so produce a fresh one for the caller. - return Some((bytes[i..].to_vec(), text_start)); + "recover_go_pclntab: recovered table at vaddr={:#x} text_start={:#x}", + candidate_vaddr, text_start + ); + return Some(location); } } + i += 1; } - i += 1; + + carry = buffer_len.min(SCAN_OVERLAP); + bytes.copy_within(buffer_len - carry..buffer_len, 0); + read_offset = read_offset.checked_add(read_len as u64)?; } - } - None + None + } } +#[cfg(test)] +use go::{go_elf_section_count_allowed, GO_ELF_SECTION_LIMIT}; +pub(crate) use go::{ + has_go_build_info, has_go_pclntab, read_go_pclntab, recover_go_pclntab, +}; +#[cfg(test)] +pub(crate) use go::recover_go_pclntab_with_chunk; + pub fn get_load_header( reader: &mut (impl Read + Seek)) -> Result { reader.seek(SeekFrom::Start(0))?; @@ -1684,6 +1866,23 @@ mod tests { #[cfg(target_os = "linux")] use std::fs::File; + #[test] + #[cfg(target_os = "linux")] + fn go_section_limit_handles_extended_numbering() { + let path = std::env::current_dir() + .unwrap() + .join("../test/assets/go/symbol_benchmark_go"); + let mut bytes = std::fs::read(path).expect("read Go fixture"); + let section_offset = u64::from_le_bytes(bytes[40..48].try_into().unwrap()) as usize; + + bytes[60..62].copy_from_slice(&0u16.to_le_bytes()); + bytes[section_offset + 32..section_offset + 40] + .copy_from_slice(&((GO_ELF_SECTION_LIMIT + 1) as u64).to_le_bytes()); + + let mut reader = std::io::Cursor::new(bytes); + assert!(!go_elf_section_count_allowed(&mut reader)); + } + #[test] #[cfg(target_os = "linux")] fn symbols() { diff --git a/one_collect/src/ruwind/go_pclntab.rs b/one_collect/src/ruwind/go_pclntab.rs index b378d597..52144e19 100644 --- a/one_collect/src/ruwind/go_pclntab.rs +++ b/one_collect/src/ruwind/go_pclntab.rs @@ -16,11 +16,14 @@ //! magic `0xfffffff0`) and Go 1.20+ (`ver120`, magic `0xfffffff1`). These share //! an identical on-disk layout and differ only by magic; one parser serves //! every Go release from 1.18 through current. Any other / older / unknown -//! magic causes [`GoPclnTab::parse`] to return `None` so the caller can fall +//! magic causes [`GoPclnTab::open`] to return `None` so the caller can fall //! back to ELF/PE symbol tables — it never panics. //! //! The field math mirrors Go's own `debug/gosym` package. +use std::fs::File; +use std::io::{Cursor, Error, ErrorKind, Read, Seek, SeekFrom}; + use tracing::debug; /// Magic for the Go 1.18/1.19 pclntab layout. @@ -28,84 +31,257 @@ const MAGIC_GO118: u32 = 0xfffffff0; /// Magic for the Go 1.20+ pclntab layout. const MAGIC_GO120: u32 = 0xfffffff1; -/// A parsed Go line table, retaining the raw blob for lazy name lookups. -pub(crate) struct GoPclnTab { - data: Vec, +pub(crate) const GO_PCLNTAB_HEADER_MAX: usize = 8 + 8 * 8; +pub(crate) const GO_PCLNTAB_BUFFER_SIZE: usize = 64 * 1024; +pub(crate) const GO_SYMBOL_NAME_MAX: usize = 4 * 1024; + +pub(crate) trait GoPclnTabRead: Read + Seek { + fn read_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error>; + + fn read_func_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.read_exact_at(offset, output) + } + + fn read_name_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.read_exact_at(offset, output) + } +} + +impl GoPclnTabRead for &mut T { + fn read_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.seek(SeekFrom::Start(offset))?; + self.read_exact(output) + } +} + +impl GoPclnTabRead for Cursor> { + fn read_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + let start = usize::try_from(offset) + .map_err(|_| Error::new(ErrorKind::InvalidInput, "offset exceeds usize"))?; + let end = start + .checked_add(output.len()) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "read range overflow"))?; + let input = self + .get_ref() + .get(start..end) + .ok_or_else(|| Error::new(ErrorKind::UnexpectedEof, "read past input"))?; + output.copy_from_slice(input); + Ok(()) + } +} + +pub(crate) struct GoPclnTabFileReader { + file: File, + functab: ReadWindow, + funcs: ReadWindow, + names: ReadWindow, +} + +impl GoPclnTabFileReader { + pub(crate) fn new(file: File, table_offset: u64) -> Self { + Self { + file, + functab: ReadWindow::new(GO_PCLNTAB_BUFFER_SIZE, table_offset, false), + funcs: ReadWindow::new(GO_PCLNTAB_BUFFER_SIZE, table_offset, false), + names: ReadWindow::new(GO_PCLNTAB_BUFFER_SIZE, table_offset, true), + } + } +} + +impl Read for GoPclnTabFileReader { + fn read(&mut self, output: &mut [u8]) -> Result { + self.file.read(output) + } +} + +impl Seek for GoPclnTabFileReader { + fn seek(&mut self, position: SeekFrom) -> Result { + self.file.seek(position) + } +} + +impl GoPclnTabRead for GoPclnTabFileReader { + fn read_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.functab.read(&self.file, offset, output) + } + + fn read_func_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.funcs.read(&self.file, offset, output) + } + + fn read_name_exact_at(&mut self, offset: u64, output: &mut [u8]) -> Result<(), Error> { + self.names.read(&self.file, offset, output) + } +} + +struct ReadWindow { + data: Box<[u8]>, + base: u64, + align: bool, + start: u64, + len: usize, + #[cfg(test)] + refills: usize, +} + +impl ReadWindow { + fn new(capacity: usize, base: u64, align: bool) -> Self { + Self { + data: vec![0; capacity].into_boxed_slice(), + base, + align, + start: 0, + len: 0, + #[cfg(test)] + refills: 0, + } + } + + fn read(&mut self, file: &File, mut offset: u64, mut output: &mut [u8]) -> Result<(), Error> { + while !output.is_empty() { + let window_end = self.start + self.len as u64; + if offset < self.start || offset >= window_end { + #[cfg(test)] + { + self.refills += 1; + } + let capacity = self.data.len() as u64; + self.start = if self.align { + let relative = offset + .checked_sub(self.base) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "read before table"))?; + self.base + relative / capacity * capacity + } else { + offset + }; + self.len = 0; + while self.len < self.data.len() { + let read = read_file_at( + file, + &mut self.data[self.len..], + self.start + self.len as u64, + )?; + if read == 0 { + break; + } + self.len += read; + } + } + + let start = usize::try_from(offset - self.start) + .map_err(|_| Error::new(ErrorKind::InvalidInput, "offset exceeds usize"))?; + let available = self + .len + .checked_sub(start) + .ok_or_else(|| Error::new(ErrorKind::UnexpectedEof, "positioned read"))?; + let copy_len = available.min(output.len()); + if copy_len == 0 { + return Err(Error::new(ErrorKind::UnexpectedEof, "positioned read")); + } + output[..copy_len].copy_from_slice(&self.data[start..start + copy_len]); + offset = offset + .checked_add(copy_len as u64) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "read range overflow"))?; + output = &mut output[copy_len..]; + } + Ok(()) + } +} + +#[cfg(unix)] +fn read_file_at(file: &File, output: &mut [u8], offset: u64) -> Result { + std::os::unix::fs::FileExt::read_at(file, output, offset) +} + +#[cfg(windows)] +fn read_file_at(file: &File, output: &mut [u8], offset: u64) -> Result { + std::os::windows::fs::FileExt::seek_read(file, output, offset) +} + +#[derive(Clone, Copy)] +pub(crate) struct GoPclnTabLocation { + file_offset: u64, + max_len: u64, + text_start: u64, +} + +impl GoPclnTabLocation { + pub(crate) fn new(file_offset: u64, max_len: u64, text_start: u64) -> Self { + Self { + file_offset, + max_len, + text_start, + } + } + + pub(crate) fn file_offset(&self) -> u64 { + self.file_offset + } + + pub(crate) fn max_len(&self) -> u64 { + self.max_len + } + + pub(crate) fn text_start(&self) -> u64 { + self.text_start + } +} + +#[derive(Clone, Copy)] +struct GoPclnTabMetadata { little_endian: bool, text_start: u64, - /// Number of functions in the table (the functab has `nfunc + 1` entries). nfunc: usize, - /// Offset within `data` of the function-name string table. - funcname_off: usize, - /// Offset within `data` of the functab (PC, funcoff pairs) and func structs. - functab_off: usize, + funcname_off: u64, + functab_off: u64, } -impl GoPclnTab { - /// Attempts to parse `data` as a supported (Go 1.18+) pclntab. - /// - /// `text_start` is the virtual address of the runtime text (the `.text` - /// section address). Returns `None` for any unsupported magic or malformed - /// header rather than panicking, so callers can fall back gracefully. - pub(crate) fn parse( - data: Vec, - text_start: u64) -> Option { - // Need at least the fixed 8-byte header prefix. +impl GoPclnTabMetadata { + fn parse_header(data: &[u8], max_len: u64, text_start: u64) -> Option { if data.len() < 8 { return None; } - // Detect magic + endianness. let le_magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]); let be_magic = u32::from_be_bytes([data[0], data[1], data[2], data[3]]); - let little_endian = match (le_magic, be_magic) { (MAGIC_GO118, _) | (MAGIC_GO120, _) => true, (_, MAGIC_GO118) | (_, MAGIC_GO120) => false, - _ => { - debug!("gopclntab: unsupported magic le={:#x} be={:#x}", le_magic, be_magic); - return None; - } + _ => return None, }; - // Bytes 4 and 5 must be zero in a valid header. if data[4] != 0 || data[5] != 0 { return None; } - let _min_lc = data[6]; let ptr_size = data[7] as usize; if ptr_size != 4 && ptr_size != 8 { return None; } - // Header words are ptr_size-wide integers starting at offset 8. - // word 0 = nfunc, word 2 = textStart, word 3 = funcnameOffset, - // word 7 = pclnOffset (functab + func structs). - let read_word = |w: usize| -> Option { - let pos = 8 + w * ptr_size; - read_uint(&data, pos, ptr_size, little_endian) + let read_word = |word: usize| -> Option { + let pos = 8usize.checked_add(word.checked_mul(ptr_size)?)?; + read_uint(data, pos, ptr_size, little_endian) }; - let nfunc = read_word(0)? as usize; - let funcname_off = read_word(3)? as usize; - let functab_off = read_word(7)? as usize; + let nfunc = usize::try_from(read_word(0)?).ok()?; + let funcname_off = read_word(3)?; + let functab_off = read_word(7)?; - // Sanity-check offsets are within the blob. - if funcname_off >= data.len() || functab_off >= data.len() { + if funcname_off >= max_len || functab_off >= max_len { return None; } - // The functab holds (nfunc + 1) pairs of 4-byte fields (Go 1.18+). - let functab_bytes = (nfunc + 1) + let functab_bytes = u64::try_from(nfunc) + .ok()? + .checked_add(1)? .checked_mul(2)? - .checked_mul(FUNCTAB_FIELD_SIZE)?; - if functab_off.checked_add(functab_bytes)? > data.len() { + .checked_mul(FUNCTAB_FIELD_SIZE as u64)?; + if functab_off.checked_add(functab_bytes)? > max_len { return None; } - Some(GoPclnTab { - data, + Some(Self { little_endian, text_start, nfunc, @@ -113,59 +289,299 @@ impl GoPclnTab { functab_off, }) } +} + +/// Incremental Go line-table cursor over any seekable reader. +/// +/// The reader supplies buffering. The cursor itself keeps only parsed header +/// metadata, sequential iteration state, and one bounded symbol-name buffer. +pub(crate) struct GoPclnTab { + reader: R, + location: GoPclnTabLocation, + metadata: GoPclnTabMetadata, + index: usize, + next_entry: Option, + current_start: u64, + current_end: u64, + current_name: [u8; GO_SYMBOL_NAME_MAX], + current_name_len: usize, +} + +impl GoPclnTab { + pub(crate) fn open(mut reader: R, location: GoPclnTabLocation) -> Option { + let metadata = read_metadata(&mut reader, &location)?; + Some(Self { + reader, + location, + metadata, + index: 0, + next_entry: None, + current_start: 0, + current_end: 0, + current_name: [0; GO_SYMBOL_NAME_MAX], + current_name_len: 0, + }) + } - /// Number of functions described by the table. pub(crate) fn len(&self) -> usize { - self.nfunc + self.metadata.nfunc } - /// Returns true if the table describes no functions. - pub(crate) fn is_empty(&self) -> bool { - self.nfunc == 0 + pub(crate) fn reset(&mut self) { + self.index = 0; + self.next_entry = None; + self.current_name_len = 0; } - /// Returns the `i`th function as `(start_va, end_va, name)`. - /// - /// `start_va`/`end_va` are virtual addresses (text_start relative). `end_va` - /// is the entry of the next function (the functab's trailing sentinel for - /// the final function), matching how Go derives function extents. Returns - /// `None` if `i` is out of range or the entry is malformed. - pub(crate) fn func(&self, i: usize) -> Option<(u64, u64, &str)> { - if i >= self.nfunc { - return None; + pub(crate) fn next(&mut self) -> bool { + while self.index < self.metadata.nfunc { + let index = self.index; + self.index += 1; + if self.read_func(index, true) { + return true; + } } + self.current_name_len = 0; + false + } - let entry_off = self.functab_field(2 * i)?; - let next_off = self.functab_field(2 * (i + 1))?; - let func_off = self.functab_field(2 * i + 1)? as usize; + pub(crate) fn start(&self) -> u64 { + self.current_start + } - let start = self.text_start.checked_add(entry_off)?; - let end = self.text_start.checked_add(next_off)?; + pub(crate) fn end(&self) -> u64 { + self.current_end + } - // The _func struct lives at functab_off + func_off. For Go 1.18+ the - // first field is a uint32 entry offset, so nameoff (field 1) is the - // uint32 at _func + 4. - let func_struct = self.functab_off.checked_add(func_off)?; - let name_off = read_u32(&self.data, func_struct.checked_add(4)?, self.little_endian)? as usize; + pub(crate) fn name(&self) -> &str { + std::str::from_utf8(&self.current_name[..self.current_name_len]).unwrap_or("") + } - let name = self.name_at(self.funcname_off.checked_add(name_off)?)?; + fn read_func(&mut self, index: usize, sequential: bool) -> bool { + if index >= self.metadata.nfunc { + return false; + } + + let entry_off = match if sequential { + self.next_entry.take() + } else { + None + } { + Some(entry) => entry, + None => match index + .checked_mul(2) + .and_then(|field| functab_field_offset(self.metadata.functab_off, field)) + .and_then(|offset| self.read_u32(offset)) + { + Some(entry) => entry as u64, + None => return false, + }, + }; + let pair_offset = match index + .checked_mul(2) + .and_then(|field| field.checked_add(1)) + .and_then(|field| functab_field_offset(self.metadata.functab_off, field)) + { + Some(offset) => offset, + None => return false, + }; + let (func_off, next_off) = match self.read_u32_pair(pair_offset) { + Some(pair) => pair, + None => return false, + }; + let func_off = func_off as u64; + let next_off = next_off as u64; + if sequential { + self.next_entry = Some(next_off); + } + + let func_struct = match self.metadata.functab_off.checked_add(func_off) { + Some(offset) => offset, + None => return false, + }; + let name_field = match func_struct.checked_add(4) { + Some(offset) => offset, + None => return false, + }; + let name_off = match self.read_func_u32(name_field) { + Some(offset) => offset as u64, + None => return false, + }; + + let start = match self.metadata.text_start.checked_add(entry_off) { + Some(start) => start, + None => return false, + }; + let end = match self.metadata.text_start.checked_add(next_off) { + Some(end) => end, + None => return false, + }; + let name_offset = match self.metadata.funcname_off.checked_add(name_off) { + Some(offset) => offset, + None => return false, + }; + if !self.read_name(name_offset) { + return false; + } + self.current_start = start; + self.current_end = end; + true + } + + fn read_u32(&mut self, relative_offset: u64) -> Option { + let mut bytes = [0u8; 4]; + self.read_exact_at(relative_offset, &mut bytes)?; + Some(if self.metadata.little_endian { + u32::from_le_bytes(bytes) + } else { + u32::from_be_bytes(bytes) + }) + } + + fn read_u32_pair(&mut self, relative_offset: u64) -> Option<(u32, u32)> { + let mut bytes = [0u8; 8]; + self.read_exact_at(relative_offset, &mut bytes)?; + Some(if self.metadata.little_endian { + ( + u32::from_le_bytes(bytes[..4].try_into().ok()?), + u32::from_le_bytes(bytes[4..].try_into().ok()?), + ) + } else { + ( + u32::from_be_bytes(bytes[..4].try_into().ok()?), + u32::from_be_bytes(bytes[4..].try_into().ok()?), + ) + }) + } + + fn read_func_u32(&mut self, relative_offset: u64) -> Option { + let mut bytes = [0u8; 4]; + self.read_func_exact_at(relative_offset, &mut bytes)?; + Some(if self.metadata.little_endian { + u32::from_le_bytes(bytes) + } else { + u32::from_be_bytes(bytes) + }) + } + + fn read_name(&mut self, relative_offset: u64) -> bool { + if relative_offset >= self.location.max_len { + return false; + } + let max_len = usize::try_from( + (self.location.max_len - relative_offset).min(GO_SYMBOL_NAME_MAX as u64), + ) + .unwrap_or(0); + if max_len == 0 { + return false; + } + + let mut name_len = 0; + let mut buffer = [0u8; 256]; + while name_len < max_len { + let read_len = buffer.len().min(max_len - name_len); + let offset = match self + .location + .file_offset + .checked_add(relative_offset) + .and_then(|offset| offset.checked_add(name_len as u64)) + { + Some(offset) => offset, + None => return false, + }; + if self + .reader + .read_name_exact_at(offset, &mut buffer[..read_len]) + .is_err() + { + return false; + } + if let Some(end) = buffer[..read_len].iter().position(|byte| *byte == 0) { + self.current_name[name_len..name_len + end].copy_from_slice(&buffer[..end]); + self.current_name_len = name_len + end; + return std::str::from_utf8(&self.current_name[..self.current_name_len]).is_ok(); + } + self.current_name[name_len..name_len + read_len].copy_from_slice(&buffer[..read_len]); + name_len += read_len; + } + debug!( + "gopclntab: symbol name exceeds {} bytes", + GO_SYMBOL_NAME_MAX + ); + false + } + + fn read_exact_at(&mut self, relative_offset: u64, output: &mut [u8]) -> Option<()> { + let len = u64::try_from(output.len()).ok()?; + if relative_offset.checked_add(len)? > self.location.max_len { + return None; + } + let absolute = self.location.file_offset.checked_add(relative_offset)?; + self.reader.read_exact_at(absolute, output).ok() + } + + fn read_func_exact_at(&mut self, relative_offset: u64, output: &mut [u8]) -> Option<()> { + let len = u64::try_from(output.len()).ok()?; + if relative_offset.checked_add(len)? > self.location.max_len { + return None; + } + let absolute = self.location.file_offset.checked_add(relative_offset)?; + self.reader.read_func_exact_at(absolute, output).ok() + } - Some((start, end, name)) + #[cfg(test)] + fn func(&mut self, index: usize) -> Option<(u64, u64, &str)> { + self.read_func(index, false) + .then(|| (self.current_start, self.current_end, self.name())) } +} - /// Reads functab field `idx` (a 4-byte value), adding `text_start` is left - /// to the caller. Returns the raw offset value. - fn functab_field(&self, idx: usize) -> Option { - let pos = self.functab_off.checked_add(idx.checked_mul(FUNCTAB_FIELD_SIZE)?)?; - read_u32(&self.data, pos, self.little_endian).map(|v| v as u64) +#[cfg(test)] +impl GoPclnTab>> { + fn parse(data: Vec, text_start: u64) -> Option { + let len = data.len() as u64; + Self::open( + Cursor::new(data), + GoPclnTabLocation::new(0, len, text_start), + ) } +} + +fn read_metadata( + reader: &mut (impl Read + Seek), + location: &GoPclnTabLocation, +) -> Option { + let header_len = usize::try_from(location.max_len.min(GO_PCLNTAB_HEADER_MAX as u64)).ok()?; + let mut header = [0u8; GO_PCLNTAB_HEADER_MAX]; + reader.seek(SeekFrom::Start(location.file_offset)).ok()?; + reader.read_exact(&mut header[..header_len]).ok()?; + GoPclnTabMetadata::parse_header(&header[..header_len], location.max_len, location.text_start) +} - /// Returns the NUL-terminated UTF-8 string starting at `off` within `data`. - fn name_at(&self, off: usize) -> Option<&str> { - let bytes = self.data.get(off..)?; - let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len()); - std::str::from_utf8(&bytes[..end]).ok() +pub(crate) fn validate_go_pclntab_location( + reader: &mut (impl Read + Seek), + location: &GoPclnTabLocation, +) -> bool { + let mut tab = match GoPclnTab::open(reader, *location) { + Some(tab) => tab, + None => return false, + }; + if tab.len() <= 16 { + return false; } + + tab.read_func(0, false) + && tab.current_end >= tab.current_start + && tab.read_func(tab.len() - 1, false) + && tab.current_end >= tab.current_start +} + +fn functab_field_offset(functab_off: u64, field_index: usize) -> Option { + functab_off.checked_add( + u64::try_from(field_index) + .ok()? + .checked_mul(FUNCTAB_FIELD_SIZE as u64)?, + ) } /// Functab field width for Go 1.18+ layouts (uint32 offsets). @@ -213,6 +629,7 @@ fn read_u64( #[cfg(test)] mod tests { use super::*; + use std::io::Write; /// Builds a minimal but valid Go 1.18+ pclntab for the given functions. /// Each function is `(entry_offset, name)`; the table is laid out as: @@ -316,7 +733,7 @@ mod tests { let funcs = [(0x1000u32, "main.main"), (0x1100u32, "runtime.mcall")]; let blob = build_pclntab(MAGIC_GO120, 8, true, text, &funcs, 0x1200); - let tab = GoPclnTab::parse(blob, text).expect("should parse"); + let mut tab = GoPclnTab::parse(blob, text).expect("should parse"); assert_eq!(tab.len(), 2); let (s0, e0, n0) = tab.func(0).unwrap(); @@ -337,7 +754,7 @@ mod tests { let text = 0x10000u64; let funcs = [(0u32, "foo.Bar")]; let blob = build_pclntab(MAGIC_GO118, 8, true, text, &funcs, 0x40); - let tab = GoPclnTab::parse(blob, text).expect("should parse"); + let mut tab = GoPclnTab::parse(blob, text).expect("should parse"); let (s, e, n) = tab.func(0).unwrap(); assert_eq!(s, text); assert_eq!(e, text + 0x40); @@ -349,7 +766,7 @@ mod tests { let text = 0x80000u64; let funcs = [(0x10u32, "pkg.fn")]; let blob = build_pclntab(MAGIC_GO120, 8, false, text, &funcs, 0x20); - let tab = GoPclnTab::parse(blob, text).expect("should parse BE"); + let mut tab = GoPclnTab::parse(blob, text).expect("should parse BE"); let (_, _, n) = tab.func(0).unwrap(); assert_eq!(n, "pkg.fn"); } @@ -361,7 +778,7 @@ mod tests { let name = "type:.eq.runtime\u{00b7}foo"; let funcs = [(0u32, name)]; let blob = build_pclntab(MAGIC_GO120, 8, true, text, &funcs, 0x10); - let tab = GoPclnTab::parse(blob, text).unwrap(); + let mut tab = GoPclnTab::parse(blob, text).unwrap(); let (_, _, n) = tab.func(0).unwrap(); assert_eq!(n, name); } @@ -391,6 +808,106 @@ mod tests { assert!(GoPclnTab::parse(blob, 0).is_none()); } + fn open_file_table( + blob: &[u8], + text_start: u64, + suffix: &str, + ) -> (GoPclnTab, std::path::PathBuf) { + let path = std::env::temp_dir().join(format!( + "one_collect_go_pclntab_{}_{}", + std::process::id(), + suffix + )); + let mut file = File::create(&path).expect("create temporary pclntab"); + file.write_all(blob).expect("write temporary pclntab"); + drop(file); + + let file = File::open(&path).expect("open temporary pclntab"); + let location = GoPclnTabLocation::new(0, blob.len() as u64, text_start); + let tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("open file-backed pclntab"); + (tab, path) + } + + #[test] + fn file_reader_large_table_is_bounded() { + const FUNCTION_COUNT: usize = 20_000; + let text = 0x400000u64; + let funcs: Vec<(u32, &str)> = (0..FUNCTION_COUNT) + .map(|index| ((index as u32) * 16, "pkg.function")) + .collect(); + let blob = build_pclntab( + MAGIC_GO120, + 8, + true, + text, + &funcs, + (FUNCTION_COUNT as u32) * 16, + ); + assert!(blob.len() > GO_PCLNTAB_BUFFER_SIZE * 3); + + let (mut tab, path) = open_file_table(&blob, text, "large"); + assert!(GO_PCLNTAB_BUFFER_SIZE * 3 + GO_SYMBOL_NAME_MAX < 256 * 1024); + for index in 0..FUNCTION_COUNT { + let (start, end, name) = tab.func(index).expect("read function"); + assert_eq!(start, text + (index as u64) * 16); + assert_eq!(end, start + 16); + assert_eq!(name, "pkg.function"); + } + assert!(tab.reader.functab.refills < 10); + assert!(tab.reader.funcs.refills < 10); + + std::fs::remove_file(path).expect("remove temporary pclntab"); + } + + #[test] + fn file_reader_bounds_symbol_names() { + let text = 0x400000u64; + let accepted = "a".repeat(GO_SYMBOL_NAME_MAX - 1); + let rejected = "b".repeat(GO_SYMBOL_NAME_MAX); + + for (suffix, name, should_read) in [ + ("max-name", accepted.as_str(), true), + ("over-name", rejected.as_str(), false), + ] { + let funcs = [(0u32, name)]; + let blob = build_pclntab(MAGIC_GO120, 8, true, text, &funcs, 16); + let (mut tab, path) = open_file_table(&blob, text, suffix); + let result = tab.func(0); + assert_eq!(result.is_some(), should_read); + if should_read { + assert_eq!(result.unwrap().2, name); + } + std::fs::remove_file(path).expect("remove temporary pclntab"); + } + } + + #[test] + fn file_reader_supports_endian_and_pointer_layouts() { + let text = 0x400000u64; + for (ptr_size, little_endian, suffix) in [ + (4usize, true, "le32"), + (8usize, true, "le64"), + (4usize, false, "be32"), + (8usize, false, "be64"), + ] { + let funcs = [(0x10u32, "pkg.first"), (0x20u32, "pkg.second")]; + let blob = build_pclntab(MAGIC_GO120, ptr_size, little_endian, text, &funcs, 0x30); + let (mut tab, path) = open_file_table(&blob, text, suffix); + + let first = tab.func(0).expect("read first function"); + assert_eq!(first, (text + 0x10, text + 0x20, "pkg.first")); + + let second = tab.func(1).expect("read second function"); + assert_eq!(second, (text + 0x20, text + 0x30, "pkg.second")); + + std::fs::remove_file(path).expect("remove temporary pclntab"); + } + } + // ----- Committed-fixture integration tests (no network) ----- /// Path to a checked-in Go test fixture under test/assets/go/. @@ -405,24 +922,25 @@ mod tests { #[cfg(target_os = "linux")] fn fixture_section_path() { use std::fs::File; - use std::io::BufReader; let path = fixture_path("pclntab_stripped_symbols"); - let file = File::open(&path).expect("open fixture"); - let mut reader = BufReader::new(file); - - let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) + let mut file = File::open(&path).expect("open fixture"); + let location = crate::ruwind::elf::read_go_pclntab(&mut file) .expect("fixture has a .gopclntab section"); - let tab = GoPclnTab::parse(data, text).expect("parse fixture pclntab"); + let mut tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("parse fixture pclntab"); assert!(tab.len() > 100, "expected many funcs, got {}", tab.len()); let mut found_main = false; let mut found_mcall = false; for i in 0..tab.len() { - let (s, e, n) = tab.func(i).unwrap(); + let (s, e, name) = tab.func(i).unwrap(); assert!(s <= e); - match n { + match name { "main.main" => found_main = true, "runtime.mcall" => found_mcall = true, _ => {} @@ -439,29 +957,43 @@ mod tests { #[cfg(target_os = "linux")] fn fixture_recovery_path() { use std::fs::File; - use std::io::BufReader; let path = fixture_path("pclntab_section_stripped"); - let file = File::open(&path).expect("open fixture"); - let mut reader = BufReader::new(file); + let mut file = File::open(&path).expect("open fixture"); // The section lookup must fail (the header was renamed)... assert!( - crate::ruwind::elf::read_go_pclntab(&mut reader).is_none(), - "section lookup should fail for the recovery fixture"); + crate::ruwind::elf::read_go_pclntab(&mut file).is_none(), + "section lookup should fail for the recovery fixture" + ); // ...but the build-info marker remains and recovery succeeds. - assert!(crate::ruwind::elf::has_go_build_info(&mut reader)); + assert!(crate::ruwind::elf::has_go_build_info(&mut file)); - let (data, text) = crate::ruwind::elf::recover_go_pclntab(&mut reader) + let location = crate::ruwind::elf::recover_go_pclntab(&mut file) .expect("recovery should find the stripped line table"); - let tab = GoPclnTab::parse(data, text).expect("parse recovered pclntab"); + let split_chunk = (4096usize..8192) + .find(|chunk| { + let remainder = location.file_offset() % *chunk as u64; + remainder >= (*chunk - 7) as u64 + }) + .expect("find a chunk size that splits the magic header"); + let split_location = + crate::ruwind::elf::recover_go_pclntab_with_chunk(&mut file, split_chunk) + .expect("recovery should find magic split across chunks"); + assert_eq!(split_location.file_offset(), location.file_offset()); + + let mut tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("parse recovered pclntab"); assert!(tab.len() > 100, "expected many funcs, got {}", tab.len()); let mut found_main = false; for i in 0..tab.len() { - let (_, _, n) = tab.func(i).unwrap(); - if n == "main.main" { + let (_, _, name) = tab.func(i).unwrap(); + if name == "main.main" { found_main = true; break; } @@ -475,7 +1007,6 @@ mod tests { #[ignore] fn real_binary() { use std::fs::File; - use std::io::BufReader; let path = std::env::var("GO_PCLNTAB_BIN") .expect("set GO_PCLNTAB_BIN"); @@ -484,27 +1015,39 @@ mod tests { .and_then(|s| s.parse::().ok()) .unwrap_or(1); - let file = File::open(&path).expect("open binary"); - let mut reader = BufReader::new(file); - - let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) - .expect("should find .gopclntab"); - let tab = GoPclnTab::parse(data, text).expect("should parse pclntab"); + let mut file = File::open(&path).expect("open binary"); + let location = + crate::ruwind::elf::read_go_pclntab(&mut file).expect("should find .gopclntab"); + let text = location.text_start(); + let mut tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("should parse pclntab"); println!("{}: funcs={} text_start={:#x}", path, tab.len(), text); - assert!(tab.len() >= min, "expected >= {} funcs, got {}", min, tab.len()); + assert!( + tab.len() >= min, + "expected >= {} funcs, got {}", + min, + tab.len() + ); // Spot-check a few well-known names resolve and look sane. let mut samples = 0; for i in 0..tab.len() { let (s, e, n) = tab.func(i).unwrap(); assert!(s <= e, "func {} start {:#x} > end {:#x}", i, s, e); - if n.contains("runtime.mcall") || n.contains("runtime.systemstack") || n == "main.main" { + if n.contains("runtime.mcall") || n.contains("runtime.systemstack") || n == "main.main" + { println!(" {:#x}-{:#x} {}", s, e, n); samples += 1; } } - assert!(samples > 0, "expected to find some well-known runtime symbols"); + assert!( + samples > 0, + "expected to find some well-known runtime symbols" + ); } /// Validates stripped-section recovery against a real binary whose @@ -516,7 +1059,6 @@ mod tests { #[ignore] fn recover_real_binary() { use std::fs::File; - use std::io::BufReader; let path = std::env::var("GO_PCLNTAB_BIN").expect("set GO_PCLNTAB_BIN"); let min = std::env::var("GO_PCLNTAB_MIN") @@ -524,23 +1066,38 @@ mod tests { .and_then(|s| s.parse::().ok()) .unwrap_or(1); - let file = File::open(&path).expect("open binary"); - let mut reader = BufReader::new(file); + let mut file = File::open(&path).expect("open binary"); assert!( - crate::ruwind::elf::has_go_build_info(&mut reader), - "binary should carry a Go build-info marker"); + crate::ruwind::elf::has_go_build_info(&mut file), + "binary should carry a Go build-info marker" + ); - let (data, text) = crate::ruwind::elf::recover_go_pclntab(&mut reader) + let location = crate::ruwind::elf::recover_go_pclntab(&mut file) .expect("should recover a stripped .gopclntab"); - let tab = GoPclnTab::parse(data, text).expect("recovered table should parse"); - - println!("{}: recovered funcs={} text_start={:#x}", path, tab.len(), text); - assert!(tab.len() >= min, "expected >= {} funcs, got {}", min, tab.len()); + let text = location.text_start(); + let mut tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("recovered table should parse"); + + println!( + "{}: recovered funcs={} text_start={:#x}", + path, + tab.len(), + text + ); + assert!( + tab.len() >= min, + "expected >= {} funcs, got {}", + min, + tab.len() + ); // Names must be sane. - let (_, _, n0) = tab.func(0).unwrap(); - assert!(!n0.is_empty()); + let (_, _, name) = tab.func(0).unwrap(); + assert!(!name.is_empty()); } /// Corpus-sweep dumper. Reads the binary at `GO_PCLNTAB_BIN`, resolves its @@ -557,17 +1114,21 @@ mod tests { #[ignore] fn dump_pclntab() { use std::fs::File; - use std::io::{BufReader, BufWriter, Write}; + use std::io::{BufWriter, Write}; let path = std::env::var("GO_PCLNTAB_BIN").expect("set GO_PCLNTAB_BIN"); - let file = File::open(&path).expect("open binary"); - let mut reader = BufReader::new(file); + let mut file = File::open(&path).expect("open binary"); // Same resolution order as GoPclnTabSymbolReader::new. - let (data, text) = crate::ruwind::elf::read_go_pclntab(&mut reader) - .or_else(|| crate::ruwind::elf::recover_go_pclntab(&mut reader)) + let location = crate::ruwind::elf::read_go_pclntab(&mut file) + .or_else(|| crate::ruwind::elf::recover_go_pclntab(&mut file)) .expect("should find or recover a .gopclntab"); - let tab = GoPclnTab::parse(data, text).expect("should parse pclntab"); + let text = location.text_start(); + let mut tab = GoPclnTab::open( + GoPclnTabFileReader::new(file, location.file_offset()), + location, + ) + .expect("should parse pclntab"); let mut out: Box = match std::env::var("GO_PCLNTAB_OUT") { Ok(p) => Box::new(BufWriter::new(File::create(p).expect("create out"))), diff --git a/test/assets/go/generate_symbol_benchmarks.sh b/test/assets/go/generate_symbol_benchmarks.sh new file mode 100755 index 00000000..faf029f6 --- /dev/null +++ b/test/assets/go/generate_symbol_benchmarks.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +set -euo pipefail + +readonly expected_version="go1.23.4" +readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +readonly go_binary="${GO:-go}" +readonly source_file="${script_dir}/symbol_benchmark.go" +readonly common_flags=( + -buildvcs=false + -trimpath +) + +actual_version="$("${go_binary}" version | awk '{print $3}')" +if [[ "${actual_version}" != "${expected_version}" ]]; then + echo "expected ${expected_version}, found ${actual_version}" >&2 + exit 1 +fi + +export CGO_ENABLED=0 +export GOARCH=amd64 +export GOOS=linux + +"${go_binary}" build \ + "${common_flags[@]}" \ + -ldflags="-buildid= -w" \ + -o "${script_dir}/symbol_benchmark_elf" \ + "${source_file}" + +"${go_binary}" build \ + "${common_flags[@]}" \ + -ldflags="-buildid= -s -w" \ + -o "${script_dir}/symbol_benchmark_go" \ + "${source_file}" diff --git a/test/assets/go/symbol_benchmark.go b/test/assets/go/symbol_benchmark.go new file mode 100644 index 00000000..59796cd7 --- /dev/null +++ b/test/assets/go/symbol_benchmark.go @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +package main + +var sink int + +//go:noinline +func benchmarkTarget(value int) int { + return value + 1 +} + +func main() { + sink = benchmarkTarget(1) +} diff --git a/test/assets/go/symbol_benchmark_elf b/test/assets/go/symbol_benchmark_elf new file mode 100755 index 00000000..af3766c3 Binary files /dev/null and b/test/assets/go/symbol_benchmark_elf differ diff --git a/test/assets/go/symbol_benchmark_go b/test/assets/go/symbol_benchmark_go new file mode 100755 index 00000000..5e369847 Binary files /dev/null and b/test/assets/go/symbol_benchmark_go differ