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
9 changes: 9 additions & 0 deletions vm/devices/support/fs/fuse/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub use reply::ReplySender;
pub use request::FuseOperation;
pub use request::Request;
pub use request::RequestReader;
pub use request::check_name;
pub use session::Session;
pub use session::SessionInfo;

Expand All @@ -29,6 +30,7 @@ use lx::LxString;
use protocol::*;
use std::time::Duration;
use zerocopy::FromBytes;
use zerocopy::FromZeros;
use zerocopy::Immutable;
use zerocopy::IntoBytes;
use zerocopy::KnownLayout;
Expand Down Expand Up @@ -436,6 +438,13 @@ impl fuse_entry_out {
attr,
}
}

pub fn new_dot(ino: u64, mode: u32) -> Self {
let mut entry = Self::new_zeroed();
entry.attr.ino = ino;
entry.attr.mode = mode;
entry
}
}

impl fuse_attr_out {
Expand Down
28 changes: 18 additions & 10 deletions vm/devices/support/fs/fuse/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ impl Request {
}
}

const NAME_MAX: usize = 255;

pub fn check_name(name: &[u8]) -> lx::Result<()> {
if name.is_empty()
|| name == b"."
|| name == b".."
|| name.contains(&b'/')
|| name.contains(&b'\0')
{
return Err(lx::Error::EINVAL);
}
if name.len() > NAME_MAX {
return Err(lx::Error::ENAMETOOLONG);
}
Ok(())
}

/// Helpers to parse FUSE messages.
pub trait RequestReader: io::Read {
/// Read until a matching byte is found.
Expand Down Expand Up @@ -224,19 +241,10 @@ pub trait RequestReader: io::Read {
Ok(lx::LxString::from_vec(buffer))
}

/// Maximum length of a file name component (NAME_MAX on Linux).
const NAME_MAX: usize = 255;

/// Read a NULL-terminated string and ensure it's a valid path name component.
fn name(&mut self) -> lx::Result<lx::LxString> {
let name = self.string()?;
if name.is_empty() || name == "." || name == ".." || name.as_bytes().contains(&b'/') {
return Err(lx::Error::EINVAL);
}
if name.len() > Self::NAME_MAX {
return Err(lx::Error::ENAMETOOLONG);
}

check_name(name.as_bytes())?;
Ok(name)
}
}
Expand Down
36 changes: 18 additions & 18 deletions vm/devices/support/fs/lxutil/src/windows/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,24 @@ pub fn analyze_delete_error(error: lx::Error, file_handle: &OwnedHandle) -> Dele
{
DeleteErrorAction::ReturnError(error)
} else if error.value() == lx::EIO {
// EIO can come from STATUS_CANNOT_DELETE, which for directories means
// the directory is not empty. Check if this is a directory and return
// ENOTEMPTY in that case.
if is_directory(file_handle) {
DeleteErrorAction::ReturnError(lx::Error::ENOTEMPTY)
} else {
DeleteErrorAction::ReturnError(error)
// EIO can come from STATUS_CANNOT_DELETE. Inspect the file's attributes
// to decide how to proceed:
// - a non-empty directory surfaces as STATUS_CANNOT_DELETE, so map it to
// ENOTEMPTY;
// - a read-only file cannot be deleted via the non-POSIX
// FileDispositionInformation path, so clear the read-only attribute and
// retry;
// - anything else (including a genuine I/O failure, or a handle without
// FILE_READ_ATTRIBUTES) is returned as-is so the read-only attribute is
// not disturbed.
match util::query_information_file::<FileSystem::FILE_BASIC_INFORMATION>(file_handle) {
Ok(info) if info.FileAttributes & W32Fs::FILE_ATTRIBUTE_DIRECTORY.0 != 0 => {
DeleteErrorAction::ReturnError(lx::Error::ENOTEMPTY)
}
Ok(info) if info.FileAttributes & W32Fs::FILE_ATTRIBUTE_READONLY.0 != 0 => {
DeleteErrorAction::TryReadOnlyWorkaround
}
_ => DeleteErrorAction::ReturnError(error),
}
} else {
DeleteErrorAction::TryReadOnlyWorkaround
Expand All @@ -322,17 +333,6 @@ pub fn delete_file(fs_context: &FsContext, file_handle: &OwnedHandle) -> lx::Res
}
}

/// Check if a file handle refers to a directory.
fn is_directory(file_handle: &OwnedHandle) -> bool {
if let Ok(info) =
util::query_information_file::<FileSystem::FILE_BASIC_INFORMATION>(file_handle)
{
info.FileAttributes & W32Fs::FILE_ATTRIBUTE_DIRECTORY.0 != 0
} else {
false
}
}

pub fn delete_file_core(fs_context: &FsContext, file_handle: &OwnedHandle) -> lx::Result<()> {
if fs_context
.compatibility_flags
Expand Down
55 changes: 54 additions & 1 deletion vm/devices/support/fs/lxutil/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,45 @@ impl LxVolume {
Default::default(),
)?;

// Emulate Linux rename semantics that NTFS enforces natively but FAT/exFAT does not.
if !self
.state
.fs_context
.compatibility_flags
.supports_posix_unlink_rename()
{
let is_dir = |h: &OwnedHandle| -> lx::Result<bool> {
Ok(
util::query_information_file::<FileSystem::FILE_BASIC_INFORMATION>(h)?
.FileAttributes
& W32Fs::FILE_ATTRIBUTE_DIRECTORY.0
!= 0,
)
};
let source_is_dir = is_dir(&handle)?;

// Reject renaming a directory into its own descendant (EINVAL).
if source_is_dir && new_path != path && new_path.starts_with(path) {
return Err(lx::Error::EINVAL);
}

// Reject cross-type replacement: file-over-directory (ENOTDIR) or directory-over-file (EISDIR).
match self.open_file(new_path, W32Fs::FILE_READ_ATTRIBUTES, Default::default()) {
Ok(target_handle) => {
let target_is_dir = is_dir(&target_handle)?;
if source_is_dir && !target_is_dir {
return Err(lx::Error::ENOTDIR);
}
if !source_is_dir && target_is_dir {
return Err(lx::Error::EISDIR);
}
}
// A missing target is fine; the rename creates it.
Err(err) if err.value() == lx::ENOENT => {}
Err(err) => return Err(err),
}
}

let flags = fs::RenameFlags::default();
let error = match fs::rename(&handle, &self.root, new_path, &self.state.fs_context, flags) {
Ok(_) => return Ok(()),
Expand All @@ -425,7 +464,11 @@ impl LxVolume {
.compatibility_flags
.supports_posix_unlink_rename()
{
match self.open_file(new_path, W32Fs::DELETE, Default::default()) {
match self.open_file(
new_path,
W32Fs::FILE_READ_ATTRIBUTES | W32Fs::DELETE,
Default::default(),
) {
Ok(target_handle) => self.delete_file(&target_handle)?,
Err(err) => {
// ENOENT means the rename can proceed.
Expand Down Expand Up @@ -700,6 +743,16 @@ impl LxVolume {
assert!(path.is_relative() && !path.as_os_str().is_empty());
self.check_sandbox_enforcement(path)?;

// Handle filesystems that do not support reparse points.
if !self
.state
.fs_context
.compatibility_flags
.supports_reparse_points()
{
return Err(lx::Error::EPERM);
}

// Convert the target to its native Windows format.
let win_target = Path::from_lx(target);

Expand Down
40 changes: 23 additions & 17 deletions vm/devices/support/fs/lxutil/src/windows/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,29 +155,32 @@ fn get_token_for_access_check() -> lx::Result<OwnedHandle> {
let mut client_token_raw = Foundation::HANDLE::default();
let mut duplicate_token_raw = Foundation::HANDLE::default();
// SAFETY: Calling Win32 API as documented.
let result = unsafe {
check_status(FileSystem::NtOpenThreadToken(
//
// N.B. The raw NTSTATUS is inspected directly (rather than going through
// `check_status`) so that `STATUS_NO_TOKEN` can be distinguished from
// other failures. `check_status` maps the NTSTATUS to an `lx::Error`
// errno, which cannot be compared against an NTSTATUS constant.
let status = unsafe {
FileSystem::NtOpenThreadToken(
Threading::GetCurrentThread(),
Security::TOKEN_QUERY.0 | Security::TOKEN_DUPLICATE.0,
true,
&mut client_token_raw,
))
)
};

if let Err(e) = result {
// Use the process token if there's no token.
if e.value() == Foundation::STATUS_NO_TOKEN.0 {
// SAFETY: Calling Win32 API as documented.
unsafe {
let _ = check_status(FileSystem::NtOpenProcessToken(
Threading::GetCurrentProcess(),
Security::TOKEN_QUERY.0 | Security::TOKEN_DUPLICATE.0,
&mut client_token_raw,
))?;
}
} else {
return Err(e);
if status == Foundation::STATUS_NO_TOKEN {
// The thread is not impersonating, so use the process token instead.
// SAFETY: Calling Win32 API as documented.
unsafe {
let _ = check_status(FileSystem::NtOpenProcessToken(
Threading::GetCurrentProcess(),
Security::TOKEN_QUERY.0 | Security::TOKEN_DUPLICATE.0,
&mut client_token_raw,
))?;
}
} else {
let _ = check_status(status)?;
}

// Create the RAII handle now so it gets closed on drop if we need
Expand Down Expand Up @@ -213,7 +216,10 @@ fn get_token_for_access_check() -> lx::Result<OwnedHandle> {
},
EffectiveOnly: false,
};
let mut object_attributes = Wdk::Foundation::OBJECT_ATTRIBUTES::default();
let mut object_attributes = Wdk::Foundation::OBJECT_ATTRIBUTES {
Length: size_of::<Wdk::Foundation::OBJECT_ATTRIBUTES>() as u32,
..Default::default()
};
object_attributes.SecurityQualityOfService =
ptr::from_ref::<W32Sec::SECURITY_QUALITY_OF_SERVICE>(&security_qos).cast();

Expand Down
17 changes: 17 additions & 0 deletions vm/devices/virtio/virtio_resources/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,23 @@ pub mod fs {
SectionFs {
root_path: String,
},
/// Expose multiple host folders behind a single device, each as a named
/// child of a synthetic root. Lets one virtio-fs device (one tag, one
/// PCI/MMIO footprint) serve many shares.
Aggregate {
children: Vec<VirtioFsAggregateChild>,
},
}

/// A single host folder exposed as a named child of a [`VirtioFsBackend::Aggregate`].
#[derive(MeshPayload)]
pub struct VirtioFsAggregateChild {
/// Name of this child's directory under the aggregate's synthetic root;
/// the guest bind-mounts `<aggregate-mount>/<name>` onto the user's
/// target path.
pub name: String,
pub root_path: String,
pub mount_options: String,
}

impl ResourceId<VirtioDeviceHandle> for VirtioFsHandle {
Expand Down
Loading
Loading