libgocryptfs/fsck.go
2021-05-18 18:38:23 +02:00

342 lines
8.7 KiB
Go

package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"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
skippedList []string
// Protects corruptList
listLock sync.Mutex
// stop a running watchMitigatedCorruptions thread
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 {
return syscall.Geteuid() == 0
}
func (ck *fsckObj) markCorrupt(path string) {
ck.listLock.Lock()
ck.corruptList = append(ck.corruptList, path)
ck.listLock.Unlock()
}
func (ck *fsckObj) markSkipped(path string) {
ck.listLock.Lock()
ck.skippedList = append(ck.skippedList, path)
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 {
select {
case item := <-ck.rootNode.MitigatedCorruptions:
fmt.Printf("fsck: corrupt entry in dir %q: %q\n", path, item)
ck.markCorrupt(filepath.Join(path, item))
case <-ck.watchDone:
return
}
}
}
// Recursively check dir for corruption
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(relPath)
f, err := os.Open(ck.abs(relPath))
ck.watchDone <- struct{}{}
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(relPath)
}
return
}
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 == "." || entry == ".." {
continue
}
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
}
filetype := st.Mode & syscall.S_IFMT
//fmt.Printf(" %q %x\n", entry.Name, entry.Mode)
switch filetype {
case syscall.S_IFDIR:
ck.dir(nextPath)
case syscall.S_IFREG:
ck.file(nextPath)
case syscall.S_IFLNK:
ck.symlink(nextPath)
case syscall.S_IFIFO, syscall.S_IFSOCK, syscall.S_IFBLK, syscall.S_IFCHR:
// nothing to check
default:
fmt.Printf("fsck: unhandled file type %x\n", filetype)
}
}
}
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)
}
}
// Watch for mitigated corruptions that occur during Read()
func (ck *fsckObj) watchMitigatedCorruptionsRead(path string) {
for {
select {
case item := <-ck.rootNode.MitigatedCorruptions:
fmt.Printf("fsck: corrupt file %q (inode %s)\n", path, item)
ck.markCorrupt(path)
case <-ck.watchDone:
return
}
}
}
// Check file for corruption
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 st.Nlink > 1 {
// Due to hard links, we may have already checked this file.
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[st.Ino] = struct{}{}
}
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(relPath)
}
return
}
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(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)
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
}
// EOF
if err == io.EOF {
return
}
off += int64(n)
// If we seem to be in the middle of a file hole, try to skip to the next
// data section.
data := buf[:n]
if bytes.Equal(data, allZero) {
tlog.Debug.Printf("ck.file: trying to skip file hole\n")
const SEEK_DATA = 3
nextOff, err := syscall.Seek(int(f.Fd()), off, SEEK_DATA)
if err == nil {
off = nextOff
}
}
}
}
// Watch for mitigated corruptions that occur during ListXAttr()
func (ck *fsckObj) watchMitigatedCorruptionsListXAttr(path string) {
for {
select {
case item := <-ck.rootNode.MitigatedCorruptions:
fmt.Printf("fsck: corrupt xattr name on file %q: %q\n", path, item)
ck.markCorrupt(path + " xattr:" + item)
case <-ck.watchDone:
return
}
}
}
// Check xattrs on file/dir at path
func (ck *fsckObj) xattrs(relPath string) {
// Run ListXAttr() and catch transparently mitigated corruptions
go ck.watchMitigatedCorruptionsListXAttr(relPath)
attrs, err := syscallcompat.Llistxattr(ck.abs(relPath))
ck.watchDone <- struct{}{}
if err != nil {
fmt.Printf("fsck: error listing xattrs on %q: %v\n", relPath, err)
ck.markCorrupt(relPath)
return
}
// Try to read all xattr values
for _, a := range attrs {
_, 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(relPath)
}
}
}
}
// entrypoint from main()
func fsck(args *argContainer) (exitcode int) {
if args.reverse {
tlog.Fatal.Printf("Running -fsck with -reverse is not supported")
os.Exit(exitcodes.Usage)
}
args.allow_other = false
var err error
args.mountpoint, err = ioutil.TempDir("", "gocryptfs.fsck.")
if err != nil {
tlog.Fatal.Printf("fsck: TmpDir: %v", err)
os.Exit(exitcodes.MountPoint)
}
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{}),
}
if args.quiet {
// go-fuse throws a lot of these:
// writer: Write/Writev failed, err: 2=no such file or directory. opcode: INTERRUPT
// This is ugly and causes failures in xfstests. Hide them away in syslog.
tlog.SwitchLoggerToSyslog()
}
// 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 exitcodes.Other
}
if len(ck.corruptList) == 0 && len(ck.skippedList) == 0 {
tlog.Info.Printf("fsck summary: no problems found\n")
return 0
}
if len(ck.skippedList) > 0 {
tlog.Warn.Printf("fsck: re-run this program as root to check all files!\n")
}
fmt.Printf("fsck summary: %d corrupt files, %d files skipped\n", len(ck.corruptList), len(ck.skippedList))
return exitcodes.FsckErrors
}
type sortableDirEntries []fuse.DirEntry
func (s sortableDirEntries) Len() int {
return len(s)
}
func (s sortableDirEntries) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s sortableDirEntries) Less(i, j int) bool {
return strings.Compare(s[i].Name, s[j].Name) < 0
}
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
}