1use std::io;
2use std::os::fd::{FromRawFd, OwnedFd, RawFd};
3use std::os::unix::fs::{MetadataExt, PermissionsExt};
4use std::path::Path;
5
6const MAX_WINSIZE: u16 = 10_000;
7
8pub fn secure_create_dir_all(path: &Path) -> io::Result<()> {
13 if path.exists() {
14 if is_trusted_root(path) {
15 return Ok(());
16 }
17 return validate_dir(path);
18 }
19
20 if let Some(parent) = path.parent() {
21 secure_create_dir_all(parent)?;
22 }
23
24 std::fs::create_dir(path)?;
25 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))?;
26 Ok(())
27}
28
29pub fn bind_unix_listener(path: &Path) -> io::Result<tokio::net::UnixListener> {
34 match tokio::net::UnixListener::bind(path) {
35 Ok(listener) => {
36 set_socket_permissions(path)?;
37 Ok(listener)
38 }
39 Err(e) if e.kind() == io::ErrorKind::AddrInUse => {
40 match std::os::unix::net::UnixStream::connect(path) {
41 Ok(_) => Err(io::Error::new(
42 io::ErrorKind::AddrInUse,
43 format!("{} is already in use by a running process", path.display()),
44 )),
45 Err(_) => {
46 std::fs::remove_file(path)?;
47 let listener = tokio::net::UnixListener::bind(path)?;
48 set_socket_permissions(path)?;
49 Ok(listener)
50 }
51 }
52 }
53 Err(e) => Err(e),
54 }
55}
56
57pub fn verify_peer_uid(stream: &tokio::net::UnixStream) -> io::Result<()> {
59 let cred = stream.peer_cred()?;
60 let my_uid = unsafe { libc::getuid() };
61 if cred.uid() != my_uid {
62 return Err(io::Error::new(
63 io::ErrorKind::PermissionDenied,
64 format!("rejecting connection from uid {} (expected {my_uid})", cred.uid()),
65 ));
66 }
67 Ok(())
68}
69
70pub fn checked_dup(fd: RawFd) -> io::Result<OwnedFd> {
72 let new_fd = unsafe { libc::dup(fd) };
73 if new_fd == -1 {
74 return Err(io::Error::last_os_error());
75 }
76 Ok(unsafe { OwnedFd::from_raw_fd(new_fd) })
77}
78
79pub fn clamp_winsize(cols: u16, rows: u16) -> (u16, u16) {
81 (cols.clamp(1, MAX_WINSIZE), rows.clamp(1, MAX_WINSIZE))
82}
83
84fn set_socket_permissions(path: &Path) -> io::Result<()> {
85 std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
86}
87
88fn is_trusted_root(path: &Path) -> bool {
89 if matches!(path.to_str(), Some("/" | "/tmp" | "/run")) {
90 return true;
91 }
92 std::env::var("XDG_RUNTIME_DIR").ok().is_some_and(|xdg| path == Path::new(&xdg))
93}
94
95fn validate_dir(path: &Path) -> io::Result<()> {
96 let meta = std::fs::symlink_metadata(path)?;
97
98 if meta.file_type().is_symlink() {
99 return Err(io::Error::new(
100 io::ErrorKind::InvalidInput,
101 format!("refusing to use symlink at {}", path.display()),
102 ));
103 }
104 if !meta.is_dir() {
105 return Err(io::Error::new(
106 io::ErrorKind::InvalidInput,
107 format!("{} is not a directory", path.display()),
108 ));
109 }
110
111 let uid = unsafe { libc::getuid() };
112 if meta.uid() != uid {
113 return Err(io::Error::new(
114 io::ErrorKind::PermissionDenied,
115 format!(
116 "{} is owned by uid {}, expected uid {uid}; \
117 set $XDG_RUNTIME_DIR or use --ctl-socket",
118 path.display(),
119 meta.uid()
120 ),
121 ));
122 }
123
124 Ok(())
125}