mirror of
https://github.com/telemt/telemt.git
synced 2026-11-02 13:52:03 +03:00
Runtime Paths Checks as opt-in #935
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(¤t).unwrap();
|
||||
let relative = directory.path().strip_prefix(¤t).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();
|
||||
|
||||
Reference in New Issue
Block a user