Skip to content
Closed
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
1 change: 1 addition & 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 session::DEFAULT_MAX_WRITE;
pub use session::Session;
pub use session::SessionInfo;

Expand Down
112 changes: 110 additions & 2 deletions vm/devices/support/fs/fuse/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ const DEFAULT_MAX_PAGES: u32 = 256;
// a difference.
const PAGE_SIZE: u32 = 4096;

/// The default maximum FUSE write size (and read/readdir buffer size)
/// advertised to the client: 256 pages of 4 KiB (1 MiB).
///
/// In the FUSE protocol the server dictates `max_write` in the FUSE_INIT
/// reply; the guest does not propose it and simply adopts the offered value.
/// Transports that bounce I/O through a limited DMA window (e.g. a guest
/// forced to use the Linux swiotlb, which has a hard 256 KiB per-mapping
/// cap) should therefore advertise a smaller value via
/// [`Session::with_max_write`] so the guest never builds request buffers the
/// transport cannot map.
pub const DEFAULT_MAX_WRITE: u32 = DEFAULT_MAX_PAGES * PAGE_SIZE;

/// A FUSE session for a file system.
///
/// Handles negotiation and dispatching requests to the file system.
Expand All @@ -48,18 +60,48 @@ pub struct Session {
// a lock, since operations mostly don't need to access the SessionInfo.
initialized: atomic::AtomicBool,
info: RwLock<SessionInfo>,
// The maximum write size to offer during FUSE_INIT negotiation. This is a
// hard upper bound: the file system's init callback may lower it, but any
// attempt to raise it is clamped back to this value (see `dispatch_init`).
default_max_write: u32,
}

impl Session {
/// Create a new `Session`.
///
/// The maximum write size advertised to the client defaults to
/// [`DEFAULT_MAX_WRITE`]. Use [`Session::with_max_write`] to advertise a
/// smaller value when the transport requires it.
pub fn new<T>(fs: T) -> Self
where
T: 'static + Fuse + Send + Sync,
{
Self::with_max_write(fs, DEFAULT_MAX_WRITE)
}

/// Create a new `Session` that advertises the specified maximum write
/// size (in bytes) to the client.
///
/// In the FUSE protocol the server dictates `max_write`; the guest adopts
/// whatever the server offers. This value is a hard upper bound: the file
/// system's `init` callback may lower the advertised `max_write`, but
/// cannot raise it above the value passed here.
///
/// This is intended for transports that cannot satisfy the default
/// [`DEFAULT_MAX_WRITE`] in a single DMA mapping. For example, a guest
/// forced to bounce I/O through the Linux swiotlb has a hard 256 KiB
/// per-mapping limit, so any request buffer larger than that fails to
/// map. Advertising a matching `max_write` keeps the guest from building
/// request buffers the transport cannot handle.
pub fn with_max_write<T>(fs: T, max_write: u32) -> Self
where
T: 'static + Fuse + Send + Sync,
{
Self {
fs: Box::new(fs),
initialized: atomic::AtomicBool::new(false),
info: RwLock::new(SessionInfo::default()),
default_max_write: max_write,
}
}

Expand Down Expand Up @@ -493,9 +535,16 @@ impl Session {
info.want2 = DEFAULT_FLAGS2 & init.flags2;
}
info.time_gran = 1;
info.max_write = DEFAULT_MAX_PAGES * PAGE_SIZE;
info.max_write = self.default_max_write;
self.fs.init(&mut info);

Comment thread
asherkariv marked this conversation as resolved.
// The filesystem's `init` callback may lower `max_write`, but it must
// never raise it above the session's negotiated maximum: that value is
// the transport's per-mapping limit (e.g. a bounce-buffered transport's
// swiotlb cap), and exceeding it reintroduces the large-request mapping
// failures the limit exists to prevent.
info.max_write = info.max_write.min(self.default_max_write);

assert!(info.want & !info.capable == 0);
// If the filesystem cleared FUSE_INIT_EXT from want, force want2 to
// zero so we never reply with flags2 the kernel won't expect.
Expand Down Expand Up @@ -1198,7 +1247,66 @@ mod tests {
);
}

/// Creates a FUSE_LOOKUP request with a name that's too long (256 bytes, exceeds NAME_MAX of 255)
#[test]
fn init_with_max_write_negotiates_smaller_max_write() {
// A bounce-buffered transport (e.g. swiotlb, with a 256 KiB
// per-mapping limit) builds the session with a reduced max_write so
// the guest never produces request buffers it cannot map.
const SWIOTLB_MAX_WRITE: u32 = 256 * 1024;
let flags = 0x003FFFFB | FUSE_MAX_PAGES;
let request_data = make_init_request(7, 39, 131072, flags, 0);

let fs = InitCapturingFs::default();
let session = Session::with_max_write(fs, SWIOTLB_MAX_WRITE);

let mut sender = CapturingSender::default();
session.dispatch(
Request::new(request_data.as_slice()).unwrap(),
&mut sender,
None,
);

assert!(session.is_initialized());

// The reply must advertise the reduced max_write (256 KiB) and the
// matching max_pages (64), rather than the 1 MiB / 256 default.
let (_hdr, init_out) = sender.parse_init_reply();
assert_eq!(init_out.max_write, SWIOTLB_MAX_WRITE);
assert_eq!(init_out.max_pages, 64);
}

#[test]
fn init_filesystem_cannot_raise_max_write_above_session_limit() {
// The session imposes a reduced max_write (e.g. a bounce-buffered
// transport's per-mapping limit). A filesystem `init` callback that
// tries to raise max_write above that limit must be clamped back down,
// otherwise the transport-imposed cap would be defeated.
const SWIOTLB_MAX_WRITE: u32 = 256 * 1024;
let flags = 0x003FFFFB | FUSE_MAX_PAGES;
let request_data = make_init_request(7, 39, 131072, flags, 0);

let fs = InitCapturingFs {
max_write_override: Some(1024 * 1024),
..Default::default()
};
let session = Session::with_max_write(fs, SWIOTLB_MAX_WRITE);

let mut sender = CapturingSender::default();
session.dispatch(
Request::new(request_data.as_slice()).unwrap(),
&mut sender,
None,
);

assert!(session.is_initialized());

// The reply must still advertise the session's cap, not the larger
// value the filesystem requested.
let (_hdr, init_out) = sender.parse_init_reply();
assert_eq!(init_out.max_write, SWIOTLB_MAX_WRITE);
assert_eq!(init_out.max_pages, 64);
}

fn make_lookup_name_too_long() -> Vec<u8> {
let mut data = vec![0u8; 297]; // 40 byte header + 256 byte name + 1 null terminator

Expand Down
Loading
Loading