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.
This commit is contained in:
Jakob Unterwurzacher 2020-07-19 23:03:47 +02:00
parent 49fc3abcb4
commit 8915785acf
3 changed files with 136 additions and 99 deletions

221
fsck.go
View File

@ -3,23 +3,27 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"log" "io"
"io/ioutil"
"os" "os"
"os/signal"
"path/filepath" "path/filepath"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse" "github.com/hanwen/go-fuse/v2/fuse"
"github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/exitcodes"
"github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/fusefrontend"
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
"github.com/rfjakob/gocryptfs/internal/tlog" "github.com/rfjakob/gocryptfs/internal/tlog"
) )
type fsckObj struct { type fsckObj struct {
rootNode *fusefrontend.RootNode rootNode *fusefrontend.RootNode
// mnt is the mountpoint of the temporary mount
mnt string
// List of corrupt files // List of corrupt files
corruptList []string corruptList []string
// List of skipped files // List of skipped files
@ -30,6 +34,8 @@ type fsckObj struct {
watchDone chan struct{} watchDone chan struct{}
// Inode numbers of hard-linked files (Nlink > 1) that we have already checked // Inode numbers of hard-linked files (Nlink > 1) that we have already checked
seenInodes map[uint64]struct{} seenInodes map[uint64]struct{}
// abort the running fsck operation? Checked in a few long-running loops.
abort bool
} }
func runsAsRoot() bool { func runsAsRoot() bool {
@ -48,6 +54,10 @@ func (ck *fsckObj) markSkipped(path string) {
ck.listLock.Unlock() 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() // Watch for mitigated corruptions that occur during OpenDir()
func (ck *fsckObj) watchMitigatedCorruptionsOpenDir(path string) { func (ck *fsckObj) watchMitigatedCorruptionsOpenDir(path string) {
for { for {
@ -62,40 +72,45 @@ func (ck *fsckObj) watchMitigatedCorruptionsOpenDir(path string) {
} }
// Recursively check dir for corruption // Recursively check dir for corruption
func (ck *fsckObj) dir(n *fusefrontend.Node) { func (ck *fsckObj) dir(relPath string) {
path := n.Path() tlog.Debug.Printf("ck.dir %q\n", relPath)
tlog.Debug.Printf("ck.dir %q\n") ck.xattrs(relPath)
ck.xattrs(n)
// Run OpenDir and catch transparently mitigated corruptions // Run OpenDir and catch transparently mitigated corruptions
go ck.watchMitigatedCorruptionsOpenDir(path) go ck.watchMitigatedCorruptionsOpenDir(relPath)
entries, errno := n.Readdir(nil) f, err := os.Open(ck.abs(relPath))
ck.watchDone <- struct{}{} ck.watchDone <- struct{}{}
// Also catch non-mitigated corruptions if err != nil {
if errno != 0 { fmt.Printf("fsck: error opening dir %q: %v\n", relPath, err)
fmt.Printf("fsck: error opening dir %q: %v\n", n, errno) if err == os.ErrPermission && !runsAsRoot() {
if errno == syscall.EACCES && !runsAsRoot() { ck.markSkipped(relPath)
ck.markSkipped(path)
} else { } else {
ck.markCorrupt(path) ck.markCorrupt(relPath)
} }
return return
} }
for entries.HasNext() { go ck.watchMitigatedCorruptionsOpenDir(relPath)
entry, errno := entries.Next() entries, err := f.Readdirnames(0)
if errno != 0 { ck.watchDone <- struct{}{}
fmt.Printf("fsck: dirstream error: %v\n", errno) if err != nil {
break 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 continue
} }
tmp, errno := n.Lookup(nil, entry.Name, &fuse.EntryOut{}) nextPath := filepath.Join(relPath, entry)
if errno != 0 { var st syscall.Stat_t
ck.markCorrupt(filepath.Join(path, entry.Name)) err := syscall.Lstat(ck.abs(nextPath), &st)
if err != nil {
ck.markCorrupt(filepath.Join(relPath, entry))
continue continue
} }
nextPath := tmp.Operations().(*fusefrontend.Node) filetype := st.Mode & syscall.S_IFMT
filetype := entry.Mode & syscall.S_IFMT
//fmt.Printf(" %q %x\n", entry.Name, entry.Mode) //fmt.Printf(" %q %x\n", entry.Name, entry.Mode)
switch filetype { switch filetype {
case syscall.S_IFDIR: case syscall.S_IFDIR:
@ -112,12 +127,11 @@ func (ck *fsckObj) dir(n *fusefrontend.Node) {
} }
} }
func (ck *fsckObj) symlink(n *fusefrontend.Node) { func (ck *fsckObj) symlink(relPath string) {
_, errno := n.Readlink(nil) _, err := os.Readlink(ck.abs(relPath))
if errno != 0 { if err != nil {
path := n.Path() ck.markCorrupt(relPath)
ck.markCorrupt(path) fmt.Printf("fsck: error reading symlink %q: %v\n", relPath, err)
fmt.Printf("fsck: error reading symlink %q: %v\n", path, errno)
} }
} }
@ -135,55 +149,55 @@ func (ck *fsckObj) watchMitigatedCorruptionsRead(path string) {
} }
// Check file for corruption // Check file for corruption
func (ck *fsckObj) file(n *fusefrontend.Node) { func (ck *fsckObj) file(relPath string) {
path := n.Path() tlog.Debug.Printf("ck.file %q\n", relPath)
tlog.Debug.Printf("ck.file %q\n", path) var st syscall.Stat_t
var attr fuse.AttrOut err := syscall.Lstat(ck.abs(relPath), &st)
errno := n.Getattr(nil, nil, &attr) if err != nil {
if errno != 0 { ck.markCorrupt(relPath)
ck.markCorrupt(path) fmt.Printf("fsck: error stating file %q: %v\n", relPath, err)
fmt.Printf("fsck: error stating file %q: %v\n", path, errno)
return return
} }
if attr.Nlink > 1 { if st.Nlink > 1 {
// Due to hard links, we may have already checked this file. // Due to hard links, we may have already checked this file.
if _, ok := ck.seenInodes[attr.Ino]; ok { if _, ok := ck.seenInodes[st.Ino]; ok {
tlog.Debug.Printf("ck.file : skipping %q (inode number %d already seen)\n", path, attr.Ino) tlog.Debug.Printf("ck.file : skipping %q (inode number %d already seen)\n", relPath, st.Ino)
return return
} }
ck.seenInodes[attr.Ino] = struct{}{} ck.seenInodes[st.Ino] = struct{}{}
} }
ck.xattrs(n) ck.xattrs(relPath)
tmp, _, errno := n.Open(nil, syscall.O_RDONLY) f, err := os.Open(ck.abs(relPath))
if errno != 0 { if err != nil {
fmt.Printf("fsck: error opening file %q: %v\n", path, errno) fmt.Printf("fsck: error opening file %q: %v\n", relPath, err)
if errno == syscall.EACCES && !runsAsRoot() { if err == os.ErrPermission && !runsAsRoot() {
ck.markSkipped(path) ck.markSkipped(relPath)
} else { } else {
ck.markCorrupt(path) ck.markCorrupt(relPath)
} }
return return
} }
f := tmp.(*fusefrontend.File2) defer f.Close()
defer f.Release(nil)
// 128 kiB of zeros // 128 kiB of zeros
allZero := make([]byte, fuse.MAX_KERNEL_WRITE) allZero := make([]byte, fuse.MAX_KERNEL_WRITE)
buf := make([]byte, fuse.MAX_KERNEL_WRITE) buf := make([]byte, fuse.MAX_KERNEL_WRITE)
var off int64 var off int64
// Read() through the whole file and catch transparently mitigated corruptions // Read() through the whole file and catch transparently mitigated corruptions
go ck.watchMitigatedCorruptionsRead(path) go ck.watchMitigatedCorruptionsRead(relPath)
defer func() { ck.watchDone <- struct{}{} }() defer func() { ck.watchDone <- struct{}{} }()
for { for {
tlog.Debug.Printf("ck.file: read %d bytes from offset %d\n", len(buf), off) if ck.abort {
result, errno := f.Read(nil, buf, off) return
if errno != 0 { }
ck.markCorrupt(path) tlog.Debug.Printf("ck.file: read %d bytes from offset %d\n", len(buf), off)
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 return
} }
n := result.Size()
// EOF // EOF
if n == 0 { if err == io.EOF {
return return
} }
off += int64(n) off += int64(n)
@ -192,7 +206,8 @@ func (ck *fsckObj) file(n *fusefrontend.Node) {
data := buf[:n] data := buf[:n]
if bytes.Equal(data, allZero) { if bytes.Equal(data, allZero) {
tlog.Debug.Printf("ck.file: trying to skip file hole\n") 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 { if err == nil {
off = nextOff off = nextOff
} }
@ -214,35 +229,25 @@ func (ck *fsckObj) watchMitigatedCorruptionsListXAttr(path string) {
} }
// Check xattrs on file/dir at path // 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 // Run ListXAttr() and catch transparently mitigated corruptions
path := n.Path() go ck.watchMitigatedCorruptionsListXAttr(relPath)
go ck.watchMitigatedCorruptionsListXAttr(path) attrs, err := syscallcompat.Llistxattr(ck.abs(relPath))
listBuf := make([]byte, 1024*1024)
cnt, errno := n.Listxattr(nil, listBuf)
ck.watchDone <- struct{}{} ck.watchDone <- struct{}{}
// Also catch non-mitigated corruptions if err != nil {
if errno != 0 { fmt.Printf("fsck: error listing xattrs on %q: %v\n", relPath, err)
fmt.Printf("fsck: error listing xattrs on %q: %v\n", path, errno) ck.markCorrupt(relPath)
ck.markCorrupt(path)
return return
} }
if cnt == 0 { // Try to read all xattr values
return
}
// Drop final trailing NULL byte
cnt--
listBuf = listBuf[:cnt]
attrs := bytes.Split(listBuf, []byte{0})
for _, a := range attrs { for _, a := range attrs {
getBuf := make([]byte, 1024*1024) _, err := syscallcompat.Lgetxattr(ck.abs(relPath), a)
_, errno := n.Getxattr(nil, string(a), getBuf) if err != nil {
if errno != 0 { fmt.Printf("fsck: error reading xattr %q from %q: %v\n", a, relPath, err)
fmt.Printf("fsck: error reading xattr %q from %q: %v\n", a, path, errno) if err == syscall.EACCES && !runsAsRoot() {
if errno == syscall.EACCES && !runsAsRoot() { ck.markSkipped(relPath)
ck.markSkipped(path)
} else { } else {
ck.markCorrupt(path) ck.markCorrupt(relPath)
} }
} }
} }
@ -254,21 +259,45 @@ func fsck(args *argContainer) {
os.Exit(exitcodes.Usage) os.Exit(exitcodes.Usage)
} }
args.allow_other = false args.allow_other = false
pfs, wipeKeys := initFuseFrontend(args) var err error
opts := fs.Options{ args.mountpoint, err = ioutil.TempDir("", "gocryptfs.fsck.")
// Enable go-fuse warnings if err != nil {
Logger: log.New(os.Stderr, "go-fuse: ", 0), tlog.Fatal.Printf("fsck: TmpDir: %v", err)
os.Exit(exitcodes.MountPoint)
} }
fs.NewNodeFS(pfs, &opts) pfs, wipeKeys := initFuseFrontend(args)
rn := pfs.(*fusefrontend.RootNode) rn := pfs.(*fusefrontend.RootNode)
rn.MitigatedCorruptions = make(chan string) rn.MitigatedCorruptions = make(chan string)
ck := fsckObj{ ck := fsckObj{
mnt: args.mountpoint,
rootNode: rn, rootNode: rn,
watchDone: make(chan struct{}), watchDone: make(chan struct{}),
seenInodes: make(map[uint64]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() wipeKeys()
if ck.abort {
tlog.Info.Printf("fsck: aborted")
return
}
if len(ck.corruptList) == 0 && len(ck.skippedList) == 0 { if len(ck.corruptList) == 0 && len(ck.skippedList) == 0 {
tlog.Info.Printf("fsck summary: no problems found\n") tlog.Info.Printf("fsck summary: no problems found\n")
return return
@ -294,8 +323,12 @@ func (s sortableDirEntries) Less(i, j int) bool {
return strings.Compare(s[i].Name, s[j].Name) < 0 return strings.Compare(s[i].Name, s[j].Name) < 0
} }
func inum(f *fusefrontend.File2) uint64 { func inum(f *os.File) uint64 {
var a fuse.AttrOut var st syscall.Stat_t
f.Getattr(nil, &a) err := syscall.Fstat(int(f.Fd()), &st)
return a.Ino if err != nil {
tlog.Warn.Printf("inum: fstat failed: %v", err)
return 0
}
return st.Ino
} }

View File

@ -221,7 +221,7 @@ func setOpenFileLimit() {
} }
} }
// initFuseFrontend - initialize gocryptfs/fusefrontend // initFuseFrontend - initialize gocryptfs/internal/fusefrontend
// Calls os.Exit on errors // Calls os.Exit on errors
func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys func()) { func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys func()) {
var err error var err error
@ -326,6 +326,9 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f
return rootNode, func() { cCore.Wipe() } 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 { func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server {
var fuseOpts *fs.Options var fuseOpts *fs.Options
sec := time.Second sec := time.Second

View File

@ -6,6 +6,7 @@ import (
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"syscall"
"testing" "testing"
"time" "time"
@ -98,8 +99,8 @@ func TestTerabyteFile(t *testing.T) {
pDir := cDir + ".mnt" pDir := cDir + ".mnt"
test_helpers.MountOrFatal(t, cDir, pDir, "-extpass", "echo test") test_helpers.MountOrFatal(t, cDir, pDir, "-extpass", "echo test")
defer test_helpers.UnmountErr(pDir) defer test_helpers.UnmountErr(pDir)
exabyteFile := pDir + "/exabyteFile" veryBigFile := pDir + "/veryBigFile"
fd, err := os.Create(exabyteFile) fd, err := os.Create(veryBigFile)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -119,8 +120,8 @@ func TestTerabyteFile(t *testing.T) {
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Start() cmd.Start()
timer := time.AfterFunc(10*time.Second, func() { timer := time.AfterFunc(10*time.Second, func() {
cmd.Process.Kill() t.Error("timeout, sending SIGINT")
t.Fatalf("timeout") syscall.Kill(cmd.Process.Pid, syscall.SIGINT)
}) })
cmd.Wait() cmd.Wait()
timer.Stop() timer.Stop()