@@ -1461,3 +1461,109 @@ fn live_tar_extract_single_file() -> Result<()> {
14611461
14621462 Ok ( ( ) )
14631463}
1464+
1465+ /// Regression test for the "backing file created but no data written"
1466+ /// report: `cp` between two paths *within the same EncFS mount*.
1467+ ///
1468+ /// Root cause, bisected to a single upstream commit: fixed in typed-fuse
1469+ /// commit `5a07205` ("fix statfs on darwin"), picked up here via the
1470+ /// `Cargo.lock` bump in this same change. Before that fix, macOS's
1471+ /// `fuse_reply_statfs` binding built a `struct statfs` and handed its
1472+ /// pointer to `fuse_reply_statfs_vanilla`, which actually expects `struct
1473+ /// statvfs*` — a different, differently-laid-out type. Every
1474+ /// `statfs()`/`statvfs()` on any file in the mount therefore returned
1475+ /// reinterpreted garbage, independently confirmed by `stat -f`: two very
1476+ /// differently sized files in the mount reported identical, nonsensical
1477+ /// `st_blocks`, matching the mountpoint's own volume-level number. Exactly
1478+ /// how `cp`'s data-copy path consumes that garbage to end up writing zero
1479+ /// bytes while still exiting 0 was not isolated (would need
1480+ /// `fs_usage`/`dtruss`, which need root) — what's verified is the A/B: the
1481+ /// same `cp` invocation loses data at the pre-fix commit and doesn't at the
1482+ /// post-fix one, with no other functional change in between. `dd`, a
1483+ /// single-call Python `write()`, `rsync`, and `cp` from an *external* source
1484+ /// into the mount were all unaffected even pre-fix.
1485+ ///
1486+ /// This is deliberately config-independent (V6 `Standard` fixture and a
1487+ /// fresh wide-file-IV V7 volume both exercise it) because the bug reproduced
1488+ /// identically on both before the fix: it is a macOS/FUSE-binding-layer bug
1489+ /// with no dependency on file-IV width, header size, or block mode, and it
1490+ /// predates the 96-bit-IV work that first surfaced it (that work simply made
1491+ /// this the new default happy path people would immediately test with `cp`).
1492+ fn run_internal_copy_same_mount (
1493+ mount : & MountGuard ,
1494+ header_size : u64 ,
1495+ expected_physical_size : u64 ,
1496+ ) -> Result < ( ) > {
1497+ let src = mount. mount_point . join ( "cp_src_internal.bin" ) ;
1498+ let dst = mount. mount_point . join ( "cp_dst_internal.bin" ) ;
1499+ let payload = pattern_bytes ( 400_000 ) ;
1500+ fs:: write ( & src, & payload) . context ( "write source inside mount" ) ?;
1501+
1502+ let before = live:: list_non_dot_entries_recursive ( & mount. backing_root ) ?
1503+ . into_iter ( )
1504+ . collect :: < std:: collections:: BTreeSet < _ > > ( ) ;
1505+
1506+ let status = Command :: new ( "cp" )
1507+ . arg ( & src)
1508+ . arg ( & dst)
1509+ . status ( )
1510+ . context ( "spawn cp" ) ?;
1511+ anyhow:: ensure!( status. success( ) , "cp exited with {status}" ) ;
1512+
1513+ let after = live:: list_non_dot_entries_recursive ( & mount. backing_root ) ?;
1514+ let new_backing_file = after
1515+ . iter ( )
1516+ . find ( |p| !before. contains ( * p) )
1517+ . context ( "no new backing file appeared for the cp destination" ) ?;
1518+ let backing_len = fs:: metadata ( new_backing_file)
1519+ . context ( "stat new backing file" ) ?
1520+ . len ( ) ;
1521+
1522+ let got = fs:: read ( & dst) . context ( "read cp destination" ) ?;
1523+ assert_eq ! (
1524+ ( backing_len, got. len( ) ) ,
1525+ ( expected_physical_size, payload. len( ) ) ,
1526+ "cp between two paths on the same EncFS mount lost data: backing file is \
1527+ {backing_len} bytes (header alone is {header_size}, full copy should be \
1528+ {expected_physical_size}), mounted read returned {} bytes, expected {} bytes of payload",
1529+ got. len( ) ,
1530+ payload. len( )
1531+ ) ;
1532+ assert_eq ! ( got, payload, "cp destination content mismatch" ) ;
1533+ Ok ( ( ) )
1534+ }
1535+
1536+ #[ test]
1537+ #[ ignore]
1538+ fn live_internal_copy_same_mount_standard ( ) -> Result < ( ) > {
1539+ require_live ( ) ;
1540+ if !live_enabled ( ) {
1541+ return Ok ( ( ) ) ;
1542+ }
1543+ let cfg = load_live_config ( live:: LiveConfigKind :: Standard ) ?;
1544+ let mount = MountGuard :: mount ( cfg, false ) ?;
1545+ // Standard fixture: uniqueIV=1, wideFileIV unset (narrow) => 8-byte
1546+ // header; blockMACBytes=0 => no per-block overhead.
1547+ run_internal_copy_same_mount ( & mount, 8 , 8 + 400_000 )
1548+ }
1549+
1550+ #[ test]
1551+ #[ ignore]
1552+ fn live_internal_copy_same_mount_wide_v7 ( ) -> Result < ( ) > {
1553+ require_live ( ) ;
1554+ if !live_enabled ( ) {
1555+ return Ok ( ( ) ) ;
1556+ }
1557+ let ( backing_root, cfg) = live:: init_wide_v7_backing_root ( ) ?;
1558+ // standard_v7() defaults: uniqueIV=1, wideFileIV=1 => 12-byte header,
1559+ // AES-GCM-SIV block mode => 16-byte per-block tag overhead.
1560+ let expected = encfs:: crypto:: file:: FileEncoder :: < fs:: File > :: calculate_physical_size_with_mode (
1561+ 400_000 ,
1562+ 12 ,
1563+ cfg. block_size ,
1564+ cfg. block_mac_bytes ,
1565+ encfs:: crypto:: block:: BlockMode :: AesGcmSiv ,
1566+ ) ;
1567+ let mount = MountGuard :: mount_existing_backing_root ( cfg, false , backing_root) ?;
1568+ run_internal_copy_same_mount ( & mount, 12 , expected)
1569+ }
0 commit comments