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" ) ) ]
89mod 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