Runtime Paths Checks as opt-in #935

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-09-29 19:24:29 +03:00
parent e3f62db347
commit 53f131e34d
16 changed files with 635 additions and 94 deletions
+3 -2
View File
@@ -1,14 +1,15 @@
//! Descriptor-anchored filesystem operations for privileged runtime paths.
//!
//! Submodules:
//! - `path`: symlink-free directory traversal and anchored path ownership
//! - `path`: strict or compatible directory traversal and anchored path ownership
//! - `write`: regular-file opening and durable atomic replacement
mod path;
mod write;
pub(crate) use path::{
AnchoredPath, chdir_nofollow_or_create, open_dir_nofollow, open_trusted_dir_nofollow_or_create,
AnchoredPath, chdir_nofollow_or_create, open_compatible_dir, open_dir_nofollow,
open_trusted_dir_nofollow_or_create,
};
pub(crate) use write::{
atomic_replace, atomic_replace_async, open_append_regular, open_append_regular_at,
+40 -1
View File
@@ -1,7 +1,7 @@
use std::ffi::{OsStr, OsString};
use std::io;
use std::os::fd::OwnedFd;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::{Component, Path};
use nix::fcntl::{OFlag, open, openat};
@@ -53,6 +53,28 @@ impl AnchoredPath {
Ok(Self { parent, name })
}
/// Anchors a runtime parent, applying trusted traversal only when explicitly requested.
pub(crate) fn open_runtime_parent(
path: &Path,
create_mode: Option<u32>,
strict_runtime_paths: bool,
) -> io::Result<Self> {
if strict_runtime_paths {
return match create_mode {
Some(mode) => Self::open_trusted_parent_or_create(path, mode),
None => Self::open_trusted_parent(path),
};
}
let name = path
.file_name()
.filter(|name| !name.is_empty())
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "path has no file name"))?
.to_os_string();
let parent =
open_compatible_dir(path.parent().unwrap_or_else(|| Path::new(".")), create_mode)?;
Ok(Self { parent, name })
}
fn open_with_parent_creation(path: &Path, create_mode: Option<u32>) -> io::Result<Self> {
let name = path
.file_name()
@@ -97,6 +119,23 @@ pub(crate) fn open_trusted_dir_nofollow_or_create(path: &Path, mode: u32) -> io:
open_dir_components(path, Some(mode), true)
}
/// Follows parent-directory symlinks without imposing ownership or permission policy.
pub(crate) fn open_compatible_dir(path: &Path, create_mode: Option<u32>) -> io::Result<OwnedFd> {
let path = if path.as_os_str().is_empty() {
Path::new(".")
} else {
path
};
if let Some(mode) = create_mode {
std::fs::DirBuilder::new()
.recursive(true)
.mode(mode)
.create(path)?;
}
// Retain the resolved directory inode so subsequent file operations do not rewalk the path.
open(path, DIRECTORY_FLAGS & !OFlag::O_NOFOLLOW, Mode::empty()).map_err(errno_to_io)
}
/// Opens a directory only when its entire path is owned by root or the effective user.
fn open_trusted_dir_nofollow(path: &Path) -> io::Result<OwnedFd> {
open_dir_components(path, None, true)
+45
View File
@@ -1,4 +1,5 @@
use std::os::unix::fs::{PermissionsExt, symlink};
use std::path::Path;
use super::path::AnchoredPath;
use super::write::atomic_replace_after_anchor;
@@ -15,6 +16,50 @@ fn directory_walk_rejects_intermediate_symlink() {
assert!(open_dir_nofollow(&link).is_err());
}
#[test]
fn runtime_parent_policy_creates_and_reads_relative_paths() {
use std::os::unix::fs::MetadataExt;
let current = std::env::current_dir().unwrap();
let directory = tempfile::tempdir_in(&current).unwrap();
let relative = directory.path().strip_prefix(&current).unwrap();
for strict_runtime_paths in [false, true] {
let mode = if strict_runtime_paths {
"strict"
} else {
"compatible"
};
let path = relative.join(mode).join("nested/telemt.pid");
let created =
AnchoredPath::open_runtime_parent(&path, Some(0o750), strict_runtime_paths).unwrap();
let opened = AnchoredPath::open_runtime_parent(&path, None, strict_runtime_paths).unwrap();
let created = std::fs::File::from(created.parent().try_clone().unwrap())
.metadata()
.unwrap();
let opened = std::fs::File::from(opened.parent().try_clone().unwrap())
.metadata()
.unwrap();
assert_eq!((created.dev(), created.ino()), (opened.dev(), opened.ino()));
assert_eq!(created.permissions().mode() & 0o777, 0o750);
}
assert!(AnchoredPath::open_runtime_parent(Path::new("telemt.pid"), None, false).is_ok());
}
#[test]
fn runtime_parent_policy_only_requires_trusted_ownership_when_strict() {
if !nix::unistd::Uid::effective().is_root() {
return;
}
let directory = tempfile::tempdir().unwrap();
let parent = directory.path().join("foreign-owner");
std::fs::create_dir(&parent).unwrap();
nix::unistd::chown(&parent, Some(nix::unistd::Uid::from_raw(65534)), None).unwrap();
let path = parent.join("telemt.pid");
assert!(AnchoredPath::open_runtime_parent(&path, None, false).is_ok());
assert!(AnchoredPath::open_runtime_parent(&path, None, true).is_err());
}
#[test]
fn atomic_replace_does_not_follow_final_symlink() {
let directory = tempfile::tempdir().unwrap();