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:
parent
04ad063515
commit
54470baa23
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user