safe_path/
pinned_path_buf.rs1use std::ffi::{CString, OsStr};
7use std::fs::{self, File, Metadata, OpenOptions};
8use std::io::{Error, ErrorKind, Result};
9use std::ops::Deref;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::fs::OpenOptionsExt;
12use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
13use std::path::{Component, Path, PathBuf};
14
15use crate::scoped_join;
16
17#[derive(Debug)]
54pub struct PinnedPathBuf {
55 handle: File,
56 path: PathBuf,
57 target: PathBuf,
58}
59
60impl PinnedPathBuf {
61 pub fn new<R: AsRef<Path>, U: AsRef<Path>>(root: R, path: U) -> Result<Self> {
65 let path = scoped_join(root, path)?;
66 Self::from_path(path)
67 }
68
69 pub fn from_path<P: AsRef<Path>>(orig_path: P) -> Result<Self> {
73 let orig_path = orig_path.as_ref();
74 let handle = Self::open_by_path(orig_path)?;
75 Self::new_from_file(handle, orig_path)
76 }
77
78 pub fn try_clone(&self) -> Result<Self> {
80 let fd = unsafe { libc::dup(self.path_fd()) };
81 if fd < 0 {
82 Err(Error::last_os_error())
83 } else {
84 Ok(Self {
85 handle: unsafe { File::from_raw_fd(fd) },
86 path: Self::get_proc_path(fd),
87 target: self.target.clone(),
88 })
89 }
90 }
91
92 pub fn path_fd(&self) -> RawFd {
100 self.handle.as_raw_fd()
101 }
102
103 pub fn as_path(&self) -> &Path {
105 self.path.as_path()
106 }
107
108 pub fn target(&self) -> &Path {
116 &self.target
117 }
118
119 pub fn metadata(&self) -> Result<Metadata> {
121 self.handle.metadata()
122 }
123
124 pub fn open_child(&self, path_comp: &OsStr) -> Result<Self> {
126 let name = Self::prepare_path_component(path_comp)?;
127 let oflags = libc::O_PATH | libc::O_CLOEXEC;
128 let res = unsafe { libc::openat(self.path_fd(), name.as_ptr(), oflags, 0) };
129 if res < 0 {
130 Err(Error::last_os_error())
131 } else {
132 let handle = unsafe { File::from_raw_fd(res) };
133 Self::new_from_file(handle, self.target.join(path_comp))
134 }
135 }
136
137 pub fn mkdir(&self, path_comp: &OsStr, mode: libc::mode_t) -> Result<Self> {
139 let path_name = Self::prepare_path_component(path_comp)?;
140 let res = unsafe { libc::mkdirat(self.handle.as_raw_fd(), path_name.as_ptr(), mode) };
141 if res < 0 {
142 Err(Error::last_os_error())
143 } else {
144 self.open_child(path_comp)
145 }
146 }
147
148 fn open_by_path<P: AsRef<Path>>(path: P) -> Result<File> {
154 let o_flags = libc::O_PATH | libc::O_CLOEXEC;
157 OpenOptions::new()
158 .read(true)
159 .custom_flags(o_flags)
160 .open(path.as_ref())
161 }
162
163 fn get_proc_path<F: AsRawFd>(file: F) -> PathBuf {
164 PathBuf::from(format!("/proc/self/fd/{}", file.as_raw_fd()))
165 }
166
167 fn new_from_file<P: AsRef<Path>>(handle: File, orig_path: P) -> Result<Self> {
168 let path = Self::get_proc_path(handle.as_raw_fd());
169 let link_path = fs::read_link(path.as_path())?;
170 if link_path != orig_path.as_ref() {
171 Err(Error::new(
172 ErrorKind::Other,
173 format!(
174 "Path changed from {} to {} on open, possible attack",
175 orig_path.as_ref().display(),
176 link_path.display()
177 ),
178 ))
179 } else {
180 Ok(PinnedPathBuf {
181 handle,
182 path,
183 target: link_path,
184 })
185 }
186 }
187
188 #[inline]
189 fn prepare_path_component(path_comp: &OsStr) -> Result<CString> {
190 let path = Path::new(path_comp);
191 let mut comps = path.components();
192 let name = comps.next();
193 if !matches!(name, Some(Component::Normal(_))) || comps.next().is_some() {
194 return Err(Error::new(
195 ErrorKind::Other,
196 format!("Path component {} is invalid", path_comp.to_string_lossy()),
197 ));
198 }
199 let name = name.unwrap();
200 if name.as_os_str() != path_comp {
201 return Err(Error::new(
202 ErrorKind::Other,
203 format!("Path component {} is invalid", path_comp.to_string_lossy()),
204 ));
205 }
206
207 CString::new(path_comp.as_bytes()).map_err(|_e| {
208 Error::new(
209 ErrorKind::Other,
210 format!("Path component {} is invalid", path_comp.to_string_lossy()),
211 )
212 })
213 }
214}
215
216impl Deref for PinnedPathBuf {
217 type Target = PathBuf;
218
219 fn deref(&self) -> &Self::Target {
220 &self.path
221 }
222}
223
224impl AsRef<Path> for PinnedPathBuf {
225 fn as_ref(&self) -> &Path {
226 self.path.as_path()
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use std::ffi::OsString;
234 use std::fs::DirBuilder;
235 use std::io::Write;
236 use std::os::unix::fs::{symlink, MetadataExt};
237 use std::sync::{Arc, Barrier};
238 use std::thread;
239
240 #[test]
241 fn test_pinned_path_buf() {
242 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
244 DirBuilder::new()
245 .create(rootfs_dir.path().join("b"))
246 .unwrap();
247 symlink(rootfs_dir.path().join("b"), rootfs_dir.path().join("a")).unwrap();
248 let rootfs_path = &rootfs_dir.path().join("a");
249
250 fs::create_dir(rootfs_path.join("symlink_dir")).unwrap();
252 symlink("/endpoint", rootfs_path.join("symlink_dir/endpoint")).unwrap();
253 fs::write(rootfs_path.join("endpoint"), "test").unwrap();
254
255 let path = PinnedPathBuf::new(rootfs_path.to_path_buf(), "symlink_dir/endpoint").unwrap();
257 assert!(!path.is_dir());
258 let path_ref = path.deref();
259 let target = fs::read_link(path_ref).unwrap();
260 assert_eq!(target, rootfs_path.join("endpoint").canonicalize().unwrap());
261 let content = fs::read_to_string(&path).unwrap();
262 assert_eq!(&content, "test");
263
264 fs::remove_file(&target).unwrap();
266 fs::read_to_string(&target).unwrap_err();
267 let content = fs::read_to_string(&path).unwrap();
268 assert_eq!(&content, "test");
269 }
270
271 #[test]
272 fn test_pinned_path_buf_race() {
273 let root_dir = tempfile::tempdir().expect("failed to create tmpdir");
274 let root_path = root_dir.path();
275 let barrier = Arc::new(Barrier::new(2));
276
277 fs::write(root_path.join("a"), b"a").unwrap();
278 fs::write(root_path.join("b"), b"b").unwrap();
279 fs::write(root_path.join("c"), b"c").unwrap();
280 symlink("a", root_path.join("s")).unwrap();
281
282 let root_path2 = root_path.to_path_buf();
283 let barrier2 = barrier.clone();
284 let thread = thread::spawn(move || {
285 barrier2.wait();
287 fs::remove_file(root_path2.join("a")).unwrap();
288 symlink("b", root_path2.join("a")).unwrap();
289 barrier2.wait();
290
291 barrier2.wait();
293 fs::remove_file(root_path2.join("b")).unwrap();
294 symlink("c", root_path2.join("b")).unwrap();
295 barrier2.wait();
296 });
297
298 let path = scoped_join(&root_path, "s").unwrap();
299 let data = fs::read_to_string(&path).unwrap();
300 assert_eq!(&data, "a");
301 assert!(path.is_file());
302 barrier.wait();
303 barrier.wait();
304 let data = fs::read_to_string(&path).unwrap();
306 assert_eq!(&data, "b");
307 PinnedPathBuf::from_path(&path).unwrap_err();
308
309 let pinned_path = PinnedPathBuf::new(&root_path, "s").unwrap();
310 let data = fs::read_to_string(&pinned_path).unwrap();
311 assert_eq!(&data, "b");
312
313 barrier.wait();
315 barrier.wait();
316 let data = fs::read_to_string(&pinned_path).unwrap();
318 assert_eq!(&data, "b");
319
320 thread.join().unwrap();
321 }
322
323 #[test]
324 fn test_new_pinned_path_buf() {
325 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
326 let rootfs_path = rootfs_dir.path();
327 let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
328 let _ = OpenOptions::new().read(true).open(&path).unwrap();
329 }
330
331 #[test]
332 fn test_pinned_path_try_clone() {
333 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
334 let rootfs_path = rootfs_dir.path();
335 let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
336 let path2 = path.try_clone().unwrap();
337 assert_ne!(path.as_path(), path2.as_path());
338 }
339
340 #[test]
341 fn test_new_pinned_path_buf_from_nonexist_file() {
342 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
343 let rootfs_path = rootfs_dir.path();
344 PinnedPathBuf::new(rootfs_path, "does_not_exist").unwrap_err();
345 }
346
347 #[test]
348 fn test_new_pinned_path_buf_without_read_perm() {
349 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
350 let rootfs_path = rootfs_dir.path();
351 let path = rootfs_path.join("write_only_file");
352
353 let mut file = OpenOptions::new()
354 .read(false)
355 .write(true)
356 .create(true)
357 .mode(0o200)
358 .open(&path)
359 .unwrap();
360 file.write_all(&[0xa5u8]).unwrap();
361 let md = fs::metadata(&path).unwrap();
362 let umask = unsafe { libc::umask(0022) };
363 unsafe { libc::umask(umask) };
364 assert_eq!(md.mode() & 0o700, 0o200 & !umask);
365 PinnedPathBuf::from_path(&path).unwrap();
366 }
367
368 #[test]
369 fn test_pinned_path_buf_path_fd() {
370 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
371 let rootfs_path = rootfs_dir.path();
372 let path = rootfs_path.join("write_only_file");
373
374 let mut file = OpenOptions::new()
375 .read(false)
376 .write(true)
377 .create(true)
378 .mode(0o200)
379 .open(&path)
380 .unwrap();
381 file.write_all(&[0xa5u8]).unwrap();
382 let handle = PinnedPathBuf::from_path(&path).unwrap();
383 let fd = handle.path_fd();
385 let mut stat: libc::stat = unsafe { std::mem::zeroed() };
386 let res = unsafe { libc::fstat(fd, &mut stat as *mut _) };
387 assert_eq!(res, 0);
388 }
389
390 #[test]
391 fn test_pinned_path_buf_open_child() {
392 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
393 let rootfs_path = rootfs_dir.path();
394 let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
395
396 fs::write(path.join("child"), "test").unwrap();
397 let path = path.open_child(OsStr::new("child")).unwrap();
398 let content = fs::read_to_string(&path).unwrap();
399 assert_eq!(&content, "test");
400
401 path.open_child(&OsString::from("__does_not_exist__"))
402 .unwrap_err();
403 path.open_child(&OsString::from("test/a")).unwrap_err();
404 }
405
406 #[test]
407 fn test_prepare_path_component() {
408 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("")).is_err());
409 assert!(PinnedPathBuf::prepare_path_component(&OsString::from(".")).is_err());
410 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("..")).is_err());
411 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("/")).is_err());
412 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("//")).is_err());
413 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/b")).is_err());
414 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("./b")).is_err());
415 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/.")).is_err());
416 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/..")).is_err());
417 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/./")).is_err());
418 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/../")).is_err());
419 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/./a")).is_err());
420 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/../a")).is_err());
421
422 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a")).is_ok());
423 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a.b")).is_ok());
424 assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a..b")).is_ok());
425 }
426
427 #[test]
428 fn test_target_fs_object_changed() {
429 let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
430 let rootfs_path = rootfs_dir.path();
431 let file = rootfs_path.join("child");
432 fs::write(&file, "test").unwrap();
433
434 let path = PinnedPathBuf::from_path(&file).unwrap();
435 let path3 = fs::read_link(path.as_path()).unwrap();
436 assert_eq!(&path3, path.target());
437 fs::rename(file, rootfs_path.join("child2")).unwrap();
438 let path4 = fs::read_link(path.as_path()).unwrap();
439 assert_ne!(&path4, path.target());
440 fs::remove_file(rootfs_path.join("child2")).unwrap();
441 let path5 = fs::read_link(path.as_path()).unwrap();
442 assert_ne!(&path4, &path5);
443 }
444}