diff --git a/fsck.go b/fsck.go index 671d5d0..6d9ca4a 100644 --- a/fsck.go +++ b/fsck.go @@ -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 { - 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) + if ck.abort { + return + } + tlog.Debug.Printf("ck.file: read %d bytes from offset %d\n", len(buf), off) + 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) + if err != nil { + fmt.Printf("fsck: error listing xattrs on %q: %v\n", relPath, err) + ck.markCorrupt(relPath) return } - if cnt == 0 { - 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 } diff --git a/mount.go b/mount.go index 693d0c0..e8fa720 100644 --- a/mount.go +++ b/mount.go @@ -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()