From 00f11556ee20d25dc989241d63d44943cfb9d4e1 Mon Sep 17 00:00:00 2001 From: Putta Khunchalee Date: Sat, 24 Feb 2024 20:15:41 +0700 Subject: [PATCH] Revises how HostFS identifies the files (#681) --- src/kernel/Cargo.toml | 8 + src/kernel/src/fs/host/file.rs | 326 +++++++++++++++++++++++++------- src/kernel/src/fs/host/mod.rs | 81 ++++---- src/kernel/src/fs/host/vnode.rs | 24 +-- 4 files changed, 322 insertions(+), 117 deletions(-) diff --git a/src/kernel/Cargo.toml b/src/kernel/Cargo.toml index 121787177..98ddb9cf8 100644 --- a/src/kernel/Cargo.toml +++ b/src/kernel/Cargo.toml @@ -32,12 +32,20 @@ tls = { path = "../tls" } [target.'cfg(windows)'.dependencies.windows-sys] version = "0.52" features = [ + "Wdk", + "Wdk_Foundation", + "Wdk_Storage", + "Wdk_Storage_FileSystem", + "Win32", "Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Diagnostics_Debug", + "Win32_System_IO", + "Win32_System_Kernel", "Win32_System_Memory", "Win32_System_SystemInformation", "Win32_System_Threading", "Win32_System_Time", + "Win32_System_WindowsProgramming", ] diff --git a/src/kernel/src/fs/host/file.rs b/src/kernel/src/fs/host/file.rs index 18bbbafda..1f5dad465 100644 --- a/src/kernel/src/fs/host/file.rs +++ b/src/kernel/src/fs/host/file.rs @@ -1,65 +1,143 @@ +use std::collections::HashMap; use std::io::Error; use std::mem::zeroed; -use std::path::{Path, PathBuf}; +use std::path::Path; +use std::sync::{Arc, Mutex, Weak}; /// Encapsulate a raw file or directory on the host. #[derive(Debug)] pub struct HostFile { - path: PathBuf, raw: RawFile, + parent: Option>, + children: Mutex>>, } impl HostFile { - pub fn open>(path: P) -> Result { - let path = path.into(); - let raw = Self::raw_open(&path)?; - - Ok(Self { path, raw }) - } - - pub fn path(&self) -> &Path { - &self.path - } - #[cfg(unix)] - pub fn is_directory(&self) -> Result { - use libc::{fstat, S_IFDIR, S_IFMT}; + pub fn root(path: impl AsRef) -> Result { + use libc::{open, O_CLOEXEC, O_DIRECTORY, O_RDONLY}; + use std::ffi::CString; + use std::os::unix::ffi::OsStrExt; - let mut stat = unsafe { zeroed() }; + let path = path.as_ref(); + let path = CString::new(path.as_os_str().as_bytes()).unwrap(); + let fd = unsafe { open(path.as_ptr(), O_RDONLY | O_CLOEXEC | O_DIRECTORY) }; - if unsafe { fstat(self.raw, &mut stat) } < 0 { - return Err(Error::last_os_error()); + if fd < 0 { + Err(Error::last_os_error()) + } else { + Ok(Self { + raw: fd, + parent: None, + children: Mutex::default(), + }) } - - Ok((stat.st_mode & S_IFMT) == S_IFDIR) } #[cfg(windows)] - pub fn is_directory(&self) -> Result { + pub fn root(path: impl AsRef) -> Result { + use std::os::windows::ffi::OsStrExt; + use std::ptr::null_mut; + use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; + use windows_sys::Wdk::Storage::FileSystem::{NtCreateFile, FILE_DIRECTORY_FILE, FILE_OPEN}; + use windows_sys::Win32::Foundation::{ + RtlNtStatusToDosError, STATUS_SUCCESS, UNICODE_STRING, + }; use windows_sys::Win32::Storage::FileSystem::{ - GetFileInformationByHandle, FILE_ATTRIBUTE_DIRECTORY, + FILE_READ_ATTRIBUTES, FILE_READ_EA, FILE_SHARE_READ, FILE_SHARE_WRITE, READ_CONTROL, }; + use windows_sys::Win32::System::Kernel::OBJ_CASE_INSENSITIVE; - let mut info = unsafe { zeroed() }; + // Encode path name. + let path = path.as_ref(); + let mut path: Vec = path.as_os_str().encode_wide().collect(); + let len: u16 = (path.len() * 2).try_into().unwrap(); + let mut path = UNICODE_STRING { + Length: len, + MaximumLength: len, + Buffer: path.as_mut_ptr(), + }; - if unsafe { GetFileInformationByHandle(self.raw, &mut info) } == 0 { - return Err(Error::last_os_error()); + // Setup OBJECT_ATTRIBUTES. + let mut attr = OBJECT_ATTRIBUTES { + Length: std::mem::size_of::().try_into().unwrap(), + RootDirectory: 0, + ObjectName: &mut path, + Attributes: OBJ_CASE_INSENSITIVE as _, + SecurityDescriptor: null_mut(), + SecurityQualityOfService: null_mut(), + }; + + // Open. + let mut handle = 0; + let mut status = unsafe { zeroed() }; + let err = unsafe { + NtCreateFile( + &mut handle, + FILE_READ_ATTRIBUTES | FILE_READ_EA | READ_CONTROL, + &mut attr, + &mut status, + null_mut(), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_OPEN, + FILE_DIRECTORY_FILE, + null_mut(), + 0, + ) + }; + + if err == STATUS_SUCCESS { + Ok(Self { + raw: handle, + parent: None, + children: Mutex::default(), + }) + } else { + Err(Error::from_raw_os_error(unsafe { + RtlNtStatusToDosError(err).try_into().unwrap() + })) } + } - Ok((info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + pub fn parent(&self) -> Option<&Arc> { + self.parent.as_ref() } #[cfg(unix)] - pub fn len(&self) -> Result { - use libc::fstat; + pub fn id(&self) -> Result { + self.stat().map(|s| HostId { + dev: s.st_dev, + ino: s.st_ino, + }) + } - let mut stat = unsafe { zeroed() }; + #[cfg(windows)] + pub fn id(&self) -> Result { + self.stat().map(|i| HostId { + volume: i.dwVolumeSerialNumber, + index: (Into::::into(i.nFileIndexHigh) << 32) | Into::::into(i.nFileIndexLow), + }) + } - if unsafe { fstat(self.raw, &mut stat) } < 0 { - return Err(Error::last_os_error()); - } + #[cfg(unix)] + pub fn is_directory(&self) -> Result { + use libc::{S_IFDIR, S_IFMT}; - Ok(stat.st_size.try_into().unwrap()) + self.stat().map(|s| (s.st_mode & S_IFMT) == S_IFDIR) + } + + #[cfg(windows)] + pub fn is_directory(&self) -> Result { + use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY; + + self.stat() + .map(|i| (i.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) + } + + #[cfg(unix)] + pub fn len(&self) -> Result { + self.stat().map(|s| s.st_size.try_into().unwrap()) } #[cfg(windows)] @@ -75,51 +153,152 @@ impl HostFile { Ok(size.try_into().unwrap()) } + pub fn open(self: &Arc, name: &str) -> Result, Error> { + // Check if active. + let mut children = self.children.lock().unwrap(); + + if let Some(v) = children.get(name).and_then(|c| c.upgrade()) { + return Ok(v); + } + + // Open a new file and add to active list. Beware of deadlock here. + let child = Arc::new(Self { + raw: Self::raw_open(self.raw, name)?, + parent: Some(self.clone()), + children: Mutex::default(), + }); + + children.insert(name.to_owned(), Arc::downgrade(&child)); + + Ok(child) + } + #[cfg(unix)] - fn raw_open(path: &Path) -> Result { - use libc::{O_NOCTTY, O_RDONLY}; - use std::ffi::CString; - use std::os::unix::ffi::OsStrExt; + fn stat(&self) -> Result { + use libc::fstat; - let path = CString::new(path.as_os_str().as_bytes()).unwrap(); - let fd = unsafe { libc::open(path.as_ptr(), O_RDONLY | O_NOCTTY) }; + let mut stat = unsafe { zeroed() }; - if fd < 0 { + if unsafe { fstat(self.raw, &mut stat) } < 0 { Err(Error::last_os_error()) } else { - Ok(fd) + Ok(stat) } } #[cfg(windows)] - fn raw_open(path: &Path) -> Result { - use std::os::windows::ffi::OsStrExt; - use std::ptr::null; - use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + fn stat( + &self, + ) -> Result { + use windows_sys::Win32::Storage::FileSystem::GetFileInformationByHandle; + + let mut info = unsafe { zeroed() }; + + if unsafe { GetFileInformationByHandle(self.raw, &mut info) } == 0 { + Err(Error::last_os_error()) + } else { + Ok(info) + } + } + + #[cfg(unix)] + fn raw_open(dir: RawFile, name: &str) -> Result { + use libc::{openat, EISDIR, ENOTDIR, O_CLOEXEC, O_DIRECTORY, O_NOCTTY, O_RDONLY, O_RDWR}; + use std::ffi::CString; + + let name = CString::new(name).unwrap(); + + loop { + // Try open as a file first. + let fd = unsafe { openat(dir, name.as_ptr(), O_RDWR | O_CLOEXEC | O_NOCTTY) }; + + if fd >= 0 { + break Ok(fd); + } + + // Check if directory. + let err = Error::last_os_error(); + + if err.raw_os_error().unwrap() != EISDIR { + break Err(err); + } + + // Try open as a directory. + let fd = unsafe { openat(dir, name.as_ptr(), O_RDONLY | O_CLOEXEC | O_DIRECTORY) }; + + if fd >= 0 { + break Ok(fd); + } + + // Check if non-directory. This is possible because someone might remove the directory + // and create a file with the same name before we try to open it as a directory. + let err = Error::last_os_error(); + + if err.raw_os_error().unwrap() != ENOTDIR { + break Err(err); + } + } + } + + #[cfg(windows)] + fn raw_open(dir: RawFile, name: &str) -> Result { + use std::ptr::null_mut; + use windows_sys::Wdk::Foundation::OBJECT_ATTRIBUTES; + use windows_sys::Wdk::Storage::FileSystem::{ + NtCreateFile, FILE_NON_DIRECTORY_FILE, FILE_OPEN, FILE_RANDOM_ACCESS, + }; + use windows_sys::Win32::Foundation::{ + RtlNtStatusToDosError, STATUS_SUCCESS, UNICODE_STRING, + }; use windows_sys::Win32::Storage::FileSystem::{ - CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE, FILE_SHARE_READ, - FILE_SHARE_WRITE, OPEN_EXISTING, + DELETE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, }; - let mut path: Vec = path.as_os_str().encode_wide().collect(); - path.push(0); + // Encode name. + let mut name: Vec = name.encode_utf16().collect(); + let len: u16 = (name.len() * 2).try_into().unwrap(); + let mut name = UNICODE_STRING { + Length: len, + MaximumLength: len, + Buffer: name.as_mut_ptr(), + }; - let handle = unsafe { - CreateFileW( - path.as_ptr(), + // Setup OBJECT_ATTRIBUTES. + let mut attr = OBJECT_ATTRIBUTES { + Length: std::mem::size_of::().try_into().unwrap(), + RootDirectory: dir, + ObjectName: &mut name, + Attributes: 0, // TODO: Verify if exfatfs on PS4 root is case-insensitive. + SecurityDescriptor: null_mut(), + SecurityQualityOfService: null_mut(), + }; + + // Try open as a file first. + let mut handle = 0; + let mut status = unsafe { zeroed() }; + let err = unsafe { + NtCreateFile( + &mut handle, + DELETE | FILE_GENERIC_READ | FILE_GENERIC_WRITE, + &mut attr, + &mut status, + null_mut(), + 0, 0, - FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, - null(), - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, + FILE_OPEN, + FILE_NON_DIRECTORY_FILE | FILE_RANDOM_ACCESS, + null_mut(), 0, ) }; - if handle == INVALID_HANDLE_VALUE { - Err(Error::last_os_error()) - } else { + if err == STATUS_SUCCESS { Ok(handle) + } else { + // TODO: Check if file is a directory. + Err(Error::from_raw_os_error(unsafe { + RtlNtStatusToDosError(err).try_into().unwrap() + })) } } } @@ -131,21 +310,40 @@ impl Drop for HostFile { if unsafe { close(self.raw) } < 0 { let e = Error::last_os_error(); - panic!("Failed to close {}: {}.", self.path.display(), e); + panic!("Failed to close FD #{}: {}.", self.raw, e); } } #[cfg(windows)] fn drop(&mut self) { - use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Wdk::Foundation::NtClose; + use windows_sys::Win32::Foundation::{RtlNtStatusToDosError, STATUS_SUCCESS}; - if unsafe { CloseHandle(self.raw) } == 0 { - let e = Error::last_os_error(); - panic!("Failed to close {}: {}.", self.path.display(), e); + let err = unsafe { NtClose(self.raw) }; + + if err != STATUS_SUCCESS { + panic!( + "Failed to close handle #{}: {}.", + self.raw, + Error::from_raw_os_error(unsafe { RtlNtStatusToDosError(err).try_into().unwrap() }) + ); } } } +/// Unique identifier for [`HostFile`]. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct HostId { + #[cfg(unix)] + dev: libc::dev_t, + #[cfg(unix)] + ino: libc::ino_t, + #[cfg(windows)] + volume: u32, + #[cfg(windows)] + index: u64, +} + #[cfg(unix)] type RawFile = std::ffi::c_int; diff --git a/src/kernel/src/fs/host/mod.rs b/src/kernel/src/fs/host/mod.rs index 3152f88b2..89fc15739 100644 --- a/src/kernel/src/fs/host/mod.rs +++ b/src/kernel/src/fs/host/mod.rs @@ -1,17 +1,15 @@ -use self::file::HostFile; +use self::file::{HostFile, HostId}; use self::vnode::VnodeBackend; use super::{Filesystem, FsConfig, Mount, MountFlags, MountOpts, VPathBuf, Vnode, VnodeType}; use crate::errno::{Errno, EIO}; use crate::ucred::Ucred; -use gmtx::{Gutex, GutexGroup}; use macros::Errno; use param::Param; use std::collections::HashMap; use std::fs::create_dir; use std::io::ErrorKind; -use std::num::NonZeroI32; -use std::path::{Path, PathBuf}; -use std::sync::{Arc, Weak}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex, Weak}; use thiserror::Error; mod file; @@ -23,9 +21,8 @@ mod vnode; /// report this as `exfatfs` otherwise it might be unexpected by the PS4. #[derive(Debug)] pub struct HostFs { - root: PathBuf, - app: Arc, - actives: Gutex>>, + root: Arc, + actives: Mutex>>, } pub fn mount( @@ -96,9 +93,13 @@ pub fn mount( map.insert(app.join("app0").unwrap(), MountSource::Bind(pfs)); - // Set mount data. - let gg = GutexGroup::new(); + // Open root directory. + let root = match HostFile::root(&system) { + Ok(v) => v, + Err(e) => return Err(Box::new(MountError::OpenRootFailed(system, e))), + }; + // Set mount data. Ok(Mount::new( conf, cred, @@ -107,17 +108,15 @@ pub fn mount( parent, flags | MountFlags::MNT_LOCAL, HostFs { - root: system, - app: Arc::new(app), - actives: gg.spawn(HashMap::new()), + root: Arc::new(root), + actives: Mutex::default(), }, )) } impl Filesystem for HostFs { fn root(self: Arc, mnt: &Arc) -> Result, Box> { - let vnode = get_vnode(&self, mnt, None)?; - + let vnode = get_vnode(&self, mnt, &self.root)?; Ok(vnode) } } @@ -125,39 +124,37 @@ impl Filesystem for HostFs { fn get_vnode( fs: &Arc, mnt: &Arc, - path: Option<&Path>, + file: &Arc, ) -> Result, GetVnodeError> { - // Get target path. - let path = match path { - Some(v) => v, - None => &fs.root, + // Get file ID. + let id = match file.id() { + Ok(v) => v, + Err(e) => return Err(GetVnodeError::GetFileIdFailed(e)), }; // Check if active. - let mut actives = fs.actives.write(); + let mut actives = fs.actives.lock().unwrap(); - if let Some(v) = actives.get(path).and_then(|v| v.upgrade()) { + if let Some(v) = actives.get(&id).and_then(|v| v.upgrade()) { return Ok(v); } - // Open the file. Beware of deadlock here. - let file = match HostFile::open(path) { - Ok(v) => v, - Err(e) => return Err(GetVnodeError::OpenFileFailed(e)), - }; - - // Get vnode type. + // Get vnode type. Beware of deadlock here. let ty = match file.is_directory() { - Ok(true) => VnodeType::Directory(path == fs.root), + Ok(true) => VnodeType::Directory(Arc::ptr_eq(file, &fs.root)), Ok(false) => VnodeType::File, Err(e) => return Err(GetVnodeError::GetFileTypeFailed(e)), }; // Allocate a new vnode. - let vn = Vnode::new(mnt, ty, "exfatfs", VnodeBackend::new(fs.clone(), file)); + let vn = Vnode::new( + mnt, + ty, + "exfatfs", + VnodeBackend::new(fs.clone(), file.clone()), + ); - actives.insert(path.to_owned(), Arc::downgrade(&vn)); - drop(actives); + actives.insert(id, Arc::downgrade(&vn)); Ok(vn) } @@ -175,19 +172,19 @@ enum MountError { #[error("cannot create {0}")] #[errno(EIO)] CreateDirectoryFailed(PathBuf, #[source] std::io::Error), + + #[error("couldn't open {0} as a root directory")] + #[errno(EIO)] + OpenRootFailed(PathBuf, #[source] std::io::Error), } /// Represents an error when [`get_vnode()`] fails. -#[derive(Debug, Error)] +#[derive(Debug, Error, Errno)] enum GetVnodeError { - #[error("cannot open the specified file")] - OpenFileFailed(#[source] std::io::Error), + #[error("couldn't get file identifier")] + #[errno(EIO)] + GetFileIdFailed(#[source] std::io::Error), #[error("cannot determine file type")] + #[errno(EIO)] GetFileTypeFailed(#[source] std::io::Error), } - -impl Errno for GetVnodeError { - fn errno(&self) -> NonZeroI32 { - todo!() - } -} diff --git a/src/kernel/src/fs/host/vnode.rs b/src/kernel/src/fs/host/vnode.rs index 327756341..6f8b5dadc 100644 --- a/src/kernel/src/fs/host/vnode.rs +++ b/src/kernel/src/fs/host/vnode.rs @@ -14,11 +14,11 @@ use thiserror::Error; #[derive(Debug)] pub struct VnodeBackend { fs: Arc, - file: HostFile, + file: Arc, } impl VnodeBackend { - pub fn new(fs: Arc, file: HostFile) -> Self { + pub fn new(fs: Arc, file: Arc) -> Self { Self { fs, file } } } @@ -78,23 +78,22 @@ impl crate::fs::VnodeBackend for VnodeBackend { .map_err(LookupError::AccessDenied)?; // Check name. - if name == "." { - return Ok(vn.clone()); - } - - let path = match name { - ".." => Cow::Borrowed(self.file.path().parent().unwrap()), + let file = match name { + "." => return Ok(vn.clone()), + ".." => Cow::Borrowed(self.file.parent().unwrap()), _ => { + // Don't allow name to be a file path. if name.contains(|c| c == '/' || c == '\\') { return Err(Box::new(LookupError::InvalidName)); } - Cow::Owned(self.file.path().join(name)) + // Lookup the file. + Cow::Owned(self.file.open(name).map_err(LookupError::OpenFailed)?) } }; // Get vnode. - let vn = get_vnode(&self.fs, vn.fs(), Some(&path)).map_err(LookupError::GetVnodeFailed)?; + let vn = get_vnode(&self.fs, vn.fs(), &file).map_err(LookupError::GetVnodeFailed)?; Ok(vn) } @@ -143,6 +142,9 @@ enum LookupError { #[error("name contains unsupported characters")] InvalidName, + #[error("couldn't open the specified file")] + OpenFailed(#[source] std::io::Error), + #[error("cannot get vnode")] GetVnodeFailed(#[source] GetVnodeError), } @@ -151,7 +153,7 @@ impl Errno for LookupError { fn errno(&self) -> NonZeroI32 { match self { Self::NotDirectory => ENOTDIR, - Self::DotdotOnRoot | Self::GetVnodeFailed(_) => EIO, + Self::DotdotOnRoot | Self::GetVnodeFailed(_) | Self::OpenFailed(_) => EIO, Self::AccessDenied(e) => e.errno(), Self::InvalidName => ENOENT, }