Skip to main content

libdd_common/unix_utils/
fork.rs

1// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4#[cfg(target_os = "macos")]
5pub fn alt_fork() -> i32 {
6    // There is a lower-level `__fork()` function in macOS, and we can call it from Rust, but the
7    // runtime is much stricter about which operations (e.g., no malloc) are allowed in the child.
8    // This somewhat defeats the purpose, so macOS for now will just have to live with atfork
9    // handlers.
10    unsafe { libc::fork() }
11}
12
13#[cfg(target_os = "linux")]
14use std::fs::File;
15#[cfg(target_os = "linux")]
16use std::io::{self, Read};
17
18#[cfg(target_os = "linux")]
19fn is_being_traced() -> io::Result<bool> {
20    // Check to see whether we are being traced.  This will fail on systems where procfs is
21    // unavailable, but presumably in those systems `ptrace()` is also unavailable.
22    // The caller is free to treat a failure as a false.
23    // This function may run in signal handler, so we should ensure that we do not allocate
24    // memory on the heap (ex: avoiding using BufReader for example).
25    let file = File::open("/proc/self/status")?;
26    is_being_traced_internal(file)
27}
28
29#[cfg(target_os = "linux")]
30const BUFFER_SIZE: usize = 1024;
31
32#[cfg(target_os = "linux")]
33fn is_being_traced_internal(mut file: File) -> io::Result<bool> {
34    const TRACER_PID_MARKER: &[u8] = b"TracerPid:";
35    let mut buffer = [0u8; BUFFER_SIZE];
36    let mut data_len = 0;
37    let mut offset = 0;
38
39    loop {
40        // Shift unread data to the start if needed
41        if offset > 0 && offset < data_len {
42            let leftover_len = data_len - offset;
43            buffer.copy_within(offset..data_len, 0);
44            data_len = leftover_len;
45            offset = 0;
46
47        // Handle edge case where no newline found in a full buffer,
48        // or all data processed: reset to read fresh data.
49        } else if offset == data_len || (offset == 0 && data_len == BUFFER_SIZE) {
50            data_len = 0;
51            offset = 0;
52        }
53
54        let bytes_read = file.read(&mut buffer[data_len..])?;
55        if bytes_read == 0 {
56            // EOF reached, no more data to read
57            break;
58        }
59        data_len += bytes_read;
60
61        // Process lines in the buffer
62        while let Some(newline_pos) = buffer[offset..data_len].iter().position(|&b| b == b'\n') {
63            let line_end = offset + newline_pos;
64
65            if let Some(result) =
66                check_tracer_pid_line(&buffer[offset..line_end], TRACER_PID_MARKER)?
67            {
68                return Ok(result);
69            }
70
71            offset = line_end + 1;
72        }
73    }
74
75    // Check remaining data without newline at EOF
76    if offset < data_len {
77        if let Some(result) = check_tracer_pid_line(&buffer[offset..data_len], TRACER_PID_MARKER)? {
78            return Ok(result);
79        }
80    }
81    Ok(false)
82}
83
84#[cfg(target_os = "linux")]
85fn check_tracer_pid_line(line: &[u8], marker: &[u8]) -> io::Result<Option<bool>> {
86    if line.starts_with(marker) && line.len() > marker.len() {
87        if let Ok(line_str) = std::str::from_utf8(line) {
88            let tracer_pid = line_str.split_whitespace().nth(1).unwrap_or("0");
89            return Ok(Some(tracer_pid != "0"));
90        }
91    }
92    Ok(None)
93}
94
95#[cfg(target_os = "linux")]
96pub fn alt_fork() -> libc::pid_t {
97    use libc::{
98        c_ulong, c_void, pid_t, syscall, SYS_clone, CLONE_CHILD_CLEARTID, CLONE_CHILD_SETTID,
99        CLONE_PTRACE, SIGCHLD,
100    };
101
102    let mut _ptid: pid_t = 0;
103    let mut _ctid: pid_t = 0;
104
105    // Check whether we're traced before we fork.
106    let being_traced = is_being_traced().unwrap_or(false);
107    let extra_flags = if being_traced { CLONE_PTRACE } else { 0 };
108
109    // Use the direct syscall interface into `clone()`.  This should replicate the parameters used
110    // for glibc `fork()`, except of course without calling the atfork handlers.
111    // One question is whether we're using the right set of flags.  For instance, does suppressing
112    // `SIGCHLD` here make it easier for us to handle some conditions in the parent process?
113    let res = unsafe {
114        syscall(
115            SYS_clone,
116            (CLONE_CHILD_CLEARTID | CLONE_CHILD_SETTID | SIGCHLD | extra_flags) as c_ulong,
117            std::ptr::null_mut::<c_void>(),
118            &mut _ptid as *mut pid_t,
119            &mut _ctid as *mut pid_t,
120            0 as c_ulong,
121        )
122    };
123
124    // The max value of a PID is configurable, but within an i32, so the failover
125    if (res as i64) > (pid_t::MAX as i64) {
126        pid_t::MAX
127    } else if (res as i64) < (pid_t::MIN as i64) {
128        pid_t::MIN
129    } else {
130        res as pid_t
131    }
132}
133
134#[cfg(target_os = "linux")]
135#[cfg(test)]
136mod tests {
137    use crate::unix_utils::fork::is_being_traced_internal;
138    use crate::unix_utils::fork::BUFFER_SIZE;
139    use std::fs::File;
140    use std::io::Seek;
141    use std::io::Write;
142
143    #[test]
144    fn test_is_being_traced_in_middle() {
145        let lines: &[&[u8]] = &[b"First:item\n", b"TracerPid: 2\n", b"Another: 21"];
146        let f = create_temp_file(lines);
147        assert!(is_being_traced_internal(f).unwrap_or(false))
148    }
149
150    #[test]
151    fn test_is_being_traced_at_the_end() {
152        let lines: &[&[u8]] = &[b"First:item\n", b"Another: 21\n", b"TracerPid: 2\n"];
153        let f = create_temp_file(lines);
154        assert!(is_being_traced_internal(f).unwrap_or(false))
155    }
156
157    #[test]
158    fn test_is_being_traced_at_the_end_with_no_newline_at_the_end() {
159        let lines: &[&[u8]] = &[b"First:item\n", b"Another: 21\n", b"TracerPid: 2"];
160        let f = create_temp_file(lines);
161        assert!(is_being_traced_internal(f).unwrap_or(false))
162    }
163
164    #[test]
165    fn test_is_being_traced_at_the_beginning() {
166        let lines: &[&[u8]] = &[b"TracerPid: 2\n", b"nFirst:item\n", b"Another: 21"];
167        let f = create_temp_file(lines);
168        assert!(is_being_traced_internal(f).unwrap_or(false))
169    }
170
171    #[test]
172    fn test_is_being_traced_with_first_string_larger_than_buffer() {
173        // Create a string larger than BUFFER_SIZE
174        let large_string = "A".repeat(BUFFER_SIZE + 20);
175        let lines: &[&[u8]] = &[
176            large_string.as_bytes(),
177            b"\n",
178            b"Another:12\n",
179            b"TracerPid: 42\n",
180        ];
181        let f = create_temp_file(lines);
182        assert!(is_being_traced_internal(f).unwrap_or(false))
183    }
184
185    #[test]
186    fn test_is_being_traced_with_marker_in_between_buffer_read() {
187        let large_string = "A".repeat(BUFFER_SIZE - 4);
188        let lines: &[&[u8]] = &[
189            large_string.as_bytes(),
190            b"\n",
191            b"TracerPid: 42\n",
192            b"AnotherItem: 42\n",
193        ];
194        let f = create_temp_file(lines);
195        assert!(is_being_traced_internal(f).unwrap_or(false))
196    }
197
198    #[test]
199    fn test_is_being_traced_with_value_zero() {
200        let lines: &[&[u8]] = &[b"First:item\n", b"TracerPid: 0\n", b"AnotherItem: 21\n"];
201        let f = create_temp_file(lines);
202        assert!(!is_being_traced_internal(f).unwrap_or(true))
203    }
204
205    #[test]
206    fn test_is_being_traced_with_no_tracerpid() {
207        let lines: &[&[u8]] = &[b"First:item\n", b"AnotherItem: 21\n"];
208        let f = create_temp_file(lines);
209        assert!(!is_being_traced_internal(f).unwrap_or(true))
210    }
211
212    #[test]
213    fn test_is_being_traced_with_very_large_content_and_no_tracerpid() {
214        // Create a file with a size twice as the buffer and no
215        let large_string = "A".repeat(2 * BUFFER_SIZE + 20);
216        let lines: &[&[u8]] = &[large_string.as_bytes(), b"\n", b"Another:12\n"];
217        let f = create_temp_file(lines);
218        assert!(!is_being_traced_internal(f).unwrap_or(true))
219    }
220
221    fn create_temp_file(lines: &[&[u8]]) -> File {
222        let mut f = tempfile::tempfile().unwrap();
223        for line in lines {
224            f.write_all(line).unwrap();
225        }
226
227        f.flush().unwrap();
228        f.rewind().unwrap();
229
230        f
231    }
232}