Skip to main content

gritty/
security.rs

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
8/// Create a directory hierarchy with mode 0700, validating ownership of existing components.
9/// Trusted system roots (`/`, `/tmp`, `/run`, `$XDG_RUNTIME_DIR`) are accepted without
10/// ownership checks. All other existing directories must be owned by the current user
11/// and must not be symlinks.
12pub 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
29/// Bind a `UnixListener` with TOCTOU-safe stale socket handling and 0600 permissions.
30///
31/// On `AddrInUse`, probes the existing socket: if it responds to a connect, returns
32/// an error (socket is alive). Otherwise, removes the stale socket and retries.
33pub 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
57/// Verify that the peer on a Unix stream has the same UID as the current process.
58pub 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
70/// `dup(2)` that returns an `OwnedFd` or an error (instead of silently returning -1).
71pub 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
79/// Clamp window-size values to a sane range, preventing zero-sized or absurdly large values.
80pub 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}