diff --git a/cryptfs/cryptfs_content.go b/cryptfs/cryptfs_content.go index 5350ec7..0494b69 100644 --- a/cryptfs/cryptfs_content.go +++ b/cryptfs/cryptfs_content.go @@ -115,6 +115,15 @@ func (be *CryptFS) PlainSize(size uint64) uint64 { return size } +// CipherSize - calculate ciphertext size from plaintext size +func (be *CryptFS) CipherSize(size uint64) uint64 { + overhead := be.cipherBS - be.plainBS + nBlocks := (size + be.plainBS - 1) / be.plainBS + size += nBlocks * overhead + + return size +} + func (be *CryptFS) minu64(x uint64, y uint64) uint64 { if x < y { return x diff --git a/main_test.go b/main_test.go index 93aa6ff..8d216ac 100644 --- a/main_test.go +++ b/main_test.go @@ -17,12 +17,23 @@ const plainDir = tmpDir + "plain/" const cipherDir = tmpDir + "cipher/" func unmount() error { - fu := exec.Command("fusermount", "-u", plainDir) + fu := exec.Command("fusermount", "-z", "-u", plainDir) fu.Stdout = os.Stdout fu.Stderr = os.Stderr return fu.Run() } +func md5fn(filename string) string { + buf, err := ioutil.ReadFile(filename) + if err != nil { + fmt.Printf("ReadFile: %v\n", err) + return "" + } + rawHash := md5.Sum(buf) + hash := hex.EncodeToString(rawHash[:]) + return hash +} + func TestMain(m *testing.M) { unmount() @@ -66,16 +77,11 @@ func testWriteN(t *testing.T, fn string, n int) string { } file.Close() - buf, err := ioutil.ReadFile(plainDir + fn) - if err != nil { - t.Fail() - } + bin := md5.Sum(d) + hashWant := hex.EncodeToString(bin[:]) - raw := md5.Sum(d) - hashWant := hex.EncodeToString(raw[:]) + hashActual := md5fn(plainDir + fn) - raw = md5.Sum(buf) - hashActual := hex.EncodeToString(raw[:]) if hashActual != hashWant { fmt.Printf("hashWant=%s hashActual=%s\n", hashWant, hashActual) t.Fail() @@ -101,12 +107,7 @@ func TestWrite1Mx100(t *testing.T) { // Read and check 100 times to catch race conditions var i int for i = 0; i < 100; i++ { - buf, err := ioutil.ReadFile(plainDir + "1M") - if err != nil { - t.Fail() - } - rawHash := md5.Sum(buf) - hashActual := hex.EncodeToString(rawHash[:]) + hashActual := md5fn(plainDir + "1M") if hashActual != hashWant { fmt.Printf("Read corruption in loop # %d\n", i) t.FailNow() @@ -116,6 +117,34 @@ func TestWrite1Mx100(t *testing.T) { } } +func TestTruncate(t *testing.T) { + fn := plainDir + "truncate" + file, err := os.Create(fn) + if err != nil { + t.FailNow() + } + // Grow to two blocks + file.Truncate(7000) + if md5fn(fn) != "95d4ec7038e3e4fdbd5f15c34c3f0b34" { + t.Fail() + } + // Shrink - needs RMW + file.Truncate(6999) + if md5fn(fn) != "35fd15873ec6c35380064a41b9b9683b" { + t.Fail() + } + // Shrink to one partial block + file.Truncate(465) + if md5fn(fn) != "a1534d6e98a6b21386456a8f66c55260" { + t.Fail() + } + // Grow to exactly one block + file.Truncate(4096) + if md5fn(fn) != "620f0b67a91f7f74151bc5be745b7110" { + t.Fail() + } +} + func BenchmarkStreamWrite(t *testing.B) { buf := make([]byte, 1024*1024) t.SetBytes(int64(len(buf))) diff --git a/pathfs_frontend/file.go b/pathfs_frontend/file.go index e3be7ff..fe346b6 100644 --- a/pathfs_frontend/file.go +++ b/pathfs_frontend/file.go @@ -51,7 +51,11 @@ func (f *file) String() string { return fmt.Sprintf("cryptFile(%s)", f.fd.Name()) } -// Called by Read() and for RMW in Write() +// doRead - returns "length" plaintext bytes from plaintext offset "offset". +// Reads the corresponding ciphertext from disk, decryptfs it, returns the relevant +// part. +// +// Called by Read(), for RMW in Write() and Truncate() func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) { // Read the backing ciphertext in one go @@ -117,7 +121,7 @@ func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) { cryptfs.Debug.Printf("Write %s: offset=%d length=%d\n", f.fd.Name(), off, len(data)) var written uint32 - var status fuse.Status + status := fuse.OK dataBuf := bytes.NewBuffer(data) blocks := f.cfs.SplitRange(uint64(off), uint64(len(data))) for _, b := range(blocks) { @@ -189,12 +193,95 @@ func (f *file) Fsync(flags int) (code fuse.Status) { return r } -func (f *file) Truncate(size uint64) fuse.Status { - f.lock.Lock() - r := fuse.ToStatus(syscall.Ftruncate(int(f.fd.Fd()), int64(size))) - f.lock.Unlock() +func (f *file) Truncate(newSize uint64) fuse.Status { - return r + // Common case: Truncate to zero + if newSize == 0 { + f.lock.Lock() + err := syscall.Ftruncate(int(f.fd.Fd()), 0) + f.lock.Unlock() + return fuse.ToStatus(err) + } + + // We need the old file size to determine if we are growing or shrinking + // the file + fi, err := f.fd.Stat() + if err != nil { + cryptfs.Warn.Printf("Truncate: fstat failed: %v\n", err) + return fuse.ToStatus(err) + } + oldSize := uint64(fi.Size()) + + // Grow file by appending zeros + if newSize > oldSize { + remaining := newSize - oldSize + offset := oldSize + var zeros []byte + // Append a maximum of 1MB in each iteration + if remaining > 1048576 { + zeros = make([]byte, 1048576) + } else { + zeros = make([]byte, remaining) + } + for remaining >= uint64(len(zeros)) { + written, status := f.Write(zeros, int64(offset)) + if status != fuse.OK { + return status + } + remaining -= uint64(written) + offset += uint64(written) + cryptfs.Debug.Printf("Truncate: written=%d remaining=%d offset=%d\n", + written, remaining, offset) + } + if remaining > 0 { + _, status := f.Write(zeros[0:remaining], int64(offset)) + return status + } + return fuse.OK + } + + // Shrink file by truncating + newBlockLen := int(newSize % f.cfs.PlainBS()) + // New file size is aligned to block size - just truncate + if newBlockLen == 0 { + cSize := int64(f.cfs.CipherSize(newSize)) + f.lock.Lock() + err := syscall.Ftruncate(int(f.fd.Fd()), cSize) + f.lock.Unlock() + return fuse.ToStatus(err) + } + // New file size is not aligned - need to do RMW on the last block + var blockOffset, blockLen uint64 + { + // Get the block the last byte belongs to. + // This is, by definition, the last block. + blockList := f.cfs.SplitRange(newSize - 1, 1) + lastBlock := blockList[0] + blockOffset, blockLen = lastBlock.PlaintextRange() + } + blockData, status := f.doRead(blockOffset, blockLen) + if status != fuse.OK { + cryptfs.Warn.Printf("Truncate: doRead failed: %v\n", err) + return status + } + if len(blockData) < newBlockLen { + cryptfs.Warn.Printf("Truncate: file has shrunk under our feet\n") + return fuse.OK + } + // Truncate the file down to the next block + { + nextBlockSz := int64(f.cfs.CipherSize(newSize - uint64(newBlockLen))) + f.lock.Lock() + err = syscall.Ftruncate(int(f.fd.Fd()), nextBlockSz) + f.lock.Unlock() + if err != nil { + cryptfs.Warn.Printf("Truncate: Intermediate Ftruncate failed: %v\n", err) + return fuse.ToStatus(err) + } + } + // Append truncated last block + _, status = f.Write(blockData[0:newBlockLen], int64(blockOffset)) + return status } func (f *file) Chmod(mode uint32) fuse.Status {