Skip to content

Commit 8db1a5d

Browse files
feat: inherit process mapping on forks
1 parent fa0bfcb commit 8db1a5d

1 file changed

Lines changed: 130 additions & 1 deletion

File tree

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

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use super::loaded_module::LoadedModule;
1+
use super::loaded_module::{LoadedModule, ProcessLoadedModule};
22
use super::module_symbols::ModuleSymbols;
33
use super::unwind_data::unwind_data_from_elf;
44
use crate::prelude::*;
@@ -57,12 +57,28 @@ pub fn parse_for_memmap2<P: AsRef<Path>>(
5757
continue;
5858
};
5959

60+
// Thread creation also emits FORK with ppid == pid (only tid differs);
61+
// skip those — there are no new process mappings to inherit.
62+
if fork_record.ppid == fork_record.pid {
63+
continue;
64+
}
65+
6066
if pid_filter.add_child_if_parent_tracked(fork_record.ppid, fork_record.pid) {
6167
trace!(
6268
"Fork: Tracking child PID {} from parent PID {}",
6369
fork_record.pid, fork_record.ppid
6470
);
6571
}
72+
73+
// The kernel does not re-emit MMAP2 for fork-inherited segments, so the
74+
// child pid would otherwise have an empty process_loaded_modules map.
75+
// Copy each module the parent has mounted to the child so downstream
76+
// consumers (symbol resolution, ignored_modules) see the full picture.
77+
inherit_parent_mappings(
78+
&mut loaded_modules_by_path,
79+
fork_record.ppid,
80+
fork_record.pid,
81+
);
6682
}
6783
RecordType::MMAP2 => {
6884
let Ok(parsed_record) = record.parse() else {
@@ -134,6 +150,41 @@ impl PidFilter {
134150
}
135151
}
136152

153+
/// Copy every module the parent pid has mounted onto the child pid.
154+
///
155+
/// Linux only emits MMAP2 records on `mmap`/`exec`, not on `fork`. Without this,
156+
/// a fork-spawned child would carry only the mappings it issues itself after
157+
/// the fork — fork-inherited segments (libpython, libc, …) would stay invisible
158+
/// to symbolization and to the ignored_modules filter. If the child later
159+
/// remaps a path via its own MMAP2 (e.g. after execve), the subsequent
160+
/// `process_mmap2_record` call will overwrite the inherited entry.
161+
fn inherit_parent_mappings(
162+
loaded_modules_by_path: &mut HashMap<PathBuf, LoadedModule>,
163+
ppid: pid_t,
164+
pid: pid_t,
165+
) {
166+
use std::collections::hash_map::Entry;
167+
168+
for loaded_module in loaded_modules_by_path.values_mut() {
169+
let inherited =
170+
loaded_module
171+
.process_loaded_modules
172+
.get(&ppid)
173+
.map(|p| ProcessLoadedModule {
174+
symbols_load_bias: p.symbols_load_bias,
175+
process_unwind_data: p.process_unwind_data.clone(),
176+
});
177+
let Some(inherited) = inherited else {
178+
continue;
179+
};
180+
// Only insert if the child has no entry yet; an existing entry came from
181+
// the child's own MMAP2 and is authoritative.
182+
if let Entry::Vacant(slot) = loaded_module.process_loaded_modules.entry(pid) {
183+
slot.insert(inherited);
184+
}
185+
}
186+
}
187+
137188
/// Process a single MMAP2 record and add it to the symbols and unwind data maps
138189
fn process_mmap2_record(
139190
record: linux_perf_data::linux_perf_event_reader::Mmap2Record,
@@ -223,3 +274,81 @@ fn process_mmap2_record(
223274
}
224275
};
225276
}
277+
278+
#[cfg(test)]
279+
mod tests {
280+
use super::*;
281+
282+
fn make_module_with_parent(ppid: pid_t, load_bias: u64) -> LoadedModule {
283+
let mut m = LoadedModule::default();
284+
m.process_loaded_modules.insert(
285+
ppid,
286+
ProcessLoadedModule {
287+
symbols_load_bias: Some(load_bias),
288+
process_unwind_data: None,
289+
},
290+
);
291+
m
292+
}
293+
294+
#[test]
295+
fn inherit_parent_mappings_copies_each_module_to_child() {
296+
let mut modules: HashMap<PathBuf, LoadedModule> = HashMap::new();
297+
modules.insert(
298+
PathBuf::from("/lib/libpython.so"),
299+
make_module_with_parent(100, 0xdead),
300+
);
301+
modules.insert(
302+
PathBuf::from("/lib/libc.so"),
303+
make_module_with_parent(100, 0xbeef),
304+
);
305+
306+
inherit_parent_mappings(&mut modules, 100, 200);
307+
308+
for (path, m) in &modules {
309+
let child = m
310+
.process_loaded_modules
311+
.get(&200)
312+
.unwrap_or_else(|| panic!("missing child entry for {path:?}"));
313+
let parent = m.process_loaded_modules.get(&100).unwrap();
314+
assert_eq!(child.symbols_load_bias, parent.symbols_load_bias);
315+
}
316+
}
317+
318+
#[test]
319+
fn inherit_parent_mappings_skips_modules_without_parent() {
320+
let mut modules: HashMap<PathBuf, LoadedModule> = HashMap::new();
321+
modules.insert(PathBuf::from("/lib/foo.so"), LoadedModule::default());
322+
323+
inherit_parent_mappings(&mut modules, 100, 200);
324+
325+
assert!(
326+
modules[&PathBuf::from("/lib/foo.so")]
327+
.process_loaded_modules
328+
.is_empty()
329+
);
330+
}
331+
332+
#[test]
333+
fn inherit_parent_mappings_does_not_overwrite_existing_child_entry() {
334+
let mut modules: HashMap<PathBuf, LoadedModule> = HashMap::new();
335+
let mut m = make_module_with_parent(100, 0xdead);
336+
// Child already has its own (post-exec) mapping at a different bias.
337+
m.process_loaded_modules.insert(
338+
200,
339+
ProcessLoadedModule {
340+
symbols_load_bias: Some(0xcafe),
341+
process_unwind_data: None,
342+
},
343+
);
344+
modules.insert(PathBuf::from("/lib/libpython.so"), m);
345+
346+
inherit_parent_mappings(&mut modules, 100, 200);
347+
348+
let child = modules[&PathBuf::from("/lib/libpython.so")]
349+
.process_loaded_modules
350+
.get(&200)
351+
.unwrap();
352+
assert_eq!(child.symbols_load_bias, Some(0xcafe));
353+
}
354+
}

0 commit comments

Comments
 (0)