Skip to content

feat: add Windows FUSE support using WinFSP#321

Open
arthur0824hao wants to merge 4 commits into
xoriors:mainfrom
arthur0824hao:windows-winfsp-support
Open

feat: add Windows FUSE support using WinFSP#321
arthur0824hao wants to merge 4 commits into
xoriors:mainfrom
arthur0824hao:windows-winfsp-support

Conversation

@arthur0824hao
Copy link
Copy Markdown

This PR implements Windows filesystem mounting support using WinFSP (Windows File System Proxy).

Summary

  • Adds Windows FUSE-like support via winfsp-rs bindings
  • Implements trait mapping to operations
  • Enables Windows users to mount encrypted filesystems

Changes

  • Cargo.toml: Added , , and dependencies for Windows target
  • src/mount/windows.rs: New WinFSP filesystem implementation (~500 lines)
  • src/mount.rs: Updated conditional compilation to include Windows module
  • src/main.rs: Enabled Windows platform support

Testing

Closes #3

Implements Windows filesystem mounting using winfsp-rs bindings.

Changes:
- Add winfsp, windows, and widestring dependencies for Windows target
- Create src/mount/windows.rs with FileSystemContext implementation
- Map EncryptedFs operations to WinFSP callbacks
- Update conditional compilation in mount.rs for Windows
- Enable Windows platform in main.rs

Closes xoriors#3
Copilot AI review requested due to automatic review settings January 19, 2026 09:05
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements Windows filesystem mounting support using WinFSP (Windows File System Proxy), enabling Windows users to mount encrypted filesystems similar to the existing Linux FUSE support.

Changes:

  • Added WinFSP integration with FileSystemContext trait implementation for Windows
  • Updated platform-specific conditional compilation to include Windows alongside Linux
  • Added Windows-specific dependencies (winfsp, windows crate, widestring)

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/mount/windows.rs New ~762-line implementation of WinFSP filesystem adapter with path resolution, file operations, and directory listing
src/mount.rs Updated conditional compilation directives to include Windows module alongside Linux
src/main.rs Enabled Windows platform support by updating conditional compilation guards
Cargo.toml Added Windows-specific dependencies for WinFSP integration
Cargo.lock Dependency resolution including winfsp, windows, and widestring crates

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/mount/windows.rs Outdated
Comment on lines +705 to +717
let mut host = FileSystemHost::new(winfsp_fs)
.map_err(|_| FsError::Other("Failed to create FileSystemHost"))?;

let mount_point_str = self
.mountpoint
.to_str()
.ok_or_else(|| FsError::Other("Invalid mount point path"))?;

host.mount(mount_point_str)
.map_err(|_| FsError::Other("Failed to mount filesystem"))?;

host.start()
.map_err(|_| FsError::Other("Failed to start filesystem service"))?;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error messages "Failed to create FileSystemHost", "Failed to mount filesystem", and "Failed to start filesystem service" discard the underlying error information from WinFSP. This makes debugging difficult for users. Consider preserving the original error details or at least logging them before converting to FsError.

Suggested change
let mut host = FileSystemHost::new(winfsp_fs)
.map_err(|_| FsError::Other("Failed to create FileSystemHost"))?;
let mount_point_str = self
.mountpoint
.to_str()
.ok_or_else(|| FsError::Other("Invalid mount point path"))?;
host.mount(mount_point_str)
.map_err(|_| FsError::Other("Failed to mount filesystem"))?;
host.start()
.map_err(|_| FsError::Other("Failed to start filesystem service"))?;
let mut host = FileSystemHost::new(winfsp_fs).map_err(|e| {
error!("Failed to create FileSystemHost: {:?}", e);
FsError::Other("Failed to create FileSystemHost")
})?;
let mount_point_str = self
.mountpoint
.to_str()
.ok_or_else(|| FsError::Other("Invalid mount point path"))?;
host.mount(mount_point_str).map_err(|e| {
error!("Failed to mount filesystem at {}: {:?}", mount_point_str, e);
FsError::Other("Failed to mount filesystem")
})?;
host.start().map_err(|e| {
error!("Failed to start filesystem service: {:?}", e);
FsError::Other("Failed to start filesystem service")
})?;

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
use crate::mount;
use crate::mount::{MountHandleInner, MountPoint};

const TTL: Duration = Duration::from_secs(1);
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant TTL is defined but never used in the implementation. Consider removing this unused constant.

Suggested change
const TTL: Duration = Duration::from_secs(1);

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs
Comment on lines +527 to +544
let attr = if is_directory {
CreateFileAttr {
kind: FileType::Directory,
perm: 0o755,
uid: 0,
gid: 0,
rdev: 0,
flags: 0,
}
} else {
CreateFileAttr {
kind: FileType::RegularFile,
perm: 0o644,
uid: 0,
gid: 0,
rdev: 0,
flags: 0,
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uid and gid fields are hardcoded to 0 in CreateFileAttr. While Windows doesn't use Unix-style user/group IDs, setting these to 0 might not be appropriate or meaningful. Consider documenting why these values are chosen or if they have any effect on Windows.

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs
Comment on lines +408 to +417
if let Err(e) = self
.runtime
.block_on(async { self.fs.set_attr(context.ino, set_attr).await })
{
error!("set_basic_info error: {}", e);
}

self.refresh_file_info(context.ino, file_info);
Ok(())
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The set_basic_info method logs the error from set_attr but still returns Ok(()), silently ignoring the failure. This inconsistent error handling differs from other methods like set_file_size which properly propagate errors. Consider returning an error when set_attr fails, or document why failures are acceptable here.

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
Comment on lines +354 to +365
fn flush(&self, context: &Self::FileContext, file_info: &mut FileInfo) -> winfsp::Result<()> {
trace!("flush: ino={}", context.ino);

if let Some(fh) = context.fh {
if let Err(e) = self.runtime.block_on(async { self.fs.flush(fh).await }) {
error!("Flush error: {}", e);
}
}

self.refresh_file_info(context.ino, file_info);
Ok(())
}
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flush method logs errors but always returns Ok(()), silently ignoring flush failures. Since flush operations are critical for data integrity, consider propagating these errors to the caller so they can be properly handled.

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
Comment on lines +67 to +70
fn get_fs(&self) -> Arc<EncryptedFs> {
self.fs.clone()
}

Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method get_fs is defined but never used in this implementation. Consider removing this unused method.

Suggested change
fn get_fs(&self) -> Arc<EncryptedFs> {
self.fs.clone()
}

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
Comment on lines +22 to +24
STATUS_FILE_IS_A_DIRECTORY, STATUS_NOT_A_DIRECTORY, STATUS_OBJECT_NAME_COLLISION,
STATUS_OBJECT_NAME_NOT_FOUND,
};
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The STATUS_FILE_IS_A_DIRECTORY constant is imported but never used in the implementation. Consider removing this unused import.

Suggested change
STATUS_FILE_IS_A_DIRECTORY, STATUS_NOT_A_DIRECTORY, STATUS_OBJECT_NAME_COLLISION,
STATUS_OBJECT_NAME_NOT_FOUND,
};
STATUS_NOT_A_DIRECTORY, STATUS_OBJECT_NAME_COLLISION, STATUS_OBJECT_NAME_NOT_FOUND,
};

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
Comment on lines +194 to +195
volume_info.total_size = 1024 * 1024 * 1024 * 100;
volume_info.free_size = 1024 * 1024 * 1024 * 50;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded volume sizes (100GB total, 50GB free) should be either documented or made configurable. These magic numbers make the implementation inflexible and might confuse users about actual storage capacity.

Copilot uses AI. Check for mistakes.
Comment thread Cargo.toml Outdated
[target.'cfg(target_os = "windows")'.dependencies]
winfsp = "0.12"
windows = { version = "0.58", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_IO"] }
widestring = "1.1"
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The widestring version specifier "1.1" in Cargo.toml will resolve to "1.2.1" in Cargo.lock. While this is not incorrect, consider using a more precise version specifier like "1.2" or "1.2.1" for better reproducibility.

Suggested change
widestring = "1.1"
widestring = "1.2.1"

Copilot uses AI. Check for mistakes.
Comment thread src/mount/windows.rs Outdated
Comment on lines +554 to +559
Ok((fh, created_attr)) => {
*file_info.as_mut() = Self::attr_to_file_info(&created_attr);

Ok(EncryptedFsFileContext {
ino: created_attr.ino,
fh: if is_directory { None } else { Some(fh) },
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating a directory, the file handle returned from self.fs.create() is discarded. If the underlying filesystem returns a valid file handle for directories that needs to be closed, this could result in a resource leak. Consider explicitly releasing the handle for directories or verify that the underlying EncryptedFs properly handles directory handles.

Copilot uses AI. Check for mistakes.
- Fix flush() to propagate errors instead of silently succeeding
- Fix set_basic_info() to propagate errors instead of silently succeeding
- Fix read_directory() to actually add entries to dir_buffer using fill()
- Fix umount() to return Unsupported error instead of Ok(())
- Fix create() to release directory file handles to prevent resource leak
- Add comments for hardcoded volume sizes (100GB/50GB placeholders)
- Add comments explaining uid/gid=0 choice for Windows
- Update widestring dependency from 1.1 to 1.2.1
Log underlying WinFSP errors before converting to FsError, as suggested
in code review. This helps users debug mount failures.
Copy link
Copy Markdown
Member

@radumarias radumarias left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: Windows FUSE Support using WinFSP

Thank you for this solid implementation! The code structure follows the Linux patterns well and includes good logging. However, I found some issues that need to be addressed before merge.


Critical Issues (Must Fix)

1. Missing File/Directory Deletion Implementation

The implementation is missing the set_delete method and proper deletion handling in cleanup(). In WinFSP, file deletion is a two-stage process:

  1. set_delete marks the file for deletion
  2. cleanup with FspCleanupDelete flag (0x01) performs actual deletion

Currently, cleanup() (line 589-595) only flushes and ignores the flags parameter.

Impact: DeleteFile() and RemoveDirectory() will appear to succeed but won't actually remove files.

Fix: Add set_delete implementation and modify cleanup to handle the deletion flag:

const FSP_CLEANUP_DELETE: u32 = 0x01;

fn cleanup(&self, context: &mut Self::FileContext, file_name: Option<&U16CStr>, flags: u32) {
    trace!("cleanup: ino={}, flags={}", context.ino, flags);

    if let Some(fh) = context.fh {
        let _ = self.runtime.block_on(async { self.fs.flush(fh).await });
    }

    // Handle deletion when FspCleanupDelete flag is set
    if (flags & FSP_CLEANUP_DELETE) != 0 {
        if let Some(file_name) = file_name {
            if let Ok((parent_ino, name)) = self.path_to_parent_and_name(file_name) {
                if let Ok(secret_name) = SecretString::from_str(&name) {
                    let result = if context.is_directory {
                        self.runtime.block_on(async {
                            self.fs.remove_dir(parent_ino, &secret_name).await
                        })
                    } else {
                        self.runtime.block_on(async {
                            self.fs.remove_file(parent_ino, &secret_name).await
                        })
                    };
                    if let Err(e) = result {
                        error!("Failed to delete during cleanup: {}", e);
                    }
                }
            }
        }
    }
}

Important Issues (Should Fix)

2. Silent Failures in Resource Cleanup

Multiple let _ = ... patterns silently ignore critical errors:

  • close() line 281: ignores release() errors
  • create() line 568: ignores directory handle release errors
  • cleanup() line 593: ignores flush() errors

For a cryptographic filesystem, silent failures could mask data loss or security issues.

Fix: Add error logging:

if let Err(e) = self.runtime.block_on(async { self.fs.release(fh).await }) {
    error!("Failed to release handle {}: {}", fh, e);
}

3. Incorrect Error Status for Read Failures (line 308)

All read errors return STATUS_END_OF_FILE. I/O errors and crypto failures should return appropriate status codes like STATUS_DISK_CORRUPT_ERROR or STATUS_ACCESS_DENIED.

4. Error Context Discarded

The pattern Err(_) => ... appears multiple times (lines 88, 121, 230, 248, 374), discarding error information. Consider logging before returning:

Err(e) => {
    debug!("Error looking up path: {}", e);
    return Err(STATUS_OBJECT_NAME_NOT_FOUND.into());
}

5. Future::poll Always Returns Pending (line 764)

Unlike Linux where the mount handle properly polls, Windows returns Poll::Pending forever. Code awaiting the mount handle will block indefinitely. Either implement proper polling, return Poll::Ready(Ok(())), or document this behavior.

6. Double-Mount Panic Risk (line 714)

self.password_provider.take().unwrap() will panic if mount() is called twice. Return an error instead:

self.password_provider
    .take()
    .ok_or_else(|| FsError::Other("Mount already called"))?

Suggestions

7. Missing Documentation

Consider adding doc comments for:

  • Constants WINDOWS_TICK and SEC_TO_UNIX_EPOCH (magic numbers for time conversion)
  • EncryptedFsFileContext struct fields (especially why fh is Option)
  • The path_to_inode function algorithm

8. Unused Parameters

Several _ prefixed parameters suggest incomplete implementations:

  • _pattern in read_directory() - directory filtering not implemented
  • _replace_if_exists in rename() - overwrite semantics ignored

What's Good

  • Consistent logging with trace/debug/error levels
  • Structure mirrors linux.rs well
  • Proper read_only mode enforcement
  • Well-implemented time conversion functions
  • Comments explain rationale (volume size, uid/gid)

Summary: Please address the critical deletion issue (#1) and the silent failure patterns (#2) before merge. The other issues are important but could be addressed in follow-up PRs if needed.

- Update to latest WinFSP API with VolumeParams and WideNameInfo
- Improve error handling in path_to_inode and path_to_parent_and_name
- Add FSP_CLEANUP_DELETE constant for proper deletion support
- Simplify volume label handling using set_volume_label()
- Update imports to use winfsp_sys for FILE_ACCESS_RIGHTS
@radumarias
Copy link
Copy Markdown
Member

Security & Privacy Review

1. Argument Injection in umount

Severity: High
File: src/mount.rs (Lines 109-110)

Description:
The umount function passes the mountpoint string directly as an argument to the system umount command without sanitization.

if process::Command::new("umount")
    .arg(mountpoint)

If a user provides a mountpoint argument that starts with a hyphen (e.g., -a), it will be interpreted as a command-line flag rather than a file path. This could lead to unintended behavior, such as unmounting all filesystems on the host (Denial of Service).

Recommendation:
Sanitize or validate the mountpoint argument before passing it to the command.

  • Preferred Fix: Use std::fs::canonicalize(mountpoint) to resolve the path to an absolute path before passing it. This ensures it is treated as a path argument (e.g., /abs/path/to/-a).
  • Alternative: explicitly check for leading hyphens or prepend ./ to relative paths.

2. Privacy Violation: Plaintext Filenames in Logs

Severity: Medium
File: src/mount/windows.rs (Lines 241, 632-633)

Description:
The Windows FUSE implementation logs plaintext filenames at the DEBUG level during open and rename operations.

debug!("open: {:?}", file_name.to_string());
// ...
debug!(
    "rename: {:?} -> {:?}",
    file_name.to_string(),
    new_file_name.to_string()
);

Since rencfs is an encrypted filesystem designed to protect metadata (including filenames), logging these names in plaintext defeats the confidentiality goals of the project. If debug logs are persisted, this information is leaked.

Recommendation:
Do not log plaintext filenames.

  • Log the inode number (ino) or a hash of the filename if debugging context is required.
  • Ensure all logging statements respect the privacy and encryption guarantees of the filesystem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FUSE (or similar) support on Windows

3 participants