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:
parent
49fc3abcb4
commit
8915785acf
221
fsck.go
221
fsck.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
5
mount.go
5
mount.go
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue