From 54470baa23bf98adde69dc1a074c852ea19127d1 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 2 Jul 2016 19:43:57 +0200 Subject: [PATCH] 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 . --- internal/fusefrontend/file.go | 2 - .../fusefrontend/file_allocate_truncate.go | 159 ++++++++++++++---- tests/matrix/matrix_test.go | 136 +++++++++++++-- tests/test_helpers/helpers.go | 11 ++ 4 files changed, 255 insertions(+), 53 deletions(-) diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 1835b53..ead253f 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -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 diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index 5be7df4..65d6df6 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -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) + } +} diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index d59a677..be5ff60 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -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) diff --git a/tests/test_helpers/helpers.go b/tests/test_helpers/helpers.go index a6c2b7d..02b9fe0 100644 --- a/tests/test_helpers/helpers.go +++ b/tests/test_helpers/helpers.go @@ -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 +}