Skip to content

virtiofs: add option for readonly mount#2827

Open
damanm24 wants to merge 9 commits intomicrosoft:mainfrom
damanm24:readonly
Open

virtiofs: add option for readonly mount#2827
damanm24 wants to merge 9 commits intomicrosoft:mainfrom
damanm24:readonly

Conversation

@damanm24
Copy link
Contributor

Adds a readonly mode to the VirtioFs implementation, allowing a virtio-fs mount to be configured as read-only via LxVolumeOptions. When enabled, all mutating operations are rejected with EROFS (Read-only file system).

Operations that are guarded:

  • open - Reject O_WRONLY and O_RDWR access modes using the O_NOACCESS mask
  • set_attr - Reject attribute modifications for all attributes (except FATTR_FH, and FATTR_LOCKOWNER which are used to track states and do not cause any filesystem modifications)
  • create, mkdir, mknod, symlink, link, write, unlink, rmdir, rename, setx_attr, remote_xattr

@damanm24 damanm24 requested a review from a team as a code owner February 23, 2026 22:40
Copilot AI review requested due to automatic review settings February 23, 2026 22:40
@github-actions
Copy link

⚠️ Unsafe Code Detected

This PR modifies files containing unsafe Rust code. Extra scrutiny is required during review.

For more on why we check whole files, instead of just diffs, check out the Rustonomicon

@github-actions github-actions bot added the unsafe Related to unsafe code label Feb 23, 2026
Copy link
Contributor

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

Adds a configurable read-only mode for the virtio-fs backend, wired through LxVolumeOptions, so the guest can mount a shared filesystem as read-only and have mutating FUSE operations rejected with EROFS.

Changes:

  • Add a readonly flag to VirtioFs and gate mutating FUSE ops via check_writable().
  • Add readonly state + ro mount-option parsing to LxVolumeOptions, plus accessors (readonly(), is_readonly()).
  • Enforce read-only behavior in virtio-fs paths such as create/mkdir/write/unlink/rename/set_xattr/remove_xattr/set_attr.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
vm/devices/virtio/virtiofs/src/lib.rs Stores a read-only flag and rejects mutating FUSE operations with EROFS.
vm/devices/support/fs/lxutil/src/lib.rs Extends LxVolumeOptions with a read-only flag, parsing support for ro, and getter/setter APIs.

Copy link
Contributor

@justus-camp-microsoft justus-camp-microsoft left a comment

Choose a reason for hiding this comment

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

I don't have all the context here, but it seems like we should try to hoist check_writable up to the Fuse session layer so that we don't have to check it in every method if possible. Otherwise LGTM, let me know if that's not possible.

@benhillis
Copy link
Member

I don't have all the context here, but it seems like we should try to hoist check_writable up to the Fuse session layer so that we don't have to check it in every method if possible. Otherwise LGTM, let me know if that's not possible.

Brian also mentioned potentially mapping things as readonly versus readwrite as an option perhaps?

@damanm24
Copy link
Contributor Author

I don't have all the context here, but it seems like we should try to hoist check_writable up to the Fuse session layer so that we don't have to check it in every method if possible. Otherwise LGTM, let me know if that's not possible.

Brian also mentioned potentially mapping things as readonly versus readwrite as an option perhaps?

So AFAICT the root LxVolume that's associated with a virtiofs share is already mapped with readonly permissions. The root volume is used to resolve paths so that subsequent filesystem operations on the share operate relative to the root. The reason the checks are interspersed within every possible destructive method is because the access rights on handles to files within the share are not inherited from the root handle.

What I can do it instead move the check into the lxutil layer, and then in the windows implementation I can centralize check_writeable in the create_file method.

However, you wouldn't be able to do the same centralization in the unix lxutil implementation because not all destructive operations require an open call with write permissions.


fn set_attr(&self, request: &Request, arg: &fuse_setattr_in) -> lx::Result<fuse_attr_out> {
// Block truncation and other modifications on readonly filesystems
if arg.valid & !(FATTR_FH | FATTR_LOCKOWNER) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this a common case? I just wonder if we should succeed a set attribute call even if it isn't actually changing anything.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So FATTR_FH and FATTR_LOCKOWNER are FUSE specific attributes that don't actually change anything about the file, so if the operation only contains these flags the call will succeed. All of the other possible set_attribute flags do modify the file in some manner that violate the readonly principle so the operation will fail.

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Copy link
Contributor

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

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


You can also share your feedback on Copilot code review. Take the survey.

// For all other flags, check existence first so ENOENT takes precedence over
// EROFS.
inode.get_attr()?;
let access_mode = (flags & lx::O_NOACCESS as u32) as i32;
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

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

In the open() method, lx::O_NOACCESS is used as a bit mask to extract the access mode from flags, but it is semantically an access mode value (meaning "no access"), not a mask. The correct constant to use as a mask here is lx::O_ACCESS_MASK, which has the same numeric value (0x3) but communicates the intent more clearly. The codebase already uses O_ACCESS_MASK for this purpose elsewhere (e.g., fuse/tests/fuse_hello.rs:93 uses flags & lx::O_ACCESS_MASK).

Suggested change
let access_mode = (flags & lx::O_NOACCESS as u32) as i32;
let access_mode = (flags & lx::O_ACCESS_MASK as u32) as i32;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

unsafe Related to unsafe code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants