Skip to main content

command_fds/
inherited.rs

1// Copyright 2024, The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utilities for safely obtaining `OwnedFd`s for inherited file descriptors.
16
17use nix::{
18    fcntl::{F_SETFD, FdFlag, fcntl},
19    libc,
20};
21use std::{
22    collections::HashMap,
23    fs::{canonicalize, read_dir},
24    os::fd::{FromRawFd, OwnedFd, RawFd},
25    sync::{Mutex, OnceLock},
26};
27use thiserror::Error;
28
29static INHERITED_FDS: OnceLock<Mutex<HashMap<RawFd, Option<OwnedFd>>>> = OnceLock::new();
30
31/// Errors that can occur while taking an ownership of `RawFd`
32#[derive(Debug, PartialEq, Error)]
33pub enum InheritedFdError {
34    /// init_inherited_fds() not called
35    #[error("init_inherited_fds() not called")]
36    NotInitialized,
37
38    /// Ownership already taken
39    #[error("Ownership of FD {0} is already taken")]
40    OwnershipTaken(RawFd),
41
42    /// Not an inherited file descriptor
43    #[error("FD {0} is either invalid file descriptor or not an inherited one")]
44    FileDescriptorNotInherited(RawFd),
45}
46
47/// Takes ownership of all open file descriptors in this process other than standard
48/// input/output/error, so that they can later be obtained by calling [`take_fd_ownership`].
49///
50/// Sets the `FD_CLOEXEC` flag on all of these file descriptors.
51///
52/// # Safety
53///
54/// This must be called very early in the program, before the ownership of any file descriptors
55/// (except stdin/out/err) is taken.
56pub unsafe fn init_inherited_fds() -> Result<(), std::io::Error> {
57    let mut fds = HashMap::new();
58
59    let fd_path = canonicalize("/proc/self/fd")?;
60
61    for entry in read_dir(&fd_path)? {
62        let entry = entry?;
63
64        // Files in /prod/self/fd are guaranteed to be numbers. So parsing is always successful.
65        let file_name = entry.file_name();
66        let raw_fd = file_name.to_str().unwrap().parse::<RawFd>().unwrap();
67
68        // We don't take ownership of the stdio FDs as the Rust runtime owns them.
69        if [libc::STDIN_FILENO, libc::STDOUT_FILENO, libc::STDERR_FILENO].contains(&raw_fd) {
70            continue;
71        }
72
73        // Exceptional case: /proc/self/fd/* may be a dir fd created by read_dir just above. Since
74        // the file descriptor is owned by read_dir (and thus closed by it), we shouldn't take
75        // ownership to it.
76        if entry.path().read_link()? == fd_path {
77            continue;
78        }
79
80        // SAFETY: /proc/self/fd/* are file descriptors that are open. If `init_inherited_fds()` was
81        // called at the very beginning of the program execution (as requested by the safety
82        // requirement of this function), this is the first time to claim the ownership of these
83        // file descriptors.
84        let owned_fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
85
86        fcntl(&owned_fd, F_SETFD(FdFlag::FD_CLOEXEC))?;
87        fds.insert(raw_fd, Some(owned_fd));
88    }
89
90    INHERITED_FDS
91        .set(Mutex::new(fds))
92        .or(Err(std::io::Error::other(
93            "Inherited fds were already initialized",
94        )))
95}
96
97/// Takes the ownership of the given `RawFd` and returns an `OwnedFd` for it.
98///
99/// The returned FD will have the `FD_CLOEXEC` flag set.
100///
101/// An error is returned when the ownership was already taken (by a prior call to this
102/// function with the same `RawFd`) or `RawFd` is not an inherited file descriptor.
103pub fn take_fd_ownership(raw_fd: RawFd) -> Result<OwnedFd, InheritedFdError> {
104    let mut fds = INHERITED_FDS
105        .get()
106        .ok_or(InheritedFdError::NotInitialized)?
107        .lock()
108        .unwrap();
109
110    if let Some(value) = fds.get_mut(&raw_fd) {
111        if let Some(owned_fd) = value.take() {
112            Ok(owned_fd)
113        } else {
114            Err(InheritedFdError::OwnershipTaken(raw_fd))
115        }
116    } else {
117        Err(InheritedFdError::FileDescriptorNotInherited(raw_fd))
118    }
119}
120
121#[cfg(test)]
122mod test {
123    use super::*;
124    use nix::unistd::close;
125    use std::{
126        io,
127        os::fd::{AsRawFd, IntoRawFd},
128    };
129    use tempfile::tempfile;
130
131    struct Fixture {
132        fds: Vec<RawFd>,
133    }
134
135    impl Fixture {
136        fn setup(num_fds: usize) -> Result<Self, io::Error> {
137            let mut fds = Vec::new();
138            for _ in 0..num_fds {
139                fds.push(tempfile()?.into_raw_fd());
140            }
141            Ok(Fixture { fds })
142        }
143
144        fn open_new_file(&mut self) -> Result<RawFd, io::Error> {
145            let raw_fd = tempfile()?.into_raw_fd();
146            self.fds.push(raw_fd);
147            Ok(raw_fd)
148        }
149    }
150
151    impl Drop for Fixture {
152        fn drop(&mut self) {
153            self.fds.iter().for_each(|fd| {
154                let _ = close(*fd);
155            });
156        }
157    }
158
159    fn is_fd_opened(raw_fd: RawFd) -> bool {
160        unsafe { libc::fcntl(raw_fd, libc::F_GETFD) != -1 }
161    }
162
163    #[test]
164    fn happy_case() {
165        let fixture = Fixture::setup(2).unwrap();
166        let f0 = fixture.fds[0];
167        let f1 = fixture.fds[1];
168
169        // SAFETY: assume files opened by Fixture are inherited ones
170        unsafe {
171            init_inherited_fds().unwrap();
172        }
173
174        let f0_owned = take_fd_ownership(f0).unwrap();
175        let f1_owned = take_fd_ownership(f1).unwrap();
176        assert_eq!(f0, f0_owned.as_raw_fd());
177        assert_eq!(f1, f1_owned.as_raw_fd());
178
179        drop(f0_owned);
180        drop(f1_owned);
181        assert!(!is_fd_opened(f0));
182        assert!(!is_fd_opened(f1));
183    }
184
185    #[test]
186    fn access_non_inherited_fd() {
187        let mut fixture = Fixture::setup(2).unwrap();
188
189        // SAFETY: assume files opened by Fixture are inherited ones
190        unsafe {
191            init_inherited_fds().unwrap();
192        }
193
194        let f = fixture.open_new_file().unwrap();
195        assert_eq!(
196            take_fd_ownership(f).err(),
197            Some(InheritedFdError::FileDescriptorNotInherited(f))
198        );
199    }
200
201    #[test]
202    fn call_init_inherited_fds_multiple_times() {
203        let _ = Fixture::setup(2).unwrap();
204
205        // SAFETY: assume files opened by Fixture are inherited ones
206        unsafe {
207            init_inherited_fds().unwrap();
208        }
209
210        // SAFETY: for testing
211        let res = unsafe { init_inherited_fds() };
212        assert!(res.is_err());
213    }
214
215    #[test]
216    fn access_without_init_inherited_fds() {
217        let fixture = Fixture::setup(2).unwrap();
218
219        let f = fixture.fds[0];
220        assert_eq!(
221            take_fd_ownership(f).err(),
222            Some(InheritedFdError::NotInitialized)
223        );
224    }
225
226    #[test]
227    fn double_ownership() {
228        let fixture = Fixture::setup(2).unwrap();
229        let f = fixture.fds[0];
230
231        // SAFETY: assume files opened by Fixture are inherited ones
232        unsafe {
233            init_inherited_fds().unwrap();
234        }
235
236        let f_owned = take_fd_ownership(f).unwrap();
237        let f_double_owned = take_fd_ownership(f);
238        assert_eq!(
239            f_double_owned.err(),
240            Some(InheritedFdError::OwnershipTaken(f)),
241        );
242
243        // just to highlight that f_owned is kept alive when the second call to take_fd_ownership
244        // is made.
245        drop(f_owned);
246    }
247
248    #[test]
249    fn take_drop_retake() {
250        let fixture = Fixture::setup(2).unwrap();
251        let f = fixture.fds[0];
252
253        // SAFETY: assume files opened by Fixture are inherited ones
254        unsafe {
255            init_inherited_fds().unwrap();
256        }
257
258        let f_owned = take_fd_ownership(f).unwrap();
259        drop(f_owned);
260
261        let f_double_owned = take_fd_ownership(f);
262        assert_eq!(
263            f_double_owned.err(),
264            Some(InheritedFdError::OwnershipTaken(f)),
265        );
266    }
267
268    #[test]
269    fn cloexec() {
270        let fixture = Fixture::setup(2).unwrap();
271        let f = fixture.fds[0];
272
273        let res = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_SETFD, 0) };
274        assert_ne!(res, -1);
275
276        // SAFETY: assume files opened by Fixture are inherited ones
277        unsafe {
278            init_inherited_fds().unwrap();
279        }
280
281        // SAFETY: F_GETFD doesn't need any extra parameters.
282        let flags = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_GETFD) };
283        assert_ne!(flags, -1);
284        // FD_CLOEXEC should be set by init_inherited_fds
285        assert_eq!(flags, FdFlag::FD_CLOEXEC.bits());
286    }
287}