Skip to content
Open
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
25 changes: 16 additions & 9 deletions nvx-rs/README.md

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions nvx-rs/src/host/platform/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,54 @@ pub(crate) fn private_file_problem(_file: &File) -> io::Result<Option<String>> {
Ok(None)
}

/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the
/// current user, if it is not, as [`check_owner_only_dir`] does: OpenVMM creates it with mode 0700,
/// and its files with mode 0600.
pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result<Option<String>> {
check_owner_only_dir(path)
}

/// Returns why another user than the current one, or root, may change the file that `file` holds
/// open, if one may: unless it belongs to the current user or to root, and grants neither its
/// group nor others the permission to write it.
pub(crate) fn foreign_writer_problem(file: &File) -> io::Result<Option<String>> {
let metadata = file.metadata()?;
Ok(foreign_writer(
metadata.uid(),
metadata.mode(),
sys::effective_ids().0,
))
}

/// Returns why another user than `euid`, or root, may change a file that the user `uid` owns with
/// the mode `mode`, if one may.
pub(crate) fn foreign_writer(uid: u32, mode: u32, euid: u32) -> Option<String> {
let mode = mode & PERMISSION_BITS;
if uid != euid && uid != 0 {
Some(format!(
"belongs to user {uid}, neither the current user {euid} nor root"
))
} else if mode & 0o022 != 0 {
Some(format!("is writable by other users (mode {mode:03o})"))
} else {
None
}
}

/// Lets the regular file that `file` holds open for writing take no space for the blocks that
/// nobody writes: nothing to do on Linux, whose filesystems allocate no block that a file's
/// extension leaves unwritten.
pub(crate) fn make_sparse(_file: &File) -> io::Result<()> {
Ok(())
}

/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with
/// [`io::ErrorKind::AlreadyExists`]: `renameat2` with `RENAME_NOREPLACE`, which a filesystem that
/// cannot rename without replacing refuses.
pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
sys::rename_no_replace(from, to)
}

/// Gives the regular file `path` the mode `mode` if it belongs to the current user, and leaves
/// anything else, which includes a symbolic link or a missing file, unchanged.
///
Expand Down
21 changes: 21 additions & 0 deletions nvx-rs/src/host/platform/other.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,27 @@ pub(crate) fn private_file_problem(_file: &File) -> io::Result<Option<String>> {
Err(unsupported())
}

/// Checks the directory `path`, a snapshot that OpenVMM published, as [`check_owner_only_dir`]
/// does.
pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result<Option<String>> {
check_owner_only_dir(path)
}

/// Fails: this host cannot check who may change a file.
pub(crate) fn foreign_writer_problem(_file: &File) -> io::Result<Option<String>> {
Err(unsupported())
}

/// Fails: this host makes no template's files.
pub(crate) fn make_sparse(_file: &File) -> io::Result<()> {
Err(unsupported())
}

/// Fails: this host makes no template's files.
pub(crate) fn rename_no_replace(_from: &Path, _to: &Path) -> io::Result<()> {
Err(unsupported())
}

/// Leaves the command as it is, unless it should inherit descriptors: this host cannot restrict
/// what the command inherits, and fails with [`io::ErrorKind::Unsupported`] instead.
pub(crate) fn detach(_command: &mut Command, inherit: &[Inherited<'_>]) -> io::Result<()> {
Expand Down
143 changes: 127 additions & 16 deletions nvx-rs/src/host/platform/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ use windows_sys::Win32::Foundation::{
ERROR_ACCESS_DENIED, ERROR_ALREADY_EXISTS, ERROR_BROKEN_PIPE, ERROR_FILE_NOT_FOUND,
ERROR_INVALID_PARAMETER, ERROR_IO_PENDING, ERROR_MORE_DATA, ERROR_NO_DATA,
ERROR_OPERATION_ABORTED, ERROR_PIPE_BUSY, ERROR_PIPE_NOT_CONNECTED, ERROR_SHARING_VIOLATION,
ERROR_SUCCESS, FILETIME, FreeLibrary, GENERIC_READ, GENERIC_WRITE, GetHandleInformation,
GetLastError, HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE, SetHandleInformation,
WAIT_OBJECT_0, WAIT_TIMEOUT,
ERROR_SUCCESS, FILETIME, FreeLibrary, GENERIC_ALL, GENERIC_READ, GENERIC_WRITE,
GetHandleInformation, GetLastError, HANDLE, HANDLE_FLAG_INHERIT, INVALID_HANDLE_VALUE,
SetHandleInformation, WAIT_OBJECT_0, WAIT_TIMEOUT,
};
use windows_sys::Win32::Security::{
ACCESS_ALLOWED_ACE, ACE_HEADER, ACL, ACL_REVISION, ACL_SIZE_INFORMATION, AclSizeInformation,
Expand All @@ -31,17 +31,21 @@ use windows_sys::Win32::Security::{
WinBuiltinAdministratorsSid, WinCreatorOwnerRightsSid, WinCreatorOwnerSid, WinLocalSystemSid,
};
use windows_sys::Win32::Storage::FileSystem::{
BY_HANDLE_FILE_INFORMATION, CreateDirectoryW, CreateFileW, FILE_ALL_ACCESS,
FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED, GetFileInformationByHandle, OPEN_EXISTING,
READ_CONTROL, ReadFile, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT, WriteFile,
BY_HANDLE_FILE_INFORMATION, CreateDirectoryW, CreateFileW, DELETE, FILE_ALL_ACCESS,
FILE_APPEND_DATA, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_DELETE_CHILD,
FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_FLAG_OVERLAPPED,
FILE_WRITE_ATTRIBUTES, FILE_WRITE_DATA, FILE_WRITE_EA, GetFileInformationByHandle, MoveFileExW,
OPEN_EXISTING, READ_CONTROL, ReadFile, SECURITY_IDENTIFICATION, SECURITY_SQOS_PRESENT,
WRITE_DAC, WRITE_OWNER, WriteFile,
};
#[cfg(feature = "cli")]
use windows_sys::Win32::System::Console::{
CTRL_C_EVENT, GetStdHandle, STD_ERROR_HANDLE, STD_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
SetConsoleCtrlHandler,
};
use windows_sys::Win32::System::IO::{CancelIoEx, GetOverlappedResult, OVERLAPPED};
use windows_sys::Win32::System::IO::{
CancelIoEx, DeviceIoControl, GetOverlappedResult, OVERLAPPED,
};
use windows_sys::Win32::System::LibraryLoader::{
GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW,
};
Expand Down Expand Up @@ -1079,6 +1083,16 @@ pub(crate) fn check_owner_only_dir(path: &Path) -> io::Result<Option<String>> {

/// Returns why the directory `path` cannot hold a sandbox store of `user`, if it cannot.
fn store_dir_problem(path: &Path, user: &CurrentUser) -> io::Result<Option<String>> {
plain_dir_problem(path, user, Required::PrivateDirectory)
}

/// Returns why the directory `path` is not a plain directory, not a link or any other reparse
/// point, whose owner `user` trusts, and whose access control list is as `required`, if it is not.
fn plain_dir_problem(
path: &Path,
user: &CurrentUser,
required: Required,
) -> io::Result<Option<String>> {
let directory = OpenOptions::new()
.access_mode(READ_CONTROL)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
Expand All @@ -1088,7 +1102,21 @@ fn store_dir_problem(path: &Path, user: &CurrentUser) -> io::Result<Option<Strin
{
return Ok(Some("is not a plain directory".to_owned()));
}
privacy_problem(&directory, user, true)
privacy_problem(&directory, user, required)
}

/// Returns why the directory `path`, a snapshot that OpenVMM published, is not private to the
/// current user, if it is not: unless it is a directory, not a link or any other reparse point,
/// that belongs to the user, or to SYSTEM or the Administrators group, whose access control list
/// is present, holds only entries that allow or deny access, and allows no one but those and the
/// owner's aliases, as [`check_owner_only_dir`] requires.
///
/// Unlike [`check_owner_only_dir`], it does not require the directory to pass entries on to the
/// files created in it: OpenVMM gives the directory, and each file that it publishes in it, a
/// protected list of their own, which allows SYSTEM and the owner, and the directory's own list
/// already stops anyone else from creating a file in it.
pub(crate) fn check_snapshot_dir(path: &Path) -> io::Result<Option<String>> {
plain_dir_problem(path, &CurrentUser::get()?, Required::Private)
}

/// Returns why the file that `file` holds open, the lock, a record, or the capability of a
Expand All @@ -1097,16 +1125,51 @@ fn store_dir_problem(path: &Path, user: &CurrentUser) -> io::Result<Option<Strin
/// A file's own access control list decides who may open it, whatever its directory allows, so
/// a file of a store that allows anyone else could have been written by them.
pub(crate) fn private_file_problem(file: &fs::File) -> io::Result<Option<String>> {
privacy_problem(file, &CurrentUser::get()?, false)
privacy_problem(file, &CurrentUser::get()?, Required::Private)
}

/// Returns why another user than the current one, SYSTEM, or the Administrators group may change
/// the file that `file` holds open, if one may: unless it belongs to the user, or to SYSTEM or the
/// Administrators group, and its access control list allows no one else to write or extend it, to
/// change its attributes, its list, or its owner, or to delete it. Others may read it.
pub(crate) fn foreign_writer_problem(file: &fs::File) -> io::Result<Option<String>> {
privacy_problem(file, &CurrentUser::get()?, Required::Unwritable)
}

/// The rights of an access control entry that change a file or directory: write or extend it, or
/// its extended attributes, delete it or its children, change its attributes, its list, or its
/// owner, and the generic rights that include those.
const CHANGE_RIGHTS: u32 = FILE_WRITE_DATA
| FILE_APPEND_DATA
| FILE_WRITE_EA
| FILE_DELETE_CHILD
| FILE_WRITE_ATTRIBUTES
| DELETE
| WRITE_DAC
| WRITE_OWNER
| GENERIC_WRITE
| GENERIC_ALL;

/// What [`privacy_problem`] requires of the access control list of a file or directory, whose
/// owner the user must trust in any case.
#[derive(Clone, Copy, PartialEq, Eq)]
enum Required {
/// That it allow no one else any access.
Private,
/// That it allow no one else any access, and pass entries on to the files created in it, as
/// a directory of a store must.
PrivateDirectory,
/// That it allow no one else any of the [`CHANGE_RIGHTS`].
Unwritable,
}

/// Returns why the file or directory that `file` holds is not private to `user`, if it is not: it
/// must belong to the user, or to SYSTEM or the Administrators group, and its access control list
/// must allow no one else. A `directory` must also pass entries on to the files created in it.
/// must be as `required`.
fn privacy_problem(
file: &fs::File,
user: &CurrentUser,
directory: bool,
required: Required,
) -> io::Result<Option<String>> {
let handle = file.as_raw_handle() as HANDLE;
let information = OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION;
Expand Down Expand Up @@ -1168,9 +1231,14 @@ fn privacy_problem(
match header.AceType {
ACCESS_DENIED_ACE_TYPE => {}
ACCESS_ALLOWED_ACE_TYPE => {
let entry = ace.cast::<ACCESS_ALLOWED_ACE>();
// SAFETY: an entry that allows access starts as an ACCESS_ALLOWED_ACE.
let mask = unsafe { (*entry).Mask };
if required == Required::Unwritable && mask & CHANGE_RIGHTS == 0 {
continue;
}
// SAFETY: an entry that allows access holds its identifier from `SidStart` on.
let sid: PSID =
unsafe { (&raw mut (*ace.cast::<ACCESS_ALLOWED_ACE>()).SidStart).cast() };
let sid: PSID = unsafe { (&raw mut (*entry).SidStart).cast() };
// The creator owner and the owner rights stand for an owner: the owner, or the
// creator of a file in the directory, which only those that it allows may be.
// SAFETY: the identifier is valid.
Expand All @@ -1179,7 +1247,13 @@ fn privacy_problem(
|| IsWellKnownSid(sid, WinCreatorOwnerRightsSid) != 0
};
if !owner_alias && !user.trusts(sid) {
return Ok(Some("allows other users".to_owned()));
return Ok(Some(
match required {
Required::Unwritable => "allows other users to change it",
Required::Private | Required::PrivateDirectory => "allows other users",
}
.to_owned(),
));
}
}
other => {
Expand All @@ -1191,14 +1265,51 @@ fn privacy_problem(
}
// A file created without entries to inherit takes its creator's default list instead, which
// may allow anyone.
if directory && !inherited_by_files {
if required == Required::PrivateDirectory && !inherited_by_files {
return Ok(Some(
"passes no access control entries on to the files created in it".to_owned(),
));
}
Ok(None)
}

/// Lets the regular file that `file` holds open for writing take no space for the ranges that
/// nobody writes: marks it sparse, which NTFS otherwise allocates whole as the file is extended.
pub(crate) fn make_sparse(file: &fs::File) -> io::Result<()> {
/// `FSCTL_SET_SPARSE`, in `winioctl.h`, which marks a file sparse without a buffer.
const FSCTL_SET_SPARSE: u32 = 0x0009_00c4;
let mut returned = 0;
// SAFETY: the handle is open for as long as `file` is borrowed, the control takes no input or
// output buffer, and the count's pointer is valid.
let set = unsafe {
DeviceIoControl(
file.as_raw_handle() as HANDLE,
FSCTL_SET_SPARSE,
ptr::null(),
0,
ptr::null_mut(),
0,
&mut returned,
ptr::null_mut(),
)
};
if set == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}

/// Renames `from` to `to` unless `to` exists, which it leaves as it is, failing with
/// [`io::ErrorKind::AlreadyExists`]: `MoveFileExW` without `MOVEFILE_REPLACE_EXISTING`.
pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
let (from, to) = (wide(from), wide(to));
// SAFETY: both names are NUL-terminated.
if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), 0) } == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}

/// Replaces the access control list of the file or directory `path` with the protected list of
/// the descriptor `sddl`, in the security descriptor definition language.
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion nvx-rs/src/host/store/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pub(super) fn private_options() -> OpenOptions {
///
/// On Unix, the umask may clear bits of the mode that the file is created with, even the
/// current user's permission to read it, so the mode is set again through the new file.
pub(super) fn create_new(path: &Path) -> io::Result<File> {
pub(crate) fn create_new(path: &Path) -> io::Result<File> {
let file = private_options().create_new(true).open(path)?;
#[cfg(unix)]
{
Expand Down
21 changes: 13 additions & 8 deletions nvx-rs/src/host/supervisor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,19 @@
//! ## The lock order
//!
//! These steps, and every other transition of a template store or a sandbox, take their locks in
//! one order: the template store's lock, then the sandbox's
//! [lifecycle lock](super::store#lifecycle-locks), which guards its clone binding and its launch
//! journal, then the scratch image's claim. None waits for a lock while it holds a later one: an
//! acquisition releases the store's lock before it returns; a release, a launch, a stop, a
//! reattach, and a removal of the sandbox take no lock of the store; and a retirement takes no
//! lock of a sandbox. No lock lasts for a VM's lifetime: the caller releases the sandbox's lock
//! once the launch commits or ends, and the scratch image's claim belongs to the VMM, which holds
//! it until it exits.
//! one order: the lifecycle lock of a capture class's slot, which serializes the class's captures,
//! as [`Store::refresh_template`](crate::host::template::Store::refresh_template) describes, then
//! the template store's lock, then the [lifecycle lock](super::store#lifecycle-locks) of a sandbox,
//! which guards its clone binding and its launch journal, or of a capture's transaction, the
//! sandbox store of the capture's VM, then the scratch image's claim. None waits for a lock while
//! it holds a later one: under its slot's lock, a capture, or its recovery, takes its
//! transaction's lock and then, once it has released it, the store's lock, to publish, and no
//! other; an acquisition releases the store's lock before it returns; a release, a launch, a stop,
//! a reattach, and a removal of the sandbox take no lock of the store; and a retirement takes no
//! lock of a sandbox. No lock but those of a capture lasts for a VM's lifetime: a capture holds
//! its slot's lock and its transaction's until its VM has ended, while the caller of a clone's
//! launch releases the sandbox's lock once the launch commits or ends, and the scratch image's
//! claim belongs to the VMM, which holds it until it exits.
//!
//! ## Crashes
//!
Expand Down
36 changes: 36 additions & 0 deletions nvx-rs/src/host/sys/fs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//! Filesystem operations of Linux hosts that the standard library lacks.

use std::ffi::CString;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

use nix::errno::Errno;

/// `renameat2`'s `RENAME_NOREPLACE`, in `linux/fs.h`.
const RENAME_NOREPLACE: nix::libc::c_uint = 1;

/// Renames `from` to `to` unless `to` exists, which it leaves as it is, with `renameat2`'s
/// `RENAME_NOREPLACE`, through the system call itself, which `nix` wraps only for glibc. Fails
/// with [`io::ErrorKind::AlreadyExists`] if `to` exists, and as `rename` fails otherwise; a
/// filesystem that cannot rename without replacing fails it with `EINVAL`.
pub(crate) fn rename_no_replace(from: &Path, to: &Path) -> io::Result<()> {
let text = |path: &Path| {
CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "a path holds a NUL byte"))
};
let (from, to) = (text(from)?, text(to)?);
// SAFETY: `renameat2` reads the two NUL-terminated paths, which outlive the call.
Errno::result(unsafe {
nix::libc::syscall(
nix::libc::SYS_renameat2,
nix::libc::AT_FDCWD,
from.as_ptr(),
nix::libc::AT_FDCWD,
to.as_ptr(),
RENAME_NOREPLACE,
)
})
.map(drop)
.map_err(io::Error::from)
}
Loading
Loading