@@ -318,6 +318,35 @@ impl From<LocalFetchCli> for composefs_oci::LocalFetchOpt {
318318 }
319319}
320320
321+ /// Options accepted by `--fuse[=<opts>]` on `mount` and `oci mount`.
322+ ///
323+ /// Pass bare `--fuse` to FUSE-mount with defaults, or `--fuse=passthrough`
324+ /// to also enable kernel-bypass reads for external files.
325+ ///
326+ /// Multiple options are comma-separated: `--fuse=passthrough,option2`
327+ /// (only `passthrough` is defined today).
328+ #[ cfg( feature = "fuse" ) ]
329+ #[ derive( Debug , Default , Clone ) ]
330+ struct FuseOptions {
331+ passthrough : bool ,
332+ }
333+
334+ #[ cfg( feature = "fuse" ) ]
335+ impl std:: str:: FromStr for FuseOptions {
336+ type Err = anyhow:: Error ;
337+
338+ fn from_str ( s : & str ) -> Result < Self , Self :: Err > {
339+ let mut opts = FuseOptions :: default ( ) ;
340+ for token in s. split ( ',' ) . map ( str:: trim) . filter ( |t| !t. is_empty ( ) ) {
341+ match token {
342+ "passthrough" => opts. passthrough = true ,
343+ other => anyhow:: bail!( "unknown fuse option: {other:?} (known: passthrough)" ) ,
344+ }
345+ }
346+ Ok ( opts)
347+ }
348+ }
349+
321350/// Common options for operations using OCI config manifest streams that may transform the image rootfs
322351#[ cfg( feature = "oci" ) ]
323352#[ derive( Debug , Parser ) ]
@@ -447,6 +476,17 @@ enum OciCommand {
447476 /// Mount read-write (requires --upperdir)
448477 #[ arg( long, requires = "upperdir" ) ]
449478 read_write : bool ,
479+ /// Serve the EROFS image over FUSE instead of using a kernel composefs mount.
480+ /// Requires /dev/fuse and blocks until the mount is detached or the process
481+ /// is killed. Does not require fs-verity on the backing store.
482+ ///
483+ /// Accepts an optional comma-separated list of options:
484+ /// --fuse basic FUSE mount
485+ /// --fuse=passthrough also enable kernel-bypass reads (Linux 6.9+, root, non-tmpfs)
486+ #[ cfg( feature = "fuse" ) ]
487+ #[ arg( long, num_args = 0 ..=1 , require_equals = false , value_name = "OPTS" ,
488+ default_missing_value = "" ) ]
489+ fuse : Option < FuseOptions > ,
450490 } ,
451491 /// Compute the composefs image ID of a stored OCI image's rootfs
452492 ///
@@ -568,13 +608,23 @@ enum Command {
568608 #[ clap( subcommand) ]
569609 cmd : OciCommand ,
570610 } ,
571- /// Mounts a composefs image, possibly enforcing fsverity of the image
611+ /// Mounts a composefs image, possibly enforcing fsverity of the image.
612+ ///
613+ /// By default the image is identified by its repo name (an fs-verity hash
614+ /// or a `ref/` tag). Pass `--raw-image` to supply a path to a bare EROFS
615+ /// file instead, which skips the repository image store lookup and is
616+ /// required when using `--fuse` without a full repository setup.
572617 Mount {
573- /// the name of the image to mount, either an fs-verity hash or prefixed with 'ref/'
618+ /// The image to mount: a repo name (fs-verity hash or `ref/<tag>`) by
619+ /// default, or a filesystem path when `--raw-image` is given.
574620 name : String ,
575621 /// the mountpoint
576622 mountpoint : String ,
577- /// Writable upper layer directory for overlayfs
623+ /// Treat <name> as a path to a raw EROFS image file instead of a repo
624+ /// image name.
625+ #[ arg( long) ]
626+ raw_image : bool ,
627+ /// Writable upper layer directory for overlayfs (kernel mount only)
578628 #[ arg( long, requires = "workdir" ) ]
579629 upperdir : Option < PathBuf > ,
580630 /// Work directory for overlayfs (required with --upperdir)
@@ -583,6 +633,15 @@ enum Command {
583633 /// Mount read-write (requires --upperdir)
584634 #[ arg( long, requires = "upperdir" ) ]
585635 read_write : bool ,
636+ /// Serve the image over FUSE instead of a kernel composefs mount.
637+ ///
638+ /// Accepts an optional comma-separated list of options:
639+ /// --fuse basic FUSE mount
640+ /// --fuse=passthrough also enable kernel-bypass reads (Linux 6.9+, root, non-tmpfs)
641+ #[ cfg( feature = "fuse" ) ]
642+ #[ arg( long, num_args = 0 ..=1 , require_equals = false , value_name = "OPTS" ,
643+ default_missing_value = "" ) ]
644+ fuse : Option < FuseOptions > ,
586645 } ,
587646 /// Read rootfs located at a path, add all files to the repo, then create the composefs image of the rootfs,
588647 /// commit it to the repo, and print its image object ID
@@ -1264,9 +1323,9 @@ where
12641323 ref upperdir,
12651324 ref workdir,
12661325 read_write,
1326+ #[ cfg( feature = "fuse") ]
1327+ fuse,
12671328 } => {
1268- let mount_options =
1269- get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
12701329 let img = if image. starts_with ( "sha256:" ) {
12711330 let digest: composefs_oci:: OciDigest =
12721331 image. parse ( ) . context ( "Parsing manifest digest" ) ?;
@@ -1289,7 +1348,45 @@ where
12891348 ) ,
12901349 }
12911350 } ;
1292- repo. mount_at ( & erofs_id. to_hex ( ) , mountpoint. as_str ( ) , & mount_options) ?;
1351+ #[ cfg( feature = "fuse" ) ]
1352+ if let Some ( fuse_opts) = fuse {
1353+ use composefs_fuse:: {
1354+ FuseConfig , erofs_fd_to_filesystem, mount_fuse, open_fuse,
1355+ serve_tree_fuse_fd,
1356+ } ;
1357+
1358+ let ( image_fd, _verified) = repo. open_image ( & erofs_id. to_hex ( ) ) ?;
1359+ let filesystem = erofs_fd_to_filesystem :: < ObjectID > ( image_fd) ?;
1360+
1361+ let dev_fuse = open_fuse ( ) ?;
1362+ let mnt_fd = mount_fuse ( & dev_fuse, & Default :: default ( ) ) ?;
1363+ composefs:: mount:: mount_at ( & mnt_fd, CWD , mountpoint. as_str ( ) )
1364+ . with_context ( || format ! ( "attaching FUSE mount at {mountpoint}" ) ) ?;
1365+
1366+ // Hold mnt_fd alive for the session duration — it pins the FUSE
1367+ // superblock so the connection stays alive while we serve.
1368+ let _mnt_fd = mnt_fd;
1369+
1370+ serve_tree_fuse_fd (
1371+ dev_fuse,
1372+ Arc :: new ( filesystem) ,
1373+ Arc :: clone ( & repo) ,
1374+ FuseConfig {
1375+ passthrough : fuse_opts. passthrough ,
1376+ } ,
1377+ )
1378+ . context ( "FUSE session error" ) ?;
1379+ } else {
1380+ let mount_options =
1381+ get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
1382+ repo. mount_at ( & erofs_id. to_hex ( ) , mountpoint. as_str ( ) , & mount_options) ?;
1383+ }
1384+ #[ cfg( not( feature = "fuse" ) ) ]
1385+ {
1386+ let mount_options =
1387+ get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
1388+ repo. mount_at ( & erofs_id. to_hex ( ) , mountpoint. as_str ( ) , & mount_options) ?;
1389+ }
12931390 }
12941391 OciCommand :: ComputeId { config_opts } => {
12951392 let mut fs = load_filesystem_from_oci_image ( & repo, config_opts) ?;
@@ -1513,13 +1610,58 @@ where
15131610 Command :: Mount {
15141611 name,
15151612 mountpoint,
1613+ raw_image,
15161614 ref upperdir,
15171615 ref workdir,
15181616 read_write,
1617+ #[ cfg( feature = "fuse") ]
1618+ fuse,
15191619 } => {
1520- let mount_options =
1521- get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
1522- repo. mount_at ( & name, & mountpoint, & mount_options) ?;
1620+ #[ cfg( feature = "fuse" ) ]
1621+ if let Some ( fuse_opts) = fuse {
1622+ use composefs_fuse:: {
1623+ FuseConfig , erofs_fd_to_filesystem, mount_fuse, open_fuse, serve_tree_fuse_fd,
1624+ } ;
1625+
1626+ let filesystem = if raw_image {
1627+ let bytes = std:: fs:: read ( & name) . with_context ( || format ! ( "reading {name}" ) ) ?;
1628+ erofs_to_filesystem :: < ObjectID > ( & bytes) . context ( "parsing EROFS image" ) ?
1629+ } else {
1630+ let ( image_fd, _verified) = repo. open_image ( & name) ?;
1631+ erofs_fd_to_filesystem :: < ObjectID > ( image_fd) ?
1632+ } ;
1633+
1634+ let dev_fuse = open_fuse ( ) ?;
1635+ let mnt_fd = mount_fuse ( & dev_fuse, & Default :: default ( ) ) ?;
1636+ composefs:: mount:: mount_at ( & mnt_fd, CWD , mountpoint. as_str ( ) )
1637+ . with_context ( || format ! ( "attaching FUSE mount at {mountpoint}" ) ) ?;
1638+
1639+ // Hold mnt_fd alive for the session duration — it pins the FUSE
1640+ // superblock so the connection stays alive while we serve.
1641+ let _mnt_fd = mnt_fd;
1642+
1643+ serve_tree_fuse_fd (
1644+ dev_fuse,
1645+ Arc :: new ( filesystem) ,
1646+ Arc :: clone ( & repo) ,
1647+ FuseConfig {
1648+ passthrough : fuse_opts. passthrough ,
1649+ } ,
1650+ )
1651+ . context ( "FUSE session error" ) ?;
1652+ } else {
1653+ anyhow:: ensure!( !raw_image, "--raw-image requires --fuse" ) ;
1654+ let mount_options =
1655+ get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
1656+ repo. mount_at ( & name, & mountpoint, & mount_options) ?;
1657+ }
1658+ #[ cfg( not( feature = "fuse" ) ) ]
1659+ {
1660+ anyhow:: ensure!( !raw_image, "--raw-image requires --fuse" ) ;
1661+ let mount_options =
1662+ get_mount_options ( upperdir. as_deref ( ) , workdir. as_deref ( ) , read_write) ?;
1663+ repo. mount_at ( & name, & mountpoint, & mount_options) ?;
1664+ }
15231665 }
15241666 Command :: ImageObjects { name } => {
15251667 let objects = repo. objects_for_image ( & name) ?;
0 commit comments