diff options
| author | Jakob Unterwurzacher | 2021-05-29 16:05:36 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2021-05-29 16:05:36 +0200 | 
| commit | 738a9e006af6f5e43871c2d8e208601c18191965 (patch) | |
| tree | 48e52a90a2d59b622453497bff18482badd3e2b2 /internal | |
| parent | c1d7e38761d35149f19524fa19d3afaaca73f302 (diff) | |
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
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/fusefrontend/file_holes.go | 68 | 
1 files changed, 64 insertions, 4 deletions
| 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  } | 
