Skip to main content

ghostscope_process/pid/
plan.rs

1use super::{PidNamespaceId, PidViews};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum PidFilterSpec {
5    /// Compare against host TGID from `bpf_get_current_pid_tgid() >> 32`.
6    HostTgid { filter_pid: u32 },
7    /// Compare against TGID in a specific PID namespace via `bpf_get_ns_current_pid_tgid`.
8    NamespaceTgid {
9        filter_pid: u32,
10        pid_ns: PidNamespaceId,
11    },
12}
13
14#[derive(Debug, Clone, Copy)]
15pub struct RuntimePidPlanInput<'a> {
16    pub target_pid_views: Option<&'a PidViews>,
17    pub self_pid_views: Option<&'a PidViews>,
18    pub in_container: bool,
19    pub helper_supported: bool,
20}
21
22#[derive(Debug, Clone, Default, PartialEq, Eq)]
23pub struct RuntimePidPlan {
24    pub pid_filter: Option<PidFilterSpec>,
25    pub special_vars_pid_ns: Option<PidNamespaceId>,
26    pub proc_offsets_pid_ns: Option<PidNamespaceId>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct PidModeFailFast {
31    pub proc_pid: u32,
32}
33
34pub fn build_runtime_pid_plan(
35    input: RuntimePidPlanInput<'_>,
36) -> Result<RuntimePidPlan, PidModeFailFast> {
37    let mut plan = RuntimePidPlan::default();
38
39    if let Some(pid_views) = input.target_pid_views {
40        if should_fail_fast_pid_mode(pid_views, input.in_container, input.helper_supported) {
41            return Err(PidModeFailFast {
42                proc_pid: pid_views.proc_pid,
43            });
44        }
45
46        let ns_context_needed =
47            pid_ns_context_needed(pid_views, input.in_container, input.helper_supported);
48
49        plan.pid_filter = if ns_context_needed && input.helper_supported {
50            helper_pid_ns(pid_views)
51                .map(|pid_ns| PidFilterSpec::NamespaceTgid {
52                    filter_pid: pid_views.container_pid.unwrap_or(pid_views.proc_pid),
53                    pid_ns,
54                })
55                .or(Some(PidFilterSpec::HostTgid {
56                    filter_pid: pid_views.host_pid,
57                }))
58        } else {
59            Some(PidFilterSpec::HostTgid {
60                filter_pid: pid_views.host_pid,
61            })
62        };
63    }
64
65    if input.helper_supported {
66        let self_pid_ns_for_target_mode = input
67            .target_pid_views
68            .is_none()
69            .then_some(input.self_pid_views.and_then(helper_pid_ns))
70            .flatten();
71        let self_pid_ns_for_container_fallback = input
72            .in_container
73            .then_some(input.self_pid_views.and_then(helper_pid_ns))
74            .flatten();
75
76        plan.special_vars_pid_ns = input
77            .target_pid_views
78            .and_then(helper_pid_ns)
79            .or_else(|| self_pid_ns_for_target_mode.or(self_pid_ns_for_container_fallback));
80
81        plan.proc_offsets_pid_ns = input
82            .target_pid_views
83            .and_then(|pid_views| {
84                should_use_target_proc_offsets_pid_ns(pid_views, plan.pid_filter.as_ref())
85                    .then_some(helper_pid_ns(pid_views))
86                    .flatten()
87            })
88            .or_else(|| self_pid_ns_for_target_mode.or(self_pid_ns_for_container_fallback));
89    }
90
91    Ok(plan)
92}
93
94fn pid_ns_context_needed(pid_views: &PidViews, in_container: bool, helper_supported: bool) -> bool {
95    // We need namespace-aware filtering in two cases:
96    // 1) `proc_pid` and `host_pid` differ (cross-namespace mapping is explicit),
97    // 2) running in a container and the helper is available, even if NSpid only has one value.
98    //    In private PID namespaces, single-value NSpid cannot prove host TGID equality.
99    pid_views.host_pid != pid_views.proc_pid || (in_container && helper_supported)
100}
101
102fn helper_pid_ns(pid_views: &PidViews) -> Option<PidNamespaceId> {
103    pid_views.pid_ns.filter(|pid_ns| pid_ns.dev.is_some())
104}
105
106fn should_use_target_proc_offsets_pid_ns(
107    pid_views: &PidViews,
108    pid_filter: Option<&PidFilterSpec>,
109) -> bool {
110    match pid_filter {
111        Some(PidFilterSpec::NamespaceTgid { filter_pid, .. }) => {
112            pid_views.container_pid == Some(*filter_pid) || pid_views.proc_pid == *filter_pid
113        }
114        _ => false,
115    }
116}
117
118fn should_fail_fast_pid_mode(
119    pid_views: &PidViews,
120    in_container: bool,
121    helper_supported: bool,
122) -> bool {
123    in_container
124        && !helper_supported
125        && !pid_views.has_explicit_host_mapping()
126        && !pid_views.is_initial_pid_namespace()
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::{PidResolveSource, INITIAL_PID_NAMESPACE_INO};
133
134    fn make_pid_views(proc_pid: u32, host_pid: u32) -> PidViews {
135        PidViews {
136            proc_pid,
137            host_pid,
138            container_pid: None,
139            pid_ns: None,
140            nspid_chain: None,
141            source: PidResolveSource::DirectProcStatus,
142        }
143    }
144
145    #[test]
146    fn host_filter_is_selected_when_pids_match_without_helper() {
147        let pid_views = make_pid_views(321, 321);
148        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
149            target_pid_views: Some(&pid_views),
150            self_pid_views: None,
151            in_container: false,
152            helper_supported: false,
153        })
154        .unwrap();
155
156        assert_eq!(
157            plan.pid_filter,
158            Some(PidFilterSpec::HostTgid { filter_pid: 321 })
159        );
160        assert_eq!(plan.special_vars_pid_ns, None);
161        assert_eq!(plan.proc_offsets_pid_ns, None);
162    }
163
164    #[test]
165    fn namespace_filter_is_selected_when_helper_is_available() {
166        let pid_views = PidViews {
167            proc_pid: 321,
168            host_pid: 4321,
169            container_pid: Some(17),
170            pid_ns: Some(PidNamespaceId {
171                dev: Some(1),
172                inode: 2,
173            }),
174            nspid_chain: Some(vec![4321, 321, 17]),
175            source: PidResolveSource::DirectProcStatus,
176        };
177
178        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
179            target_pid_views: Some(&pid_views),
180            self_pid_views: None,
181            in_container: true,
182            helper_supported: true,
183        })
184        .unwrap();
185
186        assert_eq!(
187            plan.pid_filter,
188            Some(PidFilterSpec::NamespaceTgid {
189                filter_pid: 17,
190                pid_ns: PidNamespaceId {
191                    dev: Some(1),
192                    inode: 2,
193                },
194            })
195        );
196        assert_eq!(
197            plan.special_vars_pid_ns,
198            Some(PidNamespaceId {
199                dev: Some(1),
200                inode: 2,
201            })
202        );
203        assert_eq!(
204            plan.proc_offsets_pid_ns,
205            Some(PidNamespaceId {
206                dev: Some(1),
207                inode: 2,
208            })
209        );
210    }
211
212    #[test]
213    fn target_mode_uses_self_namespace_for_special_vars_and_proc_offsets() {
214        let self_pid_views = PidViews {
215            proc_pid: 123,
216            host_pid: 456,
217            container_pid: Some(123),
218            pid_ns: Some(PidNamespaceId {
219                dev: Some(7),
220                inode: 8,
221            }),
222            nspid_chain: Some(vec![456, 123]),
223            source: PidResolveSource::DirectProcStatus,
224        };
225
226        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
227            target_pid_views: None,
228            self_pid_views: Some(&self_pid_views),
229            in_container: true,
230            helper_supported: true,
231        })
232        .unwrap();
233
234        assert_eq!(plan.pid_filter, None);
235        assert_eq!(plan.special_vars_pid_ns, self_pid_views.pid_ns);
236        assert_eq!(plan.proc_offsets_pid_ns, self_pid_views.pid_ns);
237    }
238
239    #[test]
240    fn target_mode_uses_self_namespace_when_container_detection_misses() {
241        let self_pid_views = PidViews {
242            proc_pid: 123,
243            host_pid: 456,
244            container_pid: Some(123),
245            pid_ns: Some(PidNamespaceId {
246                dev: Some(7),
247                inode: 8,
248            }),
249            nspid_chain: Some(vec![456, 123]),
250            source: PidResolveSource::DirectProcStatus,
251        };
252
253        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
254            target_pid_views: None,
255            self_pid_views: Some(&self_pid_views),
256            in_container: false,
257            helper_supported: true,
258        })
259        .unwrap();
260
261        assert_eq!(plan.pid_filter, None);
262        assert_eq!(plan.special_vars_pid_ns, self_pid_views.pid_ns);
263        assert_eq!(plan.proc_offsets_pid_ns, self_pid_views.pid_ns);
264    }
265
266    #[test]
267    fn fail_fast_in_container_without_helper_or_explicit_host_mapping() {
268        let pid_views = make_pid_views(321, 321);
269        assert_eq!(
270            build_runtime_pid_plan(RuntimePidPlanInput {
271                target_pid_views: Some(&pid_views),
272                self_pid_views: None,
273                in_container: true,
274                helper_supported: false,
275            }),
276            Err(PidModeFailFast { proc_pid: 321 })
277        );
278    }
279
280    #[test]
281    fn initial_pid_namespace_disables_fail_fast_without_helper() {
282        let pid_views = PidViews {
283            proc_pid: 321,
284            host_pid: 321,
285            container_pid: None,
286            pid_ns: Some(PidNamespaceId {
287                dev: Some(1),
288                inode: INITIAL_PID_NAMESPACE_INO,
289            }),
290            nspid_chain: None,
291            source: PidResolveSource::DirectProcStatus,
292        };
293
294        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
295            target_pid_views: Some(&pid_views),
296            self_pid_views: None,
297            in_container: true,
298            helper_supported: false,
299        })
300        .unwrap();
301
302        assert_eq!(
303            plan.pid_filter,
304            Some(PidFilterSpec::HostTgid { filter_pid: 321 })
305        );
306    }
307
308    #[test]
309    fn explicit_host_mapping_disables_fail_fast_without_helper() {
310        let pid_views = PidViews {
311            proc_pid: 321,
312            host_pid: 12345,
313            container_pid: Some(321),
314            pid_ns: None,
315            nspid_chain: Some(vec![12345, 321]),
316            source: PidResolveSource::DirectProcStatus,
317        };
318
319        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
320            target_pid_views: Some(&pid_views),
321            self_pid_views: None,
322            in_container: true,
323            helper_supported: false,
324        })
325        .unwrap();
326
327        assert_eq!(
328            plan.pid_filter,
329            Some(PidFilterSpec::HostTgid { filter_pid: 12345 })
330        );
331    }
332
333    #[test]
334    fn proc_offsets_stays_on_current_proc_view_for_host_filter() {
335        let pid_views = PidViews {
336            proc_pid: 321,
337            host_pid: 4321,
338            container_pid: Some(17),
339            pid_ns: Some(PidNamespaceId {
340                dev: Some(1),
341                inode: 2,
342            }),
343            nspid_chain: Some(vec![4321, 321, 17]),
344            source: PidResolveSource::DirectProcStatus,
345        };
346
347        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
348            target_pid_views: Some(&pid_views),
349            self_pid_views: None,
350            in_container: true,
351            helper_supported: false,
352        })
353        .unwrap();
354
355        assert_eq!(plan.proc_offsets_pid_ns, None);
356    }
357}