1#![allow(clippy::std_instead_of_alloc, clippy::std_instead_of_core)]
25#![allow(clippy::unnecessary_cast)]
28
29use std::ffi::CString;
30use std::fs::File;
31use std::io;
32use std::os::unix::ffi::OsStrExt;
33use std::path::Path;
34use std::sync::atomic::{AtomicBool, Ordering};
35
36static RESTRICT_WORKER_FILE_OUTPUTS: AtomicBool = AtomicBool::new(false);
40
41pub fn set_restrict_worker_file_outputs(on: bool) {
43 RESTRICT_WORKER_FILE_OUTPUTS.store(on, Ordering::Relaxed);
44}
45
46pub fn worker_file_outputs_restricted() -> bool {
48 RESTRICT_WORKER_FILE_OUTPUTS.load(Ordering::Relaxed)
49}
50
51#[derive(Debug, thiserror::Error)]
53pub enum RestrictedOpenError {
54 #[error("constrained file access is not supported on this kernel")]
56 Unsupported,
57 #[error("refused worker-selected path: {0}")]
59 Rejected(&'static str),
60 #[error(transparent)]
62 Io(#[from] io::Error),
63}
64
65impl From<RestrictedOpenError> for io::Error {
66 fn from(value: RestrictedOpenError) -> Self {
67 match value {
68 RestrictedOpenError::Io(e) => e,
69 RestrictedOpenError::Unsupported => {
70 io::Error::new(io::ErrorKind::Unsupported, value.to_string())
71 }
72 RestrictedOpenError::Rejected(_) => {
73 io::Error::new(io::ErrorKind::PermissionDenied, value.to_string())
74 }
75 }
76 }
77}
78
79type Result<T> = std::result::Result<T, RestrictedOpenError>;
80
81fn checked_cpath(path: &Path) -> Result<CString> {
84 if !path.is_absolute() {
85 return Err(RestrictedOpenError::Rejected("relative worker path"));
86 }
87 CString::new(path.as_os_str().as_bytes())
88 .map_err(|_| RestrictedOpenError::Rejected("path contains an interior NUL"))
89}
90
91pub fn open_regular_for_read(path: &Path) -> Result<File> {
93 platform::open_regular_for_read(&checked_cpath(path)?)
94}
95
96pub fn open_regular_for_append(path: &Path) -> Result<File> {
99 platform::open_regular_for_write(path, &checked_cpath(path)?, WriteMode::Append)
100}
101
102pub fn open_regular_for_create(path: &Path) -> Result<File> {
104 platform::open_regular_for_write(path, &checked_cpath(path)?, WriteMode::Truncate)
105}
106
107#[derive(Clone, Copy)]
108enum WriteMode {
109 Append,
110 Truncate,
111}
112
113#[cfg(target_os = "linux")]
119pub fn constrained_unix_socket_fd(
120 path: &Path,
121) -> Result<(std::os::fd::OwnedFd, std::path::PathBuf)> {
122 platform::constrained_unix_socket_fd(&checked_cpath(path)?)
123}
124
125#[cfg(all(unix, not(target_os = "linux")))]
126pub fn constrained_unix_socket_fd(
127 _path: &Path,
128) -> Result<(std::os::fd::OwnedFd, std::path::PathBuf)> {
129 Err(RestrictedOpenError::Unsupported)
130}
131
132#[cfg(target_os = "linux")]
133mod platform {
134 use super::{RestrictedOpenError, Result};
135 use std::ffi::{CStr, CString};
136 use std::fs::File;
137 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
138 use std::os::unix::ffi::OsStrExt;
139
140 fn open_path(
143 dir_fd: RawFd,
144 path: &CStr,
145 flags: libc::c_int,
146 reject_msg: &'static str,
147 ) -> Result<OwnedFd> {
148 let mut how: libc::open_how = unsafe { std::mem::zeroed() };
151 how.flags = (flags | libc::O_CLOEXEC) as u64;
152 how.resolve = libc::RESOLVE_NO_MAGICLINKS;
153
154 let rc = unsafe {
157 libc::syscall(
158 libc::SYS_openat2,
159 dir_fd,
160 path.as_ptr(),
161 &how as *const libc::open_how,
162 std::mem::size_of::<libc::open_how>(),
163 )
164 };
165 if rc >= 0 {
166 return Ok(unsafe { OwnedFd::from_raw_fd(rc as RawFd) });
168 }
169 let err = std::io::Error::last_os_error();
170 match err.raw_os_error() {
171 Some(libc::ENOSYS) | Some(libc::EPERM) => open_path_fallback(dir_fd, path, flags),
172 Some(libc::ELOOP) => Err(RestrictedOpenError::Rejected(reject_msg)),
173 _ => Err(RestrictedOpenError::Io(err)),
174 }
175 }
176
177 fn open_component(dir_fd: RawFd, path: &CStr, flags: libc::c_int) -> Result<OwnedFd> {
178 let fd = unsafe {
182 libc::openat(
183 dir_fd,
184 path.as_ptr(),
185 flags | libc::O_PATH | libc::O_NOFOLLOW | libc::O_CLOEXEC,
186 )
187 };
188 if fd < 0 {
189 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
190 }
191 Ok(unsafe { OwnedFd::from_raw_fd(fd) })
193 }
194
195 fn open_path_fallback(dir_fd: RawFd, path: &CStr, flags: libc::c_int) -> Result<OwnedFd> {
196 const MAX_PATH: usize = libc::PATH_MAX as usize;
198 const MAX_SYMLINKS: usize = 40;
199 let bytes = path.to_bytes();
200 if bytes.is_empty() {
201 return Err(RestrictedOpenError::Io(std::io::Error::from_raw_os_error(
202 libc::ENOENT,
203 )));
204 }
205 if bytes.len() >= MAX_PATH {
206 return Err(RestrictedOpenError::Io(std::io::Error::from_raw_os_error(
207 libc::ENAMETOOLONG,
208 )));
209 }
210 let mut pending = bytes.to_vec();
211 let start = if pending[0] == b'/' { c"/" } else { c"." };
212 let mut current = open_component(dir_fd, start, libc::O_DIRECTORY)?;
213 reject_pseudo_filesystem(current.as_raw_fd())?;
214 let mut offset = 0;
215 let mut symlinks = 0;
216 while offset < pending.len() {
217 if pending[offset] == b'/' {
218 offset += 1;
219 continue;
220 }
221 let end = pending[offset..]
222 .iter()
223 .position(|&b| b == b'/')
224 .map_or(pending.len(), |n| offset + n);
225 let component = CString::new(&pending[offset..end])
226 .map_err(|_| RestrictedOpenError::Rejected("path contains an interior NUL"))?;
227 let next = open_component(current.as_raw_fd(), &component, 0)?;
228 reject_pseudo_filesystem(next.as_raw_fd())?;
229 let kind = file_type(next.as_raw_fd())?;
230 let has_suffix = end < pending.len();
233 if kind == libc::S_IFLNK && (has_suffix || flags & libc::O_NOFOLLOW == 0) {
234 symlinks += 1;
235 if symlinks > MAX_SYMLINKS {
236 return Err(RestrictedOpenError::Rejected("too many symlinks"));
237 }
238 let mut target = vec![0u8; MAX_PATH];
239 let len = unsafe {
242 libc::readlinkat(
243 next.as_raw_fd(),
244 c"".as_ptr(),
245 target.as_mut_ptr().cast(),
246 target.len(),
247 )
248 };
249 if len < 0 {
250 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
251 }
252 let len = len as usize;
254 if len == 0 || len + pending.len() - end >= MAX_PATH {
255 return Err(RestrictedOpenError::Rejected(
256 "invalid or overlong symlink target",
257 ));
258 }
259 target.truncate(len);
260 target.extend_from_slice(&pending[end..]);
261 if target[0] == b'/' {
262 current = open_component(libc::AT_FDCWD, c"/", libc::O_DIRECTORY)?;
263 reject_pseudo_filesystem(current.as_raw_fd())?;
264 }
265 pending = target;
266 offset = 0;
267 continue;
268 }
269 if (has_suffix || flags & libc::O_DIRECTORY != 0) && kind != libc::S_IFDIR {
270 return Err(RestrictedOpenError::Io(std::io::Error::from_raw_os_error(
271 libc::ENOTDIR,
272 )));
273 }
274 current = next;
275 offset = end;
276 }
277 Ok(current)
278 }
279
280 fn proc_fd_path(fd: RawFd) -> Result<CString> {
281 CString::new(format!("/proc/self/fd/{fd}"))
282 .map_err(|_| RestrictedOpenError::Rejected("fd path contains an interior NUL"))
283 }
284
285 fn reject_pseudo_filesystem(fd: RawFd) -> Result<()> {
287 let mut sfs: libc::statfs = unsafe { std::mem::zeroed() };
289 let rc = unsafe { libc::fstatfs(fd, &mut sfs) };
290 if rc != 0 {
291 let err = std::io::Error::last_os_error();
292 if err.raw_os_error() != Some(libc::EBADF) {
293 return Err(RestrictedOpenError::Io(err));
294 }
295 let proc_path = proc_fd_path(fd)?;
299 if unsafe { libc::statfs(proc_path.as_ptr(), &mut sfs) } != 0 {
301 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
302 }
303 }
304 let f_type = sfs.f_type as i64;
305 if f_type == libc::PROC_SUPER_MAGIC as i64 {
306 return Err(RestrictedOpenError::Rejected("procfs path"));
307 }
308 if f_type == libc::SYSFS_MAGIC as i64 {
309 return Err(RestrictedOpenError::Rejected("sysfs path"));
310 }
311 Ok(())
312 }
313
314 fn file_type(fd: RawFd) -> Result<libc::mode_t> {
315 let mut st: libc::stat = unsafe { std::mem::zeroed() };
317 let rc = unsafe { libc::fstat(fd, &mut st) };
318 if rc != 0 {
319 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
320 }
321 Ok(st.st_mode & libc::S_IFMT)
322 }
323
324 fn require_regular_file(fd: RawFd) -> Result<()> {
325 if file_type(fd)? != libc::S_IFREG {
326 return Err(RestrictedOpenError::Rejected("not a regular file"));
327 }
328 Ok(())
329 }
330
331 fn reopen_opath(opath: &OwnedFd, flags: libc::c_int) -> Result<File> {
334 let proc_path = proc_fd_path(opath.as_raw_fd())?;
335 let rc = unsafe { libc::open(proc_path.as_ptr(), flags | libc::O_CLOEXEC) };
337 if rc < 0 {
338 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
339 }
340 Ok(File::from(unsafe { OwnedFd::from_raw_fd(rc) }))
342 }
343
344 pub(super) fn constrained_unix_socket_fd(
345 cpath: &CStr,
346 ) -> Result<(OwnedFd, std::path::PathBuf)> {
347 let opath = open_path(
348 libc::AT_FDCWD,
349 cpath,
350 libc::O_PATH,
351 "magic link in socket path",
352 )?;
353 reject_pseudo_filesystem(opath.as_raw_fd())?;
354 if file_type(opath.as_raw_fd())? != libc::S_IFSOCK {
355 return Err(RestrictedOpenError::Rejected("not a socket"));
356 }
357 let connect_path = std::path::PathBuf::from(format!("/proc/self/fd/{}", opath.as_raw_fd()));
358 Ok((opath, connect_path))
359 }
360
361 pub(super) fn open_regular_for_read(cpath: &CStr) -> Result<File> {
362 let opath = open_path(libc::AT_FDCWD, cpath, libc::O_PATH, "magic link in path")?;
363 reject_pseudo_filesystem(opath.as_raw_fd())?;
364 require_regular_file(opath.as_raw_fd())?;
365 reopen_opath(&opath, libc::O_RDONLY)
366 }
367
368 pub(super) fn open_regular_for_write(
369 path: &std::path::Path,
370 _cpath: &CStr,
371 mode: super::WriteMode,
372 ) -> Result<File> {
373 let parent = path
374 .parent()
375 .filter(|p| !p.as_os_str().is_empty())
376 .unwrap_or_else(|| std::path::Path::new("."));
377 let file_name = path.file_name().ok_or(RestrictedOpenError::Rejected(
378 "output path has no file name",
379 ))?;
380 let cparent = CString::new(parent.as_os_str().as_bytes())
381 .map_err(|_| RestrictedOpenError::Rejected("path contains an interior NUL"))?;
382 let cname = CString::new(file_name.as_bytes())
383 .map_err(|_| RestrictedOpenError::Rejected("path contains an interior NUL"))?;
384
385 let write_flag = match mode {
386 super::WriteMode::Append => libc::O_APPEND,
387 super::WriteMode::Truncate => libc::O_TRUNC,
388 };
389
390 let dir = open_path(
392 libc::AT_FDCWD,
393 &cparent,
394 libc::O_PATH | libc::O_DIRECTORY,
395 "magic link in output directory",
396 )?;
397 reject_pseudo_filesystem(dir.as_raw_fd())?;
398
399 let inspect = open_path(
402 dir.as_raw_fd(),
403 &cname,
404 libc::O_PATH | libc::O_NOFOLLOW,
405 "magic link as output file",
406 );
407 match inspect {
408 Ok(existing) => {
409 reject_pseudo_filesystem(existing.as_raw_fd())?;
410 require_regular_file(existing.as_raw_fd())?;
412 reopen_opath(&existing, libc::O_WRONLY | write_flag)
413 }
414 Err(RestrictedOpenError::Io(e)) if e.raw_os_error() == Some(libc::ENOENT) => {
415 let rc = unsafe {
417 libc::openat(
418 dir.as_raw_fd(),
419 cname.as_ptr(),
420 libc::O_WRONLY
421 | libc::O_CREAT
422 | libc::O_EXCL
423 | write_flag
424 | libc::O_NOFOLLOW
425 | libc::O_CLOEXEC,
426 0o600,
427 )
428 };
429 if rc < 0 {
430 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
431 }
432 Ok(File::from(unsafe { OwnedFd::from_raw_fd(rc) }))
434 }
435 Err(other) => Err(other),
436 }
437 }
438}
439
440#[cfg(all(unix, not(target_os = "linux")))]
441mod platform {
442 use super::{RestrictedOpenError, Result};
443 use std::ffi::{CStr, CString};
444 use std::fs::File;
445 use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
446
447 fn fstat_regular(fd: RawFd) -> Result<libc::stat> {
451 let mut st: libc::stat = unsafe { std::mem::zeroed() };
453 if unsafe { libc::fstat(fd, &mut st) } != 0 {
454 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
455 }
456 if st.st_mode & libc::S_IFMT != libc::S_IFREG {
457 return Err(RestrictedOpenError::Rejected("not a regular file"));
458 }
459 Ok(st)
460 }
461
462 fn same_object(a: &libc::stat, b: &libc::stat) -> bool {
463 a.st_dev == b.st_dev && a.st_ino == b.st_ino
464 }
465
466 fn canonical_path(fd: RawFd) -> Result<CString> {
468 let mut buf = [0 as libc::c_char; libc::PATH_MAX as usize];
469 if unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_mut_ptr()) } != 0 {
471 return Err(RestrictedOpenError::Io(std::io::Error::last_os_error()));
472 }
473 Ok(unsafe { CStr::from_ptr(buf.as_ptr()) }.to_owned())
475 }
476
477 fn open_nofollow(cpath: &CStr, flags: libc::c_int) -> Result<OwnedFd> {
478 let full = flags | libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC;
480 let rc = unsafe { libc::open(cpath.as_ptr(), full, 0o600 as libc::c_int) };
481 if rc >= 0 {
482 return Ok(unsafe { OwnedFd::from_raw_fd(rc) });
484 }
485 let err = std::io::Error::last_os_error();
486 match err.raw_os_error() {
487 Some(libc::ELOOP) => Err(RestrictedOpenError::Rejected("symlink final component")),
488 _ => Err(RestrictedOpenError::Io(err)),
489 }
490 }
491
492 fn reopen_canonical(fd: &OwnedFd, st1: &libc::stat, flags: libc::c_int) -> Result<File> {
494 let canonical = canonical_path(fd.as_raw_fd())?;
495 let reopened = open_nofollow(&canonical, flags)?;
496 let st2 = fstat_regular(reopened.as_raw_fd())?;
497 if !same_object(st1, &st2) {
498 return Err(RestrictedOpenError::Rejected(
499 "target changed identity during resolution",
500 ));
501 }
502 Ok(File::from(reopened))
503 }
504
505 pub(super) fn open_regular_for_read(cpath: &CStr) -> Result<File> {
506 let fd = open_nofollow(cpath, libc::O_RDONLY)?;
507 let st1 = fstat_regular(fd.as_raw_fd())?;
508 reopen_canonical(&fd, &st1, libc::O_RDONLY)
509 }
510
511 pub(super) fn open_regular_for_write(
512 _path: &std::path::Path,
513 cpath: &CStr,
514 mode: super::WriteMode,
515 ) -> Result<File> {
516 let write_flag = match mode {
517 super::WriteMode::Append => libc::O_APPEND,
518 super::WriteMode::Truncate => libc::O_TRUNC,
519 };
520 match open_nofollow(cpath, libc::O_RDONLY) {
522 Ok(fd) => {
523 let st1 = fstat_regular(fd.as_raw_fd())?;
524 reopen_canonical(&fd, &st1, libc::O_WRONLY | write_flag)
525 }
526 Err(RestrictedOpenError::Io(e)) if e.raw_os_error() == Some(libc::ENOENT) => {
527 let fd = open_nofollow(
529 cpath,
530 libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | write_flag,
531 )?;
532 let _ = fstat_regular(fd.as_raw_fd())?;
533 Ok(File::from(fd))
534 }
535 Err(other) => Err(other),
536 }
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use std::io::{Read, Write};
544 #[cfg(target_os = "linux")]
545 use std::os::unix::fs::symlink;
546
547 fn tmp_dir() -> std::path::PathBuf {
548 let dir = std::env::temp_dir().join(format!("ddrestrict-{}", std::process::id()));
549 let _ = std::fs::create_dir_all(&dir);
550 dir
551 }
552
553 fn read_all(path: &std::path::Path) -> Vec<u8> {
554 let mut buf = Vec::new();
555 open_regular_for_read(path)
556 .unwrap()
557 .read_to_end(&mut buf)
558 .unwrap();
559 buf
560 }
561
562 #[test]
563 #[cfg_attr(miri, ignore)]
564 fn reads_an_ordinary_file() {
565 let dir = tmp_dir();
566 let path = dir.join("ordinary.txt");
567 std::fs::write(&path, b"hello canary\n").unwrap();
568 assert_eq!(read_all(&path), b"hello canary\n");
569 }
570
571 #[cfg(target_os = "linux")]
573 #[test]
574 #[cfg_attr(miri, ignore)]
575 fn follows_an_ordinary_symlink_to_a_regular_file() {
576 let dir = tmp_dir();
577 let target = dir.join("real.txt");
578 std::fs::write(&target, b"through symlink\n").unwrap();
579 let link = dir.join("link.txt");
580 let _ = std::fs::remove_file(&link);
581 symlink(&target, &link).unwrap();
582 assert_eq!(read_all(&link), b"through symlink\n");
583 }
584
585 #[cfg(target_os = "linux")]
586 #[test]
587 #[cfg_attr(miri, ignore)]
588 fn rejects_procfs_regular_file() {
589 let err = open_regular_for_read(std::path::Path::new("/proc/self/status")).unwrap_err();
591 assert!(
592 matches!(
593 err,
594 RestrictedOpenError::Rejected(_) | RestrictedOpenError::Unsupported
595 ),
596 "unexpected: {err:?}"
597 );
598 }
599
600 #[cfg(target_os = "linux")]
601 #[test]
602 #[cfg_attr(miri, ignore)]
603 fn rejects_proc_self_fd_magic_link() {
604 let dir = tmp_dir();
605 let secret = dir.join("secret.txt");
606 std::fs::write(&secret, b"SECRET\n").unwrap();
607 let held = std::fs::File::open(&secret).unwrap();
608 let via_fd = format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&held));
609 let err = open_regular_for_read(std::path::Path::new(&via_fd)).unwrap_err();
610 assert!(
611 matches!(
612 err,
613 RestrictedOpenError::Rejected(_) | RestrictedOpenError::Unsupported
614 ),
615 "unexpected: {err:?}"
616 );
617 }
618
619 #[cfg(target_os = "linux")]
620 #[test]
621 #[cfg_attr(miri, ignore)]
622 fn rejects_symlink_to_proc_self_fd() {
623 let dir = tmp_dir();
624 let secret = dir.join("secret2.txt");
625 std::fs::write(&secret, b"SECRET2\n").unwrap();
626 let held = std::fs::File::open(&secret).unwrap();
627 let alias = dir.join("fd-alias");
628 let _ = std::fs::remove_file(&alias);
629 symlink(
630 format!("/proc/self/fd/{}", std::os::fd::AsRawFd::as_raw_fd(&held)),
631 &alias,
632 )
633 .unwrap();
634 let err = open_regular_for_read(&alias).unwrap_err();
635 assert!(
636 matches!(
637 err,
638 RestrictedOpenError::Rejected(_) | RestrictedOpenError::Unsupported
639 ),
640 "unexpected: {err:?}"
641 );
642 }
643
644 #[cfg(target_os = "linux")]
645 #[test]
646 #[cfg_attr(miri, ignore)]
647 fn rejects_a_fifo() {
648 let dir = tmp_dir();
649 let fifo = dir.join("fifo");
650 let _ = std::fs::remove_file(&fifo);
651 let cfifo = std::ffi::CString::new(fifo.as_os_str().as_bytes()).unwrap();
652 assert_eq!(unsafe { libc::mkfifo(cfifo.as_ptr(), 0o600) }, 0);
653 let err = open_regular_for_read(&fifo).unwrap_err();
654 assert!(
655 matches!(
656 err,
657 RestrictedOpenError::Rejected(_) | RestrictedOpenError::Unsupported
658 ),
659 "unexpected: {err:?}"
660 );
661 }
662
663 #[test]
664 #[cfg_attr(miri, ignore)]
665 fn append_writes_to_an_ordinary_file() {
666 let dir = tmp_dir();
667 let path = dir.join("append.log");
668 let _ = std::fs::remove_file(&path);
669 {
670 let mut f = open_regular_for_append(&path).unwrap();
671 f.write_all(b"line1\n").unwrap();
672 }
673 {
674 let mut f = open_regular_for_append(&path).unwrap();
675 f.write_all(b"line2\n").unwrap();
676 }
677 assert_eq!(std::fs::read(&path).unwrap(), b"line1\nline2\n");
678 }
679
680 #[cfg(target_os = "linux")]
681 #[test]
682 #[cfg_attr(miri, ignore)]
683 fn append_rejects_symlinked_final_component() {
684 let dir = tmp_dir();
685 let real = dir.join("real-target.log");
686 std::fs::write(&real, b"pre\n").unwrap();
687 let link = dir.join("link-target.log");
688 let _ = std::fs::remove_file(&link);
689 symlink(&real, &link).unwrap();
690 let err = open_regular_for_append(&link).unwrap_err();
691 assert!(
692 matches!(
693 err,
694 RestrictedOpenError::Rejected(_) | RestrictedOpenError::Unsupported
695 ),
696 "unexpected: {err:?}"
697 );
698 assert_eq!(std::fs::read(&real).unwrap(), b"pre\n");
699 }
700
701 #[cfg(target_os = "linux")]
702 #[test]
703 #[cfg_attr(miri, ignore)]
704 fn follows_relative_symlinks_and_parent_components() {
705 let dir = tmp_dir().join("relative-links");
706 std::fs::create_dir_all(dir.join("real/subdir")).unwrap();
707 std::fs::write(dir.join("real/attachment.txt"), b"attachment\n").unwrap();
708 symlink("real/subdir", dir.join("directory-link")).unwrap();
709 symlink("directory-link/../attachment.txt", dir.join("file-link")).unwrap();
710 assert_eq!(read_all(&dir.join("./file-link")), b"attachment\n");
711 }
712
713 #[cfg(target_os = "linux")]
714 #[test]
715 #[cfg_attr(miri, ignore)]
716 fn creates_appends_and_truncates_through_directory_symlinks() {
717 let dir = tmp_dir().join("output-directory-links");
718 std::fs::create_dir_all(dir.join("real")).unwrap();
719 symlink("real", dir.join("link")).unwrap();
720 let path = dir.join("link/output.log");
721 open_regular_for_append(&path)
722 .unwrap()
723 .write_all(b"first\n")
724 .unwrap();
725 open_regular_for_append(&path)
726 .unwrap()
727 .write_all(b"second\n")
728 .unwrap();
729 assert_eq!(std::fs::read(&path).unwrap(), b"first\nsecond\n");
730 open_regular_for_create(&path)
731 .unwrap()
732 .write_all(b"replacement\n")
733 .unwrap();
734 assert_eq!(std::fs::read(&path).unwrap(), b"replacement\n");
735
736 let alias = dir.join("output-alias");
737 symlink("real/output.log", &alias).unwrap();
738 assert!(open_regular_for_create(&alias).is_err());
739 assert_eq!(std::fs::read(&path).unwrap(), b"replacement\n");
740 }
741
742 #[cfg(target_os = "linux")]
743 #[test]
744 #[cfg_attr(miri, ignore)]
745 fn refuses_symlink_loops_and_non_directory_components() {
746 let dir = tmp_dir().join("invalid-paths");
747 std::fs::create_dir_all(&dir).unwrap();
748 symlink("loop", dir.join("loop")).unwrap();
749 assert!(open_regular_for_read(&dir.join("loop")).is_err());
750 std::fs::write(dir.join("file"), b"ordinary\n").unwrap();
751 for suffix in ["file/", "file/.", "file/../file"] {
752 let err = open_regular_for_read(&dir.join(suffix)).unwrap_err();
753 assert!(
754 matches!(err, RestrictedOpenError::Io(ref e)
755 if e.raw_os_error() == Some(libc::ENOTDIR)),
756 "{suffix}: {err:?}"
757 );
758 }
759 }
760
761 #[cfg(target_os = "linux")]
762 #[test]
763 #[cfg_attr(miri, ignore)]
764 fn rejects_devices_and_symlinked_pseudo_filesystems() {
765 for path in ["/dev/null", "/dev/zero", "/sys/kernel/uevent_seqnum"] {
766 assert!(matches!(
767 open_regular_for_read(std::path::Path::new(path)),
768 Err(RestrictedOpenError::Rejected(_))
769 ));
770 }
771 let dir = tmp_dir().join("pseudo-filesystem-links");
772 std::fs::create_dir_all(&dir).unwrap();
773 symlink("/proc", dir.join("proc-alias")).unwrap();
774 symlink("/dev/fd", dir.join("fd-alias")).unwrap();
775 let ordinary = dir.join("ordinary");
776 std::fs::write(&ordinary, b"ordinary\n").unwrap();
777 let held = std::fs::File::open(&ordinary).unwrap();
778 let fd = std::os::fd::AsRawFd::as_raw_fd(&held);
779 for path in [
780 dir.join("proc-alias/self/maps"),
781 dir.join(format!("fd-alias/{fd}")),
782 ] {
783 assert!(matches!(
784 open_regular_for_read(&path),
785 Err(RestrictedOpenError::Rejected(_))
786 ));
787 }
788 }
789}