Skip to content

Commit 9744a4d

Browse files
committed
passthrough: drop DIRECT_IO from passthrough opens
FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH for reads, writes and splice, so an O_DIRECT open registered a backing id but still sent its IO through the daemon's bounce buffer. The kernel opens the backing file with the client's flags, so without DIRECT_IO an O_DIRECT open does direct IO on the host file. The cache policies that want DIRECT_IO on every open already turn passthrough off, and mmap used the backing file either way. For sync IO, the kernel takes O_DIRECT from the backing file's open flags and not from each call, so clearing O_DIRECT with fcntl() after the open no longer reaches the IO. Async IO carries the flag per call. A new kernel passthrough test opens a PassthroughFs file with O_DIRECT, reads and writes an aligned block, and checks that the server handled no READ or WRITE. Like the other kernel passthrough tests, it skips without root in the initial user namespace. Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
1 parent e5486fb commit 9744a4d

3 files changed

Lines changed: 206 additions & 61 deletions

File tree

‎crates/fuse-backend-passthrough/src/lib.rs‎

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -648,10 +648,11 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
648648
/// set `no_writeback`, and set `no_open` to false, because `VfsOptions` enables it by default.
649649
///
650650
/// It stays off with `Config::seal_size`, because passthrough writes don't reach the size
651-
/// check, and with `CachePolicy::Never` and `CachePolicy::Metadata`. Those open files with
652-
/// `OpenOptions::DIRECT_IO`, which takes precedence over passthrough for reads and writes.
653-
/// It also stays off when `Config::allow_direct_io` is false, because the kernel opens the
654-
/// backing file with the client's `O_DIRECT`.
651+
/// check, and with `CachePolicy::Never` and `CachePolicy::Metadata`, which open files with
652+
/// `OpenOptions::DIRECT_IO` to keep file data out of the page cache. Passthrough opens drop
653+
/// `DIRECT_IO`, so the host page cache would serve their reads and writes. It also stays off
654+
/// when `Config::allow_direct_io` is false, because the kernel opens the backing file with the
655+
/// client's `O_DIRECT`.
655656
///
656657
/// Registering a backing file needs `CAP_SYS_ADMIN`, and a host file on a stacked file system
657658
/// such as overlayfs needs `Server::set_max_stack_depth(2)`. Otherwise the kernel rejects the
@@ -2066,9 +2067,8 @@ mod tests {
20662067
fs.import().unwrap();
20672068
fs.init(FsOptions::PASSTHROUGH).unwrap();
20682069

2069-
// An O_DIRECT create gets DIRECT_IO like an O_DIRECT open. Both keep the backing id,
2070-
// because the kernel fails an open without passthrough of an inode open in passthrough
2071-
// mode.
2070+
// O_DIRECT creates and opens pass through without DIRECT_IO, which would send their IO
2071+
// through the daemon. The backing file gets the client's O_DIRECT instead.
20722072
let ctx = Context::default();
20732073
let flags = libc::O_RDWR as u32 | libc::O_DIRECT as u32;
20742074
let createin = CreateIn {
@@ -2080,10 +2080,22 @@ mod tests {
20802080
let name = CString::new("file").unwrap();
20812081
let (entry, _, opts, id) = fs.create(&ctx, ROOT_ID, &name, createin).unwrap();
20822082
assert_eq!(id, Some(1));
2083-
assert_eq!(opts, OpenOptions::DIRECT_IO | OpenOptions::PASSTHROUGH);
2083+
assert_eq!(opts, OpenOptions::PASSTHROUGH);
20842084
let (_, opts, id) = fs.open(&ctx, entry.inode, flags, 0).unwrap();
20852085
assert_eq!(id, Some(1));
2086-
assert_eq!(opts, OpenOptions::DIRECT_IO | OpenOptions::PASSTHROUGH);
2086+
assert_eq!(opts, OpenOptions::PASSTHROUGH);
2087+
2088+
// Without passthrough, both get DIRECT_IO.
2089+
let fs = PassthroughFs::<()>::new(fs.cfg.clone()).unwrap();
2090+
fs.import().unwrap();
2091+
fs.init(FsOptions::empty()).unwrap();
2092+
let name = CString::new("other").unwrap();
2093+
let (entry, _, opts, id) = fs.create(&ctx, ROOT_ID, &name, createin).unwrap();
2094+
assert_eq!(id, None);
2095+
assert_eq!(opts, OpenOptions::DIRECT_IO);
2096+
let (_, opts, id) = fs.open(&ctx, entry.inode, flags, 0).unwrap();
2097+
assert_eq!(id, None);
2098+
assert_eq!(opts, OpenOptions::DIRECT_IO);
20872099
}
20882100

20892101
/// Backing files for tests in which passthrough stays off.

‎crates/fuse-backend-passthrough/src/sync_io.rs‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,15 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
8585
/// it passes through.
8686
#[cfg_attr(not(target_os = "linux"), allow(unused_variables))]
8787
fn file_open_options(&self, flags: u32, backing_id: Option<u32>) -> OpenOptions {
88+
// The kernel fails a passthrough open with `KEEP_CACHE`, and the backing file has its
89+
// own page cache anyway. `DIRECT_IO` would send reads and writes through the daemon. The
90+
// kernel opens the backing file with the client's open flags, so an O_DIRECT open still
91+
// does direct IO on the host. Changing O_DIRECT later with fcntl() only reaches the
92+
// backing file for async IO, which carries the flag per call.
93+
#[cfg(target_os = "linux")]
94+
if backing_id.is_some() {
95+
return OpenOptions::PASSTHROUGH;
96+
}
8897
let mut opts = OpenOptions::empty();
8998
// If the client requested O_DIRECT and we honor it, the host file has O_DIRECT too, so
9099
// the kernel must route IO on this handle through the direct IO path. Serving an
@@ -98,13 +107,6 @@ impl<S: BitmapSlice + Send + Sync> PassthroughFs<S> {
98107
CachePolicy::Always => opts |= OpenOptions::KEEP_CACHE,
99108
_ => {}
100109
};
101-
// The kernel fails an open with a backing id and any other flag than `DIRECT_IO`,
102-
// `PARALLEL_DIRECT_WRITES` and `NOFLUSH`, and the backing file has its own page cache, so
103-
// drop `KEEP_CACHE`. `DIRECT_IO` takes precedence over passthrough for reads and writes.
104-
#[cfg(target_os = "linux")]
105-
if backing_id.is_some() {
106-
opts = (opts - OpenOptions::KEEP_CACHE) | OpenOptions::PASSTHROUGH;
107-
}
108110
opts
109111
}
110112

@@ -746,9 +748,9 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
746748
if self.kernel_passthrough.is_some() && capable.contains(FsOptions::PASSTHROUGH) {
747749
// The kernel ignores FUSE_PASSTHROUGH along with FUSE_WRITEBACK_CACHE, and needs open
748750
// requests to get the backing ids. Passthrough writes skip the seal_size check. The
749-
// Never and Metadata cache policies open files with FOPEN_DIRECT_IO, which overrides
750-
// passthrough for reads and writes. The kernel opens the backing file with the
751-
// client's O_DIRECT, which allow_direct_io = false would filter out.
751+
// Never and Metadata cache policies want FOPEN_DIRECT_IO, which passthrough opens drop.
752+
// The kernel opens the backing file with the client's O_DIRECT, which
753+
// allow_direct_io = false would filter out.
752754
if self.writeback.load(Ordering::Relaxed)
753755
|| self.no_open.load(Ordering::Relaxed)
754756
|| self.seal_size.load(Ordering::Relaxed)

‎tests/kernel_passthrough.rs‎

Lines changed: 173 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,37 @@
11
// SPDX-License-Identifier: Apache-2.0
22

3-
//! Mounts a filesystem with one file whose reads are passed through to a backing file, served
4-
//! by the sync server and, with `async-io`, by `FuseDevTask`. Needs root in the initial user
5-
//! namespace and a kernel with CONFIG_FUSE_PASSTHROUGH, and is skipped without them.
3+
//! Mounts file systems whose open files pass through to backing files. A test file system with
4+
//! one file is served by the sync server and, with `async-io`, by `FuseDevTask`, and a
5+
//! `PassthroughFs` serves a file opened with O_DIRECT. Needs root in the initial user namespace
6+
//! and a kernel with CONFIG_FUSE_PASSTHROUGH, and is skipped without them.
67
78
#[cfg(all(feature = "fusedev", target_os = "linux"))]
89
mod fusedev_tests {
910
use std::ffi::CStr;
1011
use std::fs::File;
1112
use std::io::{self, Write};
12-
use std::os::unix::fs::MetadataExt;
13-
use std::os::unix::io::AsRawFd;
14-
use std::sync::atomic::{AtomicBool, Ordering};
15-
use std::sync::{Arc, OnceLock};
16-
use std::thread;
13+
use std::os::unix::fs::{FileExt, MetadataExt, OpenOptionsExt};
14+
use std::os::unix::io::{AsRawFd, BorrowedFd};
15+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
16+
use std::sync::Arc;
17+
use std::thread::{self, JoinHandle};
1718
use std::time::Duration;
1819

19-
use fuse_backend_rs::abi::fuse_abi::{stat64, FsOptions, OpenOptions, ROOT_ID};
20+
use fuse_backend_rs::abi::fuse_abi::{
21+
stat64, FsOptions, InHeader, Opcode, OpenOptions, OutHeader, ROOT_ID,
22+
};
2023
use fuse_backend_rs::api::filesystem::{Context, Entry, FileSystem};
21-
use fuse_backend_rs::api::server::Server;
22-
use fuse_backend_rs::transport::{fuse_backing_open, FuseSession};
24+
use fuse_backend_rs::api::server::{MetricsHook, Server};
25+
use fuse_backend_rs::passthrough::{BackingFiles, Config, PassthroughFs};
26+
use fuse_backend_rs::transport::{fuse_backing_close, fuse_backing_open, FuseSession};
2327
use vmm_sys_util::tempdir::TempDir;
2428
use vmm_sys_util::tempfile::TempFile;
2529

2630
const FILE_ID: u64 = 2;
2731

2832
struct Fs {
2933
backing: File,
30-
fuse_file: OnceLock<File>,
34+
fuse_file: File,
3135
passthrough: AtomicBool,
3236
}
3337

@@ -88,34 +92,82 @@ mod fusedev_tests {
8892
_flags: u32,
8993
_fuse_flags: u32,
9094
) -> io::Result<(Option<u64>, OpenOptions, Option<u32>)> {
91-
let id = fuse_backing_open(self.fuse_file.get().unwrap(), self.backing.as_raw_fd())?;
95+
let id = fuse_backing_open(&self.fuse_file, self.backing.as_raw_fd())?;
9296
Ok((None, OpenOptions::PASSTHROUGH, Some(id)))
9397
}
9498
}
9599

96-
/// Mount a filesystem with one file, or return `None` when not root.
97-
fn mount() -> Option<(Arc<Fs>, FuseSession, TempDir)> {
100+
/// Mount a session and return it with its mount point and a clone of its FUSE device, or
101+
/// return `None` when not root.
102+
fn mount_session(readonly: bool) -> Option<(FuseSession, TempDir, File)> {
98103
if unsafe { libc::geteuid() } != 0 {
99104
eprintln!("not root, skipping");
100105
return None;
101106
}
107+
let mountpoint = TempDir::new().unwrap();
108+
let mut se = FuseSession::new(mountpoint.as_path(), "passthrough", "", readonly).unwrap();
109+
se.mount().unwrap();
110+
let fuse_file = se.get_fuse_file().unwrap().try_clone().unwrap();
111+
Some((se, mountpoint, fuse_file))
112+
}
102113

114+
/// Mount a filesystem with one file, or return `None` when not root.
115+
fn mount() -> Option<(Arc<Fs>, FuseSession, TempDir)> {
116+
let (se, mountpoint, fuse_file) = mount_session(true)?;
103117
let mut backing = TempFile::new().unwrap().into_file();
104118
backing.write_all(b"passed through").unwrap();
105119
let fs = Arc::new(Fs {
106120
backing,
107-
fuse_file: OnceLock::new(),
121+
fuse_file,
108122
passthrough: AtomicBool::new(false),
109123
});
110-
111-
let mountpoint = TempDir::new().unwrap();
112-
let mut se = FuseSession::new(mountpoint.as_path(), "passthrough", "", true).unwrap();
113-
se.mount().unwrap();
114-
let fuse_file = se.get_fuse_file().unwrap().try_clone().unwrap();
115-
fs.fuse_file.set(fuse_file).unwrap();
116124
Some((fs, se, mountpoint))
117125
}
118126

127+
/// Counts the READ and WRITE requests the server handles.
128+
#[derive(Default)]
129+
struct DataRequests(AtomicUsize);
130+
131+
impl MetricsHook for DataRequests {
132+
fn collect(&self, ih: &InHeader) {
133+
if ih.opcode == Opcode::Read as u32 || ih.opcode == Opcode::Write as u32 {
134+
self.0.fetch_add(1, Ordering::Relaxed);
135+
}
136+
}
137+
138+
fn release(&self, _oh: Option<&OutHeader>) {}
139+
}
140+
141+
/// Serve the requests of `se` with the sync server on a thread, counting the READ and WRITE
142+
/// requests.
143+
fn serve<F: FileSystem + Send + Sync + 'static>(
144+
se: &FuseSession,
145+
fs: Arc<F>,
146+
) -> (JoinHandle<()>, Arc<DataRequests>) {
147+
let server = Server::new(fs);
148+
server.set_max_stack_depth(2).unwrap();
149+
let requests = Arc::new(DataRequests::default());
150+
let hook = requests.clone();
151+
let mut ch = se.new_channel().unwrap();
152+
let handle = thread::spawn(move || {
153+
while let Ok(Some((reader, writer))) = ch.get_request() {
154+
if server
155+
.handle_message(reader, writer, None, Some(&*hook))
156+
.is_err()
157+
{
158+
break;
159+
}
160+
}
161+
});
162+
(handle, requests)
163+
}
164+
165+
fn stop(mut se: FuseSession, handle: JoinHandle<()>) {
166+
se.umount().unwrap();
167+
se.wake().unwrap();
168+
handle.join().unwrap();
169+
}
170+
119171
/// Read the file, or return `None` if the kernel doesn't offer passthrough.
120172
fn read_file(fs: &Fs, mountpoint: &TempDir) -> Option<io::Result<Vec<u8>>> {
121173
// Stat blocks until the kernel finishes INIT.
@@ -127,43 +179,122 @@ mod fusedev_tests {
127179
}
128180
}
129181

130-
fn check(result: Option<io::Result<Vec<u8>>>) {
182+
/// Pass the result of using a file to `ok`. Skip on `None`, when the kernel doesn't offer
183+
/// passthrough, and on EPERM, when the process can't register backing files.
184+
fn check_with<T>(result: Option<io::Result<T>>, ok: impl FnOnce(T)) {
131185
match result {
132-
// The default `read` returns ENOSYS, so the data can only come from the backing file.
133-
Some(Ok(data)) => assert_eq!(data, b"passed through"),
186+
Some(Ok(value)) => ok(value),
134187
// Registering a backing file needs CAP_SYS_ADMIN in the initial user namespace, which
135188
// root in another one, such as from `unshare -r`, doesn't have.
136189
Some(Err(e)) if e.raw_os_error() == Some(libc::EPERM) => {
137190
eprintln!("no CAP_SYS_ADMIN in the initial user namespace, skipping")
138191
}
139-
Some(Err(e)) => panic!("failed to read the file: {}", e),
192+
Some(Err(e)) => panic!("failed to use the file: {}", e),
140193
None => eprintln!("kernel without FUSE passthrough, skipping"),
141194
}
142195
}
143196

197+
fn check(result: Option<io::Result<Vec<u8>>>) {
198+
// The default `read` returns ENOSYS, so the data can only come from the backing file.
199+
check_with(result, |data| assert_eq!(data, b"passed through"));
200+
}
201+
144202
#[test]
145203
fn integration_kernel_passthrough() {
146-
let Some((fs, mut se, mountpoint)) = mount() else {
204+
let Some((fs, se, mountpoint)) = mount() else {
147205
return;
148206
};
207+
let (handle, _) = serve(&se, fs.clone());
208+
let result = read_file(&fs, &mountpoint);
209+
stop(se, handle);
210+
check(result);
211+
}
149212

150-
let server = Arc::new(Server::new(fs.clone()));
151-
server.set_max_stack_depth(2).unwrap();
152-
let mut ch = se.new_channel().unwrap();
153-
let handle = thread::spawn(move || {
154-
while let Ok(Some((reader, writer))) = ch.get_request() {
155-
if server.handle_message(reader, writer, None, None).is_err() {
156-
break;
157-
}
158-
}
159-
});
213+
/// Registers backing files on a FUSE device and counts the registrations.
214+
struct DeviceFiles {
215+
fuse_file: File,
216+
opened: AtomicUsize,
217+
}
160218

161-
let result = read_file(&fs, &mountpoint);
219+
impl BackingFiles for DeviceFiles {
220+
fn open(&self, fd: BorrowedFd<'_>) -> io::Result<u32> {
221+
let id = fuse_backing_open(&self.fuse_file, fd.as_raw_fd())?;
222+
self.opened.fetch_add(1, Ordering::Relaxed);
223+
Ok(id)
224+
}
162225

163-
se.umount().unwrap();
164-
se.wake().unwrap();
165-
handle.join().unwrap();
166-
check(result);
226+
fn close(&self, backing_id: u32) -> io::Result<()> {
227+
fuse_backing_close(&self.fuse_file, backing_id)
228+
}
229+
}
230+
231+
#[repr(align(4096))]
232+
struct Block([u8; 4096]);
233+
234+
/// An O_DIRECT open of a `PassthroughFs` file passes reads and writes through to the host
235+
/// file instead of sending them to the server.
236+
#[test]
237+
fn integration_kernel_passthrough_o_direct() {
238+
let source = TempDir::new().unwrap();
239+
let host_path = source.as_path().join("file");
240+
std::fs::write(&host_path, [1u8; 4096]).unwrap();
241+
// tmpfs supports O_DIRECT only since Linux 6.6.
242+
match std::fs::OpenOptions::new()
243+
.read(true)
244+
.custom_flags(libc::O_DIRECT)
245+
.open(&host_path)
246+
{
247+
Ok(_) => {}
248+
Err(e) if e.raw_os_error() == Some(libc::EINVAL) => {
249+
eprintln!("no O_DIRECT in {:?}, skipping", source.as_path());
250+
return;
251+
}
252+
Err(e) => panic!("failed to open {:?} with O_DIRECT: {}", host_path, e),
253+
}
254+
255+
let Some((se, mountpoint, fuse_file)) = mount_session(false) else {
256+
return;
257+
};
258+
let files = Arc::new(DeviceFiles {
259+
fuse_file,
260+
opened: AtomicUsize::new(0),
261+
});
262+
let mut fs = PassthroughFs::<()>::new(Config {
263+
root_dir: source.as_path().to_string_lossy().to_string(),
264+
allow_direct_io: true,
265+
..Default::default()
266+
})
267+
.unwrap();
268+
fs.set_backing_files(files.clone());
269+
let (handle, requests) = serve(&se, Arc::new(fs));
270+
271+
// Read the first block and write a second one. No other file is open, so the server
272+
// handles no READ or WRITE at all if the handle passes through.
273+
let result = std::fs::OpenOptions::new()
274+
.read(true)
275+
.write(true)
276+
.custom_flags(libc::O_DIRECT)
277+
.open(mountpoint.as_path().join("file"))
278+
.and_then(|file| {
279+
let mut block = Block([0; 4096]);
280+
file.read_exact_at(&mut block.0, 0)?;
281+
file.write_all_at(&Block([2; 4096]).0, 4096)?;
282+
Ok(block)
283+
});
284+
// Without passthrough the open succeeds without registering the file.
285+
let result = match result {
286+
Ok(_) if files.opened.load(Ordering::Relaxed) == 0 => None,
287+
result => Some(result),
288+
};
289+
let data_requests = requests.0.load(Ordering::Relaxed);
290+
291+
stop(se, handle);
292+
check_with(result, |block| {
293+
assert_eq!(block.0, [1; 4096]);
294+
assert_eq!(data_requests, 0);
295+
let data = std::fs::read(&host_path).unwrap();
296+
assert_eq!(data[4096..], [2; 4096]);
297+
});
167298
}
168299

169300
#[cfg(feature = "async-io")]

0 commit comments

Comments
 (0)