Skip to content

Commit 07ddde1

Browse files
committed
safe traversal: use the nix crate instead of unsafe calls to libc
1 parent a7a5126 commit 07ddde1

File tree

2 files changed

+86
-160
lines changed

2 files changed

+86
-160
lines changed

src/uucore/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ 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 = ["fs", "uio", "zerocopy", "signal", "dir"] }
8484
xattr = { workspace = true, optional = true }
8585

8686
[dev-dependencies]

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

Lines changed: 85 additions & 159 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@
99
#[cfg(test)]
1010
use std::os::unix::ffi::OsStringExt;
1111

12-
use std::ffi::{CStr, CString, OsStr, OsString};
12+
use std::ffi::{CString, OsStr, OsString};
1313
use std::io;
1414
use std::os::unix::ffi::OsStrExt;
15-
use std::os::unix::io::{AsRawFd, RawFd};
15+
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
1616
use std::path::Path;
1717

18+
use nix::dir::Dir;
19+
use nix::fcntl::{OFlag, openat};
20+
use nix::sys::stat::{FileStat, Mode, fstatat};
21+
use nix::unistd::{UnlinkatFlags, unlinkat};
22+
1823
// Custom error types for better error reporting
1924
#[derive(thiserror::Error, Debug)]
2025
pub enum SafeTraversalError {
@@ -64,88 +69,48 @@ impl From<SafeTraversalError> for io::Error {
6469
}
6570
}
6671

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

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

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

89-
let entry = unsafe { libc::readdir(self.dirp) };
90-
if entry.is_null() {
91-
let errno = unsafe { *libc::__errno_location() };
92-
if errno != 0 {
93-
return Err(io::Error::from_raw_os_error(errno));
94-
}
95-
break;
96-
}
81+
for entry_result in dir.iter() {
82+
let entry = entry_result.map_err(|e| io::Error::from_raw_os_error(e as i32))?;
9783

98-
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
99-
let name_os = OsStr::from_bytes(name.to_bytes());
84+
let name = entry.file_name();
85+
let name_os = OsStr::from_bytes(name.to_bytes());
10086

101-
if name_os != "." && name_os != ".." {
102-
entries.push(name_os.to_os_string());
103-
}
87+
if name_os != "." && name_os != ".." {
88+
entries.push(name_os.to_os_string());
10489
}
105-
106-
Ok(entries)
10790
}
108-
}
10991

110-
impl Drop for Dir {
111-
fn drop(&mut self) {
112-
if !self.dirp.is_null() {
113-
unsafe {
114-
libc::closedir(self.dirp);
115-
}
116-
}
117-
}
92+
Ok(entries)
11893
}
11994

12095
/// A directory file descriptor that enables safe traversal
12196
pub struct DirFd {
122-
fd: RawFd,
123-
owned: bool,
97+
fd: OwnedFd,
12498
}
12599

126100
impl DirFd {
127101
/// Open a directory and return a file descriptor
128102
pub fn open(path: &Path) -> io::Result<Self> {
129103
let path_str = path.to_string_lossy();
130-
let path_cstr = CString::new(path.as_os_str().as_bytes())
131-
.map_err(|_| SafeTraversalError::PathContainsNull)?;
132-
133-
let fd = unsafe {
134-
libc::open(
135-
path_cstr.as_ptr(),
136-
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
137-
)
138-
};
139104

140-
if fd < 0 {
141-
Err(SafeTraversalError::OpenFailed {
105+
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
106+
let fd = nix::fcntl::open(path, flags, Mode::empty()).map_err(|e| {
107+
SafeTraversalError::OpenFailed {
142108
path: path_str.to_string(),
143-
source: io::Error::last_os_error(),
109+
source: io::Error::from_raw_os_error(e as i32),
144110
}
145-
.into())
146-
} else {
147-
Ok(DirFd { fd, owned: true })
148-
}
111+
})?;
112+
113+
Ok(DirFd { fd })
149114
}
150115

151116
/// Open a subdirectory relative to this directory
@@ -154,49 +119,37 @@ impl DirFd {
154119
let name_cstr =
155120
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
156121

157-
let fd = unsafe {
158-
libc::openat(
159-
self.fd,
160-
name_cstr.as_ptr(),
161-
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
162-
)
163-
};
164-
165-
if fd < 0 {
166-
Err(SafeTraversalError::OpenFailed {
122+
let flags = OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC;
123+
let fd = openat(&self.fd, name_cstr.as_c_str(), flags, Mode::empty()).map_err(|e| {
124+
SafeTraversalError::OpenFailed {
167125
path: name_str.to_string(),
168-
source: io::Error::last_os_error(),
126+
source: io::Error::from_raw_os_error(e as i32),
169127
}
170-
.into())
171-
} else {
172-
Ok(DirFd { fd, owned: true })
173-
}
128+
})?;
129+
130+
Ok(DirFd { fd })
174131
}
175132

176133
/// Get raw stat data for a file relative to this directory
177-
pub fn stat_at(&self, name: &OsStr, follow_symlinks: bool) -> io::Result<libc::stat> {
134+
pub fn stat_at(&self, name: &OsStr, follow_symlinks: bool) -> io::Result<FileStat> {
178135
let name_str = name.to_string_lossy();
179136
let name_cstr =
180137
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
181138

182-
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
183139
let flags = if follow_symlinks {
184-
0
140+
nix::fcntl::AtFlags::empty()
185141
} else {
186-
libc::AT_SYMLINK_NOFOLLOW
142+
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW
187143
};
188144

189-
let ret = unsafe { libc::fstatat(self.fd, name_cstr.as_ptr(), &mut stat, flags) };
190-
191-
if ret < 0 {
192-
Err(SafeTraversalError::StatFailed {
145+
let stat = fstatat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
146+
SafeTraversalError::StatFailed {
193147
path: name_str.to_string(),
194-
source: io::Error::last_os_error(),
148+
source: io::Error::from_raw_os_error(e as i32),
195149
}
196-
.into())
197-
} else {
198-
Ok(stat)
199-
}
150+
})?;
151+
152+
Ok(stat)
200153
}
201154

202155
/// Get metadata for a file relative to this directory
@@ -210,43 +163,18 @@ impl DirFd {
210163
}
211164

212165
/// Get raw stat data for this directory
213-
pub fn fstat(&self) -> io::Result<libc::stat> {
214-
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
215-
216-
let ret = unsafe { libc::fstat(self.fd, &mut stat) };
166+
pub fn fstat(&self) -> io::Result<FileStat> {
167+
let stat = nix::sys::stat::fstat(&self.fd).map_err(|e| SafeTraversalError::StatFailed {
168+
path: "<current directory>".to_string(),
169+
source: io::Error::from_raw_os_error(e as i32),
170+
})?;
217171

218-
if ret < 0 {
219-
Err(SafeTraversalError::StatFailed {
220-
path: "<current directory>".to_string(),
221-
source: io::Error::last_os_error(),
222-
}
223-
.into())
224-
} else {
225-
Ok(stat)
226-
}
172+
Ok(stat)
227173
}
228174

229175
/// Read directory entries
230176
pub fn read_dir(&self) -> io::Result<Vec<OsString>> {
231-
// Duplicate the fd for fdopendir (it takes ownership)
232-
let dup_fd = unsafe { libc::dup(self.fd) };
233-
if dup_fd < 0 {
234-
return Err(SafeTraversalError::ReadDirFailed {
235-
path: "<directory>".to_string(),
236-
source: io::Error::last_os_error(),
237-
}
238-
.into());
239-
}
240-
241-
let dir = Dir::from_fd(dup_fd).map_err(|e| {
242-
unsafe { libc::close(dup_fd) };
243-
SafeTraversalError::ReadDirFailed {
244-
path: "<directory>".to_string(),
245-
source: e,
246-
}
247-
})?;
248-
249-
dir.read_entries().map_err(|e| {
177+
read_dir_entries(&self.fd).map_err(|e| {
250178
SafeTraversalError::ReadDirFailed {
251179
path: "<directory>".to_string(),
252180
source: e,
@@ -260,46 +188,45 @@ impl DirFd {
260188
let name_str = name.to_string_lossy();
261189
let name_cstr =
262190
CString::new(name.as_bytes()).map_err(|_| SafeTraversalError::PathContainsNull)?;
263-
let flags = if is_dir { libc::AT_REMOVEDIR } else { 0 };
264-
265-
let ret = unsafe { libc::unlinkat(self.fd, name_cstr.as_ptr(), flags) };
191+
let flags = if is_dir {
192+
UnlinkatFlags::RemoveDir
193+
} else {
194+
UnlinkatFlags::NoRemoveDir
195+
};
266196

267-
if ret < 0 {
268-
Err(SafeTraversalError::UnlinkFailed {
197+
unlinkat(&self.fd, name_cstr.as_c_str(), flags).map_err(|e| {
198+
SafeTraversalError::UnlinkFailed {
269199
path: name_str.to_string(),
270-
source: io::Error::last_os_error(),
200+
source: io::Error::from_raw_os_error(e as i32),
271201
}
272-
.into())
273-
} else {
274-
Ok(())
275-
}
202+
})?;
203+
204+
Ok(())
276205
}
277206

278-
/// Create a DirFd from an existing file descriptor (does not take ownership)
207+
/// Create a DirFd from an existing file descriptor (takes ownership)
279208
pub fn from_raw_fd(fd: RawFd) -> io::Result<Self> {
280209
if fd < 0 {
281210
return Err(io::Error::new(
282211
io::ErrorKind::InvalidInput,
283212
"invalid file descriptor",
284213
));
285214
}
286-
Ok(DirFd { fd, owned: false })
215+
// SAFETY: We've verified fd >= 0, and the caller is transferring ownership
216+
let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) };
217+
Ok(DirFd { fd: owned_fd })
287218
}
288219
}
289220

290-
impl Drop for DirFd {
291-
fn drop(&mut self) {
292-
if self.owned && self.fd >= 0 {
293-
unsafe {
294-
libc::close(self.fd);
295-
}
296-
}
221+
impl AsRawFd for DirFd {
222+
fn as_raw_fd(&self) -> RawFd {
223+
self.fd.as_raw_fd()
297224
}
298225
}
299226

300-
impl AsRawFd for DirFd {
301-
fn as_raw_fd(&self) -> RawFd {
302-
self.fd
227+
impl AsFd for DirFd {
228+
fn as_fd(&self) -> BorrowedFd<'_> {
229+
self.fd.as_fd()
303230
}
304231
}
305232

@@ -371,11 +298,11 @@ impl FileType {
371298
/// Metadata wrapper for safer access to file information
372299
#[derive(Debug, Clone)]
373300
pub struct Metadata {
374-
stat: libc::stat,
301+
stat: FileStat,
375302
}
376303

377304
impl Metadata {
378-
pub fn from_stat(stat: libc::stat) -> Self {
305+
pub fn from_stat(stat: FileStat) -> Self {
379306
Self { stat }
380307
}
381308

@@ -407,11 +334,6 @@ impl Metadata {
407334
}
408335
}
409336

410-
/// Get the raw libc::stat for compatibility with existing code
411-
pub fn as_raw_stat(&self) -> &libc::stat {
412-
&self.stat
413-
}
414-
415337
/// Compatibility methods to match std::fs::Metadata interface
416338
pub fn is_dir(&self) -> bool {
417339
self.file_type().is_directory()
@@ -556,6 +478,7 @@ mod tests {
556478
use super::*;
557479
use std::fs;
558480
use std::os::unix::fs::symlink;
481+
use std::os::unix::io::IntoRawFd;
559482
use tempfile::TempDir;
560483

561484
#[test]
@@ -694,11 +617,16 @@ mod tests {
694617
fn test_from_raw_fd() {
695618
let temp_dir = TempDir::new().unwrap();
696619
let dir_fd = DirFd::open(temp_dir.path()).unwrap();
697-
let raw_fd = dir_fd.as_raw_fd();
698620

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

704632
#[test]
@@ -770,9 +698,7 @@ mod tests {
770698
assert_eq!(metadata.mode() & libc::S_IFMT as u32, libc::S_IFREG as u32);
771699
assert_eq!(metadata.nlink(), 1);
772700

773-
// Test raw stat access
774-
let raw_stat = metadata.as_raw_stat();
775-
assert_eq!(raw_stat.st_size, metadata.size() as i64);
701+
assert!(metadata.size() > 0);
776702
}
777703

778704
#[test]

0 commit comments

Comments
 (0)