diff options
| author | Jakob Unterwurzacher | 2020-07-19 23:03:47 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-07-19 23:03:47 +0200 | 
| commit | 8915785acf7e6e7908b32103406620f243093931 (patch) | |
| tree | 94b7e6f1636f4d3355f8c13723125fc40a45f562 | |
| parent | 49fc3abcb4867ea4ce2426340dbe583ef6de055a (diff) | |
v2api: fsck: use a temporary mount
Directly accessing the Nodes does not work properly,
as there is no way to attach a newly LOOKUPped Node
to the tree. This means Path() does not work.
Use an actual mount instead and walk the tree.
| -rw-r--r-- | fsck.go | 219 | ||||
| -rw-r--r-- | mount.go | 5 | ||||
| -rw-r--r-- | tests/fsck/fsck_test.go | 9 | 
3 files changed, 135 insertions, 98 deletions
| @@ -3,23 +3,27 @@ package main  import (  	"bytes"  	"fmt" -	"log" +	"io" +	"io/ioutil"  	"os" +	"os/signal"  	"path/filepath"  	"strings"  	"sync"  	"syscall" -	"github.com/hanwen/go-fuse/v2/fs"  	"github.com/hanwen/go-fuse/v2/fuse"  	"github.com/rfjakob/gocryptfs/internal/exitcodes"  	"github.com/rfjakob/gocryptfs/internal/fusefrontend" +	"github.com/rfjakob/gocryptfs/internal/syscallcompat"  	"github.com/rfjakob/gocryptfs/internal/tlog"  )  type fsckObj struct {  	rootNode *fusefrontend.RootNode +	// mnt is the mountpoint of the temporary mount +	mnt string  	// List of corrupt files  	corruptList []string  	// List of skipped files @@ -30,6 +34,8 @@ type fsckObj struct {  	watchDone chan struct{}  	// Inode numbers of hard-linked files (Nlink > 1) that we have already checked  	seenInodes map[uint64]struct{} +	// abort the running fsck operation? Checked in a few long-running loops. +	abort bool  }  func runsAsRoot() bool { @@ -48,6 +54,10 @@ func (ck *fsckObj) markSkipped(path string) {  	ck.listLock.Unlock()  } +func (ck *fsckObj) abs(relPath string) (absPath string) { +	return filepath.Join(ck.mnt, relPath) +} +  // Watch for mitigated corruptions that occur during OpenDir()  func (ck *fsckObj) watchMitigatedCorruptionsOpenDir(path string) {  	for { @@ -62,40 +72,45 @@ func (ck *fsckObj) watchMitigatedCorruptionsOpenDir(path string) {  }  // Recursively check dir for corruption -func (ck *fsckObj) dir(n *fusefrontend.Node) { -	path := n.Path() -	tlog.Debug.Printf("ck.dir %q\n") -	ck.xattrs(n) +func (ck *fsckObj) dir(relPath string) { +	tlog.Debug.Printf("ck.dir %q\n", relPath) +	ck.xattrs(relPath)  	// Run OpenDir and catch transparently mitigated corruptions -	go ck.watchMitigatedCorruptionsOpenDir(path) -	entries, errno := n.Readdir(nil) +	go ck.watchMitigatedCorruptionsOpenDir(relPath) +	f, err := os.Open(ck.abs(relPath))  	ck.watchDone <- struct{}{} -	// Also catch non-mitigated corruptions -	if errno != 0 { -		fmt.Printf("fsck: error opening dir %q: %v\n", n, errno) -		if errno == syscall.EACCES && !runsAsRoot() { -			ck.markSkipped(path) +	if err != nil { +		fmt.Printf("fsck: error opening dir %q: %v\n", relPath, err) +		if err == os.ErrPermission && !runsAsRoot() { +			ck.markSkipped(relPath)  		} else { -			ck.markCorrupt(path) +			ck.markCorrupt(relPath)  		}  		return  	} -	for entries.HasNext() { -		entry, errno := entries.Next() -		if errno != 0 { -			fmt.Printf("fsck: dirstream error: %v\n", errno) -			break +	go ck.watchMitigatedCorruptionsOpenDir(relPath) +	entries, err := f.Readdirnames(0) +	ck.watchDone <- struct{}{} +	if err != nil { +		fmt.Printf("fsck: error reading dir %q: %v\n", relPath, err) +		ck.markCorrupt(relPath) +		return +	} +	for _, entry := range entries { +		if ck.abort { +			return  		} -		if entry.Name == "." || entry.Name == ".." { +		if entry == "." || entry == ".." {  			continue  		} -		tmp, errno := n.Lookup(nil, entry.Name, &fuse.EntryOut{}) -		if errno != 0 { -			ck.markCorrupt(filepath.Join(path, entry.Name)) +		nextPath := filepath.Join(relPath, entry) +		var st syscall.Stat_t +		err := syscall.Lstat(ck.abs(nextPath), &st) +		if err != nil { +			ck.markCorrupt(filepath.Join(relPath, entry))  			continue  		} -		nextPath := tmp.Operations().(*fusefrontend.Node) -		filetype := entry.Mode & syscall.S_IFMT +		filetype := st.Mode & syscall.S_IFMT  		//fmt.Printf("  %q %x\n", entry.Name, entry.Mode)  		switch filetype {  		case syscall.S_IFDIR: @@ -112,12 +127,11 @@ func (ck *fsckObj) dir(n *fusefrontend.Node) {  	}  } -func (ck *fsckObj) symlink(n *fusefrontend.Node) { -	_, errno := n.Readlink(nil) -	if errno != 0 { -		path := n.Path() -		ck.markCorrupt(path) -		fmt.Printf("fsck: error reading symlink %q: %v\n", path, errno) +func (ck *fsckObj) symlink(relPath string) { +	_, err := os.Readlink(ck.abs(relPath)) +	if err != nil { +		ck.markCorrupt(relPath) +		fmt.Printf("fsck: error reading symlink %q: %v\n", relPath, err)  	}  } @@ -135,55 +149,55 @@ func (ck *fsckObj) watchMitigatedCorruptionsRead(path string) {  }  // Check file for corruption -func (ck *fsckObj) file(n *fusefrontend.Node) { -	path := n.Path() -	tlog.Debug.Printf("ck.file %q\n", path) -	var attr fuse.AttrOut -	errno := n.Getattr(nil, nil, &attr) -	if errno != 0 { -		ck.markCorrupt(path) -		fmt.Printf("fsck: error stating file %q: %v\n", path, errno) +func (ck *fsckObj) file(relPath string) { +	tlog.Debug.Printf("ck.file %q\n", relPath) +	var st syscall.Stat_t +	err := syscall.Lstat(ck.abs(relPath), &st) +	if err != nil { +		ck.markCorrupt(relPath) +		fmt.Printf("fsck: error stating file %q: %v\n", relPath, err)  		return  	} -	if attr.Nlink > 1 { +	if st.Nlink > 1 {  		// Due to hard links, we may have already checked this file. -		if _, ok := ck.seenInodes[attr.Ino]; ok { -			tlog.Debug.Printf("ck.file : skipping %q (inode number %d already seen)\n", path, attr.Ino) +		if _, ok := ck.seenInodes[st.Ino]; ok { +			tlog.Debug.Printf("ck.file : skipping %q (inode number %d already seen)\n", relPath, st.Ino)  			return  		} -		ck.seenInodes[attr.Ino] = struct{}{} +		ck.seenInodes[st.Ino] = struct{}{}  	} -	ck.xattrs(n) -	tmp, _, errno := n.Open(nil, syscall.O_RDONLY) -	if errno != 0 { -		fmt.Printf("fsck: error opening file %q: %v\n", path, errno) -		if errno == syscall.EACCES && !runsAsRoot() { -			ck.markSkipped(path) +	ck.xattrs(relPath) +	f, err := os.Open(ck.abs(relPath)) +	if err != nil { +		fmt.Printf("fsck: error opening file %q: %v\n", relPath, err) +		if err == os.ErrPermission && !runsAsRoot() { +			ck.markSkipped(relPath)  		} else { -			ck.markCorrupt(path) +			ck.markCorrupt(relPath)  		}  		return  	} -	f := tmp.(*fusefrontend.File2) -	defer f.Release(nil) +	defer f.Close()  	// 128 kiB of zeros  	allZero := make([]byte, fuse.MAX_KERNEL_WRITE)  	buf := make([]byte, fuse.MAX_KERNEL_WRITE)  	var off int64  	// Read() through the whole file and catch transparently mitigated corruptions -	go ck.watchMitigatedCorruptionsRead(path) +	go ck.watchMitigatedCorruptionsRead(relPath)  	defer func() { ck.watchDone <- struct{}{} }()  	for { +		if ck.abort { +			return +		}  		tlog.Debug.Printf("ck.file: read %d bytes from offset %d\n", len(buf), off) -		result, errno := f.Read(nil, buf, off) -		if errno != 0 { -			ck.markCorrupt(path) -			fmt.Printf("fsck: error reading file %q (inum %d): %v\n", path, inum(f), errno) +		n, err := f.ReadAt(buf, off) +		if err != nil && err != io.EOF { +			ck.markCorrupt(relPath) +			fmt.Printf("fsck: error reading file %q (inum %d): %v\n", relPath, inum(f), err)  			return  		} -		n := result.Size()  		// EOF -		if n == 0 { +		if err == io.EOF {  			return  		}  		off += int64(n) @@ -192,7 +206,8 @@ func (ck *fsckObj) file(n *fusefrontend.Node) {  		data := buf[:n]  		if bytes.Equal(data, allZero) {  			tlog.Debug.Printf("ck.file: trying to skip file hole\n") -			nextOff, err := f.SeekData(off) +			const SEEK_DATA = 3 +			nextOff, err := syscall.Seek(int(f.Fd()), off, SEEK_DATA)  			if err == nil {  				off = nextOff  			} @@ -214,35 +229,25 @@ func (ck *fsckObj) watchMitigatedCorruptionsListXAttr(path string) {  }  // Check xattrs on file/dir at path -func (ck *fsckObj) xattrs(n *fusefrontend.Node) { +func (ck *fsckObj) xattrs(relPath string) {  	// Run ListXAttr() and catch transparently mitigated corruptions -	path := n.Path() -	go ck.watchMitigatedCorruptionsListXAttr(path) -	listBuf := make([]byte, 1024*1024) -	cnt, errno := n.Listxattr(nil, listBuf) +	go ck.watchMitigatedCorruptionsListXAttr(relPath) +	attrs, err := syscallcompat.Llistxattr(ck.abs(relPath))  	ck.watchDone <- struct{}{} -	// Also catch non-mitigated corruptions -	if errno != 0 { -		fmt.Printf("fsck: error listing xattrs on %q: %v\n", path, errno) -		ck.markCorrupt(path) -		return -	} -	if cnt == 0 { +	if err != nil { +		fmt.Printf("fsck: error listing xattrs on %q: %v\n", relPath, err) +		ck.markCorrupt(relPath)  		return  	} -	// Drop final trailing NULL byte -	cnt-- -	listBuf = listBuf[:cnt] -	attrs := bytes.Split(listBuf, []byte{0}) +	// Try to read all xattr values  	for _, a := range attrs { -		getBuf := make([]byte, 1024*1024) -		_, errno := n.Getxattr(nil, string(a), getBuf) -		if errno != 0 { -			fmt.Printf("fsck: error reading xattr %q from %q: %v\n", a, path, errno) -			if errno == syscall.EACCES && !runsAsRoot() { -				ck.markSkipped(path) +		_, err := syscallcompat.Lgetxattr(ck.abs(relPath), a) +		if err != nil { +			fmt.Printf("fsck: error reading xattr %q from %q: %v\n", a, relPath, err) +			if err == syscall.EACCES && !runsAsRoot() { +				ck.markSkipped(relPath)  			} else { -				ck.markCorrupt(path) +				ck.markCorrupt(relPath)  			}  		}  	} @@ -254,21 +259,45 @@ func fsck(args *argContainer) {  		os.Exit(exitcodes.Usage)  	}  	args.allow_other = false -	pfs, wipeKeys := initFuseFrontend(args) -	opts := fs.Options{ -		// Enable go-fuse warnings -		Logger: log.New(os.Stderr, "go-fuse: ", 0), +	var err error +	args.mountpoint, err = ioutil.TempDir("", "gocryptfs.fsck.") +	if err != nil { +		tlog.Fatal.Printf("fsck: TmpDir: %v", err) +		os.Exit(exitcodes.MountPoint)  	} -	fs.NewNodeFS(pfs, &opts) +	pfs, wipeKeys := initFuseFrontend(args)  	rn := pfs.(*fusefrontend.RootNode)  	rn.MitigatedCorruptions = make(chan string)  	ck := fsckObj{ +		mnt:        args.mountpoint,  		rootNode:   rn,  		watchDone:  make(chan struct{}),  		seenInodes: make(map[uint64]struct{}),  	} -	ck.dir(&rn.Node) +	// Mount +	srv := initGoFuse(pfs, args) +	// Handle SIGINT & SIGTERM +	ch := make(chan os.Signal, 1) +	signal.Notify(ch, os.Interrupt) +	signal.Notify(ch, syscall.SIGTERM) +	go func() { +		<-ch +		ck.abort = true +	}() +	defer func() { +		err = srv.Unmount() +		if err != nil { +			tlog.Warn.Printf("failed to unmount %q: %v", ck.mnt, err) +		} +	}() +	// Recursively check the root dir +	ck.dir("") +	// Report results  	wipeKeys() +	if ck.abort { +		tlog.Info.Printf("fsck: aborted") +		return +	}  	if len(ck.corruptList) == 0 && len(ck.skippedList) == 0 {  		tlog.Info.Printf("fsck summary: no problems found\n")  		return @@ -294,8 +323,12 @@ func (s sortableDirEntries) Less(i, j int) bool {  	return strings.Compare(s[i].Name, s[j].Name) < 0  } -func inum(f *fusefrontend.File2) uint64 { -	var a fuse.AttrOut -	f.Getattr(nil, &a) -	return a.Ino +func inum(f *os.File) uint64 { +	var st syscall.Stat_t +	err := syscall.Fstat(int(f.Fd()), &st) +	if err != nil { +		tlog.Warn.Printf("inum: fstat failed: %v", err) +		return 0 +	} +	return st.Ino  } @@ -221,7 +221,7 @@ func setOpenFileLimit() {  	}  } -// initFuseFrontend - initialize gocryptfs/fusefrontend +// initFuseFrontend - initialize gocryptfs/internal/fusefrontend  // Calls os.Exit on errors  func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys func()) {  	var err error @@ -326,6 +326,9 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f  	return rootNode, func() { cCore.Wipe() }  } +// initGoFuse calls into go-fuse to mount `rootNode` on `args.mountpoint`. +// The mountpoint is ready to use when the functions returns. +// On error, it calls os.Exit and does not return.  func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server {  	var fuseOpts *fs.Options  	sec := time.Second diff --git a/tests/fsck/fsck_test.go b/tests/fsck/fsck_test.go index f56e21c..3aaba9e 100644 --- a/tests/fsck/fsck_test.go +++ b/tests/fsck/fsck_test.go @@ -6,6 +6,7 @@ import (  	"os/exec"  	"runtime"  	"strings" +	"syscall"  	"testing"  	"time" @@ -98,8 +99,8 @@ func TestTerabyteFile(t *testing.T) {  	pDir := cDir + ".mnt"  	test_helpers.MountOrFatal(t, cDir, pDir, "-extpass", "echo test")  	defer test_helpers.UnmountErr(pDir) -	exabyteFile := pDir + "/exabyteFile" -	fd, err := os.Create(exabyteFile) +	veryBigFile := pDir + "/veryBigFile" +	fd, err := os.Create(veryBigFile)  	if err != nil {  		t.Fatal(err)  	} @@ -119,8 +120,8 @@ func TestTerabyteFile(t *testing.T) {  	cmd.Stdout = os.Stdout  	cmd.Start()  	timer := time.AfterFunc(10*time.Second, func() { -		cmd.Process.Kill() -		t.Fatalf("timeout") +		t.Error("timeout, sending SIGINT") +		syscall.Kill(cmd.Process.Pid, syscall.SIGINT)  	})  	cmd.Wait()  	timer.Stop() | 
