blob: bcb32da727df32e0154c51c0b2815291de31b94d [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::Error;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::path::{Path, PathBuf};
use tokio::net::{UnixListener, UnixStream};
/// Refuses to overwrite an existing socket. Its owner cleans up only its own
/// inode after all connections have drained.
pub struct LocalListener {
listener: UnixListener,
path: PathBuf,
identity: (u64, u64),
}
impl LocalListener {
pub fn bind(path: &Path) -> Result<Self, Error> {
// Socket mode is not an atomic bind option: the enclosing directory
// must already exclude other accounts before the socket exists.
let parent = path
.parent()
.ok_or("socket parent directory required")?
.canonicalize()?;
let directory = parent.metadata()?;
if directory.uid() != unsafe { libc::geteuid() } || directory.mode() & 0o077 != 0 {
return Err("socket parent must be a private current-account directory".into());
}
let path = parent.join(path.file_name().ok_or("socket filename required")?);
let listener = UnixListener::bind(&path)?;
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
let meta = path.symlink_metadata()?;
Ok(Self {
listener,
path,
identity: (meta.dev(), meta.ino()),
})
}
pub async fn accept(&mut self) -> std::io::Result<UnixStream> {
self.listener.accept().await.map(|(socket, _)| socket)
}
}
impl Drop for LocalListener {
fn drop(&mut self) {
if self
.path
.symlink_metadata()
.is_ok_and(|m| (m.dev(), m.ino()) == self.identity)
{
let _ = std::fs::remove_file(&self.path);
}
}
}
/// Connect only to the private endpoint selected by the trusted embedding.
pub async fn connect(path: &Path) -> Result<UnixStream, Error> {
let stream = UnixStream::connect(path).await?;
if stream.peer_cred()?.uid() != unsafe { libc::geteuid() } {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"IPC peer must belong to the current account",
)
.into());
}
Ok(stream)
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::test]
async fn private_socket_keeps_readable_terminal_after_write_eof_and_owns_only_its_inode() {
let directory = tempfile::tempdir().unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o700))
.unwrap();
}
let path = directory.path().join("ipc.sock");
let mut listener = LocalListener::bind(&path).unwrap();
assert_eq!(path.symlink_metadata().unwrap().mode() & 0o777, 0o600);
assert!(LocalListener::bind(&path).is_err());
let mut waiting = Box::pin(listener.accept());
assert!(futures_util::poll!(&mut waiting).is_pending());
drop(waiting);
let mut client = connect(&path).await.unwrap();
let mut server = listener.accept().await.unwrap();
client.write_all("私有".as_bytes()).await.unwrap();
client.shutdown().await.unwrap();
let mut input = Vec::new();
server.read_to_end(&mut input).await.unwrap();
assert_eq!(input, "私有".as_bytes());
server.write_all(b"terminal").await.unwrap();
server.flush().await.unwrap();
let mut terminal = [0; 8];
client.read_exact(&mut terminal).await.unwrap();
assert_eq!(&terminal, b"terminal");
drop(client);
drop(server);
// A pathname replacement belongs to someone else, even in this private
// test directory. The original listener cannot unlink the replacement.
std::fs::remove_file(&path).unwrap();
std::fs::write(&path, b"replacement").unwrap();
drop(listener);
assert_eq!(std::fs::read(&path).unwrap(), b"replacement");
}
#[tokio::test]
async fn public_socket_parent_is_rejected_before_any_endpoint_is_created() {
let directory = tempfile::tempdir().unwrap();
std::fs::set_permissions(directory.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
let path = directory.path().join("ipc.sock");
assert!(LocalListener::bind(&path).is_err());
assert!(!path.exists());
}
}