Skip to content
  • Filipe Manana's avatar
    Btrfs: fix fsync data loss after adding hard link to inode · 1a4bcf47
    Filipe Manana authored
    We have a scenario where after the fsync log replay we can lose file data
    that had been previously fsync'ed if we added an hard link for our inode
    and after that we sync'ed the fsync log (for example by fsync'ing some
    other file or directory).
    
    This is because when adding an hard link we updated the inode item in the
    log tree with an i_size value of 0. At that point the new inode item was
    in memory only and a subsequent fsync log replay would not make us lose
    the file data. However if after adding the hard link we sync the log tree
    to disk, by fsync'ing some other file or directory for example, we ended
    up losing the file data after log replay, because the inode item in the
    persisted log tree had an an i_size of zero.
    
    This is easy to reproduce, and the following excerpt from my test for
    xfstests shows this:
    
      _scratch_mkfs >> $seqres.full 2>&1
      _init_flakey
      _mount_flakey
    
      # Create one file with data and fsync it.
      # This made the btrfs fsync log persist the data and the inode metadata with
      # a correct inode->i_size (4096 bytes).
      $XFS_IO_PROG -f -c "pwrite -S 0xaa -b 4K 0 4K" -c "fsync" \
           $SCRATCH_MNT/foo | _filter_xfs_io
    
      # Now add one hard link to our file. This made the btrfs code update the fsync
      # log, in memory only, with an inode metadata having a size of 0.
      ln $SCRATCH_MNT/foo $SCRATCH_MNT/foo_link
    
      # Now force persistence of the fsync log to disk, for example, by fsyncing some
      # other file.
      touch $SCRATCH_MNT/bar
      $XFS_IO_PROG -c "fsync" $SCRATCH_MNT/bar
    
      # Before a power loss or crash, we could read the 4Kb of data from our file as
      # expected.
      echo "File content before:"
      od -t x1 $SCRATCH_MNT/foo
    
      # Simulate a crash/power loss.
      _load_flakey_table $FLAKEY_DROP_WRITES
      _unmount_flakey
    
      _load_flakey_table $FLAKEY_ALLOW_WRITES
      _mount_flakey
    
      # After the fsync log replay, because the fsync log had a value of 0 for our
      # inode's i_size, we couldn't read anymore the 4Kb of data that we previously
      # wrote and fsync'ed. The size of the file became 0 after the fsync log replay.
      echo "File content after:"
      od -t x1 $SCRATCH_MNT/foo
    
    Another alternative test, that doesn't need to fsync an inode in the same
    transaction it was created, is:
    
      _scratch_mkfs >> $seqres.full 2>&1
      _init_flakey
      _mount_flakey
    
      # Create our test file with some data.
      $XFS_IO_PROG -f -c "pwrite -S 0xaa -b 8K 0 8K" \
           $SCRATCH_MNT/foo | _filter_xfs_io
    
      # Make sure the file is durably persisted.
      sync
    
      # Append some data to our file, to increase its size.
      $XFS_IO_PROG -f -c "pwrite -S 0xcc -b 4K 8K 4K" \
           $SCRATCH_MNT/foo | _filter_xfs_io
    
      # Fsync the file, so from this point on if a crash/power failure happens, our
      # new data is guaranteed to be there next time the fs is mounted.
      $XFS_IO_PROG -c "fsync" $SCRATCH_MNT/foo
    
      # Add one hard link to our file. This made btrfs write into the in memory fsync
      # log a special inode with generation 0 and an i_size of 0 too. Note that this
      # didn't update the inode in the fsync log on disk.
      ln $SCRATCH_MNT/foo $SCRATCH_MNT/foo_link
    
      # Now make sure the in memory fsync log is durably persisted.
      # Creating and fsync'ing another file will do it.
      touch $SCRATCH_MNT/bar
      $XFS_IO_PROG -c "fsync" $SCRATCH_MNT/bar
    
      # As expected, before the crash/power failure, we should be able to read the
      # 12Kb of file data.
      echo "File content before:"
      od -t x1 $SCRATCH_MNT/foo
    
      # Simulate a crash/power loss.
      _load_flakey_table $FLAKEY_DROP_WRITES
      _unmount_flakey
    
      _load_flakey_table $FLAKEY_ALLOW_WRITES
      _mount_flakey
    
      # After mounting the fs again, the fsync log was replayed.
      # The btrfs fsync log replay code didn't update the i_size of the persisted
      # inode because the inode item in the log had a special generation with a
      # value of 0 (and it couldn't know the correct i_size, since that inode item
      # had a 0 i_size too). This made the last 4Kb of file data inaccessible and
      # effectively lost.
      echo "File content after:"
      od -t x1 $SCRATCH_MNT/foo
    
    This isn't a new issue/regression. This problem has been around since the
    log tree code was added in 2008:
    
      Btrfs: Add a write ahead tree log to optimize synchronous operations
      (commit e02119d5
    
    )
    
    Test cases for xfstests follow soon.
    
    CC: <stable@vger.kernel.org>
    Signed-off-by: default avatarFilipe Manana <fdmanana@suse.com>
    Signed-off-by: default avatarChris Mason <clm@fb.com>
    1a4bcf47