diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index e20b786..1c7e7b9 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -19,6 +19,9 @@ SYNOPSIS #### Change password `gocryptfs -passwd [OPTIONS] CIPHERDIR` +#### Check consistency +`gocryptfs -fsck [OPTIONS] CIPHERDIR` + DESCRIPTION =========== @@ -96,6 +99,10 @@ that uses built-in Go crypto. Setting this option forces the filesystem to read-only and noexec. +#### -fsck +Check CIPHERDIR for consistency. If corruption is found, the +exit code is 26. + #### -fsname string Override the filesystem name (first column in df -T). Can also be passed as "-o fsname=" and is equivalent to libfuse's option of the @@ -351,6 +358,7 @@ EXIT CODES 22: password is empty (on "-init") 23: could not read gocryptfs.conf 24: could not write gocryptfs.conf (on "-init" or "-password") +26: fsck found errors other: please check the error message SEE ALSO diff --git a/README.md b/README.md index 5b53061..ec8a8b6 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,8 @@ vNEXT, in progress ([#218](https://github.com/rfjakob/gocryptfs/issues/218)) * Support extended attributes (xattr) in forward mode ([#217](https://github.com/rfjakob/gocryptfs/issues/217)) +* Add `-fsck` function + ([#191](https://github.com/rfjakob/gocryptfs/issues/191)) v1.4.4, 2018-03-18 * Overwrite secrets in memory with zeros as soon as possible diff --git a/fsck.go b/fsck.go index 9eff72e..d1dc7e5 100644 --- a/fsck.go +++ b/fsck.go @@ -6,6 +6,7 @@ import ( "path/filepath" "sort" "strings" + "sync" "syscall" "github.com/hanwen/go-fuse/fuse" @@ -19,17 +20,34 @@ type fsckObj struct { fs *fusefrontend.FS // List of corrupt files corruptList []string + // Protects corruptList + corruptListLock sync.Mutex } func (ck *fsckObj) markCorrupt(path string) { + ck.corruptListLock.Lock() ck.corruptList = append(ck.corruptList, path) + ck.corruptListLock.Unlock() } // Recursively check dir for corruption func (ck *fsckObj) dir(path string) { //fmt.Printf("ck.dir %q\n", path) ck.xattrs(path) + done := make(chan struct{}) + go func() { + for { + select { + case item := <-ck.fs.CorruptItems: + fmt.Printf("fsck: corrupt entry in dir %q: %q\n", path, item) + ck.markCorrupt(filepath.Join(path, item)) + case <-done: + return + } + } + }() entries, status := ck.fs.OpenDir(path, nil) + done <- struct{}{} if !status.Ok() { ck.markCorrupt(path) fmt.Printf("fsck: error opening dir %q: %v\n", path, status) @@ -80,6 +98,19 @@ func (ck *fsckObj) file(path string) { defer f.Release() buf := make([]byte, fuse.MAX_KERNEL_WRITE) var off int64 + done := make(chan struct{}) + go func() { + for { + select { + case item := <-ck.fs.CorruptItems: + fmt.Printf("fsck: corrupt file %q (inode %s)\n", path, item) + ck.markCorrupt(path) + case <-done: + return + } + } + }() + defer func() { done <- struct{}{} }() for { result, status := f.Read(buf, off) if !status.Ok() { @@ -97,7 +128,20 @@ func (ck *fsckObj) file(path string) { // Check xattrs on file/dir at path func (ck *fsckObj) xattrs(path string) { + done := make(chan struct{}) + go func() { + for { + select { + case item := <-ck.fs.CorruptItems: + fmt.Printf("fsck: corrupt xattr name on file %q: %q\n", path, item) + ck.markCorrupt(path + " xattr:" + item) + case <-done: + return + } + } + }() attrs, status := ck.fs.ListXAttr(path, nil) + done <- struct{}{} if !status.Ok() { fmt.Printf("fsck: error listing xattrs on %q: %v\n", path, status) ck.markCorrupt(path) @@ -120,19 +164,17 @@ func fsck(args *argContainer) { args.allow_other = false pfs, wipeKeys := initFuseFrontend(args) fs := pfs.(*fusefrontend.FS) + fs.CorruptItems = make(chan string) ck := fsckObj{ fs: fs, } ck.dir("") wipeKeys() if len(ck.corruptList) == 0 { - fmt.Printf("fsck summary: no problems found") + fmt.Printf("fsck summary: no problems found\n") return } - fmt.Printf("fsck summary: found %d corrupt files:\n", len(ck.corruptList)) - for _, path := range ck.corruptList { - fmt.Printf(" %q\n", path) - } + fmt.Printf("fsck summary: %d corrupt files\n", len(ck.corruptList)) os.Exit(exitcodes.FsckErrors) } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index af13170..72319e9 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -4,6 +4,7 @@ package fusefrontend import ( "bytes" + "fmt" "io" "log" "os" @@ -99,6 +100,7 @@ func (f *file) readFileID() ([]byte, error) { if err == io.EOF && n != 0 { tlog.Warn.Printf("readFileID %d: incomplete file, got %d instead of %d bytes", f.qIno.Ino, n, readLen) + f.fs.reportCorruptItem(fmt.Sprint(f.qIno.Ino)) } return nil, err } diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index 5f84541..8a3935f 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -38,6 +38,11 @@ type FS struct { // This lock is used by openWriteOnlyFile() to block concurrent opens while // it relaxes the permissions on a file. openWriteOnlyLock sync.RWMutex + // CorruptItems is filled with file or xattr names that have been + // skipped (ignored) because they were corrupt. This is used by fsck + // to inform the user. + // Use the reportCorruptItem() function to push an item. + CorruptItems chan string } var _ pathfs.FileSystem = &FS{} // Verify that interface is implemented. @@ -601,3 +606,16 @@ func (fs *FS) Access(path string, mode uint32, context *fuse.Context) (code fuse } return fuse.ToStatus(syscall.Access(cPath, mode)) } + +func (fs *FS) reportCorruptItem(item string) { + if fs.CorruptItems == nil { + return + } + select { + case fs.CorruptItems <- item: + case <-time.After(1 * time.Second): + tlog.Warn.Printf("BUG: reportCorruptItem: timeout") + //debug.PrintStack() + return + } +} diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go index e13afed..089429e 100644 --- a/internal/fusefrontend/fs_dir.go +++ b/internal/fusefrontend/fs_dir.go @@ -326,6 +326,7 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f if err != nil { tlog.Warn.Printf("OpenDir %q: invalid entry %q: Could not read .name: %v", cDirName, cName, err) + fs.reportCorruptItem(cName) errorCount++ continue } @@ -338,6 +339,7 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f if err != nil { tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v", cDirName, cName, err) + fs.reportCorruptItem(cName) if runtime.GOOS == "darwin" && cName == dsStoreName { // MacOS creates lots of these files. Log the warning but don't // increment errorCount - does not warrant returning EIO. diff --git a/internal/fusefrontend/xattr.go b/internal/fusefrontend/xattr.go index 1d628b7..faaebd4 100644 --- a/internal/fusefrontend/xattr.go +++ b/internal/fusefrontend/xattr.go @@ -115,6 +115,7 @@ func (fs *FS) ListXAttr(path string, context *fuse.Context) ([]string, fuse.Stat name, err := fs.decryptXattrName(curName) if err != nil { tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err) + fs.reportCorruptItem(curName) continue } names = append(names, name)