use std::{fmt, str}; use nix::{ libc::mode_t, sys::stat::{self, Mode}, }; #[derive(Clone, Copy)] pub enum Umask { Masked, Inherit, Set(Mode), } impl Umask { pub fn set(self) { match self { Self::Masked => Self::masked(), Self::Inherit => Self::inherit(), Self::Set(mode) => Self::set_to(mode), } } fn masked() { // In the year 2025, this is _still_ the only reasonable way to check the // process' current umask. This is also why we don't calculate a default // umask and stick it in Clap's `default_value_t` - I'd prefer not to touch // the process' umask before we know what we're doing with it, but there's // no other way to look at the current one. // // The choice of `all` here is a complicated compromise. In theory, nothing // will ever observe this umask, as we immediately overwrite it immediately // below. As of this writing, there is nothing in this service that could // create a file in this interval, which could then be affected by the // temporary umask. However, future code changes _could_ introduce something // that races with this, such as a signal handler that does more than just // exiting the process. // // Using `all` makes sure that _any_ file created in that interval is // created with "deny all" file permission bits, which will make it useless // even for this program. Hopefully, that failure will be immediate enough // to attract attention. The other alternatives which might work are zero // (don't mask off any permissions, things might be globally readable) // or 0o0027 (the permissions we'd want to use on most Linux distros, which // could then silently hide the above race condition). let current = stat::umask(Mode::all()); // In addition to whatever we inherit, by default, we ensure that files // created by pilcrow are not world-readable or world-writeable. However, // we respect the inherited group and user permissions on the assumption // that the user, or their administrator, has either made a decision about // group permissions, or are relying on the distro's defaults. // // The main thing `pilcrow` creates are its database and its backup // database, which can contain both confidential information (users' // conversations) and sensitive information (the service's configuration // and any API tokens, keys, &c used by the service). // // There is no way to tell `sqlite` to restrict the permissions of database // files directly, which is otherwise what we'd prefer. let desired = current | Mode::S_IRWXO; stat::umask(desired); } fn inherit() { // Here's a complete list of steps required to inherit the umask from the calling process: } fn set_to(mode: Mode) { stat::umask(mode); } } impl str::FromStr for Umask { type Err = Error; fn from_str(s: &str) -> Result { let umask = match s { "masked" => Self::Masked, "inherit" => Self::Inherit, octal => { let mode = mode_t::from_str_radix(octal, 8)?; let mode = Mode::from_bits(mode).ok_or(Error::UnknownBits)?; Self::Set(mode) } }; Ok(umask) } } impl fmt::Display for Umask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Masked => "masked".fmt(f), Self::Inherit => "inherit".fmt(f), Self::Set(mode) => write!(f, "{mode:o}"), } } } /// Errors occurring during umask option parsing. #[derive(Debug, thiserror::Error)] pub enum Error { /// Failed to parse a umask value from the input. #[error(transparent)] Parse(#[from] std::num::ParseIntError), /// The provided umask contained invalid bits. (See the constants associated with [`Mode`] for /// valid umask bits.) // We dont need to hold onto the actual umask value here - Clap does that for us, and prints // the value as the user input it, which beats anything we could do here. #[error("unknown bits in umask")] UnknownBits, }