use std::{
ffi::OsString,
fmt,
sync::{atomic::AtomicUsize, OnceLock},
};
pub const LAUNCH_NONCE_FD: i32 = 3;
pub const LAUNCH_NONCE_FD_ENV: &str = "SUBC_LAUNCH_NONCE_FD";
pub const LAUNCH_NONCE_ENV: &str = "SUBC_LAUNCH_NONCE";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum LaunchNonceSource {
Fd,
Env,
}
impl LaunchNonceSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Fd => "fd",
Self::Env => "env",
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct LaunchNonce {
value: String,
source: LaunchNonceSource,
}
impl LaunchNonce {
pub fn value(&self) -> &str {
&self.value
}
pub fn source(&self) -> LaunchNonceSource {
self.source
}
}
impl fmt::Debug for LaunchNonce {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("LaunchNonce")
.field(
"value",
&format_args!("<{} bytes redacted>", self.value.len()),
)
.field("source", &self.source)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum LaunchNonceError {
Malformed { value: String },
NotOpen { fd: i32, errno: i32 },
NotAPipe { fd: i32 },
WrongPipe {
fd: i32,
expected_inode: u64,
found_inode: u64,
},
Empty { fd: i32 },
Unreadable { fd: i32, errno: Option<i32> },
NotUtf8 { fd: i32 },
}
impl fmt::Display for LaunchNonceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Malformed { value } => write!(
f,
"{LAUNCH_NONCE_FD_ENV}={value:?} is not <fd>:<inode>"
),
Self::NotOpen { fd, errno } => write!(
f,
"{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not open (errno {errno}); \
a process spawned by a module inherits the variable but not the descriptor"
),
Self::NotAPipe { fd } => write!(
f,
"{LAUNCH_NONCE_FD_ENV} names descriptor {fd}, which is not a pipe; left it untouched"
),
Self::WrongPipe {
fd,
expected_inode,
found_inode,
} => write!(
f,
"{LAUNCH_NONCE_FD_ENV} names descriptor {fd} with inode {expected_inode}, but it has \
inode {found_inode}; left it untouched"
),
Self::Empty { fd } => write!(
f,
"the launch nonce pipe at descriptor {fd} is empty; left it untouched"
),
Self::Unreadable { fd, errno } => write!(
f,
"could not read the launch nonce from descriptor {fd} (errno {errno:?})"
),
Self::NotUtf8 { fd } => write!(
f,
"the launch nonce pipe at descriptor {fd} held bytes that are not UTF-8"
),
}
}
}
impl std::error::Error for LaunchNonceError {}
type Cached = Result<Option<LaunchNonce>, LaunchNonceError>;
pub(crate) struct LaunchNonceCell {
value: OnceLock<Cached>,
descriptor_reads: AtomicUsize,
}
impl LaunchNonceCell {
pub(crate) const fn new() -> Self {
Self {
value: OnceLock::new(),
descriptor_reads: AtomicUsize::new(0),
}
}
pub(crate) fn get(&self, lookup: impl FnMut(&str) -> Option<OsString>) -> Cached {
self.value
.get_or_init(|| read_launch_nonce(lookup, &self.descriptor_reads))
.clone()
}
#[cfg(all(test, unix))]
pub(crate) fn descriptor_reads(&self) -> usize {
self.descriptor_reads
.load(std::sync::atomic::Ordering::SeqCst)
}
}
static PROCESS_NONCE: LaunchNonceCell = LaunchNonceCell::new();
pub fn launch_nonce() -> Result<Option<LaunchNonce>, LaunchNonceError> {
PROCESS_NONCE.get(|key| std::env::var_os(key))
}
fn read_launch_nonce(
mut lookup: impl FnMut(&str) -> Option<OsString>,
descriptor_reads: &AtomicUsize,
) -> Cached {
#[cfg(unix)]
if let Some(value) = lookup(LAUNCH_NONCE_FD_ENV) {
return unix::read_descriptor(&value, descriptor_reads);
}
#[cfg(not(unix))]
let _ = descriptor_reads;
Ok(lookup(LAUNCH_NONCE_ENV)
.and_then(|value| value.into_string().ok())
.filter(|value| !value.is_empty())
.map(|value| LaunchNonce {
value,
source: LaunchNonceSource::Env,
}))
}
#[cfg(unix)]
pub use unix::LaunchNonceHandoff;
#[cfg(unix)]
mod unix {
use std::{
ffi::OsStr,
fs::File,
io::{self, Read, Write},
os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd},
sync::atomic::{AtomicUsize, Ordering},
};
use super::{Cached, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_FD};
#[derive(Debug)]
pub struct LaunchNonceHandoff {
read_end: OwnedFd,
inode: u64,
target: RawFd,
}
impl LaunchNonceHandoff {
pub fn new(nonce: &str) -> io::Result<Self> {
let (reader, mut writer) = io::pipe()?;
writer.write_all(nonce.as_bytes())?;
drop(writer);
let mut read_end = OwnedFd::from(reader);
if read_end.as_raw_fd() < LAUNCH_NONCE_FD {
#[allow(unsafe_code)]
let copy = unsafe {
libc::fcntl(read_end.as_raw_fd(), libc::F_DUPFD_CLOEXEC, LAUNCH_NONCE_FD)
};
if copy == -1 {
return Err(io::Error::last_os_error());
}
#[allow(unsafe_code)]
{
read_end = unsafe { OwnedFd::from_raw_fd(copy) };
}
}
let inode = fstat(read_end.as_raw_fd())?.st_ino as u64;
Ok(Self {
read_end,
inode,
target: LAUNCH_NONCE_FD,
})
}
pub fn fd_env_value(&self) -> String {
format!("{}:{}", self.target, self.inode)
}
pub fn install_last(self, command: &mut std::process::Command) {
use std::os::unix::process::CommandExt;
#[allow(unsafe_code)]
unsafe {
command.pre_exec(move || self.install_in_child());
}
}
pub(crate) fn install_in_child(&self) -> io::Result<()> {
let source = self.read_end.as_raw_fd();
if source == self.target {
#[allow(unsafe_code)]
let flags = unsafe { libc::fcntl(source, libc::F_GETFD) };
if flags == -1 {
return Err(io::Error::last_os_error());
}
#[allow(unsafe_code)]
let set = unsafe { libc::fcntl(source, libc::F_SETFD, flags & !libc::FD_CLOEXEC) };
if set == -1 {
return Err(io::Error::last_os_error());
}
return Ok(());
}
#[allow(unsafe_code)]
if unsafe { libc::dup2(source, self.target) } == -1 {
return Err(io::Error::last_os_error());
}
Ok(())
}
#[cfg(test)]
pub(crate) fn with_target(mut self, target: RawFd) -> Self {
self.target = target;
self
}
#[cfg(test)]
pub(crate) fn read_end_fd(&self) -> RawFd {
self.read_end.as_raw_fd()
}
#[cfg(test)]
pub(crate) fn inode(&self) -> u64 {
self.inode
}
}
pub(super) fn read_descriptor(value: &OsStr, descriptor_reads: &AtomicUsize) -> Cached {
let text = value.to_string_lossy();
let malformed = || LaunchNonceError::Malformed {
value: text.to_string(),
};
let (fd_text, inode_text) = text.split_once(':').ok_or_else(malformed)?;
let fd: RawFd = fd_text.parse().map_err(|_| malformed())?;
let expected_inode: u64 = inode_text.parse().map_err(|_| malformed())?;
if fd < 0 {
return Err(malformed());
}
let stat = fstat(fd).map_err(|error| LaunchNonceError::NotOpen {
fd,
errno: error.raw_os_error().unwrap_or(0),
})?;
if stat.st_mode & libc::S_IFMT != libc::S_IFIFO {
return Err(LaunchNonceError::NotAPipe { fd });
}
let found_inode = stat.st_ino as u64;
if found_inode != expected_inode {
return Err(LaunchNonceError::WrongPipe {
fd,
expected_inode,
found_inode,
});
}
let mut waiting: libc::c_int = 0;
#[allow(unsafe_code)]
if unsafe { libc::ioctl(fd, libc::FIONREAD, &mut waiting) } == -1 {
return Err(LaunchNonceError::Unreadable {
fd,
errno: io::Error::last_os_error().raw_os_error(),
});
}
if waiting <= 0 {
return Err(LaunchNonceError::Empty { fd });
}
descriptor_reads.fetch_add(1, Ordering::SeqCst);
#[allow(unsafe_code)]
let mut file = File::from(unsafe { OwnedFd::from_raw_fd(fd) });
let mut bytes = Vec::with_capacity(64);
let read = file.read_to_end(&mut bytes);
drop(file);
read.map_err(|error| LaunchNonceError::Unreadable {
fd,
errno: error.raw_os_error(),
})?;
let value = String::from_utf8(bytes).map_err(|_| LaunchNonceError::NotUtf8 { fd })?;
Ok(Some(LaunchNonce {
value,
source: LaunchNonceSource::Fd,
}))
}
pub(super) fn fstat(fd: RawFd) -> io::Result<libc::stat> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
#[allow(unsafe_code)]
if unsafe { libc::fstat(fd, stat.as_mut_ptr()) } == -1 {
return Err(io::Error::last_os_error());
}
#[allow(unsafe_code)]
Ok(unsafe { stat.assume_init() })
}
}
#[cfg(test)]
pub(crate) mod tests;