Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8fc888e

Browse files
committed
safe traversal: use the nix crate instead of unsafe calls to libc
1 parent 2bb097b commit 8fc888e

File tree

2 files changed

+92
-161
lines changed

2 files changed

+92
-161
lines changed

src/uucore/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,13 @@ fluent-bundle = { workspace = true }
8080
thiserror = { workspace = true }
8181
[target.'cfg(unix)'.dependencies]
8282
walkdir = { workspace = true, optional = true }
83-
nix = { workspace = true, features = ["fs", "uio", "zerocopy", "signal"] }
83+
nix = { workspace = true, features = [
84+
"fs",
85+
"uio",
86+
"zerocopy",
87+
"signal",
88+
"dir",
89+
] }
8490
xattr = { workspace = true, optional = true }
8591

8692
[dev-dependencies]

src/uucore/src/lib/features/safe_traversal.rs

Lines changed: 85 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
#[cfg(test)]
1717
use std::os::unix::ffi::OsStringExt;
1818

19-
use std::ffi::{CStr, CString, OsStr, OsString};
19+
use std::ffi::{CString, OsStr, OsString};
2020
use std::io;
2121
use std::os::unix::ffi::OsStrExt;
22-
use std::os::unix::io::{AsRawFd, RawFd};
22+
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
2323
use std::path::Path;
2424

25+
use nix::dir::Dir;
26+
use nix::fcntl::{OFlag, openat};
27+
use nix::sys::stat::{FileStat, Mode, fstatat};
28+
use nix::unistd::{UnlinkatFlags, unlinkat};
29+
2530
// Custom error types for better error reporting
2631
#[derive(thiserror::Error, Debug)]
2732
pub enum SafeTraversalError {
@@ -71,136 +76,83 @@ impl From<SafeTraversalError> for io::Error {
7176
}
7277
}
7378

74-
// RAII wrapper for DIR pointer
75-
struct Dir {
76-
dirp: *mut libc::DIR,
77-
}
78-
79-
impl Dir {
80-
fn from_fd(fd: RawFd) -> io::Result<Self> {
81-
let dirp = unsafe { libc::fdopendir(fd) };
82-
if dirp.is_null() {
83-
Err(io::Error::last_os_error())
84-
} else {
85-
Ok(Dir { dirp })
86-
}
87-
}
79+
// Helper function to read directory entries using nix
80+
fn read_dir_entries(fd: &OwnedFd) -> io::Result<Vec<OsString>> {
81+
let mut entries = Vec::new();
8882

89-
fn read_entries(&self) -> io::Result<Vec<OsString>> {
90-
let mut entries = Vec::new();
83+
// Duplicate the fd for Dir (it takes ownership)
84+
let dup_fd = nix::unistd::dup(fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
9185

92-
loop {
93-
// Clear errno before readdir as per POSIX requirements
94-
unsafe { *libc::__errno_location() = 0 };
86+
let mut dir = Dir::from_fd(dup_fd).map_err(|e| io::Error::from_raw_os_error(e as i32))?;
9587

96-
let entry = unsafe { libc::readdir(self.dirp) };
97-
if entry.is_null() {
98-
let errno = unsafe { *libc::__errno_location() };
99-
if errno != 0 {
100-
return Err(io::Error::from_raw_os_error(errno));
101-
}
102-
break;
103-
}
88+
for entry_result in dir.iter() {
89+
let entry = entry_result.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
10490

105-
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
106-
let name_os = OsStr::from_bytes(name.to_bytes());
91+
let name = entry.file_name();
92+
let name_os = OsStr::from_bytes(name.to_bytes());
10793

108-
if name_os != "." && name_os != ".." {
109-
entries.push(name_os.to_os_string());
110-
}
94+
if name_os != "." && name_os != ".." {
95+
entries.push(name_os.to_os_string());
11196
}
112-
113-
Ok(entries)
11497
}
115-
}
11698

117-
impl Drop for Dir {
118-
fn drop(&mut self) {
119-
if !self.dirp.is_null() {
120-
unsafe {
121-
libc::closedir(self.dirp);
122-
}
123-
}
124-
}
99+
Ok(entries)
125100
}
126101

127102
/// A directory file descriptor that enables safe traversal
128103
pub struct DirFd {
129-
fd: RawFd,
130-
owned: bool,
104+
fd: OwnedFd,
131105
}
132106

133107
impl DirFd {
134108
/// Open a directory and return a file descriptor
135109
pub fn open(path: &Path) -> io::Result<Self> {
136-
let path_cstr = CString::new(path.as_os_str().as_bytes())
137-
.map_err(|_| SafeTraversalError::PathContainsNull)?;
138-
139-
let fd = unsafe {
140-
libc::open(
141-
path_cstr.as_ptr(),
142-
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
143-
)
144-
};
145-
146-
if fd < 0 {
147-
Err(SafeTraversalError::OpenFailed {
110+
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
111+
let fd = nix::fcntl::open(path, flags, Mode::empty()).map_err(|e| {
112+
SafeTraversalError::OpenFailed {
148113
path: path.to_string_lossy().into_owned(),
149-
source: io::Error::last_os_error(),
114+
source: io::Error::from_raw_os_error(e as i32),
150115
}
151-
.into())
152-
} else {
153-
Ok(DirFd { fd, owned: true })
154-
}
116+
})?;
117+
118+
Ok(DirFd { fd })
155119
}
156120

157121
/// Open a subdirectory relative to this directory
158122
pub fn open_subdir(&self, name: &OsStr) -> io::Result<Self> {
159123
let name_cstr =
160124
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
161125

162-
let fd = unsafe {
163-
libc::openat(
164-
self.fd,
165-
name_cstr.as_ptr(),
166-
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
167-
)
168-
};
169-
170-
if fd < 0 {
171-
Err(SafeTraversalError::OpenFailed {
126+
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
127+
let fd = openat(&self.fd, name_cstr.as_c_str(), flags, Mode::empty()).map_err(|e| {
128+
SafeTraversalError::OpenFailed {
172129
path: name.to_string_lossy().into_owned(),
173-
source: io::Error::last_os_error(),
130+
source: io::Error::from_raw_os_error(e as i32),
174131
}
175-
.into())
176-
} else {
177-
Ok(DirFd { fd, owned: true })
178-
}
132+
})?;
133+
134+
Ok(DirFd { fd })
179135
}
180136

181137
/// Get raw stat data for a file relative to this directory
182-
pub fn stat_at(&self, name: &OsStr, follow_symlinks: bool) -> io::Result<libc::stat> {
138+
pub fn stat_at(&self, name: &OsStr, follow_symlinks: bool) -> io::Result<FileStat> {
183139
let name_cstr =
184140
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
185141

186-
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
187142
let flags = if follow_symlinks {
188-
0
143+
nix::fcntl::AtFlags::empty()
189144
} else {
190-
libc::AT_SYMLINK_NOFOLLOW
145+
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW
191146
};
192147

193-
let ret = unsafe { libc::fstatat(self.fd, name_cstr.as_ptr(), &mut stat, flags) };
194-
195-
if ret < 0 {
196-
Err(SafeTraversalError::StatFailed {
148+
let stat = fstatat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
149+
SafeTraversalError::StatFailed {
197150
path: name.to_string_lossy().into_owned(),
198-
source: io::Error::last_os_error(),
151+
source: io::Error::from_raw_os_error(e as i32),
199152
}
200-
.into())
201-
} else {
202-
Ok(stat)
203-
}
153+
})?;
154+
155+
Ok(stat)
204156
}
205157

206158
/// Get metadata for a file relative to this directory
@@ -214,43 +166,18 @@ impl DirFd {
214166
}
215167

216168
/// Get raw stat data for this directory
217-
pub fn fstat(&self) -> io::Result<libc::stat> {
218-
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
219-
220-
let ret = unsafe { libc::fstat(self.fd, &mut stat) };
169+
pub fn fstat(&self) -> io::Result<FileStat> {
170+
let stat = nix::sys::stat::fstat(&self.fd).map_err(|e| SafeTraversalError::StatFailed {
171+
path: "<current directory>".to_string(),
172+
source: io::Error::from_raw_os_error(e as i32),
173+
})?;
221174

222-
if ret < 0 {
223-
Err(SafeTraversalError::StatFailed {
224-
path: "<current directory>".to_string(),
225-
source: io::Error::last_os_error(),
226-
}
227-
.into())
228-
} else {
229-
Ok(stat)
230-
}
175+
Ok(stat)
231176
}
232177

233178
/// Read directory entries
234179
pub fn read_dir(&self) -> io::Result<Vec<OsString>> {
235-
// Duplicate the fd for fdopendir (it takes ownership)
236-
let dup_fd = unsafe { libc::dup(self.fd) };
237-
if dup_fd < 0 {
238-
return Err(SafeTraversalError::ReadDirFailed {
239-
path: "<directory>".to_string(),
240-
source: io::Error::last_os_error(),
241-
}
242-
.into());
243-
}
244-
245-
let dir = Dir::from_fd(dup_fd).map_err(|e| {
246-
unsafe { libc::close(dup_fd) };
247-
SafeTraversalError::ReadDirFailed {
248-
path: "<directory>".to_string(),
249-
source: e,
250-
}
251-
})?;
252-
253-
dir.read_entries().map_err(|e| {
180+
read_dir_entries(&self.fd).map_err(|e| {
254181
SafeTraversalError::ReadDirFailed {
255182
path: "<directory>".to_string(),
256183
source: e,
@@ -263,46 +190,45 @@ impl DirFd {
263190
pub fn unlink_at(&self, name: &OsStr, is_dir: bool) -> io::Result<()> {
264191
let name_cstr =
265192
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
266-
let flags = if is_dir { libc::AT_REMOVEDIR } else { 0 };
267-
268-
let ret = unsafe { libc::unlinkat(self.fd, name_cstr.as_ptr(), flags) };
193+
let flags = if is_dir {
194+
UnlinkatFlags::RemoveDir
195+
} else {
196+
UnlinkatFlags::NoRemoveDir
197+
};
269198

270-
if ret < 0 {
271-
Err(SafeTraversalError::UnlinkFailed {
199+
unlinkat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
200+
SafeTraversalError::UnlinkFailed {
272201
path: name.to_string_lossy().into_owned(),
273-
source: io::Error::last_os_error(),
202+
source: io::Error::from_raw_os_error(e as i32),
274203
}
275-
.into())
276-
} else {
277-
Ok(())
278-
}
204+
})?;
205+
206+
Ok(())
279207
}
280208

281-
/// Create a DirFd from an existing file descriptor (does not take ownership)
209+
/// Create a DirFd from an existing file descriptor (takes ownership)
282210
pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
283211
if fd < 0 {
284212
return Err(io::Error::new(
285213
io::ErrorKind::InvalidInput,
286214
"invalid file descriptor",
287215
));
288216
}
289-
Ok(DirFd { fd, owned: false })
217+
// SAFETY: We've verified fd >= 0, and the caller is transferring ownership
218+
let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) };
219+
Ok(DirFd { fd: owned_fd })
290220
}
291221
}
292222

293-
impl Drop for DirFd {
294-
fn drop(&mut self) {
295-
if self.owned && self.fd >= 0 {
296-
unsafe {
297-
libc::close(self.fd);
298-
}
299-
}
223+
impl AsRawFd for DirFd {
224+
fn as_raw_fd(&self) -> RawFd {
225+
self.fd.as_raw_fd()
300226
}
301227
}
302228

303-
impl AsRawFd for DirFd {
304-
fn as_raw_fd(&self) -> RawFd {
305-
self.fd
229+
impl AsFd for DirFd {
230+
fn as_fd(&self) -> BorrowedFd<'_> {
231+
self.fd.as_fd()
306232
}
307233
}
308234

@@ -374,11 +300,11 @@ impl FileType {
374300
/// Metadata wrapper for safer access to file information
375301
#[derive(Debug, Clone)]
376302
pub struct Metadata {
377-
stat: libc::stat,
303+
stat: FileStat,
378304
}
379305

380306
impl Metadata {
381-
pub fn from_stat(stat: libc::stat) -> Self {
307+
pub fn from_stat(stat: FileStat) -> Self {
382308
Self { stat }
383309
}
384310

@@ -410,11 +336,6 @@ impl Metadata {
410336
}
411337
}
412338

413-
/// Get the raw libc::stat for compatibility with existing code
414-
pub fn as_raw_stat(&self) -> &libc::stat {
415-
&self.stat
416-
}
417-
418339
/// Compatibility methods to match std::fs::Metadata interface
419340
pub fn is_dir(&self) -> bool {
420341
self.file_type().is_directory()
@@ -558,6 +479,7 @@ mod tests {
558479
use super::*;
559480
use std::fs;
560481
use std::os::unix::fs::symlink;
482+
use std::os::unix::io::IntoRawFd;
561483
use tempfile::TempDir;
562484

563485
#[test]
@@ -696,11 +618,16 @@ mod tests {
696618
fn test_from_raw_fd() {
697619
let temp_dir = TempDir::new().unwrap();
698620
let dir_fd = DirFd::open(temp_dir.path()).unwrap();
699-
let raw_fd = dir_fd.as_raw_fd();
700621

701-
let borrowed_fd = DirFd::from_raw_fd(raw_fd).unwrap();
702-
assert_eq!(borrowed_fd.as_raw_fd(), raw_fd);
703-
assert!(!borrowed_fd.owned); // Should not own the FD
622+
// Duplicate the fd first so we don't have ownership conflicts
623+
let dup_fd = nix::unistd::dup(&dir_fd).unwrap();
624+
let from_raw_fd = DirFd::from_raw_fd(dup_fd.into_raw_fd()).unwrap();
625+
626+
// Both should refer to the same directory
627+
let stat1 = dir_fd.fstat().unwrap();
628+
let stat2 = from_raw_fd.fstat().unwrap();
629+
assert_eq!(stat1.st_ino, stat2.st_ino);
630+
assert_eq!(stat1.st_dev, stat2.st_dev);
704631
}
705632

706633
#[test]
@@ -772,9 +699,7 @@ mod tests {
772699
assert_eq!(metadata.mode() & libc::S_IFMT as u32, libc::S_IFREG as u32);
773700
assert_eq!(metadata.nlink(), 1);
774701

775-
// Test raw stat access
776-
let raw_stat = metadata.as_raw_stat();
777-
assert_eq!(raw_stat.st_size, metadata.size() as i64);
702+
assert!(metadata.size() > 0);
778703
}
779704

780705
#[test]

0 commit comments

Comments
 (0)