Skip to content

Commit 61d22ab

Browse files
committed
passthrough: drop DIRECT_IO from passthrough opens
FOPEN_DIRECT_IO overrides FOPEN_PASSTHROUGH for reads, writes and splice, so O_DIRECT opens still sent their IO through the daemon. The kernel opens the backing file with the client's flags, so without DIRECT_IO they do direct IO on the host file. For sync IO that open decides, so clearing O_DIRECT later with fcntl() doesn't reach the backing file. The new test opens a file with O_DIRECT and checks that the server sees no READ or WRITE. It needs root, like the other kernel passthrough tests. Signed-off-by: Ilan Joselevich <personal@ilanjoselevich.com>
1 parent facd84b commit 61d22ab

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

@@ -752,9 +754,9 @@ impl<S: BitmapSlice + Send + Sync> FileSystem for PassthroughFs<S> {
752754
if self.kernel_passthrough.is_some() && capable.contains(FsOptions::PASSTHROUGH) {
753755
// The kernel ignores FUSE_PASSTHROUGH along with FUSE_WRITEBACK_CACHE, and needs open
754756
// requests to get the backing ids. Passthrough writes skip the seal_size check. The
755-
// Never and Metadata cache policies open files with FOPEN_DIRECT_IO, which overrides
756-
// passthrough for reads and writes. The kernel opens the backing file with the
757-
// client's O_DIRECT, which allow_direct_io = false would filter out.
757+
// Never and Metadata cache policies want FOPEN_DIRECT_IO, which passthrough opens drop.
758+
// The kernel opens the backing file with the client's O_DIRECT, which
759+
// allow_direct_io = false would filter out.
758760
if self.writeback.load(Ordering::Relaxed)
759761
|| self.no_open.load(Ordering::Relaxed)
760762
|| 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)