Skip to content

Commit e32babe

Browse files
committed
feat(runner-shared): make eh_frame_hdr optional in unwind data (V4)
Add UnwindDataV4 where eh_frame_hdr and eh_frame_hdr_svma are optional. The hdr is only a binary-search index into .eh_frame: binaries linked without `ld --eh-frame-hdr` (e.g. Valgrind's statically-linked tools) don't carry it, but can still be unwound since the parser rebuilds the index from .eh_frame. V3 data is transparently upgraded to V4 on parse, following the existing V1->V2->V3 compat pattern. This must land in perf-parser before runners start emitting V4.
1 parent f8f7496 commit e32babe

2 files changed

Lines changed: 125 additions & 17 deletions

File tree

crates/runner-shared/src/unwind_data.rs

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,26 @@ use std::{hash::DefaultHasher, ops::Range};
88

99
pub const UNWIND_FILE_EXT: &str = "unwind_data";
1010

11-
pub type UnwindData = UnwindDataV3;
11+
pub type UnwindData = UnwindDataV4;
1212
impl UnwindData {
1313
pub fn parse(reader: &[u8]) -> anyhow::Result<Self> {
1414
let compat: UnwindDataCompat = bincode::deserialize(reader)?;
1515

1616
match compat {
1717
UnwindDataCompat::V1(_) => {
18-
anyhow::bail!("Cannot parse V1 unwind data as V3 (breaking changes)")
18+
anyhow::bail!("Cannot parse V1 unwind data as V4 (breaking changes)")
1919
}
2020
UnwindDataCompat::V2(_) => {
21-
anyhow::bail!("Cannot parse V2 unwind data as V3 (breaking changes)")
21+
anyhow::bail!("Cannot parse V2 unwind data as V4 (breaking changes)")
2222
}
23-
UnwindDataCompat::V3(v3) => Ok(v3),
23+
UnwindDataCompat::V3(v3) => Ok(v3.into()),
24+
UnwindDataCompat::V4(v4) => Ok(v4),
2425
}
2526
}
2627

2728
pub fn save_to<P: AsRef<std::path::Path>>(&self, folder: P, key: &str) -> anyhow::Result<()> {
2829
let path = folder.as_ref().join(format!("{key}.{UNWIND_FILE_EXT}"));
29-
let compat = UnwindDataCompat::V3(self.clone());
30+
let compat = UnwindDataCompat::V4(self.clone());
3031
let file = std::fs::File::create(&path)?;
3132
const BUFFER_SIZE: usize = 256 * 1024;
3233
let writer = BufWriter::with_capacity(BUFFER_SIZE, file);
@@ -41,6 +42,7 @@ enum UnwindDataCompat {
4142
V1(UnwindDataV1),
4243
V2(UnwindDataV2),
4344
V3(UnwindDataV3),
45+
V4(UnwindDataV4),
4446
}
4547

4648
#[doc(hidden)]
@@ -90,6 +92,9 @@ impl UnwindDataV2 {
9092
UnwindDataCompat::V3(_) => {
9193
anyhow::bail!("Cannot parse V3 unwind data as V2 (missing per-pid fields)")
9294
}
95+
UnwindDataCompat::V4(_) => {
96+
anyhow::bail!("Cannot parse V4 unwind data as V2 (missing per-pid fields)")
97+
}
9398
}
9499
}
95100
}
@@ -135,6 +140,35 @@ impl From<UnwindDataV2> for UnwindDataV3 {
135140
}
136141
}
137142

143+
/// Pid-agnostic unwind data with an optional `.eh_frame_hdr`.
144+
///
145+
/// The hdr is only a binary-search index into `.eh_frame` — some binaries
146+
/// (e.g. Valgrind's statically-linked tools) are linked without
147+
/// `ld --eh-frame-hdr` and don't carry it. The parser rebuilds the index from
148+
/// `.eh_frame` in that case.
149+
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
150+
pub struct UnwindDataV4 {
151+
pub path: String,
152+
pub base_svma: u64,
153+
pub eh_frame_hdr: Option<Vec<u8>>,
154+
pub eh_frame_hdr_svma: Option<Range<u64>>,
155+
pub eh_frame: Vec<u8>,
156+
pub eh_frame_svma: Range<u64>,
157+
}
158+
159+
impl From<UnwindDataV3> for UnwindDataV4 {
160+
fn from(v3: UnwindDataV3) -> Self {
161+
Self {
162+
path: v3.path,
163+
base_svma: v3.base_svma,
164+
eh_frame_hdr: Some(v3.eh_frame_hdr),
165+
eh_frame_hdr_svma: Some(v3.eh_frame_hdr_svma),
166+
eh_frame: v3.eh_frame,
167+
eh_frame_svma: v3.eh_frame_svma,
168+
}
169+
}
170+
}
171+
138172
impl Debug for UnwindDataV2 {
139173
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140174
let eh_frame_hdr_hash = {
@@ -192,6 +226,33 @@ impl Debug for UnwindDataV3 {
192226
}
193227
}
194228

229+
impl Debug for UnwindDataV4 {
230+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231+
let eh_frame_hdr_hash = self.eh_frame_hdr.as_ref().map(|eh_frame_hdr| {
232+
let mut hasher = DefaultHasher::new();
233+
eh_frame_hdr.hash(&mut hasher);
234+
hasher.finish()
235+
});
236+
let eh_frame_hash = {
237+
let mut hasher = DefaultHasher::new();
238+
self.eh_frame.hash(&mut hasher);
239+
hasher.finish()
240+
};
241+
242+
f.debug_struct("UnwindData")
243+
.field("path", &self.path)
244+
.field("base_svma", &format_args!("{:x}", self.base_svma))
245+
.field(
246+
"eh_frame_hdr_svma",
247+
&format_args!("{:x?}", self.eh_frame_hdr_svma),
248+
)
249+
.field("eh_frame_hdr_hash", &format_args!("{eh_frame_hdr_hash:x?}"))
250+
.field("eh_frame_hash", &format_args!("{eh_frame_hash:x}"))
251+
.field("eh_frame_svma", &format_args!("{:x?}", self.eh_frame_svma))
252+
.finish()
253+
}
254+
}
255+
195256
/// Per-pid mounting info referencing a deduplicated unwind data entry.
196257
#[derive(Debug, Serialize, Deserialize, Clone)]
197258
pub struct MappedProcessUnwindData {
@@ -223,6 +284,7 @@ mod tests {
223284

224285
const V2_BINARY: &[u8] = include_bytes!("../testdata/unwind_data_v2.bin");
225286
const V3_BINARY: &[u8] = include_bytes!("../testdata/unwind_data_v3.bin");
287+
const V4_BINARY: &[u8] = include_bytes!("../testdata/unwind_data_v4.bin");
226288

227289
fn create_sample_v2() -> UnwindDataV2 {
228290
UnwindDataV2 {
@@ -249,18 +311,38 @@ mod tests {
249311
}
250312
}
251313

314+
fn create_sample_v4() -> UnwindDataV4 {
315+
UnwindDataV4 {
316+
path: "/lib/test.so".to_string(),
317+
base_svma: 0x0,
318+
// No `.eh_frame_hdr`, like Valgrind's statically-linked tools
319+
eh_frame_hdr: None,
320+
eh_frame_hdr_svma: None,
321+
eh_frame: vec![5, 6, 7, 8],
322+
eh_frame_svma: 0x200..0x300,
323+
}
324+
}
325+
252326
#[test]
253-
fn test_parse_v2_as_v3_should_error() {
254-
// Try to parse V2 binary artifact as V3 using UnwindData::parse
255-
let result = UnwindDataV3::parse(V2_BINARY);
327+
#[ignore = "one-off generator for the V4 testdata artifact"]
328+
fn generate_v4_testdata() {
329+
let compat = UnwindDataCompat::V4(create_sample_v4());
330+
let bytes = bincode::serialize(&compat).unwrap();
331+
std::fs::write("testdata/unwind_data_v4.bin", bytes).unwrap();
332+
}
256333

257-
// Should error due to breaking changes between V2 and V3
334+
#[test]
335+
fn test_parse_v2_as_v4_should_error() {
336+
// Try to parse V2 binary artifact as V4 using UnwindData::parse
337+
let result = UnwindData::parse(V2_BINARY);
338+
339+
// Should error due to breaking changes between V2 and V4
258340
assert!(result.is_err());
259341
let err = result.unwrap_err();
260342
assert!(
261343
err.to_string()
262-
.contains("Cannot parse V2 unwind data as V3"),
263-
"Expected error message about V2->V3 incompatibility, got: {err}"
344+
.contains("Cannot parse V2 unwind data as V4"),
345+
"Expected error message about V2->V4 incompatibility, got: {err}"
264346
);
265347
}
266348

@@ -280,13 +362,39 @@ mod tests {
280362
}
281363

282364
#[test]
283-
fn test_parse_v3_as_v3() {
284-
// Parse V3 binary artifact as V3 using UnwindData::parse
285-
let parsed_v3 = UnwindData::parse(V3_BINARY).expect("Failed to parse V3 data as V3");
365+
fn test_parse_v4_as_v2_should_error() {
366+
// Try to parse V4 binary artifact as V2 using UnwindDataV2::parse
367+
let result = UnwindDataV2::parse(V4_BINARY);
368+
369+
// Should error with specific message about missing per-pid fields
370+
assert!(result.is_err());
371+
let err = result.unwrap_err();
372+
assert!(
373+
err.to_string()
374+
.contains("Cannot parse V4 unwind data as V2"),
375+
"Expected error message about V4->V2 incompatibility, got: {err}"
376+
);
377+
}
378+
379+
#[test]
380+
fn test_parse_v3_as_v4() {
381+
// Parse V3 binary artifact using UnwindData::parse — it converts to V4
382+
let parsed = UnwindData::parse(V3_BINARY).expect("Failed to parse V3 data as V4");
383+
384+
// Should match the V3 data with the hdr fields wrapped in `Some`
385+
let expected: UnwindDataV4 = create_sample_v3().into();
386+
assert_eq!(parsed, expected);
387+
assert!(parsed.eh_frame_hdr.is_some());
388+
}
389+
390+
#[test]
391+
fn test_parse_v4_as_v4() {
392+
// Parse V4 binary artifact as V4 using UnwindData::parse
393+
let parsed_v4 = UnwindData::parse(V4_BINARY).expect("Failed to parse V4 data as V4");
286394

287-
// Should match expected V3 data
288-
let expected_v3 = create_sample_v3();
289-
assert_eq!(parsed_v3, expected_v3);
395+
// Should match expected V4 data (without an eh_frame_hdr)
396+
let expected_v4 = create_sample_v4();
397+
assert_eq!(parsed_v4, expected_v4);
290398
}
291399

292400
#[test]
62 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)