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        plan.special_vars_pid_ns = input.target_pid_views.and_then(helper_pid_ns).or_else(|| {
67            input
68                .in_container
69                .then_some(input.self_pid_views.and_then(helper_pid_ns))
70                .flatten()
71        });
72
73        plan.proc_offsets_pid_ns = input
74            .target_pid_views
75            .and_then(|pid_views| {
76                should_use_target_proc_offsets_pid_ns(pid_views, plan.pid_filter.as_ref())
77                    .then_some(helper_pid_ns(pid_views))
78                    .flatten()
79            })
80            .or_else(|| {
81                input
82                    .in_container
83                    .then_some(input.self_pid_views.and_then(helper_pid_ns))
84                    .flatten()
85            });
86    }
87
88    Ok(plan)
89}
90
91fn pid_ns_context_needed(pid_views: &PidViews, in_container: bool, helper_supported: bool) -> bool {
92    // We need namespace-aware filtering in two cases:
93    // 1) `proc_pid` and `host_pid` differ (cross-namespace mapping is explicit),
94    // 2) running in a container and the helper is available, even if NSpid only has one value.
95    //    In private PID namespaces, single-value NSpid cannot prove host TGID equality.
96    pid_views.host_pid != pid_views.proc_pid || (in_container && helper_supported)
97}
98
99fn helper_pid_ns(pid_views: &PidViews) -> Option<PidNamespaceId> {
100    pid_views.pid_ns.filter(|pid_ns| pid_ns.dev.is_some())
101}
102
103fn should_use_target_proc_offsets_pid_ns(
104    pid_views: &PidViews,
105    pid_filter: Option<&PidFilterSpec>,
106) -> bool {
107    match pid_filter {
108        Some(PidFilterSpec::NamespaceTgid { filter_pid, .. }) => {
109            pid_views.container_pid == Some(*filter_pid) || pid_views.proc_pid == *filter_pid
110        }
111        _ => false,
112    }
113}
114
115fn should_fail_fast_pid_mode(
116    pid_views: &PidViews,
117    in_container: bool,
118    helper_supported: bool,
119) -> bool {
120    in_container
121        && !helper_supported
122        && !pid_views.has_explicit_host_mapping()
123        && !pid_views.is_initial_pid_namespace()
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::{PidResolveSource, INITIAL_PID_NAMESPACE_INO};
130
131    fn make_pid_views(proc_pid: u32, host_pid: u32) -> PidViews {
132        PidViews {
133            proc_pid,
134            host_pid,
135            container_pid: None,
136            pid_ns: None,
137            nspid_chain: None,
138            source: PidResolveSource::DirectProcStatus,
139        }
140    }
141
142    #[test]
143    fn host_filter_is_selected_when_pids_match_without_helper() {
144        let pid_views = make_pid_views(321, 321);
145        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
146            target_pid_views: Some(&pid_views),
147            self_pid_views: None,
148            in_container: false,
149            helper_supported: false,
150        })
151        .unwrap();
152
153        assert_eq!(
154            plan.pid_filter,
155            Some(PidFilterSpec::HostTgid { filter_pid: 321 })
156        );
157        assert_eq!(plan.special_vars_pid_ns, None);
158        assert_eq!(plan.proc_offsets_pid_ns, None);
159    }
160
161    #[test]
162    fn namespace_filter_is_selected_when_helper_is_available() {
163        let pid_views = PidViews {
164            proc_pid: 321,
165            host_pid: 4321,
166            container_pid: Some(17),
167            pid_ns: Some(PidNamespaceId {
168                dev: Some(1),
169                inode: 2,
170            }),
171            nspid_chain: Some(vec![4321, 321, 17]),
172            source: PidResolveSource::DirectProcStatus,
173        };
174
175        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
176            target_pid_views: Some(&pid_views),
177            self_pid_views: None,
178            in_container: true,
179            helper_supported: true,
180        })
181        .unwrap();
182
183        assert_eq!(
184            plan.pid_filter,
185            Some(PidFilterSpec::NamespaceTgid {
186                filter_pid: 17,
187                pid_ns: PidNamespaceId {
188                    dev: Some(1),
189                    inode: 2,
190                },
191            })
192        );
193        assert_eq!(
194            plan.special_vars_pid_ns,
195            Some(PidNamespaceId {
196                dev: Some(1),
197                inode: 2,
198            })
199        );
200        assert_eq!(
201            plan.proc_offsets_pid_ns,
202            Some(PidNamespaceId {
203                dev: Some(1),
204                inode: 2,
205            })
206        );
207    }
208
209    #[test]
210    fn target_mode_uses_self_namespace_for_special_vars_and_proc_offsets() {
211        let self_pid_views = PidViews {
212            proc_pid: 123,
213            host_pid: 456,
214            container_pid: Some(123),
215            pid_ns: Some(PidNamespaceId {
216                dev: Some(7),
217                inode: 8,
218            }),
219            nspid_chain: Some(vec![456, 123]),
220            source: PidResolveSource::DirectProcStatus,
221        };
222
223        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
224            target_pid_views: None,
225            self_pid_views: Some(&self_pid_views),
226            in_container: true,
227            helper_supported: true,
228        })
229        .unwrap();
230
231        assert_eq!(plan.pid_filter, None);
232        assert_eq!(plan.special_vars_pid_ns, self_pid_views.pid_ns);
233        assert_eq!(plan.proc_offsets_pid_ns, self_pid_views.pid_ns);
234    }
235
236    #[test]
237    fn fail_fast_in_container_without_helper_or_explicit_host_mapping() {
238        let pid_views = make_pid_views(321, 321);
239        assert_eq!(
240            build_runtime_pid_plan(RuntimePidPlanInput {
241                target_pid_views: Some(&pid_views),
242                self_pid_views: None,
243                in_container: true,
244                helper_supported: false,
245            }),
246            Err(PidModeFailFast { proc_pid: 321 })
247        );
248    }
249
250    #[test]
251    fn initial_pid_namespace_disables_fail_fast_without_helper() {
252        let pid_views = PidViews {
253            proc_pid: 321,
254            host_pid: 321,
255            container_pid: None,
256            pid_ns: Some(PidNamespaceId {
257                dev: Some(1),
258                inode: INITIAL_PID_NAMESPACE_INO,
259            }),
260            nspid_chain: None,
261            source: PidResolveSource::DirectProcStatus,
262        };
263
264        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
265            target_pid_views: Some(&pid_views),
266            self_pid_views: None,
267            in_container: true,
268            helper_supported: false,
269        })
270        .unwrap();
271
272        assert_eq!(
273            plan.pid_filter,
274            Some(PidFilterSpec::HostTgid { filter_pid: 321 })
275        );
276    }
277
278    #[test]
279    fn explicit_host_mapping_disables_fail_fast_without_helper() {
280        let pid_views = PidViews {
281            proc_pid: 321,
282            host_pid: 12345,
283            container_pid: Some(321),
284            pid_ns: None,
285            nspid_chain: Some(vec![12345, 321]),
286            source: PidResolveSource::DirectProcStatus,
287        };
288
289        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
290            target_pid_views: Some(&pid_views),
291            self_pid_views: None,
292            in_container: true,
293            helper_supported: false,
294        })
295        .unwrap();
296
297        assert_eq!(
298            plan.pid_filter,
299            Some(PidFilterSpec::HostTgid { filter_pid: 12345 })
300        );
301    }
302
303    #[test]
304    fn proc_offsets_stays_on_current_proc_view_for_host_filter() {
305        let pid_views = PidViews {
306            proc_pid: 321,
307            host_pid: 4321,
308            container_pid: Some(17),
309            pid_ns: Some(PidNamespaceId {
310                dev: Some(1),
311                inode: 2,
312            }),
313            nspid_chain: Some(vec![4321, 321, 17]),
314            source: PidResolveSource::DirectProcStatus,
315        };
316
317        let plan = build_runtime_pid_plan(RuntimePidPlanInput {
318            target_pid_views: Some(&pid_views),
319            self_pid_views: None,
320            in_container: true,
321            helper_supported: false,
322        })
323        .unwrap();
324
325        assert_eq!(plan.proc_offsets_pid_ns, None);
326    }
327}