Skip to content

Commit f39ba10

Browse files
committed
Move inherited_fds module from rustutils library.
1 parent 35e63d0 commit f39ba10

2 files changed

Lines changed: 277 additions & 0 deletions

File tree

src/inherited.rs

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

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
//! child.wait().unwrap();
5151
//! ```
5252
53+
pub mod inherited;
5354
#[cfg(feature = "tokio")]
5455
pub mod tokio;
5556

0 commit comments

Comments
 (0)