Skip to content

Commit dcbc74c

Browse files
fix(perf): purge inherited mappings on execve to fix unknown symbols (#392)
A child forked from a parent (e.g. bash) inherits the parent's memory mappings. When it then execve's its own binary, the inherited PIE mappings remain and can collide with the new binary's mappings at the same base address, leading to wrong symbolication and unknown symbols. Handle COMM records flagged as execve and purge the exec'ing process's inherited mappings so only its post-exec mappings are used. Refs COD-2779
1 parent bfff150 commit dcbc74c

1 file changed

Lines changed: 79 additions & 0 deletions

File tree

src/executor/wall_time/profiler/perf/parse_perf_file.rs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub fn parse_for_memmap2<P: AsRef<Path>>(
3939
mut record_iter,
4040
} = PerfFileReader::parse_pipe(reader)?;
4141

42+
// This loop relies on the events being in chronological order, which is guaranteed by the perf file format.
4243
while let Some(record) = record_iter.next_record(&mut perf_file)? {
4344
let PerfFileRecord::EventRecord { record, .. } = record else {
4445
continue;
@@ -75,6 +76,30 @@ pub fn parse_for_memmap2<P: AsRef<Path>>(
7576
fork_record.pid,
7677
);
7778
}
79+
RecordType::COMM => {
80+
// An execve() replaces the entire address space: the mappings inherited from the
81+
// parent on fork (and any pre-exec mappings of this pid) are no longer valid.
82+
// TODO(COD-1377): Manage timing in module mapping to improve symbolication.
83+
let Ok(parsed_record) = record.parse() else {
84+
continue;
85+
};
86+
87+
let EventRecord::Comm(comm_record) = parsed_record else {
88+
continue;
89+
};
90+
91+
if !comm_record.is_execve {
92+
continue;
93+
}
94+
95+
if pid_filter.should_include(comm_record.pid) {
96+
trace!(
97+
"Exec: Purging inherited mappings for PID {}",
98+
comm_record.pid
99+
);
100+
purge_process_mappings(&mut loaded_modules_by_path, comm_record.pid);
101+
}
102+
}
78103
RecordType::MMAP2 => {
79104
let Ok(parsed_record) = record.parse() else {
80105
continue;
@@ -176,6 +201,13 @@ fn inherit_parent_mappings(
176201
}
177202
}
178203

204+
/// Drop every mapping recorded for `pid` across all modules.
205+
fn purge_process_mappings(loaded_modules_by_path: &mut HashMap<PathBuf, LoadedModule>, pid: pid_t) {
206+
for loaded_module in loaded_modules_by_path.values_mut() {
207+
loaded_module.process_loaded_modules.remove(&pid);
208+
}
209+
}
210+
179211
/// Process a single MMAP2 record and add it to the symbols and unwind data maps
180212
fn process_mmap2_record(
181213
record: linux_perf_data::linux_perf_event_reader::Mmap2Record,
@@ -319,4 +351,51 @@ mod tests {
319351
.unwrap();
320352
assert_eq!(child.symbols_load_bias, Some(0xcafe));
321353
}
354+
355+
#[test]
356+
fn purge_process_mappings_removes_only_the_target_pid() {
357+
// Reproduces the fork+exec collision: child 200 forked from bash (100) and inherited
358+
// bash's PIE mapping at 0xaaaaaaaa0000, then exec'd its own binary at the same base.
359+
let mut modules: HashMap<PathBuf, LoadedModule> = HashMap::new();
360+
361+
// Inherited-from-bash module that must be dropped on exec.
362+
let mut bash = make_module_with_parent(100, 0xaaaaaaaa0000);
363+
bash.process_loaded_modules.insert(
364+
200,
365+
ProcessLoadedModule {
366+
symbols_load_bias: Some(0xaaaaaaaa0000),
367+
process_unwind_data: None,
368+
},
369+
);
370+
modules.insert(PathBuf::from("/usr/bin/bash"), bash);
371+
372+
// The real post-exec binary, also based at 0xaaaaaaaa0000 for pid 200.
373+
modules.insert(
374+
PathBuf::from("/cpp/build/fractal_main"),
375+
make_module_with_parent(200, 0xaaaaaaaa0000),
376+
);
377+
378+
purge_process_mappings(&mut modules, 200);
379+
380+
// Pid 200 is gone from every module...
381+
assert!(
382+
!modules[&PathBuf::from("/usr/bin/bash")]
383+
.process_loaded_modules
384+
.contains_key(&200)
385+
);
386+
assert!(
387+
!modules[&PathBuf::from("/cpp/build/fractal_main")]
388+
.process_loaded_modules
389+
.contains_key(&200)
390+
);
391+
// ...but bash's own pid 100 mapping is untouched.
392+
assert_eq!(
393+
modules[&PathBuf::from("/usr/bin/bash")]
394+
.process_loaded_modules
395+
.get(&100)
396+
.unwrap()
397+
.symbols_load_bias,
398+
Some(0xaaaaaaaa0000)
399+
);
400+
}
322401
}

0 commit comments

Comments
 (0)