fusefrontend: add fallocate support

Mode=0 (default) and mode=1 (keep size) are supported.
The patch includes test cases and the whole thing passed xfstests.

Fixes https://github.com/rfjakob/gocryptfs/issues/1 .
This commit is contained in:
Jakob Unterwurzacher 2016-07-02 19:43:57 +02:00
parent 04ad063515
commit 54470baa23
4 changed files with 255 additions and 53 deletions

View File

@ -208,8 +208,6 @@ func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fus
return fuse.ReadResultData(out), status
}
const FALLOC_FL_KEEP_SIZE = 0x01
// doWrite - encrypt "data" and write it to plaintext offset "off"
//
// Arguments do not have to be block-aligned, read-modify-write is

View File

@ -4,6 +4,7 @@ package fusefrontend
// i.e. ftruncate and fallocate
import (
"log"
"sync"
"syscall"
@ -12,17 +13,78 @@ import (
"github.com/rfjakob/gocryptfs/internal/tlog"
)
const FALLOC_DEFAULT = 0x00
const FALLOC_FL_KEEP_SIZE = 0x01
// Only warn once
var allocateWarnOnce sync.Once
// Allocate - FUSE call, fallocate(2)
// This is not implemented yet in gocryptfs, but it is neither in EncFS. This
// suggests that the user demand is low.
// Allocate - FUSE call for fallocate(2)
//
// mode=FALLOC_FL_KEEP_SIZE is implemented directly.
//
// mode=FALLOC_DEFAULT is implemented as a two-step process:
//
// (1) Allocate the space using FALLOC_FL_KEEP_SIZE
// (2) Set the file size using ftruncate (via truncateGrowFile)
//
// This allows us to reuse the file grow mechanics from Truncate as they are
// complicated and hard to get right.
//
// Other modes (hole punching, zeroing) are not supported.
func (f *file) Allocate(off uint64, sz uint64, mode uint32) fuse.Status {
allocateWarnOnce.Do(func() {
tlog.Warn.Printf("fallocate(2) is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1")
})
return fuse.ENOSYS
if mode != FALLOC_DEFAULT && mode != FALLOC_FL_KEEP_SIZE {
f := func() {
tlog.Warn.Print("fallocate: only mode 0 (default) and 1 (keep size) are supported")
}
allocateWarnOnce.Do(f)
return fuse.Status(syscall.EOPNOTSUPP)
}
f.fdLock.RLock()
defer f.fdLock.RUnlock()
if f.released {
return fuse.EBADF
}
wlock.lock(f.ino)
defer wlock.unlock(f.ino)
blocks := f.contentEnc.ExplodePlainRange(off, sz)
firstBlock := blocks[0]
lastBlock := blocks[len(blocks)-1]
// Step (1): Allocate the space the user wants using FALLOC_FL_KEEP_SIZE.
// This will fill file holes and/or allocate additional space past the end of
// the file.
cipherOff := firstBlock.BlockCipherOff()
cipherSz := lastBlock.BlockCipherOff() - cipherOff +
f.contentEnc.PlainSizeToCipherSize(lastBlock.Skip+lastBlock.Length)
err := syscall.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz))
tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n",
off, sz, mode, cipherOff, cipherSz)
if err != nil {
return fuse.ToStatus(err)
}
if mode == FALLOC_FL_KEEP_SIZE {
// The user did not want to change the apparent size. We are done.
return fuse.OK
}
// Step (2): Grow the apparent file size
// We need the old file size to determine if we are growing the file at all.
newPlainSz := off + sz
oldPlainSz, err := f.statPlainSize()
if err != nil {
return fuse.ToStatus(err)
}
if newPlainSz <= oldPlainSz {
// The new size is smaller (or equal). Fallocate with mode = 0 never
// truncates a file, so we are done.
return fuse.OK
}
// The file grows. The space has already been allocated in (1), so what is
// left to do is to pad the first and last block and call truncate.
// truncateGrowFile does just that.
return f.truncateGrowFile(oldPlainSz, newPlainSz)
}
// Truncate - FUSE call
@ -50,13 +112,10 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
}
// We need the old file size to determine if we are growing or shrinking
// the file
fi, err := f.fd.Stat()
oldSize, err := f.statPlainSize()
if err != nil {
tlog.Warn.Printf("ino%d fh%d: Truncate: Fstat failed: %v", f.ino, f.intFd(), err)
return fuse.ToStatus(err)
}
oldSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size()))
{
} else {
oldB := float32(oldSize) / float32(f.contentEnc.PlainBS())
newB := float32(newSize) / float32(f.contentEnc.PlainBS())
tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.ino, oldB, newB, oldSize, newSize)
@ -67,31 +126,7 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
}
// File grows
if newSize > oldSize {
// File was empty, create new header
if oldSize == 0 {
err = f.createHeader()
if err != nil {
return fuse.ToStatus(err)
}
}
// New blocks to add
addBlocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize)
if len(addBlocks) >= 2 {
f.zeroPad(oldSize)
}
lastBlock := addBlocks[len(addBlocks)-1]
if lastBlock.IsPartial() {
off := lastBlock.BlockPlainOff()
_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip))
return status
} else {
off := lastBlock.BlockCipherOff()
err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS()))
if err != nil {
tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
}
return fuse.ToStatus(err)
}
return f.truncateGrowFile(oldSize, newSize)
} else {
// File shrinks
blockNo := f.contentEnc.PlainOffToBlockNo(newSize)
@ -121,3 +156,53 @@ func (f *file) Truncate(newSize uint64) fuse.Status {
return fuse.OK
}
}
// statPlainSize stats the file and returns the plaintext size
func (f *file) statPlainSize() (uint64, error) {
fi, err := f.fd.Stat()
if err != nil {
tlog.Warn.Printf("ino%d fh%d: statPlainSize: %v", f.ino, f.intFd(), err)
return 0, err
}
cipherSz := uint64(fi.Size())
plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz))
return plainSz, nil
}
// truncateGrowFile extends a file using seeking or ftruncate performing RMW on
// the first and last block as neccessary. New blocks in the middle become
// file holes unless they have been fallocate()'d beforehand.
func (f *file) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) fuse.Status {
if newPlainSz <= oldPlainSz {
log.Panicf("BUG: newSize=%d <= oldSize=%d", newPlainSz, oldPlainSz)
}
var err error
// File was empty, create new header
if oldPlainSz == 0 {
err = f.createHeader()
if err != nil {
return fuse.ToStatus(err)
}
}
// New blocks to add
addBlocks := f.contentEnc.ExplodePlainRange(oldPlainSz, newPlainSz-oldPlainSz)
if oldPlainSz > 0 && len(addBlocks) >= 2 {
// Zero-pad the first block (unless the first block is also the last block)
f.zeroPad(oldPlainSz)
}
lastBlock := addBlocks[len(addBlocks)-1]
if lastBlock.IsPartial() {
// Write at the new end of the file. The seek implicitly grows the file
// (creates a file hole) and doWrite() takes care of RMW.
off := lastBlock.BlockPlainOff()
_, status := f.doWrite(make([]byte, lastBlock.Length), int64(off+lastBlock.Skip))
return status
} else {
off := lastBlock.BlockCipherOff()
err = syscall.Ftruncate(f.intFd(), int64(off+f.contentEnc.CipherBS()))
if err != nil {
tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err)
}
return fuse.ToStatus(err)
}
}

View File

@ -124,26 +124,26 @@ func TestTruncate(t *testing.T) {
// Grow to two blocks
file.Truncate(7000)
test_helpers.VerifySize(t, fn, 7000)
if test_helpers.Md5fn(fn) != "95d4ec7038e3e4fdbd5f15c34c3f0b34" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "95d4ec7038e3e4fdbd5f15c34c3f0b34" {
t.Errorf("Wrong md5 %s", md5)
}
// Shrink - needs RMW
file.Truncate(6999)
test_helpers.VerifySize(t, fn, 6999)
if test_helpers.Md5fn(fn) != "35fd15873ec6c35380064a41b9b9683b" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "35fd15873ec6c35380064a41b9b9683b" {
t.Errorf("Wrong md5 %s", md5)
}
// Shrink to one partial block
file.Truncate(465)
test_helpers.VerifySize(t, fn, 465)
if test_helpers.Md5fn(fn) != "a1534d6e98a6b21386456a8f66c55260" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "a1534d6e98a6b21386456a8f66c55260" {
t.Errorf("Wrong md5 %s", md5)
}
// Grow to exactly one block
file.Truncate(4096)
test_helpers.VerifySize(t, fn, 4096)
if test_helpers.Md5fn(fn) != "620f0b67a91f7f74151bc5be745b7110" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "620f0b67a91f7f74151bc5be745b7110" {
t.Errorf("Wrong md5 %s", md5)
}
// Truncate to zero
file.Truncate(0)
@ -153,25 +153,133 @@ func TestTruncate(t *testing.T) {
sz = 10 * 1024 * 1024
file.Truncate(int64(sz))
test_helpers.VerifySize(t, fn, sz)
if test_helpers.Md5fn(fn) != "f1c9645dbc14efddc7d8a322685f26eb" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "f1c9645dbc14efddc7d8a322685f26eb" {
t.Errorf("Wrong md5 %s", md5)
}
// Grow to 10MB + 100B (partial block on the end)
sz = 10*1024*1024 + 100
file.Truncate(int64(sz))
test_helpers.VerifySize(t, fn, sz)
if test_helpers.Md5fn(fn) != "c23ea79b857b91a7ff07c6ecf185f1ca" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "c23ea79b857b91a7ff07c6ecf185f1ca" {
t.Errorf("Wrong md5 %s", md5)
}
// Grow to 20MB (creates file holes, partial block on the front)
sz = 20 * 1024 * 1024
file.Truncate(int64(sz))
test_helpers.VerifySize(t, fn, sz)
if test_helpers.Md5fn(fn) != "8f4e33f3dc3e414ff94e5fb6905cba8c" {
t.Errorf("wrong content")
if md5 := test_helpers.Md5fn(fn); md5 != "8f4e33f3dc3e414ff94e5fb6905cba8c" {
t.Errorf("Wrong md5 %s", md5)
}
}
const FALLOC_DEFAULT = 0x00
const FALLOC_FL_KEEP_SIZE = 0x01
func TestFallocate(t *testing.T) {
fn := test_helpers.DefaultPlainDir + "/fallocate"
file, err := os.Create(fn)
if err != nil {
t.FailNow()
}
var nBlocks int64
fd := int(file.Fd())
_, nBlocks = test_helpers.Du(t, fd)
if nBlocks != 0 {
t.Fatalf("Empty file has %d blocks", nBlocks)
}
// Allocate 30 bytes, keep size
// gocryptfs || (0 blocks)
// ext4 | d | (1 block)
err = syscall.Fallocate(fd, FALLOC_FL_KEEP_SIZE, 0, 30)
if err != nil {
t.Error(err)
}
_, nBlocks = test_helpers.Du(t, fd)
if want := 1; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
test_helpers.VerifySize(t, fn, 0)
// Three ciphertext blocks. The middle one should be a file hole.
// gocryptfs | h | h | d| (1 block)
// ext4 | d | h | d | (2 blocks)
// (Note that gocryptfs blocks are slightly bigger than the ext4 blocks,
// but the last one is partial)
err = file.Truncate(9000)
if err != nil {
t.Fatal(err)
}
_, nBlocks = test_helpers.Du(t, fd)
if want := 2; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
if md5 := test_helpers.Md5fn(fn); md5 != "5420afa22f6423a9f59e669540656bb4" {
t.Errorf("Wrong md5 %s", md5)
}
// Allocate the whole file space
// gocryptfs | h | h | d| (1 block)
// ext4 | d | d | d | (3 blocks
err = syscall.Fallocate(fd, FALLOC_DEFAULT, 0, 9000)
if err != nil {
t.Fatal(err)
}
_, nBlocks = test_helpers.Du(t, fd)
if want := 3; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
// Neither apparent size nor content should have changed
test_helpers.VerifySize(t, fn, 9000)
if md5 := test_helpers.Md5fn(fn); md5 != "5420afa22f6423a9f59e669540656bb4" {
t.Errorf("Wrong md5 %s", md5)
}
// Partial block on the end. The first ext4 block is dirtied by the header.
// gocryptfs | h | h | d| (1 block)
// ext4 | d | h | d | (2 blocks)
file.Truncate(0)
file.Truncate(9000)
_, nBlocks = test_helpers.Du(t, fd)
if want := 2; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
// Allocate 10 bytes in the second block
// gocryptfs | h | h | d| (1 block)
// ext4 | d | d | d | (2 blocks)
syscall.Fallocate(fd, FALLOC_DEFAULT, 5000, 10)
_, nBlocks = test_helpers.Du(t, fd)
if want := 3; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
// Neither apparent size nor content should have changed
test_helpers.VerifySize(t, fn, 9000)
if md5 := test_helpers.Md5fn(fn); md5 != "5420afa22f6423a9f59e669540656bb4" {
t.Errorf("Wrong md5 %s", md5)
}
// Grow the file to 4 blocks
// gocryptfs | h | h | d |d| (2 blocks)
// ext4 | d | d | d | d | (3 blocks)
syscall.Fallocate(fd, FALLOC_DEFAULT, 15000, 10)
_, nBlocks = test_helpers.Du(t, fd)
if want := 4; nBlocks/8 != int64(want) {
t.Errorf("Expected %d 4k block(s), got %d", want, nBlocks/8)
}
test_helpers.VerifySize(t, fn, 15010)
if md5 := test_helpers.Md5fn(fn); md5 != "c4c44c7a41ab7798a79d093eb44f99fc" {
t.Errorf("Wrong md5 %s", md5)
}
// Shrinking a file using fallocate should have no effect
for _, off := range []int64{0, 10, 2000, 5000} {
for _, sz := range []int64{0, 1, 42, 6000} {
syscall.Fallocate(fd, FALLOC_DEFAULT, off, sz)
test_helpers.VerifySize(t, fn, 15010)
if md5 := test_helpers.Md5fn(fn); md5 != "c4c44c7a41ab7798a79d093eb44f99fc" {
t.Errorf("Wrong md5 %s", md5)
}
}
}
// Cleanup
syscall.Unlink(fn)
}
func TestAppend(t *testing.T) {
fn := test_helpers.DefaultPlainDir + "/append"
file, err := os.Create(fn)

View File

@ -283,3 +283,14 @@ func VerifyExistence(path string) bool {
}
return false
}
// Du returns the disk usage of the file "fd" points to, in bytes.
// Same as "du --block-size=1".
func Du(t *testing.T, fd int) (nBytes int64, nBlocks int64) {
var st syscall.Stat_t
err := syscall.Fstat(fd, &st)
if err != nil {
t.Fatal(err)
}
return st.Blocks * st.Blksize, st.Blocks
}