This commit is contained in:
Mateusz Gruszczyński committed 2026-10-17 11:57:16 +02:00
1 parent 6b937aaf85
commit 87c8393d86
79 files changed
+6727 -297

No files matched your search

+53 -23
View File
@@ -1,7 +1,32 @@
use super::{CompiledService, SymlinkPolicy};
use std::{fs::File, io, path::Path};
use std::{
fs::File,
io,
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::ffi::CString;
use std::{ffi::CString, sync::Arc};
/// Only filesystem state crosses into the blocking pool. Cloning an entire
/// CompiledService here would also copy MIME, rewrite, PHP and security rules
/// for every file or candidate opened during a request.
pub(super) struct FilesystemRoot {
root: PathBuf,
symlink_policy: SymlinkPolicy,
#[cfg(unix)]
root_handle: Arc<File>,
}
impl FilesystemRoot {
pub(super) fn for_service(service: &CompiledService) -> Self {
Self {
root: service.root.clone(),
symlink_policy: service.symlink_policy,
#[cfg(unix)]
root_handle: service.root_handle.clone(),
}
}
}
#[derive(Debug)]
pub(super) struct Opened {
@@ -54,7 +79,7 @@ pub(super) fn path_components(path: &str) -> io::Result<Vec<String>> {
}
#[cfg(unix)]
pub(super) fn secure_open_sync(service: &CompiledService, components: &[String]) -> io::Result<Opened> {
pub(super) fn secure_open_sync(root: &FilesystemRoot, components: &[String]) -> io::Result<Opened> {
use std::os::unix::io::{AsRawFd, FromRawFd};
if components.is_empty() {
@@ -66,7 +91,7 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
let dot = cstring_component(".")?;
let fd = unsafe {
libc::openat(
service.root_handle.as_raw_fd(),
root.root_handle.as_raw_fd(),
dot.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW,
)
@@ -78,7 +103,10 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
let metadata = current.metadata()?;
return Ok(Opened { file: current, metadata });
}
let mut current = service.root_handle.try_clone()?;
// Borrow the already-owned root descriptor for the first openat. Opening a
// child does not change its directory offset, so a dup/close pair buys no
// isolation. Empty paths still open "." above for independent readdir state.
let mut current: Option<File> = None;
for (index, component) in components.iter().enumerate() {
let name = cstring_component(component)?;
let intermediate = index + 1 != components.len();
@@ -86,7 +114,7 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
// explicit Linux-only opt-in: the opened descriptor is checked through
// /proc/self/fd after each resolution step before it can be used further.
let mut flags = libc::O_RDONLY | libc::O_CLOEXEC;
if service.symlink_policy == SymlinkPolicy::Deny {
if root.symlink_policy == SymlinkPolicy::Deny {
flags |= libc::O_NOFOLLOW;
}
if intermediate {
@@ -95,13 +123,14 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
// Avoid blocking on special files. Only regular files/directories are served.
flags |= libc::O_NONBLOCK;
}
let fd = unsafe { libc::openat(current.as_raw_fd(), name.as_ptr(), flags) };
let parent = current.as_ref().unwrap_or(root.root_handle.as_ref());
let fd = unsafe { libc::openat(parent.as_raw_fd(), name.as_ptr(), flags) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
if service.symlink_policy == SymlinkPolicy::InsideRoot {
if root.symlink_policy == SymlinkPolicy::InsideRoot {
#[cfg(target_os = "linux")]
verify_linux_fd_beneath_root(service, fd)?;
verify_linux_fd_beneath_root(root, fd)?;
#[cfg(not(target_os = "linux"))]
{
unsafe { libc::close(fd) };
@@ -111,8 +140,9 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
));
}
}
current = unsafe { File::from_raw_fd(fd) };
current = Some(unsafe { File::from_raw_fd(fd) });
}
let current = current.expect("nonempty path opened its final component");
let metadata = current.metadata()?;
if !metadata.is_file() && !metadata.is_dir() {
return Err(io::Error::new(
@@ -124,7 +154,7 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
}
#[cfg(target_os = "linux")]
fn verify_linux_fd_beneath_root(service: &CompiledService, fd: libc::c_int) -> io::Result<()> {
fn verify_linux_fd_beneath_root(root: &FilesystemRoot, fd: libc::c_int) -> io::Result<()> {
let target = match std::fs::read_link(format!("/proc/self/fd/{fd}")) {
Ok(target) => target,
Err(error) => {
@@ -132,7 +162,7 @@ fn verify_linux_fd_beneath_root(service: &CompiledService, fd: libc::c_int) -> i
return Err(error);
}
};
if target.starts_with(&service.root) {
if target.starts_with(&root.root) {
Ok(())
} else {
unsafe { libc::close(fd) };
@@ -144,13 +174,13 @@ fn verify_linux_fd_beneath_root(service: &CompiledService, fd: libc::c_int) -> i
}
#[cfg(not(unix))]
pub(super) fn secure_open_sync(service: &CompiledService, components: &[String]) -> io::Result<Opened> {
let mut candidate = service.root.clone();
pub(super) fn secure_open_sync(root: &FilesystemRoot, components: &[String]) -> io::Result<Opened> {
let mut candidate = root.root.clone();
for component in components {
candidate.push(component);
}
let canonical = std::fs::canonicalize(&candidate)?;
if !canonical.starts_with(&service.root) {
if !canonical.starts_with(&root.root) {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"path escaped document root",
@@ -169,13 +199,13 @@ pub(super) fn secure_open_sync(service: &CompiledService, components: &[String])
#[cfg(unix)]
pub(super) fn list_directory_sync(
service: &CompiledService,
root: &FilesystemRoot,
components: &[String],
) -> io::Result<Vec<DirectoryEntry>> {
use std::ffi::CStr;
use std::os::unix::io::IntoRawFd;
let opened = secure_open_sync(service, components)?;
let opened = secure_open_sync(root, components)?;
if !opened.metadata.is_dir() {
return Err(io::Error::new(io::ErrorKind::NotADirectory, "not a directory"));
}
@@ -206,7 +236,7 @@ pub(super) fn list_directory_sync(
// Re-open through the root fd using the configured symlink policy. The
// default omits every symlink; Linux inside_root may include a target only
// after the opened descriptor has been proven to remain below the root.
let Ok(child_opened) = secure_open_sync(service, &child) else {
let Ok(child_opened) = secure_open_sync(root, &child) else {
continue;
};
entries.push(DirectoryEntry {
@@ -226,19 +256,19 @@ pub(super) fn list_directory_sync(
#[cfg(not(unix))]
pub(super) fn list_directory_sync(
service: &CompiledService,
root: &FilesystemRoot,
components: &[String],
) -> io::Result<Vec<DirectoryEntry>> {
let opened = secure_open_sync(service, components)?;
let opened = secure_open_sync(root, components)?;
if !opened.metadata.is_dir() {
return Err(io::Error::new(io::ErrorKind::NotADirectory, "not a directory"));
}
let mut path = service.root.clone();
let mut path = root.root.clone();
for component in components {
path.push(component);
}
let canonical = std::fs::canonicalize(path)?;
if !canonical.starts_with(&service.root) {
if !canonical.starts_with(&root.root) {
return Err(io::Error::new(io::ErrorKind::PermissionDenied, "path escaped document root"));
}
let mut entries = Vec::new();
@@ -249,7 +279,7 @@ pub(super) fn list_directory_sync(
};
let mut child = components.to_vec();
child.push(name.clone());
let Ok(child_opened) = secure_open_sync(service, &child) else {
let Ok(child_opened) = secure_open_sync(root, &child) else {
continue;
};
entries.push(DirectoryEntry {