From 738a9e006af6f5e43871c2d8e208601c18191965 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 29 May 2021 16:05:36 +0200 Subject: fusefrontend: rewrite Lseek SEEK_DATA / SEEK_HOLE In response to the discussion of the xfstests mailing list [1], I looked at the Lseek implementation, which was naive and did not handle all cases correctly. The new implementation aligns the returned values to 4096 bytes as most callers expect. A lot of tests are added to verify that we handle all cases correctly now. [1]: https://www.spinics.net/lists/fstests/msg16554.html --- internal/fusefrontend/file_holes.go | 68 ++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) (limited to 'internal') diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go index 0d28004..cb44803 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -4,6 +4,7 @@ package fusefrontend import ( "context" + "runtime" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -57,12 +58,71 @@ func (f *File) zeroPad(plainSize uint64) syscall.Errno { } // Lseek - FUSE call. +// +// Looking at +// fuse_file_llseek @ https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/fs/fuse/file.c?h=v5.12.7#n2634 +// this function is only called for SEEK_HOLE & SEEK_DATA. func (f *File) Lseek(ctx context.Context, off uint64, whence uint32) (uint64, syscall.Errno) { - cipherOff := f.rootNode.contentEnc.PlainSizeToCipherSize(off) - newCipherOff, err := syscall.Seek(f.intFd(), int64(cipherOff), int(whence)) + const ( + SEEK_DATA = 3 // find next data segment at or above `off` + SEEK_HOLE = 4 // find next hole at or above `off` + + // On error, we return -1 as the offset as per man lseek. + MinusOne = ^uint64(0) + ) + if whence != SEEK_DATA && whence != SEEK_HOLE { + tlog.Warn.Printf("BUG: Lseek was called with whence=%d. This is not supported!", whence) + return 0, syscall.EINVAL + } + if runtime.GOOS != "linux" { + // MacOS has broken (different?) SEEK_DATA / SEEK_HOLE semantics, see + // https://lists.gnu.org/archive/html/bug-gnulib/2018-09/msg00051.html + tlog.Warn.Printf("buggy on non-linux platforms, disabling SEEK_DATA & SEEK_HOLE") + return MinusOne, syscall.ENOSYS + } + + // We will need the file size + var st syscall.Stat_t + err := syscall.Fstat(f.intFd(), &st) if err != nil { return 0, fs.ToErrno(err) } - newOff := f.contentEnc.CipherSizeToPlainSize(uint64(newCipherOff)) - return newOff, 0 + fileSize := st.Size + // Better safe than sorry. The logic is only tested for 4k blocks. + if st.Blksize != 4096 { + tlog.Warn.Printf("unsupported block size of %d bytes, disabling SEEK_DATA & SEEK_HOLE", st.Blksize) + return MinusOne, syscall.ENOSYS + } + + // man lseek: offset beyond end of file -> ENXIO + if f.rootNode.contentEnc.PlainOffToCipherOff(off) >= uint64(fileSize) { + return MinusOne, syscall.ENXIO + } + + // Round down to start of block: + cipherOff := f.rootNode.contentEnc.BlockNoToCipherOff(f.rootNode.contentEnc.PlainOffToBlockNo(off)) + newCipherOff, err := syscall.Seek(f.intFd(), int64(cipherOff), int(whence)) + if err != nil { + return MinusOne, fs.ToErrno(err) + } + // already in data/hole => return original offset + if newCipherOff == int64(cipherOff) { + return off, 0 + } + // If there is no further hole, SEEK_HOLE returns the file size + // (SEEK_DATA returns ENXIO in this case). + if whence == SEEK_HOLE { + fi, err := f.fd.Stat() + if err != nil { + return MinusOne, fs.ToErrno(err) + } + if newCipherOff == fi.Size() { + return f.rootNode.contentEnc.CipherSizeToPlainSize(uint64(newCipherOff)), 0 + } + } + // syscall.Seek gave us the beginning of the next ext4 data/hole section. + // The next gocryptfs data/hole block starts at the next block boundary, + // so we have to round up: + newBlockNo := f.rootNode.contentEnc.CipherOffToBlockNo(uint64(newCipherOff) + f.rootNode.contentEnc.CipherBS() - 1) + return f.rootNode.contentEnc.BlockNoToPlainOff(newBlockNo), 0 } -- cgit v1.2.3