diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 9d49add..7505f92 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -69,7 +69,13 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch if err != nil { return nil, fs.ToErrno(err) } + + // Create new inode and fill `out` ch = n.newChild(ctx, st, out) + + // Translate ciphertext size in `out.Attr.Size` to plaintext size + n.translateSize(dirfd, cName, &out.Attr) + return ch, 0 } @@ -98,13 +104,9 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) rn.inoMap.TranslateStat(st) out.Attr.FromStat(st) - // Fix size - if out.IsRegular() { - out.Size = rn.contentEnc.CipherSizeToPlainSize(out.Size) - } else if out.IsSymlink() { - target, _ := n.Readlink(ctx) - out.Size = uint64(len(target)) - } + // Translate ciphertext size in `out.Attr.Size` to plaintext size + n.translateSize(dirfd, cName, &out.Attr) + if rn.args.ForceOwner != nil { out.Owner = *rn.args.ForceOwner } @@ -221,21 +223,7 @@ func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) { } defer syscall.Close(dirfd) - cTarget, err := syscallcompat.Readlinkat(dirfd, cName) - if err != nil { - return nil, fs.ToErrno(err) - } - rn := n.rootNode() - if rn.args.PlaintextNames { - return []byte(cTarget), 0 - } - // Symlinks are encrypted like file contents (GCM) and base64-encoded - target, err := rn.decryptSymlinkTarget(cTarget) - if err != nil { - tlog.Warn.Printf("Readlink %q: decrypting target failed: %v", cName, err) - return nil, syscall.EIO - } - return []byte(target), 0 + return n.readlink(dirfd, cName) } // Open - FUSE call. Open already-existing file. diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index a7a32af..a31a41d 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -2,10 +2,14 @@ package fusefrontend import ( "context" + "syscall" "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" ) // toFuseCtx tries to extract a fuse.Context from a generic context.Context. @@ -29,3 +33,33 @@ func toNode(op fs.InodeEmbedder) *Node { } return op.(*Node) } + +// readlink reads and decrypts a symlink. Used by Readlink, Getattr, Lookup. +func (n *Node) readlink(dirfd int, cName string) (out []byte, errno syscall.Errno) { + cTarget, err := syscallcompat.Readlinkat(dirfd, cName) + if err != nil { + return nil, fs.ToErrno(err) + } + rn := n.rootNode() + if rn.args.PlaintextNames { + return []byte(cTarget), 0 + } + // Symlinks are encrypted like file contents (GCM) and base64-encoded + target, err := rn.decryptSymlinkTarget(cTarget) + if err != nil { + tlog.Warn.Printf("Readlink %q: decrypting target failed: %v", cName, err) + return nil, syscall.EIO + } + return []byte(target), 0 +} + +// translateSize translates the ciphertext size in `out` into plaintext size. +func (n *Node) translateSize(dirfd int, cName string, out *fuse.Attr) { + if out.IsRegular() { + rn := n.rootNode() + out.Size = rn.contentEnc.CipherSizeToPlainSize(out.Size) + } else if out.IsSymlink() { + target, _ := n.readlink(dirfd, cName) + out.Size = uint64(len(target)) + } +} diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index f59ea38..1982c90 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -293,3 +293,79 @@ func TestSeekData(t *testing.T) { } f.Close() } + +/* +TestMd5sumMaintainers tries to repro this interesting +bug that was seen during gocryptfs v2.0 development: + +$ md5sum linux-3.0/MAINTAINERS linux-3.0/MAINTAINERS linux-3.0/MAINTAINERS linux-3.0/MAINTAINERS +279b6ab0491e7532132e8f32afe6c04d linux-3.0/MAINTAINERS <-- WRONG!!!! +99cc9f0dfd86e63231b94edd43a43e02 linux-3.0/MAINTAINERS <-- correct +99cc9f0dfd86e63231b94edd43a43e02 linux-3.0/MAINTAINERS +99cc9f0dfd86e63231b94edd43a43e02 linux-3.0/MAINTAINERS + +strace shows: + +Bad +--- +fstat(3, {st_mode=S_IFREG|0644, st_size=196745, ...}) = 0 +read(3, "\n\tList of maintainers and how to"..., 32768) = 32768 +read(3, "M:\tSylwester Nawrocki \nL:\tlinux"..., 32768) = 32768 +read(3, "ach-spear3xx/\n\nSPEAR6XX MACHINE "..., 32768) = 32768 +read(3, "", 32768) = 0 +lseek(3, 0, SEEK_CUR) = 196608 +close(3) = 0 +fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0 +write(1, "279b6ab0491e7532132e8f32afe6c04d"..., 56279b6ab0491e7532132e8f32afe6c04d linux-3.0/MAINTAINERS + +Good +---- +fstat(3, {st_mode=S_IFREG|0644, st_size=195191, ...}) = 0 +read(3, "\n\tList of maintainers and how to"..., 32768) = 32768 +read(3, "M:\tSylwester Nawrocki \nL:\tlinux"..., 32768) = 32768 +read(3, "ach-spear3xx/\n\nSPEAR6XX MACHINE "..., 32768) = 31351 +read(3, "", 4096) = 0 +lseek(3, 0, SEEK_CUR) = 195191 +close(3) = 0 +fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0 +write(1, "99cc9f0dfd86e63231b94edd43a43e02"..., 5699cc9f0dfd86e63231b94edd43a43e02 linux-3.0/MAINTAINERS +*/ +func TestMd5sumMaintainers(t *testing.T) { + fn := filepath.Join(test_helpers.DefaultPlainDir, t.Name()) + f, err := os.Create(fn) + if err != nil { + t.Fatal(err) + } + // Size of the MAINTAINERS file = 195191 + const sizeWant = 195191 + content := make([]byte, sizeWant) + _, err = f.Write(content) + if err != nil { + t.Fatal(err) + } + f.Close() + + // Remount to clear the linux kernel attr cache + // (otherwise we would have to wait 2 seconds for the entry to expire) + test_helpers.UnmountPanic(test_helpers.DefaultPlainDir) + test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, "-zerokey") + + cmd := exec.Command("md5sum", fn, fn, fn, fn) + out2, err := cmd.CombinedOutput() + out := string(out2) + + // 195191 zero bytes have this md5sum + const md5Want = "b99bf6917f688068acd49126f3b1b005" + + n := strings.Count(out, md5Want) + if n != 4 { + t.Errorf("found %d instead of %d instances of %q", n, 4, md5Want) + t.Logf("full output:\n%s", out) + } +}