Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
300 changes: 291 additions & 9 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions crates/volt-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ global-hotkey = "0.7"
dirs = "6"
notify = "9.0.0-rc.2"
crossbeam-channel = "0.5"
cap-std = "3"
unicode-normalization = "0.1"
uuid = { version = "1", features = ["v4"] }

[target.'cfg(target_os = "linux")'.dependencies]
gtk = "0.18"
Expand Down
90 changes: 90 additions & 0 deletions crates/volt-core/src/fs/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use cap_std::ambient_authority;
use cap_std::fs::Dir;
use std::fs;
use std::path::{Path, PathBuf};

use super::FsError;

pub(super) fn open_scoped_dir(base: &Path) -> Result<(PathBuf, Dir), FsError> {
let canonical_base = canonical_base_dir(base)?;
let dir = Dir::open_ambient_dir(&canonical_base, ambient_authority()).map_err(FsError::Io)?;
Ok((canonical_base, dir))
}

pub(super) fn scoped_path(path: &str) -> &Path {
if path.is_empty() {
Path::new(".")
} else {
Path::new(path)
}
}

/// Materialize a directory chain below `base` one component at a time and
/// reject symlink substitutions while walking it.
pub(super) fn ensure_scoped_directory(base: &Path, directory: &Path) -> Result<(), FsError> {
let canonical_base = canonical_base_dir(base)?;
let relative = directory
.strip_prefix(&canonical_base)
.map_err(|_| FsError::OutOfScope)?;
let mut current = canonical_base.clone();

for component in relative.components() {
current.push(component.as_os_str());
match fs::symlink_metadata(&current) {
Ok(metadata) => {
if metadata.file_type().is_symlink() {
return Err(FsError::Security(format!(
"symlink component is not allowed: '{}'",
current.display()
)));
}
if !metadata.is_dir() {
return Err(FsError::Security(format!(
"path component is not a directory: '{}'",
current.display()
)));
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
fs::create_dir(&current)?;
}
Err(error) => return Err(FsError::Io(error)),
}

let canonical_current = current.canonicalize()?;
if !canonical_current.starts_with(&canonical_base) {
return Err(FsError::OutOfScope);
}
current = canonical_current;
}

Ok(())
}

/// Ensure the parent directory for a to-be-created path exists within the
/// scoped base directory before returning that path to external callers.
pub(super) fn ensure_scoped_parent_dirs(base: &Path, resolved: &Path) -> Result<(), FsError> {
let Some(parent) = resolved.parent() else {
return Err(FsError::Security(
"Cannot resolve parent directory".to_string(),
));
};
ensure_scoped_directory(base, parent)
}

pub(super) fn ensure_not_symlink(path: &Path) -> Result<(), FsError> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.file_type().is_symlink() => Err(FsError::Security(format!(
"symlink targets are not allowed: '{}'",
path.display()
))),
Ok(_) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(FsError::Io(error)),
}
}

pub(super) fn canonical_base_dir(base: &Path) -> Result<PathBuf, FsError> {
base.canonicalize()
.map_err(|_| FsError::Security("Base directory does not exist".to_string()))
}
Loading
Loading