summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/fusefrontend/file.go2
-rw-r--r--internal/fusefrontend/file_allocate_truncate.go159
-rw-r--r--tests/matrix/matrix_test.go136
-rw-r--r--tests/test_helpers/helpers.go11
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
+}