diff options
29 files changed, 441 insertions, 158 deletions
diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index c7a1c03..64bbaa8 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -208,6 +208,16 @@ Show all invalid filenames: -badname '*' +#### -context string +Set the SELinux context. See mount(8) for details. + +This option was added for compatibility with xfstests which sets +this option via `-o context="system_u:object_r:root_t:s0"`. + +Only works when mounting as root, otherwise you get this error from fusermount3: + + fusermount3: unknown option 'context="system_u:object_r:root_t:s0"' + #### -ctlsock string Create a control socket at the specified location. The socket can be used to decrypt and encrypt paths inside the filesystem. When using diff --git a/Documentation/file-format.md b/Documentation/file-format.md index 7cce72c..7c2e2c8 100644 --- a/Documentation/file-format.md +++ b/Documentation/file-format.md @@ -1,43 +1,67 @@ File Format =========== +Empty files are stored as empty files. + +Non-empty files contain a *Header* and one or more *Data blocks*. + Header +------ 2 bytes header version (big endian uint16, currently 2) 16 bytes file id Data block, default AES-GCM mode +-------------------------------- 16 bytes GCM IV (nonce) 1-4096 bytes encrypted data 16 bytes GHASH -Data block, AES-SIV mode (used in reverse mode, or when explicitly enabled with `-init -aessiv`) +Overhead = (16+16)/4096 = 1/128 = 0.78125 % + +Data block, AES-SIV mode +------------------------ + +AES-SIV is used in reverse mode, or when explicitly enabled with `-init -aessiv`. 16 bytes nonce 16 bytes SIV 1-4096 bytes encrypted data -Data block, XChaCha20-Poly1305 (enabled via `-init -xchacha`) +Overhead = (16+16)/4096 = 1/128 = 0.78125 % + +Data block, XChaCha20-Poly1305 +------------------------------ + +Enabled via `-init -xchacha` 24 bytes nonce 1-4096 bytes encrypted data 16 bytes Poly1305 tag -Full block overhead (AES-GCM and AES-SIV mode) = 32/4096 = 1/128 = 0.78125 % +Overhead = (24+16)/4096 = 0.98 % -Full block overhead (XChaCha20-Poly1305 mode) = 40/4096 = \~1 % +Examples +======== -Example: 1-byte file, AES-GCM and AES-SIV mode ----------------------------------------------- +0-byte file (all modes) +----------------------- + + (empty) + +Total: 0 bytes + +1-byte file, AES-GCM and AES-SIV mode +------------------------------------- Header 18 bytes Data block 33 bytes Total: 51 bytes -Example: 5000-byte file, , AES-GCM and AES-SIV mode ---------------------------------------------------- +5000-byte file, , AES-GCM and AES-SIV mode +------------------------------------------ Header 18 bytes Data block 4128 bytes @@ -45,19 +69,24 @@ Example: 5000-byte file, , AES-GCM and AES-SIV mode Total: 5082 bytes -Example: 1-byte file, XChaCha20-Poly1305 mode ----------------------------------------------- +1-byte file, XChaCha20-Poly1305 mode +------------------------------------ Header 18 bytes Data block 41 bytes Total: 59 bytes -Example: 5000-byte file, XChaCha20-Poly1305 mode ----------------------------------------------- +5000-byte file, XChaCha20-Poly1305 mode +--------------------------------------- Header 18 bytes Data block 4136 bytes Data block 944 bytes Total: 5098 bytes + +See Also +======== + +https://nuetzlich.net/gocryptfs/forward_mode_crypto/ / https://github.com/rfjakob/gocryptfs-website/blob/master/docs/forward_mode_crypto.md @@ -50,8 +50,14 @@ of macOS support but please create a new ticket if you hit a problem. For Windows, an independent C++ reimplementation can be found here: [cppcryptfs](https://github.com/bailey27/cppcryptfs) -A standalone Python tool that can decrypt files & file names is here: +Standalone tools: + [gocryptfs-inspect](https://github.com/slackner/gocryptfs-inspect) +is Python tool that can decrypt files & file names without +using FUSE. + +[gocryptfs-create-folder](https://codeberg.org/LGLQ/gocryptfs-create-folder) +is a Python tool can encrypt a directory without using FUSE. Installation ------------ @@ -195,6 +201,16 @@ RM: 2,367 Changelog --------- +#### v2.5.4, 2025-04-13 +* Drop `GOAMD64=v2` from `build.bash`, there's user(s) still running `GOAMD64=v1` CPUs + ([#908](https://github.com/rfjakob/gocryptfs/issues/908), + [commit](https://github.com/rfjakob/gocryptfs/commit/4851c322d5ce06c559eed9e9f3cb0a5c2c72fd5e)) + +#### v2.5.3, 2025-04-05 +* Fix `go install` failing with `g: malformed file path "tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z\ng": invalid char '\n'` + ([c80558](https://github.com/rfjakob/gocryptfs/commit/c8055829c311ecaf532fd171f3a5d104f873272d)) +* Fix panic when go-fuse is newer than specified in go.mod ([#897](https://github.com/rfjakob/gocryptfs/issues/897)) + #### v2.5.2, 2025-03-19 * Use our own `syscallcompat.Setreuid` (and friends) wrappers ([6b1ba584](https://github.com/rfjakob/gocryptfs/commit/6b1ba5846b17eec710a70cb6c6bf23e3f2024289)) diff --git a/build-without-openssl.bash b/build-without-openssl.bash index d5dc218..e965951 100755 --- a/build-without-openssl.bash +++ b/build-without-openssl.bash @@ -3,8 +3,3 @@ cd "$(dirname "$0")" CGO_ENABLED=0 source ./build.bash -tags without_openssl - -if ldd gocryptfs 2> /dev/null ; then - echo "build-without-openssl.bash: error: compiled binary is not static" - exit 1 -fi @@ -85,9 +85,6 @@ if [[ -n ${LDFLAGS:-} ]] ; then GO_LDFLAGS="$GO_LDFLAGS \"-extldflags=$LDFLAGS\"" fi -# Set GOAMD64 version to v2 -export GOAMD64=v2 - # Actual "go build" call for gocryptfs go build "-ldflags=$GO_LDFLAGS" "$@" # Additional binaries diff --git a/cli_args.go b/cli_args.go index 2e9e796..4101b86 100644 --- a/cli_args.go +++ b/cli_args.go @@ -35,9 +35,9 @@ type argContainer struct { // Mount options with opposites dev, nodev, suid, nosuid, exec, noexec, rw, ro, kernel_cache, acl bool masterkey, mountpoint, cipherdir, cpuprofile, - memprofile, ko, ctlsock, fsname, force_owner, trace string + memprofile, ko, ctlsock, fsname, force_owner, trace, context string // FIDO2 - fido2 string + fido2 string fido2_assert_options []string // -extpass, -badname, -passfile can be passed multiple times extpass, badname, passfile []string @@ -211,6 +211,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership") flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file") flagSet.StringVar(&args.fido2, "fido2", "", "Protect the masterkey using a FIDO2 token instead of a password") + flagSet.StringVar(&args.context, "context", "", "Set SELinux context (see mount(8) for details)") flagSet.StringArrayVar(&args.fido2_assert_options, "fido2-assert-option", nil, "Options to be passed with `fido2-assert -t`") // Exclusion options diff --git a/crossbuild.bash b/crossbuild.bash index db4b05b..ff773ec 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -18,8 +18,9 @@ set -eux export CGO_ENABLED=0 GOOS=linux GOARCH=amd64 build +GOOS=linux GOARCH=386 build -# See https://github.com/golang/go/wiki/GoArm +# See https://go.dev/wiki/GoArm GOOS=linux GOARCH=arm GOARM=7 build GOOS=linux GOARCH=arm64 build @@ -3,9 +3,9 @@ module github.com/rfjakob/gocryptfs/v2 go 1.19 require ( - github.com/aperturerobotics/jacobsa-crypto v1.0.2 - github.com/hanwen/go-fuse/v2 v2.5.0 - github.com/moby/sys/mountinfo v0.6.2 + github.com/aperturerobotics/jacobsa-crypto v1.1.0 + github.com/hanwen/go-fuse/v2 v2.7.3-0.20250306214706-e3463465126a + github.com/moby/sys/mountinfo v0.7.2 github.com/pkg/xattr v0.4.9 github.com/rfjakob/eme v1.1.2 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 @@ -1,17 +1,16 @@ -github.com/aperturerobotics/jacobsa-crypto v1.0.2 h1:tNvVy1rev9FagnOyBmTcI6d23FfNceG9IujZROTRtlc= -github.com/aperturerobotics/jacobsa-crypto v1.0.2/go.mod h1:buWU1iY+FjIcfpb1aYfFJZfl07WlS7O30lTyC2iwjv8= +github.com/aperturerobotics/jacobsa-crypto v1.1.0 h1:0hig54FMzU80OHrqSfqmj/W8HydRymVdz2K6D9Guffs= +github.com/aperturerobotics/jacobsa-crypto v1.1.0/go.mod h1:buWU1iY+FjIcfpb1aYfFJZfl07WlS7O30lTyC2iwjv8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hanwen/go-fuse/v2 v2.5.0 h1:JSJcwHQ1V9EGRy6QsosoLDMX6HaLdzyLOJpKdPqDt9k= -github.com/hanwen/go-fuse/v2 v2.5.0/go.mod h1:xKwi1cF7nXAOBCXujD5ie0ZKsxc8GGSA1rlMJc+8IJs= +github.com/hanwen/go-fuse/v2 v2.7.3-0.20250306214706-e3463465126a h1:Q+A/Qcj02oRubB/7+18SGNxAG/GtnoXxf0UKRJ2/ZE4= +github.com/hanwen/go-fuse/v2 v2.7.3-0.20250306214706-e3463465126a/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= github.com/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb h1:uSWBjJdMf47kQlXMwWEfmc864bA1wAC+Kl3ApryuG9Y= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/pkg/xattr v0.4.9 h1:5883YPCtkSd8LFbs13nXplj9g9tlrwoJRjgpgMu1/fE= github.com/pkg/xattr v0.4.9/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -28,9 +27,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 0e25de3..103c217 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -50,6 +50,8 @@ type File struct { lastOpCount uint64 // Parent filesystem rootNode *RootNode + // If this open file is a directory, dirHandle will be set, otherwise it's nil. + dirHandle *DirHandle } // NewFile returns a new go-fuse File instance based on an already-open file diff --git a/internal/fusefrontend/file_dir_ops.go b/internal/fusefrontend/file_dir_ops.go new file mode 100644 index 0000000..b69e7bc --- /dev/null +++ b/internal/fusefrontend/file_dir_ops.go @@ -0,0 +1,177 @@ +package fusefrontend + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/v2/internal/configfile" + "github.com/rfjakob/gocryptfs/v2/internal/nametransform" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +func (n *Node) OpendirHandle(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + var fd int = -1 + var fdDup int = -1 + var file *File + var dirIV []byte + var ds fs.DirStream + rn := n.rootNode() + + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + // Open backing directory + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + + // NewLoopbackDirStreamFd gets its own fd to untangle Release vs Releasedir + fdDup, err = syscall.Dup(fd) + + if err != nil { + errno = fs.ToErrno(err) + goto err_out + } + + ds, errno = fs.NewLoopbackDirStreamFd(fdDup) + if errno != 0 { + goto err_out + } + + if !rn.args.PlaintextNames { + // Read the DirIV from disk + dirIV, err = rn.nameTransform.ReadDirIVAt(fd) + if err != nil { + tlog.Warn.Printf("OpendirHandle: could not read %s: %v", nametransform.DirIVFilename, err) + errno = syscall.EIO + goto err_out + } + } + + file, _, errno = NewFile(fd, cName, rn) + if errno != 0 { + goto err_out + } + + file.dirHandle = &DirHandle{ + ds: ds, + dirIV: dirIV, + isRootDir: n.IsRoot(), + } + + return file, fuseFlags, errno + +err_out: + if fd >= 0 { + syscall.Close(fd) + } + if fdDup >= 0 { + syscall.Close(fdDup) + } + if errno == 0 { + tlog.Warn.Printf("BUG: OpendirHandle: err_out called with errno == 0") + errno = syscall.EIO + } + return nil, 0, errno +} + +type DirHandle struct { + // Content of gocryptfs.diriv. nil if plaintextnames is used. + dirIV []byte + + isRootDir bool + + // fs.loopbackDirStream with a private dup of the file descriptor + ds fs.FileHandle +} + +var _ = (fs.FileReleasedirer)((*File)(nil)) + +func (f *File) Releasedir(ctx context.Context, flags uint32) { + // Does its own locking + f.dirHandle.ds.(fs.FileReleasedirer).Releasedir(ctx, flags) + // Does its own locking + f.Release(ctx) +} + +var _ = (fs.FileSeekdirer)((*File)(nil)) + +func (f *File) Seekdir(ctx context.Context, off uint64) syscall.Errno { + return f.dirHandle.ds.(fs.FileSeekdirer).Seekdir(ctx, off) +} + +var _ = (fs.FileFsyncdirer)((*File)(nil)) + +func (f *File) Fsyncdir(ctx context.Context, flags uint32) syscall.Errno { + return f.dirHandle.ds.(fs.FileFsyncdirer).Fsyncdir(ctx, flags) +} + +var _ = (fs.FileReaddirenter)((*File)(nil)) + +// This function is symlink-safe through use of openBackingDir() and +// ReadDirIVAt(). +func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno syscall.Errno) { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + + for { + entry, errno = f.dirHandle.ds.(fs.FileReaddirenter).Readdirent(ctx) + if errno != 0 || entry == nil { + return + } + + cName := entry.Name + if cName == "." || cName == ".." { + // We want these as-is + return + } + if f.dirHandle.isRootDir && cName == configfile.ConfDefaultName { + // silently ignore "gocryptfs.conf" in the top level dir + continue + } + if f.rootNode.args.PlaintextNames { + return + } + if !f.rootNode.args.DeterministicNames && cName == nametransform.DirIVFilename { + // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled + continue + } + // Handle long file name + isLong := nametransform.LongNameNone + if f.rootNode.args.LongNames { + isLong = nametransform.NameType(cName) + } + if isLong == nametransform.LongNameContent { + cNameLong, err := nametransform.ReadLongNameAt(f.intFd(), cName) + if err != nil { + tlog.Warn.Printf("Readdirent: incomplete entry %q: Could not read .name: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + cName = cNameLong + } else if isLong == nametransform.LongNameFilename { + // ignore "gocryptfs.longname.*.name" + continue + } + name, err := f.rootNode.nameTransform.DecryptName(cName, f.dirHandle.dirIV) + if err != nil { + tlog.Warn.Printf("Readdirent: could not decrypt entry %q: %v", + cName, err) + f.rootNode.reportMitigatedCorruption(cName) + continue + } + // Override the ciphertext name with the plaintext name but reuse the rest + // of the structure + entry.Name = name + return + } +} diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 91c947e..95be48d 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -75,26 +75,50 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) return fga.Getattr(ctx, out) } } + rn := n.rootNode() + var st *syscall.Stat_t + var err error dirfd, cName, errno := n.prepareAtSyscallMyself() + // Hack for deleted fifos. As OPEN on a fifo does not reach + // the filesystem, we have no fd to access it. To make "cat" and git's + // t9300-fast-import.sh happy, we fake it as best as we can. + // https://github.com/rfjakob/gocryptfs/issues/929 + if errno == syscall.ENOENT && n.StableAttr().Mode == syscall.S_IFIFO { + out.Mode = syscall.S_IFIFO + out.Ino = n.StableAttr().Ino + // cat looks at this to determine the optimal io size. Seems to be always 4096 for fifos. + out.Blksize = 4096 + // We don't know what the owner was. Set it to nobody which seems safer + // than leaving it at 0 (root). + out.Owner.Gid = 65534 + out.Owner.Uid = 65534 + // All the other fields stay 0. This is what cat sees (strace -v log): + // + // fstat(1, {st_dev=makedev(0, 0x4d), st_ino=3838227, st_mode=S_IFIFO|000, st_nlink=0, + // st_uid=65534, st_gid=65534, st_blksize=4096, st_blocks=0, st_size=0, st_atime=0, + // st_atime_nsec=0, st_mtime=0, st_mtime_nsec=0, st_ctime=0, st_ctime_nsec=0}) = 0 + goto out + } if errno != 0 { return } + defer syscall.Close(dirfd) - st, err := syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) + st, err = syscallcompat.Fstatat2(dirfd, cName, unix.AT_SYMLINK_NOFOLLOW) if err != nil { return fs.ToErrno(err) } // Fix inode number - rn := n.rootNode() rn.inoMap.TranslateStat(st) out.Attr.FromStat(st) // Translate ciphertext size in `out.Attr.Size` to plaintext size n.translateSize(dirfd, cName, &out.Attr) +out: if rn.args.ForceOwner != nil { out.Owner = *rn.args.ForceOwner } diff --git a/internal/fusefrontend/node_api_check.go b/internal/fusefrontend/node_api_check.go index 0f60c74..37d4293 100644 --- a/internal/fusefrontend/node_api_check.go +++ b/internal/fusefrontend/node_api_check.go @@ -7,7 +7,6 @@ import ( // Check that we have implemented the fs.Node* interfaces var _ = (fs.NodeGetattrer)((*Node)(nil)) var _ = (fs.NodeLookuper)((*Node)(nil)) -var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) var _ = (fs.NodeRmdirer)((*Node)(nil)) diff --git a/internal/fusefrontend/node_dir_ops.go b/internal/fusefrontend/node_dir_ops.go index 97e4caa..11ff83d 100644 --- a/internal/fusefrontend/node_dir_ops.go +++ b/internal/fusefrontend/node_dir_ops.go @@ -12,7 +12,6 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/rfjakob/gocryptfs/v2/internal/configfile" "github.com/rfjakob/gocryptfs/v2/internal/cryptocore" "github.com/rfjakob/gocryptfs/v2/internal/nametransform" "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" @@ -159,91 +158,6 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En return ch, 0 } -// Readdir - FUSE call. -// -// This function is symlink-safe through use of openBackingDir() and -// ReadDirIVAt(). -func (n *Node) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { - parentDirFd, cDirName, errno := n.prepareAtSyscallMyself() - if errno != 0 { - return nil, errno - } - defer syscall.Close(parentDirFd) - - // Read ciphertext directory - fd, err := syscallcompat.Openat(parentDirFd, cDirName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - if err != nil { - return nil, fs.ToErrno(err) - } - defer syscall.Close(fd) - cipherEntries, specialEntries, err := syscallcompat.GetdentsSpecial(fd) - if err != nil { - return nil, fs.ToErrno(err) - } - // Get DirIV (stays nil if PlaintextNames is used) - var cachedIV []byte - rn := n.rootNode() - if !rn.args.PlaintextNames { - // Read the DirIV from disk - cachedIV, err = rn.nameTransform.ReadDirIVAt(fd) - if err != nil { - tlog.Warn.Printf("OpenDir %q: could not read %s: %v", cDirName, nametransform.DirIVFilename, err) - return nil, syscall.EIO - } - } - // Decrypted directory entries - var plain []fuse.DirEntry - // Add "." and ".." - plain = append(plain, specialEntries...) - // Filter and decrypt filenames - for i := range cipherEntries { - cName := cipherEntries[i].Name - if n.IsRoot() && cName == configfile.ConfDefaultName { - // silently ignore "gocryptfs.conf" in the top level dir - continue - } - if rn.args.PlaintextNames { - plain = append(plain, cipherEntries[i]) - continue - } - if !rn.args.DeterministicNames && cName == nametransform.DirIVFilename { - // silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled - continue - } - // Handle long file name - isLong := nametransform.LongNameNone - if rn.args.LongNames { - isLong = nametransform.NameType(cName) - } - if isLong == nametransform.LongNameContent { - cNameLong, err := nametransform.ReadLongNameAt(fd, cName) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: Could not read .name: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - cName = cNameLong - } else if isLong == nametransform.LongNameFilename { - // ignore "gocryptfs.longname.*.name" - continue - } - name, err := rn.nameTransform.DecryptName(cName, cachedIV) - if err != nil { - tlog.Warn.Printf("OpenDir %q: invalid entry %q: %v", - cDirName, cName, err) - rn.reportMitigatedCorruption(cName) - continue - } - // Override the ciphertext name with the plaintext name but reuse the rest - // of the structure - cipherEntries[i].Name = name - plain = append(plain, cipherEntries[i]) - } - - return fs.NewListDirStream(plain), 0 -} - // Rmdir - FUSE call. // // Symlink-safe through Unlinkat() + AT_REMOVEDIR. diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index f5dfeb6..e8fca80 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -2,7 +2,6 @@ package fusefrontend import ( "context" - "sync/atomic" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -91,12 +90,12 @@ func (n *Node) newChild(ctx context.Context, st *syscall.Stat_t, out *fuse.Entry if rn.args.SharedStorage || rn.quirks&syscallcompat.QuirkDuplicateIno1 != 0 { // Make each directory entry a unique node by using a unique generation // value - see the comment at RootNode.gen for details. - gen = atomic.AddUint64(&rn.gen, 1) + gen = rn.gen.Add(1) } // Create child node id := fs.StableAttr{ - Mode: uint32(st.Mode), + Mode: uint32(st.Mode), // go-fuse masks this with syscall.S_IFMT Gen: gen, Ino: st.Ino, } diff --git a/internal/fusefrontend/node_prepare_syscall.go b/internal/fusefrontend/node_prepare_syscall.go index 2a4d6ab..9021350 100644 --- a/internal/fusefrontend/node_prepare_syscall.go +++ b/internal/fusefrontend/node_prepare_syscall.go @@ -1,7 +1,6 @@ package fusefrontend import ( - "sync/atomic" "syscall" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -24,7 +23,7 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy // All filesystem operations go through here, so this is a good place // to reset the idle marker. - atomic.StoreUint32(&rn.IsIdle, 0) + rn.IsIdle.Store(false) if n.IsRoot() && rn.isFiltered(child) { return -1, "", syscall.EPERM diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index ac814ad..8464c5f 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -4,6 +4,7 @@ import ( "os" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -44,7 +45,7 @@ type RootNode struct { // (uint32 so that it can be reset with CompareAndSwapUint32). // When -idle was used when mounting, idleMonitor() sets it to 1 // periodically. - IsIdle uint32 + IsIdle atomic.Bool // dirCache caches directory fds dirCache dirCache // inoMap translates inode numbers from different devices to unique inode @@ -55,7 +56,7 @@ type RootNode struct { // This makes each directory entry unique (even hard links), // makes go-fuse hand out separate FUSE Node IDs for each, and prevents // bizarre problems when inode numbers are reused behind our back. - gen uint64 + gen atomic.Uint64 // quirks is a bitmap that enables workaround for quirks in the filesystem // backing the cipherdir quirks uint64 diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index cb04151..9c2de28 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -21,7 +21,7 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" "github.com/rfjakob/gocryptfs/v2/internal/tlog" - "github.com/sabhiram/go-gitignore" + ignore "github.com/sabhiram/go-gitignore" ) // RootNode is the root directory in a `gocryptfs -reverse` mount @@ -50,7 +50,7 @@ type RootNode struct { // makes go-fuse hand out separate FUSE Node IDs for each, and prevents // bizarre problems when inode numbers are reused behind our back, // like this one: https://github.com/rfjakob/gocryptfs/issues/802 - gen uint64 + gen atomic.Uint64 // rootIno is the inode number that we report for the root node on mount rootIno uint64 } @@ -175,7 +175,7 @@ func (rn *RootNode) uniqueStableAttr(mode uint32, ino uint64) fs.StableAttr { Ino: ino, // Make each directory entry a unique node by using a unique generation // value. Also see the comment at RootNode.gen for details. - Gen: atomic.AddUint64(&rn.gen, 1), + Gen: rn.gen.Add(1), } } diff --git a/internal/inomap/inomap.go b/internal/inomap/inomap.go index b4dbf27..5749202 100644 --- a/internal/inomap/inomap.go +++ b/internal/inomap/inomap.go @@ -45,7 +45,7 @@ type InoMap struct { // spill is used once the namespaces map is full spillMap map[QIno]uint64 // spillNext is the next free inode number in the spill map - spillNext uint64 + spillNext atomic.Uint64 } // New returns a new InoMap. @@ -57,8 +57,9 @@ func New(rootDev uint64) *InoMap { namespaceMap: make(map[namespaceData]uint16), namespaceNext: 0, spillMap: make(map[QIno]uint64), - spillNext: spillSpaceStart, } + m.spillNext.Store(spillSpaceStart) + if rootDev > 0 { // Reserve namespace 0 for rootDev m.namespaceMap[namespaceData{rootDev, 0}] = 0 @@ -74,10 +75,10 @@ var spillWarn sync.Once // Reverse mode NextSpillIno() for gocryptfs.longname.*.name files where a stable // mapping is not needed. func (m *InoMap) NextSpillIno() (out uint64) { - if m.spillNext == math.MaxUint64 { - log.Panicf("spillMap overflow: spillNext = 0x%x", m.spillNext) + if m.spillNext.Load() == math.MaxUint64 { + log.Panicf("spillMap overflow: spillNext = 0x%x", m.spillNext.Load()) } - return atomic.AddUint64(&m.spillNext, 1) - 1 + return m.spillNext.Add(1) - 1 } func (m *InoMap) spill(in QIno) (out uint64) { diff --git a/internal/openfiletable/open_file_table.go b/internal/openfiletable/open_file_table.go index ce8df76..420d070 100644 --- a/internal/openfiletable/open_file_table.go +++ b/internal/openfiletable/open_file_table.go @@ -29,7 +29,7 @@ type table struct { // The variable is accessed without holding any locks so atomic operations // must be used. It must be the first element of the struct to guarantee // 64-bit alignment. - writeOpCount uint64 + writeOpCount atomic.Uint64 // Protects map access sync.Mutex // Table entries @@ -85,13 +85,13 @@ type countingMutex struct { func (c *countingMutex) Lock() { c.RWMutex.Lock() - atomic.AddUint64(&t.writeOpCount, 1) + t.writeOpCount.Add(1) } // WriteOpCount returns the write lock counter value. This value is incremented // each time writeLock.Lock() on a file table entry is called. func WriteOpCount() uint64 { - return atomic.LoadUint64(&t.writeOpCount) + return t.writeOpCount.Load() } // CountOpenFiles returns how many entries are currently in the table diff --git a/internal/syscallcompat/thread_credentials_linux_368_arm.go b/internal/syscallcompat/thread_credentials_linux_32.go index b39fa09..69fffca 100644 --- a/internal/syscallcompat/thread_credentials_linux_368_arm.go +++ b/internal/syscallcompat/thread_credentials_linux_32.go @@ -1,5 +1,8 @@ //go:build (linux && 386) || (linux && arm) +// Linux on i386 and 32-bit ARM has SYS_SETREUID and friends returning 16-bit values. +// We need to use SYS_SETREUID32 instead. + package syscallcompat import ( @@ -13,7 +13,6 @@ import ( "runtime" "runtime/debug" "strings" - "sync/atomic" "syscall" "time" @@ -181,7 +180,7 @@ func idleMonitor(idleTimeout time.Duration, fs *fusefrontend.RootNode, srv *fuse } for { // Atomically check whether the flag is 0 and reset it to 1 if so. - isIdle := !atomic.CompareAndSwapUint32(&fs.IsIdle, 0, 1) + isIdle := !fs.IsIdle.CompareAndSwap(false, true) // Any form of current or recent access resets the idle counter. openFileCount := openfiletable.CountOpenFiles() if !isIdle || openFileCount > 0 { @@ -316,9 +315,13 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f } } } - // If allow_other is set and we run as root, try to give newly created files to - // the right user. - if args.allow_other && os.Getuid() == 0 { + // If allow_other is set and we run as root, create files as the accessing + // user. + // Except when -force_owner is set, because in this case the user may + // not have write permissions. And the point of -force_owner is to map uids, + // so we want the files on the backing dir to get the uid the gocryptfs process + // is running as. + if args.allow_other && os.Getuid() == 0 && args._forceOwner == nil { frontendArgs.PreserveOwner = true } @@ -469,6 +472,9 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { } else if args.exec { opts["exec"] = "" } + if args.context != "" { + opts["context"] = args.context + } // Add additional mount options (if any) after the stock ones, so the user has // a chance to override them. if args.ko != "" { diff --git a/tests/defaults/getdents_linux.go b/tests/defaults/getdents_linux.go new file mode 100644 index 0000000..57956ce --- /dev/null +++ b/tests/defaults/getdents_linux.go @@ -0,0 +1,9 @@ +package defaults + +import ( + "golang.org/x/sys/unix" +) + +func getdents(fd int, buf []byte) (int, error) { + return unix.Getdents(fd, buf) +} diff --git a/tests/defaults/getdents_other.go b/tests/defaults/getdents_other.go new file mode 100644 index 0000000..c0552a8 --- /dev/null +++ b/tests/defaults/getdents_other.go @@ -0,0 +1,11 @@ +//go:build !linux + +package defaults + +import ( + "golang.org/x/sys/unix" +) + +func getdents(fd int, buf []byte) (int, error) { + return unix.Getdirentries(fd, buf, nil) +} diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index d0210e2..75a5dae 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -3,6 +3,7 @@ package defaults import ( "bytes" + "fmt" "io" "io/ioutil" "os" @@ -506,3 +507,51 @@ func TestForceOwner(t *testing.T) { t.Errorf("LOOKUP returned uid or gid != 1234: %#v", st) } } + +func TestSeekDir(t *testing.T) { + wd := test_helpers.DefaultPlainDir + "/" + t.Name() + err := os.Mkdir(wd, 0700) + if err != nil { + t.Fatal(err) + } + for i := 0; i < 10; i++ { + path := fmt.Sprintf(wd+"/%3d", i) + err = os.WriteFile(path, nil, 0600) + if err != nil { + t.Fatal(err) + } + } + + fd, err := syscall.Open(wd, syscall.O_DIRECTORY|syscall.O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + + buf := make([]byte, 1000) + n, err := getdents(fd, buf) + if err != nil { + t.Fatal(err) + } + t.Logf("1st getdents returned %d bytes", n) + + n, err = getdents(fd, buf) + if err != nil { + t.Fatal(err) + } + t.Logf("2nd getdents returned %d bytes", n) + + _, err = unix.Seek(fd, 0, 0) + if err != nil { + t.Error(err) + } + + n, err = getdents(fd, buf) + if err != nil { + t.Fatal(err) + } + t.Logf("3rd getdents (after seek) returned %d bytes", n) + if n == 0 { + t.Error("Seek did not have any effect") + } +} diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index e432ce0..9ce1f4b 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -379,3 +379,39 @@ func TestOverlay(t *testing.T) { } defer syscall.Unmount(ovlMnt, 0) } + +// Check that mkdir and file create works with force_owner and runnung as root +// https://github.com/rfjakob/gocryptfs/issues/783 +func TestRootForceOwner(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("must run as root") + } + cDir := test_helpers.InitFS(t) + pDir := cDir + ".mnt" + test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test", "-force_owner=1234:1234") + defer test_helpers.UnmountPanic(pDir) + + err := asUser(1234, 1234, nil, func() error { + return os.Mkdir(pDir+"/dir1", 0700) + }) + if err != nil { + t.Error(err) + } + err = asUser(1234, 1234, nil, func() error { + f, err := os.Create(pDir + "/file1") + if err == nil { + f.Close() + } + return err + }) + if err != nil { + t.Error(err) + } + err = asUser(1234, 1234, nil, func() error { + sock := pDir + "/sock" + return syscall.Mknod(sock, syscall.S_IFSOCK|0600, 0) + }) + if err != nil { + t.Errorf("mknod: %v", err) + } +} diff --git a/tests/stress_tests/fsstress-gocryptfs.bash b/tests/stress_tests/fsstress-gocryptfs.bash index 30ea456..e6c3281 100755 --- a/tests/stress_tests/fsstress-gocryptfs.bash +++ b/tests/stress_tests/fsstress-gocryptfs.bash @@ -27,11 +27,11 @@ MYNAME=$(basename "$0") source ../fuse-unmount.bash # fsstress binary -FSSTRESS=/opt/fuse-xfstests/ltp/fsstress +FSSTRESS=/var/lib/xfstests/ltp/fsstress if [[ ! -x $FSSTRESS ]] then echo "$MYNAME: fsstress binary not found at $FSSTRESS" - echo "Please clone and compile https://github.com/rfjakob/fuse-xfstests" + echo "Please build and \"sudo make install\" git://git.kernel.org/pub/scm/fs/xfs/xfstests-dev.git" exit 1 fi @@ -62,9 +62,8 @@ if [[ $MYNAME = fsstress-loopback.bash ]]; then $GOPATH/bin/loopback $OPTS "$MNT" "$DIR" & disown elif [[ $MYNAME = fsstress-gocryptfs.bash ]]; then - echo "Recompile gocryptfs" - cd "$GOPATH/src/github.com/rfjakob/gocryptfs" - ./build.bash # also prints the version + echo "$MYNAME: using gocryptfs at $GOPATH/bin/gocryptfs" + $GOPATH/bin/gocryptfs --version $GOPATH/bin/gocryptfs -q -init -extpass "echo test" -scryptn=10 "$DIR" $GOPATH/bin/gocryptfs -q -extpass "echo test" -nosyslog -fusedebug="$DEBUG" "$DIR" "$MNT" elif [[ $MYNAME = fsstress-encfs.bash ]]; then @@ -84,7 +83,7 @@ done echo " ok: $MNT" # Cleanup trap -trap "kill %1 ; cd / ; fuse-unmount -z $MNT ; rm -rf $DIR $MNT" EXIT +trap "echo ' cleaning up...' ; kill %1 ; cd / ; fuse-unmount -z $MNT ; rm -rf $DIR $MNT" EXIT echo "Starting fsstress loop" N=1 diff --git a/tests/stress_tests/parallel_cp.sh b/tests/stress_tests/parallel_cp.sh index cd08d31..f113248 100755 --- a/tests/stress_tests/parallel_cp.sh +++ b/tests/stress_tests/parallel_cp.sh @@ -2,15 +2,16 @@ # # Simplified xfstests generic/273 # -# Fails with +# Used to fail with # # cp: cannot create regular file 'sub_49/file_773': No such file or directory # # If you cannot reproduce, try running this in the background: # -# while sleep 0.1 ; do echo 3 > /proc/sys/vm/drop_caches ; done" +# while sleep 0.1 ; do echo 3 > /proc/sys/vm/drop_caches ; done # # See https://github.com/rfjakob/gocryptfs/issues/322 for details. +# Fixed by https://github.com/hanwen/go-fuse/commit/d0fca860a5759d17592becfa1b8e5b1bd354b24a . if [[ -z $TMPDIR ]]; then TMPDIR=/var/tmp @@ -24,6 +25,9 @@ source ../fuse-unmount.bash # Set the GOPATH variable to the default if it is empty GOPATH=$(go env GOPATH) +echo "$MYNAME: using gocryptfs at $GOPATH/bin/gocryptfs" +$GOPATH/bin/gocryptfs --version + # Backing directory DIR=$(mktemp -d "$TMPDIR/$MYNAME.XXX") $GOPATH/bin/gocryptfs -q -init -extpass "echo test" -scryptn=10 "$DIR" diff --git a/tests/stress_tests/pingpong.bash b/tests/stress_tests/pingpong.bash index 4b8346e..d0d21b3 100755 --- a/tests/stress_tests/pingpong.bash +++ b/tests/stress_tests/pingpong.bash @@ -1,7 +1,8 @@ #!/bin/bash # # Mounts two gocryptfs filesystems, "ping" and "pong" and moves the -# linux-3.0 kernel tree back and forth between them. +# linux-3.0 kernel tree back and forth between them, checking integrity +# using md5sum. # # When called as "pingpong-rsync.bash" it uses "rsync --remove-source-files" # for moving the files, otherwise plain "mv". @@ -29,6 +30,9 @@ mkdir "$PING.mnt" "$PONG.mnt" # Just ignore unmount errors. trap "set +e ; cd /tmp; fuse-unmount -z $PING.mnt ; fuse-unmount -z $PONG.mnt ; rm -rf $PING $PONG $PING.mnt $PONG.mnt" EXIT +echo "$MYNAME: using gocryptfs at $(command -v gocryptfs)" +gocryptfs --version + gocryptfs -q -init -extpass="echo test" -scryptn=10 "$PING" gocryptfs -q -init -extpass="echo test" -scryptn=10 "$PONG" |