diff options
170 files changed, 2551 insertions, 1125 deletions
diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..bf7c985 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abda518..792b879 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,19 +14,22 @@ jobs: # Each major Go release is supported until there are two newer major releases. # https://go.dev/doc/devel/release#policy go: - - "1.18.x" # Ubuntu 22.04 LTS "jammy" - - "1.19.x" # Debian bookworm, bullseye-backports - - "1.20.x" - - "1.21.x" - "oldstable" # 2nd-latest Golang upstream stable - "stable" # Latest Go upstream stable + os: + - "ubuntu-24.04" + - "ubuntu-24.04-arm" # Don't cancel everything when one Go version fails fail-fast: false - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 # Make "git describe" work + - name: Install Go ${{ matrix.go }} - uses: actions/setup-go@v3 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go }} @@ -34,21 +37,20 @@ jobs: # https://github.com/actions/runner/issues/1188 - run: ls -l /proc/self/fd - - uses: actions/checkout@v3 - with: - fetch-depth: 0 # Make "git describe" work - # CI platform specific setup steps happen here - run: sudo apt-get install -qq fuse3 libssl-dev # Fix "/usr/bin/fusermount: option allow_other only allowed if 'user_allow_other' is set in /etc/fuse.conf" - - run: echo user_allow_other | sudo tee -a /etc/fuse.conf + # and "/usr/bin/fusermount3: too many FUSE filesystems mounted" + # The latter is likely a bug in fusermount, fixed in Oct 2025 by + # https://github.com/libfuse/libfuse/commit/bf3dd153fbfcd610d799562490f6555b9d708905 + - run: echo -e 'user_allow_other\nmount_max = 10000' | sudo tee -a /etc/fuse.conf && cat /etc/fuse.conf # Build & upload static binary - run: ./build-without-openssl.bash - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v6 with: - name: gocryptfs static binary (Go ${{ matrix.go }}) + name: gocryptfs ${{ github.sha }} static ${{ runner.arch }} binary, Go ${{ matrix.go }} path: gocryptfs # Actual test steps are in the Makefile @@ -20,3 +20,6 @@ gocryptfs.1 # Source tarball version. Should never be committed to git. /VERSION + +# Ignore macos specific files +.DS_Store
\ No newline at end of file diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 1d3ab6c..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 @@ -580,7 +590,7 @@ files. They are concatenated for the effective password. Example: echo hello > hello.txt - echo word > world.txt + echo world > world.txt gocryptfs -passfile hello.txt -passfile world.txt The effective password will be "helloworld". 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 diff --git a/Documentation/performance.txt b/Documentation/performance.txt index 24265f5..7e964e0 100644 --- a/Documentation/performance.txt +++ b/Documentation/performance.txt @@ -73,6 +73,10 @@ v2.0-beta2-37-g24d5d39 558 1000 12.3 6.4 4.4 2.8 v2.0-beta2-42-g4a07d65 549 1000 8.2 4.7 1.8 2.4 fusefrontend: make dirCache work for "node itself" v2.0 420 1000 8.5 4.5 1.8 2.3 go1.16.5, Linux 5.11.21 v2.0.1-28-g49507ea 471 991 8.6 4.5 1.7 2.2 +v2.0.1-28-g49507ea 335 951 10.2 5.4 4.1 2.0 go1.25.4, Linux 6.18.6 +v2.6.1-22-gbc94538 432 950 10.0 5.4 3.8 2.0 +v2.6.1-24-gb239d51 426 941 9.9 5.5 3.7 2.0 go-fuse v2.9.0 +v2.6.1-26-g700432e 461 962 9.8 5.4 2.0 2.0 Results for EncFS for comparison (benchmark.bash -encfs): @@ -1,7 +1,7 @@ [](https://nuetzlich.net/gocryptfs/) [](https://github.com/rfjakob/gocryptfs/actions/workflows/ci.yml) [](LICENSE) -[](https://goreportcard.com/report/github.com/rfjakob/gocryptfs) +[](https://goreportcard.com/report/github.com/rfjakob/gocryptfs/v2) [](https://github.com/rfjakob/gocryptfs/releases) [](https://formulae.brew.sh/formula/gocryptfs#default) @@ -30,7 +30,7 @@ hours and hours of stress (fsstress, extractloop.bash) and correctness testing (xfstests). It is now considered ready for general consumption. The old principle still applies: Important data should have a backup. -Also, keep a copy of your master key (printed on mount) in a safe place. +Also, keep a copy of your master key (printed at init) in a safe place. This allows you to access the data even if the gocryptfs.conf config file is damaged or you lose the password. @@ -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://github.com/lquenti/gocryptfs-create-folder) +is a Python tool can encrypt a directory without using FUSE. Installation ------------ @@ -195,6 +201,59 @@ RM: 2,367 Changelog --------- +#### v2.6.1, 2025-08-10 +* Fix warnings `cipherSize X: incomplete last block (Y bytes), padding to Z bytes` + (harmless but annoying, [#951](https://github.com/rfjakob/gocryptfs/issues/951)) +* MacOS: Fix GUI apps reporting failure to save files [#914](https://github.com/rfjakob/gocryptfs/issues/914) +* MacOS: Fix `test-without-openssl.bash` trying to build tests with openssl enabled + ([2ebd0d754b8ee4](https://github.com/rfjakob/gocryptfs/commit/2ebd0d754b8ee46e6c65e90e1d1e13491b03b7b5)) + +#### v2.6.0, 2025-07-14 +* Upgrade to go-fuse v2.8.0 +* Switch to the new go-fuse directory API( https://github.com/rfjakob/gocryptfs/commit/ae3c859c1179498a4882b4bd69c2243aa6912332 ) +* Fix `-force_owner` not allowing file/dir create ( https://github.com/rfjakob/gocryptfs/issues/783 ) +* Skip `TestBtrfsQuirks` if mkfs.btrfs is not installed ( https://github.com/rfjakob/gocryptfs/issues/930 ) + +#### 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)) +* Upgrade `golang.org/x/sys` again +* Provide arm64 binaries in releases in addition to amd64 + +#### v2.5.1, 2025-01-23 +* **Downgrade `golang.org/x/sys` to unbreak `unix.Setreuid` and `-allow_other` + ([6d342f3](https://github.com/rfjakob/gocryptfs/commit/6d342f3f4f1e9468da00b141b2abaf1e55f28665), + [#893](https://github.com/rfjakob/gocryptfs/issues/893), [#892](https://github.com/rfjakob/gocryptfs/issues/892))** + +#### v2.5.0, 2025-01-18 +* **Important fixes for `-reverse` mode affecting the virtual `gocryptfs.diriv` and + `gocryptfs.longname.*.name` files.** The bug can cause file *names* to become + undecryptable. To make sure that sync tools like rsync copy new, good copies, + gocryptfs v2.5.0 and later advance ctime and mtime for these files by 10 seconds. + * Fix `-reverse` mode sometimes (triggered by inode number reuse) returning stale + data for `gocryptfs.diriv` (#802) + * Fix `-reverse` mode hardlinking `gocryptfs.longname.*.name` files of hardlinked + files together (#802) +* Fix `-reverse` mode ignoring `-force-owner` (#809) +* Add workaround for excessive file fragementation on btrfs (#811) +* `-ctlsock`: automatically delete orphaned colliding socket file (#776) +* MacOS: Fix XTIMES panic on startup (#823) + * Fixed by updating the go-fuse library to v2.5.0 +* MacOS: merge kernel options before passing them on (#854, #557) +* Add `-fido2-assert-option` (#807) +* `-init` now accepts `-masterkey` +* `-passwd` now ignores `-extpass` and `-passfile` for the *new* password (#287, #882) + #### v2.4.0, 2023-06-10 * Try the `mount(2)` syscall before falling back to `fusermount(1)`. This means we don't need `fusermount(1)` at all if running as root or in a root-like namespace diff --git a/build-without-openssl.bash b/build-without-openssl.bash index d5dc218..c09e7f3 100755 --- a/build-without-openssl.bash +++ b/build-without-openssl.bash @@ -2,9 +2,4 @@ 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 +CGO_ENABLED=0 source ./build.bash @@ -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..707b453 100644 --- a/cli_args.go +++ b/cli_args.go @@ -31,13 +31,13 @@ type argContainer struct { longnames, allow_other, reverse, aessiv, nonempty, raw64, noprealloc, speed, hkdf, serialize_reads, hh, info, sharedstorage, fsck, one_file_system, deterministic_names, - xchacha bool + xchacha, noxattr bool // 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 @@ -84,7 +84,7 @@ func prefixOArgs(osArgs []string) ([]string, error) { if osArgs[i] == "-o" { // Last argument? if i+1 >= len(osArgs) { - return nil, fmt.Errorf("The \"-o\" option requires an argument") + return nil, fmt.Errorf("the \"-o\" option requires an argument") } oOpts = strings.Split(osArgs[i+1], ",") // Skip over the arguments to "-o" @@ -188,6 +188,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.BoolVar(&args.one_file_system, "one-file-system", false, "Don't cross filesystem boundaries") flagSet.BoolVar(&args.deterministic_names, "deterministic-names", false, "Disable diriv file name randomisation") flagSet.BoolVar(&args.xchacha, "xchacha", false, "Use XChaCha20-Poly1305 file content encryption") + flagSet.BoolVar(&args.noxattr, "noxattr", false, "Disable extended attribute operations") // Mount options with opposites flagSet.BoolVar(&args.dev, "dev", false, "Allow device files") @@ -211,6 +212,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/contrib/atomicrename/main.go b/contrib/atomicrename/main.go index 394753b..337371c 100644 --- a/contrib/atomicrename/main.go +++ b/contrib/atomicrename/main.go @@ -4,7 +4,6 @@ import ( "bytes" "flag" "fmt" - "io/ioutil" "os" "strings" "sync/atomic" @@ -49,7 +48,7 @@ func main() { srcFiles[srcName] = struct{}{} buf := bytes.Repeat([]byte("_"), i) buf = append(buf, hello...) - if err := ioutil.WriteFile(srcName, buf, 0600); err != nil { + if err := os.WriteFile(srcName, buf, 0600); err != nil { panic(err) } fmt.Print(".") @@ -58,7 +57,7 @@ func main() { // prepare destination file const dstName = "dst.atomicrename" - if err := ioutil.WriteFile(dstName, hello, 0600); err != nil { + if err := os.WriteFile(dstName, hello, 0600); err != nil { panic(err) } @@ -69,7 +68,7 @@ func main() { // read thread go func() { for atomic.LoadInt32(&running) == 1 { - have, err := ioutil.ReadFile(dstName) + have, err := os.ReadFile(dstName) if err != nil { fmt.Println(err) stats.readError++ diff --git a/contrib/findholes/holes/holes.go b/contrib/findholes/holes/holes.go index 95c9d2b..72f5114 100644 --- a/contrib/findholes/holes/holes.go +++ b/contrib/findholes/holes/holes.go @@ -8,7 +8,6 @@ import ( "math/rand" "os" "syscall" - "time" ) const ( @@ -130,7 +129,7 @@ func Find(fd int) (segments []Segment, err error) { cursor = off if oldCursor == cursor { - return nil, fmt.Errorf("%s\nerror: seek loop!", PrettyPrint(segments)) + return nil, fmt.Errorf("%s\nerror: seek loop", PrettyPrint(segments)) } } return segments, nil @@ -176,7 +175,6 @@ func Create(path string) { } defer f.Close() - rand.Seed(time.Now().UnixNano()) offsets := make([]int64, 10) for i := range offsets { offsets[i] = int64(rand.Int31n(60000)) diff --git a/crossbuild.bash b/crossbuild.bash index a9fc6ae..ff773ec 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -15,12 +15,12 @@ function compile_tests { set -eux -export GO111MODULE=on 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 @@ -30,8 +30,4 @@ GOOS=darwin GOARCH=amd64 build time GOOS=darwin GOARCH=amd64 compile_tests # MacOS on Apple Silicon M1. -# Go 1.16 added support for the M1 and added ios/arm64, -# so we use this to check if we should attempt a build. -if go tool dist list | grep ios/arm64 ; then - GOOS=darwin GOARCH=arm64 build -fi +GOOS=darwin GOARCH=arm64 build @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io" - "io/ioutil" "os" "os/signal" "path/filepath" @@ -264,7 +263,7 @@ func fsck(args *argContainer) (exitcode int) { args.allow_other = false args.ro = true var err error - args.mountpoint, err = ioutil.TempDir("", "gocryptfs.fsck.") + args.mountpoint, err = os.MkdirTemp("", "gocryptfs.fsck.") if err != nil { tlog.Fatal.Printf("fsck: TmpDir: %v", err) os.Exit(exitcodes.MountPoint) @@ -1,16 +1,17 @@ module github.com/rfjakob/gocryptfs/v2 -go 1.19 +go 1.24.0 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.9.0 + github.com/moby/sys/mountinfo v0.7.2 github.com/pkg/xattr v0.4.9 - github.com/rfjakob/eme v1.1.2 + github.com/rfjakob/eme v1.2.0 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/spf13/pflag v1.0.5 - golang.org/x/crypto v0.18.0 - golang.org/x/sys v0.16.0 - golang.org/x/term v0.16.0 + golang.org/x/crypto v0.45.0 + golang.org/x/sys v0.38.0 + golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 ) @@ -1,23 +1,27 @@ -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.9.0 h1:0AOGUkHtbOVeyGLr0tXupiid1Vg7QB7M6YUcdmVdC58= +github.com/hanwen/go-fuse/v2 v2.9.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= +github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd/go.mod h1:TlmyIZDpGmwRoTWiakdr+HA1Tukze6C6XbRVidYq02M= github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff h1:2xRHTvkpJ5zJmglXLRqHiZQNjUoOkhUyhTAhEQvPAWw= +github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff/go.mod h1:gJWba/XXGl0UoOmBQKRWCJdHrr3nE0T65t6ioaj3mLI= github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11 h1:BMb8s3ENQLt5ulwVIHVDWFHp8eIXmbfSExkvdn9qMXI= +github.com/jacobsa/ogletest v0.0.0-20170503003838-80d50a735a11/go.mod h1:+DBdDyfoO2McrOyDemRBq0q9CMEByef7sYl7JH5Q3BI= 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/jacobsa/reqtrace v0.0.0-20150505043853-245c9e0234cb/go.mod h1:ivcmUvxXWjb27NsPEaiYK7AidlZXS7oQ5PowUS9z3I4= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rfjakob/eme v1.1.2 h1:SxziR8msSOElPayZNFfQw4Tjx/Sbaeeh3eRvrHVMUs4= -github.com/rfjakob/eme v1.1.2/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= +github.com/rfjakob/eme v1.2.0 h1:8dAHL+WVAw06+7DkRKnRiFp1JL3QjcJEZFqDnndUaSI= +github.com/rfjakob/eme v1.2.0/go.mod h1:cVvpasglm/G3ngEfcfT/Wt0GwhkuO32pf/poW6Nyk1k= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 h1:OkMGxebDjyw0ULyrTYWeN0UNCCkmCWfjPnIA2W6oviI= github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+ePHsJ1keEjQtpvf9HHw0f4ZeJ0TLRsxhunSI2hYJSs= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -25,16 +29,17 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gocryptfs-xray/xray_tests/xray_test.go b/gocryptfs-xray/xray_tests/xray_test.go index 6dc7c13..e9b9eab 100644 --- a/gocryptfs-xray/xray_tests/xray_test.go +++ b/gocryptfs-xray/xray_tests/xray_test.go @@ -3,7 +3,7 @@ package xray_tests import ( "bytes" "fmt" - "io/ioutil" + "os" "os/exec" "testing" @@ -11,7 +11,7 @@ import ( ) func TestAesgcmXray(t *testing.T) { - expected, err := ioutil.ReadFile("aesgcm_fs.xray.txt") + expected, err := os.ReadFile("aesgcm_fs.xray.txt") if err != nil { t.Fatal(err) } @@ -28,7 +28,7 @@ func TestAesgcmXray(t *testing.T) { } func TestAessivXray(t *testing.T) { - expected, err := ioutil.ReadFile("aessiv_fs.xray.txt") + expected, err := os.ReadFile("aessiv_fs.xray.txt") if err != nil { t.Fatal(err) } diff --git a/golint.bash b/golint.bash deleted file mode 100755 index d6fe729..0000000 --- a/golint.bash +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash -u - -OUTPUT=$( - golint ./... | \ - grep -v "don't use an underscore in package name" | \ - grep -v "don't use ALL_CAPS in Go names; use CamelCase" | - grep -v "don't use underscores in Go names" -) - -# No output --> all good -if [[ -z $OUTPUT ]] ; then - exit 0 -fi - -echo "golint.bash:" -echo "$OUTPUT" -exit 1 @@ -13,8 +13,8 @@ const tUsage = "" + // helpShort is what gets displayed when passed "-h" or on syntax error. func helpShort() { printVersion() - fmt.Printf("\n") - fmt.Printf(tUsage) + fmt.Print("\n") + fmt.Print(tUsage) fmt.Printf(` Common Options (use -hh to show all): -aessiv Use AES-SIV encryption (with -init) @@ -48,8 +48,8 @@ Common Options (use -hh to show all): // helpLong gets only displayed on "-hh" func helpLong() { printVersion() - fmt.Printf("\n") - fmt.Printf(tUsage) + fmt.Print("\n") + fmt.Print(tUsage) fmt.Printf(` Notes: All options can equivalently use "-" (single dash) or "--" (double dash). A standalone "--" stops option parsing. diff --git a/init_dir.go b/init_dir.go index d79a4b7..870604a 100644 --- a/init_dir.go +++ b/init_dir.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -26,7 +25,7 @@ func isEmptyDir(dir string) error { if err != nil { return err } - entries, err := ioutil.ReadDir(dir) + entries, err := os.ReadDir(dir) if err != nil { return err } @@ -69,14 +68,14 @@ func initDir(args *argContainer) { os.Exit(exitcodes.CipherDir) } if !args.xchacha && !stupidgcm.HasAESGCMHardwareSupport() { - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println(tlog.ColorYellow + "Notice: Your CPU does not have AES-GCM acceleration. Consider using -xchacha for better performance." + tlog.ColorReset) } } // Choose password for config file if len(args.extpass) == 0 && args.fido2 == "" { - tlog.Info.Printf("Choose a password for protecting your files.") + tlog.Info.Println("Choose a password for protecting your files.") } { var password []byte diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index 995a0c8..28a1ca5 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -5,7 +5,6 @@ package configfile import ( "encoding/json" "fmt" - "io/ioutil" "syscall" "os" @@ -183,12 +182,12 @@ func Load(filename string) (*ConfFile, error) { cf.filename = filename // Read from disk - js, err := ioutil.ReadFile(filename) + js, err := os.ReadFile(filename) if err != nil { return nil, err } if len(js) == 0 { - return nil, fmt.Errorf("Config file is empty") + return nil, fmt.Errorf("config file is empty") } // Unmarshal diff --git a/internal/configfile/scrypt.go b/internal/configfile/scrypt.go index 0ce8777..f6201ba 100644 --- a/internal/configfile/scrypt.go +++ b/internal/configfile/scrypt.go @@ -87,19 +87,19 @@ func (s *ScryptKDF) LogN() int { func (s *ScryptKDF) validateParams() error { minN := 1 << scryptMinLogN if s.N < minN { - return fmt.Errorf("Fatal: scryptn below 10 is too low to make sense") + return fmt.Errorf("fatal: scryptn below 10 is too low to make sense") } if s.R < scryptMinR { - return fmt.Errorf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) + return fmt.Errorf("fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) } if s.P < scryptMinP { - return fmt.Errorf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) + return fmt.Errorf("fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) } if len(s.Salt) < scryptMinSaltLen { - return fmt.Errorf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) + return fmt.Errorf("fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) } if s.KeyLen < cryptocore.KeyLen { - return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) + return fmt.Errorf("fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) } return nil } diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go index ab8917d..5428a7b 100644 --- a/internal/configfile/validate.go +++ b/internal/configfile/validate.go @@ -9,7 +9,7 @@ import ( // Validate that the combination of settings makes sense and is supported func (cf *ConfFile) Validate() error { if cf.Version != contentenc.CurrentVersion { - return fmt.Errorf("Unsupported on-disk format %d", cf.Version) + return fmt.Errorf("unsupported on-disk format %d", cf.Version) } // scrypt params ok? if err := cf.ScryptObject.validateParams(); err != nil { @@ -18,13 +18,13 @@ func (cf *ConfFile) Validate() error { // All feature flags that are in the config file are known? for _, flag := range cf.FeatureFlags { if !isFeatureFlagKnown(flag) { - return fmt.Errorf("Unknown feature flag %q", flag) + return fmt.Errorf("unknown feature flag %q", flag) } } // File content encryption { if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV) { - return fmt.Errorf("Can't have both XChaCha20Poly1305 and AESSIV feature flags") + return fmt.Errorf("can't have both XChaCha20Poly1305 and AESSIV feature flags") } if cf.IsFeatureFlagSet(FlagAESSIV) && !cf.IsFeatureFlagSet(FlagGCMIV128) { diff --git a/internal/contentenc/bpool.go b/internal/contentenc/bpool.go index c4517d3..c62170d 100644 --- a/internal/contentenc/bpool.go +++ b/internal/contentenc/bpool.go @@ -26,6 +26,7 @@ func (b *bPool) Put(s []byte) { if len(s) != b.sliceLen { log.Panicf("wrong len=%d, want=%d", len(s), b.sliceLen) } + //lint:ignore SA6002 We intentionally pass slice by value to avoid allocation overhead in this specific use case b.Pool.Put(s) } diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index 3005bf5..5bf0b3c 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -151,7 +151,7 @@ func (be *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileID []b if len(ciphertext) < be.cryptoCore.IVLen { tlog.Warn.Printf("DecryptBlock: Block is too short: %d bytes", len(ciphertext)) - return nil, errors.New("Block is too short") + return nil, errors.New("block is too short") } // Extract nonce diff --git a/internal/cryptocore/hkdf.go b/internal/cryptocore/hkdf.go index b56f507..369616a 100644 --- a/internal/cryptocore/hkdf.go +++ b/internal/cryptocore/hkdf.go @@ -1,10 +1,9 @@ package cryptocore import ( + "crypto/hkdf" "crypto/sha256" "log" - - "golang.org/x/crypto/hkdf" ) const ( @@ -19,12 +18,10 @@ const ( // hkdfDerive derives "outLen" bytes from "masterkey" and "info" using // HKDF-SHA256 (RFC 5869). // It returns the derived bytes or panics. -func hkdfDerive(masterkey []byte, info string, outLen int) (out []byte) { - h := hkdf.New(sha256.New, masterkey, nil, []byte(info)) - out = make([]byte, outLen) - n, err := h.Read(out) - if n != outLen || err != nil { - log.Panicf("hkdfDerive: hkdf read failed, got %d bytes, error: %v", n, err) +func hkdfDerive(masterkey []byte, info string, outLen int) []byte { + key, err := hkdf.Key(sha256.New, masterkey, nil, info, outLen) + if err != nil { + log.Panicf("hkdfDerive: hkdf failed with error: %v", err) } - return out + return key } diff --git a/internal/cryptocore/nonce.go b/internal/cryptocore/nonce.go index 9df094c..c800807 100644 --- a/internal/cryptocore/nonce.go +++ b/internal/cryptocore/nonce.go @@ -11,6 +11,8 @@ func RandBytes(n int) []byte { b := make([]byte, n) _, err := rand.Read(b) if err != nil { + // crypto/rand.Read() is documented to never return an + // error, so this should never happen. Still, better safe than sorry. log.Panic("Failed to read random bytes: " + err.Error()) } return b diff --git a/internal/ctlsocksrv/ctlsock_serve.go b/internal/ctlsocksrv/ctlsock_serve.go index 85f5b65..25d1e44 100644 --- a/internal/ctlsocksrv/ctlsock_serve.go +++ b/internal/ctlsocksrv/ctlsock_serve.go @@ -101,7 +101,7 @@ func (ch *ctlSockHandler) handleRequest(in *ctlsock.RequestStruct, conn *net.Uni } // Neither encryption nor encryption has been requested, makes no sense if in.DecryptPath == "" && in.EncryptPath == "" { - err = errors.New("Empty input") + err = errors.New("empty input") sendResponse(conn, err, "", "") return } @@ -118,7 +118,7 @@ func (ch *ctlSockHandler) handleRequest(in *ctlsock.RequestStruct, conn *net.Uni } // Error out if the canonical path is now empty if clean == "" { - err = errors.New("Empty input after canonicalization") + err = errors.New("empty input after canonicalization") sendResponse(conn, err, "", warnText) return } diff --git a/internal/exitcodes/exitcodes.go b/internal/exitcodes/exitcodes.go index 508ba38..881afda 100644 --- a/internal/exitcodes/exitcodes.go +++ b/internal/exitcodes/exitcodes.go @@ -3,7 +3,7 @@ package exitcodes import ( - "fmt" + "errors" "os" ) @@ -83,7 +83,7 @@ type Err struct { // NewErr returns an error containing "msg" and the exit code "code". func NewErr(msg string, code int) Err { return Err{ - error: fmt.Errorf(msg), + error: errors.New(msg), code: code, } } diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 64a5923..ec3d1c2 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -51,4 +51,6 @@ type Args struct { OneFileSystem bool // DeterministicNames disables gocryptfs.diriv files DeterministicNames bool + // NoXattr disables extended attribute operations + NoXattr bool } diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 0e25de3..64c6ca0 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -37,8 +37,6 @@ type File struct { // Every FUSE entrypoint should RLock(). The only user of Lock() is // Release(), which closes the fd and sets "released" to true. fdLock sync.RWMutex - // Content encryption helper - contentEnc *contentenc.ContentEnc // Device and inode number uniquely identify the backing file qIno inomap.QIno // Entry in the open file table @@ -50,6 +48,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 @@ -71,7 +71,6 @@ func NewFile(fd int, cName string, rn *RootNode) (f *File, st *syscall.Stat_t, e f = &File{ fd: osFile, - contentEnc: rn.contentEnc, qIno: qi, fileTableEntry: e, rootNode: rn, @@ -116,7 +115,7 @@ func (f *File) createHeader() (fileID []byte, err error) { h := contentenc.RandomHeader() buf := h.Pack() // Prevent partially written (=corrupt) header by preallocating the space beforehand - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), 0, contentenc.HeaderLen) if err != nil { if !syscallcompat.IsENOSPC(err) { @@ -175,7 +174,7 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er log.Panicf("fileID=%v", fileID) } // Read the backing ciphertext in one go - blocks := f.contentEnc.ExplodePlainRange(off, length) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, length) alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) // f.fd.ReadAt takes an int64! if alignedOffset > math.MaxInt64 { @@ -204,10 +203,10 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er tlog.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d", alignedOffset, firstBlockNo, alignedLength, n) // Decrypt it - plaintext, err := f.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) + plaintext, err := f.rootNode.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) f.rootNode.contentEnc.CReqPool.Put(ciphertext) if err != nil { - corruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) + corruptBlockNo := firstBlockNo + f.rootNode.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, corruptBlockNo, err) return nil, syscall.EIO } @@ -285,20 +284,20 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { } // Handle payload data dataBuf := bytes.NewBuffer(data) - blocks := f.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) + blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) toEncrypt := make([][]byte, len(blocks)) for i, b := range blocks { blockData := dataBuf.Next(int(b.Length)) // Incomplete block -> Read-Modify-Write if b.IsPartial() { // Read - oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.contentEnc.PlainBS()) + oldData, errno := f.doRead(nil, b.BlockPlainOff(), f.rootNode.contentEnc.PlainBS()) if errno != 0 { tlog.Warn.Printf("ino%d fh%d: RMW read failed: errno=%d", f.qIno.Ino, f.intFd(), errno) return 0, errno } // Modify - blockData = f.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) + blockData = f.rootNode.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) tlog.Debug.Printf("len(oldData)=%d len(blockData)=%d", len(oldData), len(blockData)) } tlog.Debug.Printf("ino%d: Writing %d bytes to block #%d", @@ -307,7 +306,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { toEncrypt[i] = blockData } // Encrypt all blocks - ciphertext := f.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) + ciphertext := f.rootNode.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, f.fileTableEntry.ID) // Preallocate so we cannot run out of space in the middle of the write. // This prevents partially written (=corrupt) blocks. var err error @@ -316,7 +315,7 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { if cOff > math.MaxInt64 { return 0, syscall.EFBIG } - if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBrokenFalloc == 0 { + if !f.rootNode.args.NoPrealloc && f.rootNode.quirks&syscallcompat.QuirkBtrfsBrokenFalloc == 0 { err = syscallcompat.EnospcPrealloc(f.intFd(), int64(cOff), int64(len(ciphertext))) if err != nil { if !syscallcompat.IsENOSPC(err) { @@ -437,7 +436,10 @@ func (f *File) Getattr(ctx context.Context, a *fuse.AttrOut) syscall.Errno { } f.rootNode.inoMap.TranslateStat(&st) a.FromStat(&st) - a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) + if a.IsRegular() { + a.Size = f.rootNode.contentEnc.CipherSizeToPlainSize(a.Size) + } + // TODO: Handle symlink size similar to node.translateSize() if f.rootNode.args.ForceOwner != nil { a.Owner = *f.rootNode.args.ForceOwner } diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index cae796e..a3decf9 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -54,7 +54,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) f.fileTableEntry.ContentLock.Lock() defer f.fileTableEntry.ContentLock.Unlock() - blocks := f.contentEnc.ExplodePlainRange(off, sz) + blocks := f.rootNode.contentEnc.ExplodePlainRange(off, sz) firstBlock := blocks[0] lastBlock := blocks[len(blocks)-1] @@ -63,7 +63,7 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) // the file. cipherOff := firstBlock.BlockCipherOff() cipherSz := lastBlock.BlockCipherOff() - cipherOff + - f.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length + f.rootNode.contentEnc.BlockOverhead() + lastBlock.Skip + lastBlock.Length err := syscallcompat.Fallocate(f.intFd(), FALLOC_FL_KEEP_SIZE, int64(cipherOff), int64(cipherSz)) tlog.Debug.Printf("Allocate off=%d sz=%d mode=%x cipherOff=%d cipherSz=%d\n", off, sz, mode, cipherOff, cipherSz) @@ -113,8 +113,8 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { return fs.ToErrno(err) } - oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) - newB := float32(newSize) / float32(f.contentEnc.PlainBS()) + oldB := float32(oldSize) / float32(f.rootNode.contentEnc.PlainBS()) + newB := float32(newSize) / float32(f.rootNode.contentEnc.PlainBS()) tlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.qIno.Ino, oldB, newB, oldSize, newSize) // File size stays the same - nothing to do @@ -127,9 +127,9 @@ func (f *File) truncate(newSize uint64) (errno syscall.Errno) { } // File shrinks - blockNo := f.contentEnc.PlainOffToBlockNo(newSize) - cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) - plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) + blockNo := f.rootNode.contentEnc.PlainOffToBlockNo(newSize) + cipherOff := f.rootNode.contentEnc.BlockNoToCipherOff(blockNo) + plainOff := f.rootNode.contentEnc.BlockNoToPlainOff(blockNo) lastBlockLen := newSize - plainOff var data []byte if lastBlockLen > 0 { @@ -161,7 +161,7 @@ func (f *File) statPlainSize() (uint64, error) { return 0, err } cipherSz := uint64(fi.Size()) - plainSz := uint64(f.contentEnc.CipherSizeToPlainSize(cipherSz)) + plainSz := uint64(f.rootNode.contentEnc.CipherSizeToPlainSize(cipherSz)) return plainSz, nil } @@ -174,8 +174,8 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } newEOFOffset := newPlainSz - 1 if oldPlainSz > 0 { - n1 := f.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) - n2 := f.contentEnc.PlainOffToBlockNo(newEOFOffset) + n1 := f.rootNode.contentEnc.PlainOffToBlockNo(oldPlainSz - 1) + n2 := f.rootNode.contentEnc.PlainOffToBlockNo(newEOFOffset) // The file is grown within one block, no need to pad anything. // Write a single zero to the last byte and let doWrite figure out the RMW. if n1 == n2 { @@ -194,7 +194,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } // The new size is block-aligned. In this case we can do everything ourselves // and avoid the call to doWrite. - if newPlainSz%f.contentEnc.PlainBS() == 0 { + if newPlainSz%f.rootNode.contentEnc.PlainBS() == 0 { // The file was empty, so it did not have a header. Create one. if oldPlainSz == 0 { id, err := f.createHeader() @@ -203,7 +203,7 @@ func (f *File) truncateGrowFile(oldPlainSz uint64, newPlainSz uint64) syscall.Er } f.fileTableEntry.ID = id } - cSz := int64(f.contentEnc.PlainSizeToCipherSize(newPlainSz)) + cSz := int64(f.rootNode.contentEnc.PlainSizeToCipherSize(newPlainSz)) err := syscall.Ftruncate(f.intFd(), cSz) if err != nil { tlog.Warn.Printf("Truncate: grow Ftruncate returned error: %v", err) 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/file_holes.go b/internal/fusefrontend/file_holes.go index f35fa70..fc58898 100644 --- a/internal/fusefrontend/file_holes.go +++ b/internal/fusefrontend/file_holes.go @@ -21,12 +21,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { tlog.Warn.Printf("checkAndPadHole: Fstat failed: %v", err) return fs.ToErrno(err) } - plainSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) + plainSize := f.rootNode.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) // Appending a single byte to the file (equivalent to writing to // offset=plainSize) would write to "nextBlock". - nextBlock := f.contentEnc.PlainOffToBlockNo(plainSize) + nextBlock := f.rootNode.contentEnc.PlainOffToBlockNo(plainSize) // targetBlock is the block the user wants to write to. - targetBlock := f.contentEnc.PlainOffToBlockNo(uint64(targetOff)) + targetBlock := f.rootNode.contentEnc.PlainOffToBlockNo(uint64(targetOff)) // The write goes into an existing block or (if the last block was full) // starts a new one directly after the last block. Nothing to do. if targetBlock <= nextBlock { @@ -45,12 +45,12 @@ func (f *File) writePadHole(targetOff int64) syscall.Errno { // Zero-pad the file of size plainSize to the next block boundary. This is a no-op // if the file is already block-aligned. func (f *File) zeroPad(plainSize uint64) syscall.Errno { - lastBlockLen := plainSize % f.contentEnc.PlainBS() + lastBlockLen := plainSize % f.rootNode.contentEnc.PlainBS() if lastBlockLen == 0 { // Already block-aligned return 0 } - missing := f.contentEnc.PlainBS() - lastBlockLen + missing := f.rootNode.contentEnc.PlainBS() - lastBlockLen pad := make([]byte, missing) tlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) _, errno := f.doWrite(pad, int64(plainSize)) diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 687a386..95be48d 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -71,28 +71,54 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) (errno syscall.Errno) { // If the kernel gives us a file handle, use it. if f != nil { - return f.(fs.FileGetattrer).Getattr(ctx, out) + if fga, ok := f.(fs.FileGetattrer); ok { + 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..97327ce 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" @@ -137,12 +136,6 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En } defer syscall.Close(fd) - err = syscall.Fstat(fd, &st) - if err != nil { - tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) - return nil, fs.ToErrno(err) - } - // Fix permissions if origMode != mode { // Preserve SGID bit if it was set due to inheritance. @@ -151,97 +144,17 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En if err != nil { tlog.Warn.Printf("Mkdir %q: Fchmod %#o -> %#o failed: %v", cName, mode, origMode, err) } - } - // Create child node & return - ch := n.newChild(ctx, &st, out) - 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) + err = syscall.Fstat(fd, &st) if err != nil { + tlog.Warn.Printf("Mkdir %q: Fstat failed: %v", cName, err) 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 + // Create child node & return + ch := n.newChild(ctx, &st, out) + return ch, 0 } // Rmdir - FUSE call. 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_open_create.go b/internal/fusefrontend/node_open_create.go index 9598559..622d5dc 100644 --- a/internal/fusefrontend/node_open_create.go +++ b/internal/fusefrontend/node_open_create.go @@ -2,6 +2,7 @@ package fusefrontend import ( "context" + "os" "syscall" "github.com/hanwen/go-fuse/v2/fs" @@ -12,6 +13,30 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// mangleOpenCreateFlags is used by Create() and Open() to convert the open flags the user +// wants to the flags we internally use to open the backing file using Openat(). +// The returned flags always contain O_NOFOLLOW/O_SYMLINK. +func mangleOpenCreateFlags(flags uint32) (newFlags int) { + newFlags = int(flags) + // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. + if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { + newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR + } + // We also cannot open the file in append mode, we need to seek back for RMW + newFlags = newFlags &^ os.O_APPEND + // O_DIRECT accesses must be aligned in both offset and length. Due to our + // crypto header, alignment will be off, even if userspace makes aligned + // accesses. Running xfstests generic/013 on ext4 used to trigger lots of + // EINVAL errors due to missing alignment. Just fall back to buffered IO. + newFlags = newFlags &^ syscallcompat.O_DIRECT + // Create and Open are two separate FUSE operations, so O_CREAT should usually not + // be part of the Open() flags. Create() will add O_CREAT back itself. + newFlags = newFlags &^ syscall.O_CREAT + // We always want O_NOFOLLOW/O_SYMLINK to be safe against symlink races + newFlags |= syscallcompat.OpenatFlagNofollowSymlink + return newFlags +} + // Open - FUSE call. Open already-existing file. // // Symlink-safe through Openat(). @@ -23,7 +48,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl defer syscall.Close(dirfd) rn := n.rootNode() - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Taking this lock makes sure we don't race openWriteOnlyFile() rn.openWriteOnlyLock.RLock() defer rn.openWriteOnlyLock.RUnlock() @@ -71,7 +96,7 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3 if !rn.args.PreserveOwner { ctx = nil } - newFlags := rn.mangleOpenFlags(flags) + newFlags := mangleOpenCreateFlags(flags) // Handle long file name ctx2 := toFuseCtx(ctx) if !rn.args.PlaintextNames && nametransform.IsLongContent(cName) { 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/node_xattr.go b/internal/fusefrontend/node_xattr.go index 44bc502..1470a2a 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -12,9 +12,6 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) -// -1 as uint32 -const minus1 = ^uint32(0) - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." @@ -35,6 +32,10 @@ func isAcl(attr string) bool { // This function is symlink-safe through Fgetxattr. func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } // If we are not mounted with -suid, reading the capability xattr does not // make a lot of sense, so reject the request and gain a massive speedup. // See https://github.com/rfjakob/gocryptfs/issues/515 . @@ -50,13 +51,13 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, var errno syscall.Errno data, errno = n.getXAttr(attr) if errno != 0 { - return minus1, errno + return 0, errno } } else { // encrypted user xattr cAttr, err := rn.encryptXattrName(attr) if err != nil { - return minus1, syscall.EIO + return 0, syscall.EIO } cData, errno := n.getXAttr(cAttr) if errno != 0 { @@ -65,15 +66,11 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, data, err = rn.decryptXattrValue(cData) if err != nil { tlog.Warn.Printf("GetXAttr: %v", err) - return minus1, syscall.EIO + return 0, syscall.EIO } } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(len(data)), 0 - } if len(dest) < len(data) { - return minus1, syscall.ERANGE + return uint32(len(data)), syscall.ERANGE } l := copy(dest, data) return uint32(l), 0 @@ -84,6 +81,10 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, // This function is symlink-safe through Fsetxattr. func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uint32) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all setxattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } flags = uint32(filterXattrSetFlags(int(flags))) // ACLs are passed through without encryption @@ -109,6 +110,10 @@ func (n *Node) Setxattr(ctx context.Context, attr string, data []byte, flags uin // This function is symlink-safe through Fremovexattr. func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { rn := n.rootNode() + // If -noxattr is enabled, fail all removexattr calls + if rn.args.NoXattr { + return syscall.EOPNOTSUPP + } // ACLs are passed through without encryption if isAcl(attr) { @@ -126,11 +131,15 @@ func (n *Node) Removexattr(ctx context.Context, attr string) syscall.Errno { // // This function is symlink-safe through Flistxattr. func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } cNames, errno := n.listXAttr() if errno != 0 { return 0, errno } - rn := n.rootNode() var buf bytes.Buffer for _, curName := range cNames { // ACLs are passed through without encryption @@ -155,12 +164,8 @@ func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errn } buf.WriteString(name + "\000") } - // Caller passes size zero to find out how large their buffer should be - if len(dest) == 0 { - return uint32(buf.Len()), 0 - } if buf.Len() > len(dest) { - return minus1, syscall.ERANGE + return uint32(buf.Len()), syscall.ERANGE } return uint32(copy(dest, buf.Bytes())), 0 } diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index a539847..1d25f3d 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -11,6 +11,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + // On Darwin we have to unset XATTR_NOSECURITY 0x0008 func filterXattrSetFlags(flags int) int { // See https://opensource.apple.com/source/xnu/xnu-1504.15.3/bsd/sys/xattr.h.auto.html @@ -26,8 +29,8 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { } defer syscall.Close(dirfd) - // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + // O_NONBLOCK to not block on FIFOs, O_SYMLINK to open the symlink itself (if it is one). + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } @@ -49,10 +52,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { fs.ToErrno(err) @@ -71,10 +74,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) // Directories cannot be opened read-write. Retry. if err == syscall.EISDIR { - fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) } if err != nil { return fs.ToErrno(err) @@ -93,7 +96,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) { defer syscall.Close(dirfd) // O_NONBLOCK to not block on FIFOs. - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_SYMLINK, 0) if err != nil { return nil, fs.ToErrno(err) } diff --git a/internal/fusefrontend/node_xattr_linux.go b/internal/fusefrontend/node_xattr_linux.go index 4a356a5..9964212 100644 --- a/internal/fusefrontend/node_xattr_linux.go +++ b/internal/fusefrontend/node_xattr_linux.go @@ -12,6 +12,9 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" ) +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + func filterXattrSetFlags(flags int) int { return flags } diff --git a/internal/fusefrontend/prepare_syscall_test.go b/internal/fusefrontend/prepare_syscall_test.go index acddaf3..e2c7d08 100644 --- a/internal/fusefrontend/prepare_syscall_test.go +++ b/internal/fusefrontend/prepare_syscall_test.go @@ -1,6 +1,7 @@ package fusefrontend import ( + "context" "strings" "syscall" "testing" @@ -22,13 +23,13 @@ func TestPrepareAtSyscall(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } @@ -43,7 +44,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) // Again, but populate the cache for "" by looking up a non-existing file - rn.Lookup(nil, "xyz1234", &fuse.EntryOut{}) + rn.Lookup(context.TODO(), "xyz1234", &fuse.EntryOut{}) dirfd, cName, errno = rn.prepareAtSyscallMyself() if errno != 0 { t.Fatal(errno) @@ -89,7 +90,7 @@ func TestPrepareAtSyscall(t *testing.T) { syscall.Close(dirfd) n255 := strings.Repeat("n", 255) - dir1.Mkdir(nil, n255, 0700, out) + dir1.Mkdir(context.TODO(), n255, 0700, out) dirfd, cName, errno = dir1.prepareAtSyscall(n255) if errno != 0 { t.Fatal(errno) @@ -116,13 +117,13 @@ func TestPrepareAtSyscallPlaintextnames(t *testing.T) { rn := newTestFS(args) out := &fuse.EntryOut{} - child, errno := rn.Mkdir(nil, "dir1", 0700, out) + child, errno := rn.Mkdir(context.TODO(), "dir1", 0700, out) if errno != 0 { t.Fatal(errno) } rn.AddChild("dir1", child, false) dir1 := toNode(child.Operations()) - _, errno = dir1.Mkdir(nil, "dir2", 0700, out) + _, errno = dir1.Mkdir(context.TODO(), "dir2", 0700, out) if errno != 0 { t.Fatal(errno) } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index ac814ad..38d070d 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -1,9 +1,9 @@ package fusefrontend import ( - "os" "strings" "sync" + "sync/atomic" "syscall" "time" @@ -44,7 +44,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 +55,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 @@ -90,6 +90,12 @@ func NewRootNode(args Args, c *contentenc.ContentEnc, n *nametransform.NameTrans dirCache: dirCache{ivLen: ivLen}, quirks: syscallcompat.DetectQuirks(args.Cipherdir), } + // Suppress the message if the user has already specified -noprealloc + if rn.quirks&syscallcompat.QuirkBtrfsBrokenFalloc != 0 && !args.NoPrealloc { + syscallcompat.LogQuirk("Btrfs detected, forcing -noprealloc. " + + "Use \"chattr +C\" on the backing directory to enable NOCOW and allow preallocation. " + + "See https://github.com/rfjakob/gocryptfs/issues/395 for details.") + } if statErr == nil { rn.inoMap.TranslateStat(&st) rn.rootIno = st.Ino @@ -103,30 +109,6 @@ func (rn *RootNode) AfterUnmount() { rn.dirCache.stats() } -// mangleOpenFlags is used by Create() and Open() to convert the open flags the user -// wants to the flags we internally use to open the backing file. -// The returned flags always contain O_NOFOLLOW. -func (rn *RootNode) mangleOpenFlags(flags uint32) (newFlags int) { - newFlags = int(flags) - // Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles. - if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY { - newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR - } - // We also cannot open the file in append mode, we need to seek back for RMW - newFlags = newFlags &^ os.O_APPEND - // O_DIRECT accesses must be aligned in both offset and length. Due to our - // crypto header, alignment will be off, even if userspace makes aligned - // accesses. Running xfstests generic/013 on ext4 used to trigger lots of - // EINVAL errors due to missing alignment. Just fall back to buffered IO. - newFlags = newFlags &^ syscallcompat.O_DIRECT - // Create and Open are two separate FUSE operations, so O_CREAT should not - // be part of the open flags. - newFlags = newFlags &^ syscall.O_CREAT - // We always want O_NOFOLLOW to be safe against symlink races - newFlags |= syscall.O_NOFOLLOW - return newFlags -} - // reportMitigatedCorruption is used to report a corruption that was transparently // mitigated and did not return an error to the user. Pass the name of the corrupt // item (filename for OpenDir(), xattr name for ListXAttr() etc). diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 86c87a7..7d8e32e 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -21,10 +21,10 @@ func newTestFS(args Args) *RootNode { cEnc := contentenc.New(cCore, contentenc.DefaultBS) n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false) rn := NewRootNode(args, cEnc, n) - oneSec := time.Second + oneSecond := time.Second options := &fs.Options{ - EntryTimeout: &oneSec, - AttrTimeout: &oneSec, + EntryTimeout: &oneSecond, + AttrTimeout: &oneSecond, } fs.NewNodeFS(rn, options) return rn diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go index 0faadfa..1cb4b80 100644 --- a/internal/fusefrontend_reverse/excluder.go +++ b/internal/fusefrontend_reverse/excluder.go @@ -1,7 +1,6 @@ package fusefrontend_reverse import ( - "io/ioutil" "log" "os" "strings" @@ -50,7 +49,7 @@ func getExclusionPatterns(args fusefrontend.Args) []string { // getLines reads a file and splits it into lines func getLines(file string) ([]string, error) { - buffer, err := ioutil.ReadFile(file) + buffer, err := os.ReadFile(file) if err != nil { return nil, err } diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go index bb041ce..b44ddce 100644 --- a/internal/fusefrontend_reverse/excluder_test.go +++ b/internal/fusefrontend_reverse/excluder_test.go @@ -1,7 +1,6 @@ package fusefrontend_reverse import ( - "io/ioutil" "os" "reflect" "testing" @@ -23,7 +22,7 @@ func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { } func TestShouldReadExcludePatternsFromFiles(t *testing.T) { - tmpfile1, err := ioutil.TempFile("", "excludetest") + tmpfile1, err := os.CreateTemp("", "excludetest") if err != nil { t.Fatal(err) } @@ -31,7 +30,7 @@ func TestShouldReadExcludePatternsFromFiles(t *testing.T) { defer os.Remove(exclude1) defer tmpfile1.Close() - tmpfile2, err := ioutil.TempFile("", "excludetest") + tmpfile2, err := os.CreateTemp("", "excludetest") if err != nil { t.Fatal(err) } diff --git a/internal/fusefrontend_reverse/node_api_check.go b/internal/fusefrontend_reverse/node_api_check.go index f8ec9ce..eb608f9 100644 --- a/internal/fusefrontend_reverse/node_api_check.go +++ b/internal/fusefrontend_reverse/node_api_check.go @@ -11,14 +11,8 @@ var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeReadlinker)((*Node)(nil)) var _ = (fs.NodeOpener)((*Node)(nil)) var _ = (fs.NodeStatfser)((*Node)(nil)) - -/* -TODO but low prio. reverse mode in gocryptfs v1 did not have xattr support -either. - var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) -*/ /* Not needed var _ = (fs.NodeOpendirer)((*Node)(nil)) diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 30361bc..f733689 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -24,7 +24,6 @@ const ( // * base64(192 bytes) = 256 bytes (over 255!) // But the PKCS#7 padding is at least one byte. This means we can only use // 175 bytes for the file name. - shortNameMax = 175 ) // translateSize translates the ciphertext size in `out` into plaintext size. @@ -135,7 +134,7 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus if errno != 0 { return } - if rn.isExcludedPlain(filepath.Join(d.cPath, pName)) { + if rn.isExcludedPlain(filepath.Join(d.pPath, pName)) { errno = syscall.EPERM return } @@ -175,7 +174,7 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod errno = fs.ToErrno(err) return } - content := pathiv.Derive(d.cPath, pathiv.PurposeDirIV) + content := rn.deriveDirIV(d.cPath) var vf *VirtualMemNode vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV) if errno != 0 { diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go new file mode 100644 index 0000000..b940339 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr.go @@ -0,0 +1,89 @@ +// Package fusefrontend_reverse interfaces directly with the go-fuse library. +package fusefrontend_reverse + +import ( + "bytes" + "context" + "syscall" + + "github.com/rfjakob/gocryptfs/v2/internal/pathiv" +) + +// We store encrypted xattrs under this prefix plus the base64-encoded +// encrypted original name. +var xattrStorePrefix = "user.gocryptfs." + +// isAcl returns true if the attribute name is for storing ACLs +// +// ACLs are passed through without encryption +func isAcl(attr string) bool { + return attr == "system.posix_acl_access" || attr == "system.posix_acl_default" +} + +// GetXAttr - FUSE call. Reads the value of extended attribute "attr". +// +// This function is symlink-safe through Fgetxattr. +func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return ENOATTR for all getxattr calls + if rn.args.NoXattr { + return 0, noSuchAttributeError + } + var data []byte + // ACLs are passed through without encryption + if isAcl(attr) { + var errno syscall.Errno + data, errno = n.getXAttr(attr) + if errno != 0 { + return 0, errno + } + } else { + pAttr, err := rn.decryptXattrName(attr) + if err != nil { + return 0, noSuchAttributeError + } + pData, errno := n.getXAttr(pAttr) + if errno != 0 { + return 0, errno + } + nonce := pathiv.Derive(n.Path()+"\000"+attr, pathiv.PurposeXattrIV) + data = rn.encryptXattrValue(pData, nonce) + } + if len(dest) < len(data) { + return uint32(len(data)), syscall.ERANGE + } + l := copy(dest, data) + return uint32(l), 0 +} + +// ListXAttr - FUSE call. Lists extended attributes on the file at "relPath". +// +// This function is symlink-safe through Flistxattr. +func (n *Node) Listxattr(ctx context.Context, dest []byte) (uint32, syscall.Errno) { + rn := n.rootNode() + // If -noxattr is enabled, return zero results for listxattr + if rn.args.NoXattr { + return 0, 0 + } + pNames, errno := n.listXAttr() + if errno != 0 { + return 0, errno + } + var buf bytes.Buffer + for _, pName := range pNames { + // ACLs are passed through without encryption + if isAcl(pName) { + buf.WriteString(pName + "\000") + continue + } + cName, err := rn.encryptXattrName(pName) + if err != nil { + continue + } + buf.WriteString(cName + "\000") + } + if buf.Len() > len(dest) { + return uint32(buf.Len()), syscall.ERANGE + } + return uint32(copy(dest, buf.Bytes())), 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_darwin.go b/internal/fusefrontend_reverse/node_xattr_darwin.go new file mode 100644 index 0000000..6816a18 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_darwin.go @@ -0,0 +1,55 @@ +package fusefrontend_reverse + +import ( + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Darwin, ENOATTR is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENOATTR + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + cData, err := syscallcompat.Fgetxattr(fd, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + + return cData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + // O_NONBLOCK to not block on FIFOs. + fd, err := syscallcompat.Openat(d.dirfd, d.pName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0) + if err != nil { + return nil, fs.ToErrno(err) + } + defer syscall.Close(fd) + + pNames, err := syscallcompat.Flistxattr(fd) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go new file mode 100644 index 0000000..3c574f5 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_linux.go @@ -0,0 +1,43 @@ +package fusefrontend_reverse + +import ( + "fmt" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +// On Linux, ENODATA is returned when an attribute is not found. +const noSuchAttributeError = syscall.ENODATA + +func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pData, err := syscallcompat.Lgetxattr(procPath, cAttr) + if err != nil { + return nil, fs.ToErrno(err) + } + return pData, 0 +} + +func (n *Node) listXAttr() (out []string, errno syscall.Errno) { + d, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(d.dirfd) + + procPath := fmt.Sprintf("/proc/self/fd/%d/%s", d.dirfd, d.pName) + pNames, err := syscallcompat.Llistxattr(procPath) + if err != nil { + return nil, fs.ToErrno(err) + } + return pNames, 0 +} diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index cb04151..1a668af 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -13,6 +13,7 @@ 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/contentenc" "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" "github.com/rfjakob/gocryptfs/v2/internal/fusefrontend" @@ -21,7 +22,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 +51,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 } @@ -136,7 +137,8 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p // excluded (used when -exclude is passed by the user). func (rn *RootNode) isExcludedPlain(pPath string) bool { // root dir can't be excluded - if pPath == "" { + // Don't exclude gocryptfs.conf too + if pPath == "" || pPath == configfile.ConfReverseName { return false } return rn.excluder != nil && rn.excluder.MatchesPath(pPath) @@ -175,10 +177,45 @@ 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), } } func (rn *RootNode) RootIno() uint64 { return rn.rootIno } + +// encryptXattrValue encrypts the xattr value "data". +// The data is encrypted like a file content block, but without binding it to +// a file location (block number and file id are set to zero). +// Special case: an empty value is encrypted to an empty value. +func (rn *RootNode) encryptXattrValue(data []byte, nonce []byte) (cData []byte) { + if len(data) == 0 { + return []byte{} + } + return rn.contentEnc.EncryptBlockNonce(data, 0, nil, nonce) +} + +// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" +func (rn *RootNode) encryptXattrName(attr string) (string, error) { + // xattr names are encrypted like file names, but with a fixed IV. + cAttr, err := rn.nameTransform.EncryptXattrName(attr) + if err != nil { + return "", err + } + return xattrStorePrefix + cAttr, nil +} + +func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) { + // Reject anything that does not start with "user.gocryptfs." + if !strings.HasPrefix(cAttr, xattrStorePrefix) { + return "", syscall.EINVAL + } + // Strip "user.gocryptfs." prefix + cAttr = cAttr[len(xattrStorePrefix):] + attr, err = rn.nameTransform.DecryptXattrName(cAttr) + if err != nil { + return "", err + } + return attr, nil +} diff --git a/internal/fusefrontend_reverse/virtualnode.go b/internal/fusefrontend_reverse/virtualnode.go index 732564a..95e71ab 100644 --- a/internal/fusefrontend_reverse/virtualnode.go +++ b/internal/fusefrontend_reverse/virtualnode.go @@ -100,6 +100,15 @@ func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, ino st.Nlink = 1 var a fuse.Attr a.FromStat(st) + // With inode number reuse and hard links, we could have returned + // wrong data for gocryptfs.diriv and gocryptfs.xyz.longname files, respectively + // (https://github.com/rfjakob/gocryptfs/issues/802). + // + // Now that this is fixed, ensure that rsync and similar tools pick up the new + // correct files by advancing mtime and ctime by 10 seconds, which should be more + // than any filesytems' timestamp granularity (FAT32 has 2 seconds). + a.Mtime += 10 + a.Ctime += 10 if rn.args.ForceOwner != nil { a.Owner = *rn.args.ForceOwner } 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/nametransform/diriv.go b/internal/nametransform/diriv.go index 7929c40..5dd4940 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -67,7 +67,7 @@ func fdReadDirIV(fd *os.File) (iv []byte, err error) { func WriteDirIVAt(dirfd int) error { iv := cryptocore.RandBytes(DirIVLen) // 0400 permissions: gocryptfs.diriv should never be modified after creation. - // Don't use "ioutil.WriteFile", it causes trouble on NFS: + // Don't use "os.WriteFile", it causes trouble on NFS: // https://github.com/rfjakob/gocryptfs/commit/7d38f80a78644c8ec4900cc990bfb894387112ed fd, err := syscallcompat.Openat(dirfd, DirIVFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, dirivPerms) if err != nil { diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 3313a7c..0389c95 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -7,9 +7,12 @@ import ( "errors" "math" "path/filepath" + "runtime" "strings" "syscall" + "golang.org/x/text/unicode/norm" + "github.com/rfjakob/eme" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -32,6 +35,12 @@ type NameTransform struct { // Patterns to bypass decryption badnamePatterns []string deterministicNames bool + // Convert filenames to NFC before encrypting, + // and to NFD when decrypting. + // For MacOS compatibility. + // Automatically enabled on MacOS, off otherwise, + // except in tests (see nfc_test.go). + nfd2nfc bool } // New returns a new NameTransform instance. @@ -55,34 +64,44 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam effectiveLongNameMax = int(longNameMax) } } + nfd2nfc := runtime.GOOS == "darwin" + if nfd2nfc { + tlog.Info.Printf("Running on MacOS, enabling Unicode normalization") + } return &NameTransform{ emeCipher: e, longNameMax: effectiveLongNameMax, B64: b64, badnamePatterns: badname, deterministicNames: deterministicNames, + nfd2nfc: nfd2nfc, } } // DecryptName calls decryptName to try and decrypt a base64-encoded encrypted // filename "cipherName", and failing that checks if it can be bypassed -func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { - res, err := n.decryptName(cipherName, iv) +func (n *NameTransform) DecryptName(cipherName string, iv []byte) (plainName string, err error) { + plainName, err = n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - res, err = n.decryptBadname(cipherName, iv) + plainName, err = n.decryptBadname(cipherName, iv) } if err != nil { return "", err } - if err := IsValidName(res); err != nil { + if err := IsValidName(plainName); err != nil { tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) return "", syscall.EBADMSG } - return res, err + if n.nfd2nfc { + // MacOS expects file names in NFD form. Present them as NFD. + // They are converted back to NFC in EncryptName. + plainName = norm.NFD.String(plainName) + } + return plainName, err } -// decryptName decrypts a base64-encoded encrypted filename "cipherName" using the -// initialization vector "iv". +// decryptName decrypts a base64-encoded encrypted file- or xattr-name "cipherName" +// using the initialization vector "iv". func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error) { // From https://pkg.go.dev/encoding/base64#Encoding.Strict : // > Note that the input is still malleable, as new line characters @@ -126,6 +145,16 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + if n.nfd2nfc { + // MacOS GUI apps expect Unicode in NFD form. + // But MacOS CLI apps, Linux and Windows use NFC form. + // We normalize to NFC for two reasons: + // 1) Make sharing gocryptfs filesystems from MacOS to other systems + // less painful + // 2) Enable DecryptName to normalize to NFD, which works for both + // GUI and CLI on MacOS. + plainName = norm.NFC.String(plainName) + } return n.encryptName(plainName, iv), nil } diff --git a/internal/nametransform/nfc_test.go b/internal/nametransform/nfc_test.go new file mode 100644 index 0000000..aad1d7d --- /dev/null +++ b/internal/nametransform/nfc_test.go @@ -0,0 +1,29 @@ +package nametransform + +import ( + "strconv" + "testing" + + "golang.org/x/text/unicode/norm" +) + +func TestNFD2NFC(t *testing.T) { + n := newLognamesTestInstance(NameMax) + n.nfd2nfc = true + iv := make([]byte, DirIVLen) + srcNFC := "Österreich Café" + srcNFD := norm.NFD.String(srcNFC) + + // cipherName should get normalized to NFC + cipherName, _ := n.EncryptName(srcNFD, iv) + // Decrypt without changing normalization + decryptedRaw, _ := n.decryptName(cipherName, iv) + if srcNFC != decryptedRaw { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFC), strconv.QuoteToASCII(decryptedRaw)) + } + // Decrypt with normalizing to NFD + decrypted, _ := n.DecryptName(cipherName, iv) + if srcNFD != decrypted { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFD), strconv.QuoteToASCII(decrypted)) + } +} diff --git a/internal/nametransform/pad16.go b/internal/nametransform/pad16.go index 833be0e..2c2466a 100644 --- a/internal/nametransform/pad16.go +++ b/internal/nametransform/pad16.go @@ -32,10 +32,10 @@ func pad16(orig []byte) (padded []byte) { func unPad16(padded []byte) ([]byte, error) { oldLen := len(padded) if oldLen == 0 { - return nil, errors.New("Empty input") + return nil, errors.New("empty input") } if oldLen%aes.BlockSize != 0 { - return nil, errors.New("Unaligned size") + return nil, errors.New("unaligned size") } // The last byte is always a padding byte padByte := padded[oldLen-1] @@ -43,20 +43,20 @@ func unPad16(padded []byte) ([]byte, error) { padLen := int(padByte) // Padding must be at least 1 byte if padLen == 0 { - return nil, errors.New("Padding cannot be zero-length") + return nil, errors.New("padding cannot be zero-length") } // Padding more than 16 bytes make no sense if padLen > aes.BlockSize { - return nil, fmt.Errorf("Padding too long, padLen=%d > 16", padLen) + return nil, fmt.Errorf("padding too long, padLen=%d > 16", padLen) } // Padding cannot be as long as (or longer than) the whole string, if padLen >= oldLen { - return nil, fmt.Errorf("Padding too long, oldLen=%d >= padLen=%d", oldLen, padLen) + return nil, fmt.Errorf("padding too long, oldLen=%d >= padLen=%d", oldLen, padLen) } // All padding bytes must be identical for i := oldLen - padLen; i < oldLen; i++ { if padded[i] != padByte { - return nil, fmt.Errorf("Padding byte at i=%d is invalid", i) + return nil, fmt.Errorf("padding byte at i=%d is invalid", i) } } newLen := oldLen - padLen 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/pathiv/pathiv.go b/internal/pathiv/pathiv.go index 48f8426..11a7189 100644 --- a/internal/pathiv/pathiv.go +++ b/internal/pathiv/pathiv.go @@ -20,6 +20,8 @@ const ( PurposeSymlinkIV Purpose = "SYMLINKIV" // PurposeBlock0IV means the value will be used as the IV of ciphertext block #0. PurposeBlock0IV Purpose = "BLOCK0IV" + // PurposeXattrIV means the value will be used as a xattr IV + PurposeXattrIV Purpose = "XATTRIV" ) // Derive derives an IV from an encrypted path by hashing it with sha256 diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index 582a104..9193ae8 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -58,7 +58,7 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { return nil, err } if !bytes.Equal(p1, p2) { - return nil, fmt.Errorf("Passwords do not match") + return nil, fmt.Errorf("passwords do not match") } // Wipe the password duplicate from memory for i := range p2 { @@ -71,15 +71,15 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { // Exits on read error or empty result. func readPasswordTerminal(prompt string) ([]byte, error) { fd := int(os.Stdin.Fd()) - fmt.Fprintf(os.Stderr, prompt) + fmt.Fprint(os.Stderr, prompt) // term.ReadPassword removes the trailing newline p, err := term.ReadPassword(fd) if err != nil { - return nil, fmt.Errorf("Could not read password from terminal: %v\n", err) + return nil, fmt.Errorf("could not read password from terminal: %v", err) } fmt.Fprintf(os.Stderr, "\n") if len(p) == 0 { - return nil, fmt.Errorf("Password is empty") + return nil, fmt.Errorf("password is empty") } return p, nil } @@ -100,7 +100,7 @@ func readPasswordStdin(prompt string) ([]byte, error) { return nil, err } if len(p) == 0 { - return nil, fmt.Errorf("Got empty %s from stdin", prompt) + return nil, fmt.Errorf("got empty %s from stdin", prompt) } return p, nil } diff --git a/internal/siv_aead/correctness_test.go b/internal/siv_aead/correctness_test.go index 0653e26..7be97bc 100644 --- a/internal/siv_aead/correctness_test.go +++ b/internal/siv_aead/correctness_test.go @@ -50,7 +50,7 @@ func TestK32(t *testing.T) { expectedResult, _ := hex.DecodeString( "02020202020202020202020202020202ad7a4010649a84d8c1dd5f752e935eed57d45b8b10008f3834") if !bytes.Equal(aResult, expectedResult) { - t.Errorf(hex.EncodeToString(aResult)) + t.Error(hex.EncodeToString(aResult)) } // Verify overhead overhead := len(aResult) - len(plaintext) - len(nonce) @@ -108,7 +108,7 @@ func TestK64(t *testing.T) { expectedResult, _ := hex.DecodeString( "02020202020202020202020202020202317b316f67c3ad336c01c9a01b4c5e552ba89e966bc4c1ade1") if !bytes.Equal(aResult, expectedResult) { - t.Errorf(hex.EncodeToString(aResult)) + t.Error(hex.EncodeToString(aResult)) } // Verify overhead overhead := len(aResult) - len(plaintext) - len(nonce) diff --git a/internal/speed/cpuinfo.go b/internal/speed/cpuinfo.go index df3177d..636a4f0 100644 --- a/internal/speed/cpuinfo.go +++ b/internal/speed/cpuinfo.go @@ -1,7 +1,7 @@ package speed import ( - "io/ioutil" + "io" "os" "runtime" "strings" @@ -33,7 +33,7 @@ func cpuModelName() string { if err != nil { return "" } - content, err := ioutil.ReadAll(f) + content, err := io.ReadAll(f) if err != nil { return "" } diff --git a/internal/stupidgcm/chacha.go b/internal/stupidgcm/chacha.go index de0c2e8..c500ea5 100644 --- a/internal/stupidgcm/chacha.go +++ b/internal/stupidgcm/chacha.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/chacha_test.go b/internal/stupidgcm/chacha_test.go index 542ff15..356c813 100644 --- a/internal/stupidgcm/chacha_test.go +++ b/internal/stupidgcm/chacha_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common.go b/internal/stupidgcm/common.go index d88dc62..03698b9 100644 --- a/internal/stupidgcm/common.go +++ b/internal/stupidgcm/common.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index 7f38e90..633f279 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -1,5 +1,4 @@ //go:build cgo && !without_openssl -// +build cgo,!without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm.go b/internal/stupidgcm/gcm.go index 2e5aac4..274d3df 100644 --- a/internal/stupidgcm/gcm.go +++ b/internal/stupidgcm/gcm.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm_test.go b/internal/stupidgcm/gcm_test.go index c730a87..041dcab 100644 --- a/internal/stupidgcm/gcm_test.go +++ b/internal/stupidgcm/gcm_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl // We compare against Go's built-in GCM implementation. Since stupidgcm only // supports 128-bit IVs and Go only supports that from 1.5 onward, we cannot diff --git a/internal/stupidgcm/locking.go b/internal/stupidgcm/locking.go index 04cf232..00cc361 100644 --- a/internal/stupidgcm/locking.go +++ b/internal/stupidgcm/locking.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go index 8c950f8..3a72f9c 100644 --- a/internal/stupidgcm/openssl.go +++ b/internal/stupidgcm/openssl.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/stupidgcm/openssl_aead.c b/internal/stupidgcm/openssl_aead.c index e02466f..a7f4f59 100644 --- a/internal/stupidgcm/openssl_aead.c +++ b/internal/stupidgcm/openssl_aead.c @@ -1,4 +1,4 @@ -// +build !without_openssl +//go:build cgo && !without_openssl #include "openssl_aead.h" #include <openssl/evp.h> diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go index fcef793..c59ebe6 100644 --- a/internal/stupidgcm/without_openssl.go +++ b/internal/stupidgcm/without_openssl.go @@ -1,5 +1,4 @@ -//go:build without_openssl -// +build without_openssl +//go:build !cgo || without_openssl package stupidgcm diff --git a/internal/stupidgcm/xchacha.go b/internal/stupidgcm/xchacha.go index 3c121ba..cfd69d7 100644 --- a/internal/stupidgcm/xchacha.go +++ b/internal/stupidgcm/xchacha.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl // Copyright 2018 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style @@ -7,7 +6,7 @@ // // Copied from // https://github.com/golang/crypto/blob/32db794688a5a24a23a43f2a984cecd5b3d8da58/chacha20poly1305/xchacha20poly1305.go -// and adapted for stupidgcm by @rfjakob. +// and adapted to use OpenSSL ChaCha20 (see chacha.go) instaed of stdlib by @rfjakob. package stupidgcm diff --git a/internal/stupidgcm/xchacha_test.go b/internal/stupidgcm/xchacha_test.go index 676a023..a448f20 100644 --- a/internal/stupidgcm/xchacha_test.go +++ b/internal/stupidgcm/xchacha_test.go @@ -1,5 +1,4 @@ -//go:build !without_openssl -// +build !without_openssl +//go:build cgo && !without_openssl package stupidgcm diff --git a/internal/syscallcompat/asuser.go b/internal/syscallcompat/asuser.go new file mode 100644 index 0000000..0c083ec --- /dev/null +++ b/internal/syscallcompat/asuser.go @@ -0,0 +1,59 @@ +package syscallcompat + +import ( + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// OpenatUser runs the Openat syscall in the context of a different user. +// It switches the current thread to the new user, performs the syscall, +// and switches back. +// +// If `context` is nil, this function behaves like ordinary Openat (no +// user switching). +func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { + f := func() (int, error) { + return Openat(dirfd, path, flags, mode) + } + return asUser(f, context) +} + +// MknodatUser runs the Mknodat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Mknodat. +// +// See OpenatUser() for how this works. +func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { + f := func() (int, error) { + err := Mknodat(dirfd, path, mode, dev) + return -1, err + } + _, err = asUser(f, context) + return err +} + +// SymlinkatUser runs the Symlinkat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Symlinkat. +// +// See OpenatUser() for how this works. +func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Symlinkat(oldpath, newdirfd, newpath) + return -1, err + } + _, err = asUser(f, context) + return err +} + +// MkdiratUser runs the Mkdirat syscall in the context of a different user. +// If `context` is nil, this function behaves like ordinary Mkdirat. +// +// See OpenatUser() for how this works. +func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Mkdirat(dirfd, path, mode) + return -1, err + } + _, err = asUser(f, context) + return err +} diff --git a/internal/syscallcompat/asuser_darwin.go b/internal/syscallcompat/asuser_darwin.go new file mode 100644 index 0000000..5da2782 --- /dev/null +++ b/internal/syscallcompat/asuser_darwin.go @@ -0,0 +1,46 @@ +package syscallcompat + +import ( + "runtime" + "syscall" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + if context == nil { + return f() + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + err := pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) + if err != nil { + return -1, err + } + + const ( + // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to + // revert permissions to the process credentials. + KAUTH_UID_NONE = ^uint32(0) - 100 + KAUTH_GID_NONE = ^uint32(0) - 100 + ) + + defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) + + return f() +} + +// Unfortunately pthread_setugid_np does not have a syscall wrapper yet. +func pthread_setugid_np(uid uint32, gid uint32) (err error) { + _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/syscallcompat/asuser_linux.go b/internal/syscallcompat/asuser_linux.go new file mode 100644 index 0000000..39e3ff2 --- /dev/null +++ b/internal/syscallcompat/asuser_linux.go @@ -0,0 +1,80 @@ +package syscallcompat + +import ( + "fmt" + "os" + "runtime" + "strconv" + "strings" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + if context == nil { + return f() + } + + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c , + // https://go-review.googlesource.com/c/go/+/210639 ) + // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which + // is exactly what we not want. + // + // And unix.{Setgroups,Setregid,Setreuid} also changed to this behavoir in + // v0.1.0 (commit d0df966e6959f00dc1c74363e537872647352d51 , + // https://go-review.googlesource.com/c/sys/+/428174 ), so we use + // our own syscall wrappers. + + err := Setgroups(getSupplementaryGroups(context.Pid)) + if err != nil { + return -1, err + } + defer SetgroupsPanic(nil) + + err = Setregid(-1, int(context.Owner.Gid)) + if err != nil { + return -1, err + } + defer SetregidPanic(-1, 0) + + err = Setreuid(-1, int(context.Owner.Uid)) + if err != nil { + return -1, err + } + defer SetreuidPanic(-1, 0) + + return f() +} + +func getSupplementaryGroups(pid uint32) (gids []int) { + procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid) + blob, err := os.ReadFile(procPath) + if err != nil { + return nil + } + + lines := strings.Split(string(blob), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "Groups:") { + f := strings.Fields(line[7:]) + gids = make([]int, len(f)) + for i := range gids { + val, err := strconv.ParseInt(f[i], 10, 32) + if err != nil { + return nil + } + gids[i] = int(val) + } + return gids + } + } + + return nil +} diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go index eb670d6..c9e6a99 100644 --- a/internal/syscallcompat/getdents_test.go +++ b/internal/syscallcompat/getdents_test.go @@ -4,7 +4,6 @@ package syscallcompat import ( - "io/ioutil" "os" "runtime" "strings" @@ -49,13 +48,13 @@ func testGetdents(t *testing.T) { getdentsUnderTest = emulateGetdents } // Fill a directory with filenames of length 1 ... 255 - testDir, err := ioutil.TempDir(tmpDir, "TestGetdents") + testDir, err := os.MkdirTemp(tmpDir, "TestGetdents") if err != nil { t.Fatal(err) } for i := 1; i <= unix.NAME_MAX; i++ { n := strings.Repeat("x", i) - err = ioutil.WriteFile(testDir+"/"+n, nil, 0600) + err = os.WriteFile(testDir+"/"+n, nil, 0600) if err != nil { t.Fatal(err) } diff --git a/internal/syscallcompat/main_test.go b/internal/syscallcompat/main_test.go index ddf6bc4..7183f5a 100644 --- a/internal/syscallcompat/main_test.go +++ b/internal/syscallcompat/main_test.go @@ -2,7 +2,6 @@ package syscallcompat import ( "fmt" - "io/ioutil" "os" "testing" ) @@ -23,7 +22,7 @@ func TestMain(m *testing.M) { fmt.Println(err) os.Exit(1) } - tmpDir, err = ioutil.TempDir(parent, "syscallcompat") + tmpDir, err = os.MkdirTemp(parent, "syscallcompat") if err != nil { fmt.Println(err) os.Exit(1) diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go index 858f16d..36bcb9f 100644 --- a/internal/syscallcompat/quirks.go +++ b/internal/syscallcompat/quirks.go @@ -5,18 +5,17 @@ import ( ) const ( - // QuirkBrokenFalloc means the falloc is broken. + // QuirkBtrfsBrokenFalloc means the falloc is broken. // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). - QuirkBrokenFalloc = uint64(1 << iota) + QuirkBtrfsBrokenFalloc = uint64(1 << iota) // QuirkDuplicateIno1 means that we have duplicate inode numbers. // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 QuirkDuplicateIno1 - // QuirkNoUserXattr means that user.* xattrs are not supported - QuirkNoUserXattr ) -func logQuirk(s string) { - tlog.Info.Printf(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) +// LogQuirk prints a yellow message about a detected quirk. +func LogQuirk(s string) { + tlog.Info.Println(tlog.ColorYellow + "DetectQuirks: " + s + tlog.ColorReset) } diff --git a/internal/syscallcompat/quirks_darwin.go b/internal/syscallcompat/quirks_darwin.go index 4adeea1..c4d5006 100644 --- a/internal/syscallcompat/quirks_darwin.go +++ b/internal/syscallcompat/quirks_darwin.go @@ -33,7 +33,7 @@ func DetectQuirks(cipherdir string) (q uint64) { // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 if fstypename == FstypenameExfat { - logQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") + LogQuirk("ExFAT detected, disabling hard links. See https://github.com/rfjakob/gocryptfs/issues/585 for why.") q |= QuirkDuplicateIno1 } diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index 5ef2d8a..35f754d 100644 --- a/internal/syscallcompat/quirks_linux.go +++ b/internal/syscallcompat/quirks_linux.go @@ -1,11 +1,38 @@ package syscallcompat import ( + "syscall" + "golang.org/x/sys/unix" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) +// FS_NOCOW_FL is the flag set by "chattr +C" to disable copy-on-write on +// btrfs. Not exported by golang.org/x/sys/unix, value from linux/fs.h. +const FS_NOCOW_FL = 0x00800000 + +// dirHasNoCow checks whether the directory at the given path has the +// NOCOW (No Copy-on-Write) attribute set (i.e. "chattr +C"). +// When a directory has this attribute, files created within it inherit +// NOCOW, which makes fallocate work correctly on btrfs because writes +// go in-place rather than through COW. +func dirHasNoCow(path string) bool { + fd, err := syscall.Open(path, syscall.O_RDONLY|syscall.O_DIRECTORY, 0) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: Open %q failed: %v", path, err) + return false + } + defer syscall.Close(fd) + + flags, err := unix.IoctlGetInt(fd, unix.FS_IOC_GETFLAGS) + if err != nil { + tlog.Debug.Printf("dirHasNoCow: FS_IOC_GETFLAGS on %q failed: %v", path, err) + return false + } + return flags&FS_NOCOW_FL != 0 +} + // DetectQuirks decides if there are known quirks on the backing filesystem // that need to be workarounded. // @@ -21,14 +48,19 @@ func DetectQuirks(cipherdir string) (q uint64) { // Preallocation on Btrfs is broken ( https://github.com/rfjakob/gocryptfs/issues/395 ) // and slow ( https://github.com/rfjakob/gocryptfs/issues/63 ). // + // The root cause is that btrfs COW allocates new blocks on write even for + // preallocated extents, defeating the purpose of fallocate. However, if the + // backing directory has the NOCOW attribute (chattr +C), writes go in-place + // and fallocate works correctly. + // // Cast to uint32 avoids compile error on arm: "constant 2435016766 overflows int32" if uint32(st.Type) == unix.BTRFS_SUPER_MAGIC { - logQuirk("Btrfs detected, forcing -noprealloc. See https://github.com/rfjakob/gocryptfs/issues/395 for why.") - q |= QuirkBrokenFalloc - } - - if uint32(st.Type) == unix.TMPFS_MAGIC { - logQuirk("tmpfs detected, no extended attributes except acls will work.") + if dirHasNoCow(cipherdir) { + tlog.Debug.Printf("DetectQuirks: Btrfs detected but cipherdir has NOCOW attribute (chattr +C), fallocate should work correctly") + } else { + // LogQuirk is called in fusefrontend/root_node.go + q |= QuirkBtrfsBrokenFalloc + } } return q diff --git a/internal/syscallcompat/rename_exchange_test.go b/internal/syscallcompat/rename_exchange_test.go new file mode 100644 index 0000000..97a95f8 --- /dev/null +++ b/internal/syscallcompat/rename_exchange_test.go @@ -0,0 +1,58 @@ +package syscallcompat + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/sys/unix" +) + +func TestRenameExchange(t *testing.T) { + // Create a temporary directory for testing + tmpDir, err := os.MkdirTemp("", "renameat2_test") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Test basic exchange functionality + file1 := filepath.Join(tmpDir, "file1.txt") + file2 := filepath.Join(tmpDir, "file2.txt") + + content1 := []byte("content of file 1") + content2 := []byte("content of file 2") + + if err := os.WriteFile(file1, content1, 0644); err != nil { + t.Fatalf("Failed to create file1: %v", err) + } + + if err := os.WriteFile(file2, content2, 0644); err != nil { + t.Fatalf("Failed to create file2: %v", err) + } + + // Test RENAME_EXCHANGE - this is the core functionality for issue #914 + err = Renameat2(unix.AT_FDCWD, file1, unix.AT_FDCWD, file2, RENAME_EXCHANGE) + if err != nil { + t.Fatalf("RENAME_EXCHANGE failed: %v", err) + } + + // Verify that the files have been swapped + newContent1, err := os.ReadFile(file1) + if err != nil { + t.Fatalf("Failed to read file1 after exchange: %v", err) + } + + newContent2, err := os.ReadFile(file2) + if err != nil { + t.Fatalf("Failed to read file2 after exchange: %v", err) + } + + if string(newContent1) != string(content2) { + t.Errorf("file1 content after exchange. Expected: %s, Got: %s", content2, newContent1) + } + + if string(newContent2) != string(content1) { + t.Errorf("file2 content after exchange. Expected: %s, Got: %s", content1, newContent2) + } +} diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index 1aa6a6e..3cb9ffa 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -54,10 +54,10 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) flags |= syscall.O_EXCL } } else { - // If O_CREAT is not used, we should use O_NOFOLLOW - if flags&syscall.O_NOFOLLOW == 0 { - tlog.Warn.Printf("Openat: O_NOFOLLOW missing: flags = %#x", flags) - flags |= syscall.O_NOFOLLOW + // If O_CREAT is not used, we should use O_NOFOLLOW or O_SYMLINK + if flags&(unix.O_NOFOLLOW|OpenatFlagNofollowSymlink) == 0 { + tlog.Warn.Printf("Openat: O_NOFOLLOW/O_SYMLINK missing: flags = %#x", flags) + flags |= unix.O_NOFOLLOW } } @@ -112,10 +112,10 @@ const XATTR_SIZE_MAX = 65536 // Make the buffer 1kB bigger so we can detect overflows. Unfortunately, // slices larger than 64kB are always allocated on the heap. -const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024 +const GETXATTR_BUFSZ_BIG = XATTR_SIZE_MAX + 1024 // We try with a small buffer first - this one can be allocated on the stack. -const XATTR_BUFSZ_SMALL = 500 +const GETXATTR_BUFSZ_SMALL = 500 // Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing. func Fgetxattr(fd int, attr string) (val []byte, err error) { @@ -135,7 +135,7 @@ func Lgetxattr(path string, attr string) (val []byte, err error) { func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // Fastpaths. Important for security.capabilities, which gets queried a lot. - buf := make([]byte, XATTR_BUFSZ_SMALL) + buf := make([]byte, GETXATTR_BUFSZ_SMALL) sz, err := fn(buf) // Non-existing xattr if err == unix.ENODATA { @@ -159,7 +159,7 @@ func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { // We choose the simple approach of buffer that is bigger than the limit on // Linux, and return an error for everything that is bigger (which can // only happen on MacOS). - buf = make([]byte, XATTR_BUFSZ) + buf = make([]byte, GETXATTR_BUFSZ_BIG) sz, err = fn(buf) if err == syscall.ERANGE { // Do NOT return ERANGE - the user might retry ad inifinitum! @@ -182,42 +182,44 @@ out: // Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Flistxattr(fd int) (attrs []string, err error) { - // See the buffer sizing comments in getxattrSmartBuf. - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Flistxattr(fd, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Flistxattr(fd, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return listxattrSmartBuf(listxattrSyscall) } // Llistxattr is a wrapper for unix.Llistxattr that handles buffer sizing and // parsing the returned blob to a string slice. func Llistxattr(path string) (attrs []string, err error) { - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Llistxattr(path, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW + listxattrSyscall := func(buf []byte) (int, error) { + return unix.Llistxattr(path, buf) } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW + return listxattrSmartBuf(listxattrSyscall) +} + +// listxattrSmartBuf handles smart buffer sizing for Flistxattr and Llistxattr +func listxattrSmartBuf(listxattrSyscall func([]byte) (int, error)) ([]string, error) { + const LISTXATTR_BUFSZ_SMALL = 100 + + // Blindly try with the small buffer first + buf := make([]byte, LISTXATTR_BUFSZ_SMALL) + sz, err := listxattrSyscall(buf) + if err == syscall.ERANGE { + // Did not fit. Find the actual size + sz, err = listxattrSyscall(nil) + if err != nil { + return nil, err + } + // ...and allocate the buffer to fit + buf = make([]byte, sz) + sz, err = listxattrSyscall(buf) + if err != nil { + // When an xattr got added between the size probe and here, + // we could fail with ERANGE. This is ok as the caller will retry. + return nil, err + } } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil + return parseListxattrBlob(buf[:sz]), nil } func parseListxattrBlob(buf []byte) (attrs []string) { diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 06f09f0..ef19f24 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -3,7 +3,6 @@ package syscallcompat import ( "log" "path/filepath" - "runtime" "syscall" "time" "unsafe" @@ -21,26 +20,18 @@ const ( // O_PATH is only defined on Linux O_PATH = 0 + // Same meaning, different name + RENAME_NOREPLACE = unix.RENAME_EXCL + RENAME_EXCHANGE = unix.RENAME_SWAP + // Only exists on Linux. Define here to fix build failure, even though - // we will never see the flags. - RENAME_NOREPLACE = 1 - RENAME_EXCHANGE = 2 - RENAME_WHITEOUT = 4 - - // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to - // revert permissions to the process credentials. - KAUTH_UID_NONE = ^uint32(0) - 100 - KAUTH_GID_NONE = ^uint32(0) - 100 -) + // we will never see this flag. + RENAME_WHITEOUT = 1 << 30 -// Unfortunately pthread_setugid_np does not have a syscall wrapper yet. -func pthread_setugid_np(uid uint32, gid uint32) (err error) { - _, _, e1 := syscall.RawSyscall(syscall.SYS_SETTID, uintptr(uid), uintptr(gid), 0) - if e1 != 0 { - err = e1 - } - return -} + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_SYMLINK +) // Unfortunately fsetattrlist does not have a syscall wrapper yet. func fsetattrlist(fd int, list unsafe.Pointer, buf unsafe.Pointer, size uintptr, options int) (err error) { @@ -84,74 +75,14 @@ func Dup3(oldfd int, newfd int, flags int) (err error) { //// Emulated Syscalls (see emulate.go) //////////////// //////////////////////////////////////////////////////// -func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return -1, err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return Openat(dirfd, path, flags, mode) -} - func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { return emulateMknodat(dirfd, path, mode, dev) } -func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return Mknodat(dirfd, path, mode, dev) -} - func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW) } -func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return unix.Symlinkat(oldpath, newdirfd, newpath) -} - -func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { - if context != nil { - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - err = pthread_setugid_np(context.Owner.Uid, context.Owner.Gid) - if err != nil { - return err - } - defer pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) - } - - return unix.Mkdirat(dirfd, path, mode) -} - type attrList struct { bitmapCount uint16 _ uint16 @@ -227,7 +158,12 @@ func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.Dir return emulateGetdents(fd) } -// Renameat2 does not exist on Darwin, so we call Renameat and ignore the flags. +// Renameat2 does not exist on Darwin, but RenameatxNp does. func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { - return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) + // If no flags are set, use tried and true renameat + if flags == 0 { + return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) + } + // Let RenameatxNp handle everything else + return unix.RenameatxNp(olddirfd, oldpath, newdirfd, newpath, uint32(flags)) } diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index a64b27e..71478af 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -3,10 +3,6 @@ package syscallcompat import ( "fmt" - "io/ioutil" - "runtime" - "strconv" - "strings" "sync" "syscall" "time" @@ -32,6 +28,10 @@ const ( RENAME_NOREPLACE = unix.RENAME_NOREPLACE RENAME_WHITEOUT = unix.RENAME_WHITEOUT RENAME_EXCHANGE = unix.RENAME_EXCHANGE + + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_NOFOLLOW ) var preallocWarn sync.Once @@ -67,104 +67,11 @@ func Fallocate(fd int, mode uint32, off int64, len int64) (err error) { return syscall.Fallocate(fd, mode, off, len) } -func getSupplementaryGroups(pid uint32) (gids []int) { - procPath := fmt.Sprintf("/proc/%d/task/%d/status", pid, pid) - blob, err := ioutil.ReadFile(procPath) - if err != nil { - return nil - } - - lines := strings.Split(string(blob), "\n") - for _, line := range lines { - if strings.HasPrefix(line, "Groups:") { - f := strings.Fields(line[7:]) - gids = make([]int, len(f)) - for i := range gids { - val, err := strconv.ParseInt(f[i], 10, 32) - if err != nil { - return nil - } - gids[i] = int(val) - } - return gids - } - } - - return nil -} - -// asUser runs `f()` under the effective uid, gid, groups specified -// in `context`. -// -// If `context` is nil, `f()` is executed directly without switching user id. -func asUser(f func() (int, error), context *fuse.Context) (int, error) { - if context == nil { - return f() - } - - runtime.LockOSThread() - defer runtime.UnlockOSThread() - - // Since go1.16beta1 (commit d1b1145cace8b968307f9311ff611e4bb810710c , - // https://go-review.googlesource.com/c/go/+/210639 ) - // syscall.{Setgroups,Setregid,Setreuid} affects all threads, which - // is exactly what we not want. - // - // We now use unix.{Setgroups,Setregid,Setreuid} instead. - - err := unix.Setgroups(getSupplementaryGroups(context.Pid)) - if err != nil { - return -1, err - } - defer unix.Setgroups(nil) - - err = unix.Setregid(-1, int(context.Owner.Gid)) - if err != nil { - return -1, err - } - defer unix.Setregid(-1, 0) - - err = unix.Setreuid(-1, int(context.Owner.Uid)) - if err != nil { - return -1, err - } - defer unix.Setreuid(-1, 0) - - return f() -} - -// OpenatUser runs the Openat syscall in the context of a different user. -// -// It switches the current thread to the new user, performs the syscall, -// and switches back. -// -// If `context` is nil, this function behaves like ordinary Openat (no -// user switching). -func OpenatUser(dirfd int, path string, flags int, mode uint32, context *fuse.Context) (fd int, err error) { - f := func() (int, error) { - return Openat(dirfd, path, flags, mode) - } - return asUser(f, context) -} - // Mknodat wraps the Mknodat syscall. func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { return syscall.Mknodat(dirfd, path, mode, dev) } -// MknodatUser runs the Mknodat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Mknodat. -// -// See OpenatUser() for how this works. -func MknodatUser(dirfd int, path string, mode uint32, dev int, context *fuse.Context) (err error) { - f := func() (int, error) { - err := Mknodat(dirfd, path, mode, dev) - return -1, err - } - _, err = asUser(f, context) - return err -} - // Dup3 wraps the Dup3 syscall. We want to use Dup3 rather than Dup2 because Dup2 // is not implemented on arm64. func Dup3(oldfd int, newfd int, flags int) (err error) { @@ -205,32 +112,6 @@ func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { return syscall.Chmod(procPath, mode) } -// SymlinkatUser runs the Symlinkat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Symlinkat. -// -// See OpenatUser() for how this works. -func SymlinkatUser(oldpath string, newdirfd int, newpath string, context *fuse.Context) (err error) { - f := func() (int, error) { - err := unix.Symlinkat(oldpath, newdirfd, newpath) - return -1, err - } - _, err = asUser(f, context) - return err -} - -// MkdiratUser runs the Mkdirat syscall in the context of a different user. -// If `context` is nil, this function behaves like ordinary Mkdirat. -// -// See OpenatUser() for how this works. -func MkdiratUser(dirfd int, path string, mode uint32, context *fuse.Context) (err error) { - f := func() (int, error) { - err := unix.Mkdirat(dirfd, path, mode) - return -1, err - } - _, err = asUser(f, context) - return err -} - // LsetxattrUser runs the Lsetxattr syscall in the context of a different user. // This is useful when setting ACLs, as the result depends on the user running // the operation (see fuse-xfstests generic/375). @@ -247,8 +128,16 @@ func LsetxattrUser(path string, attr string, data []byte, flags int, context *fu func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec { ts := make([]unix.Timespec, 2) - ts[0] = unix.Timespec(fuse.UtimeToTimespec(a)) - ts[1] = unix.Timespec(fuse.UtimeToTimespec(m)) + if a == nil { + ts[0] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[0], _ = unix.TimeToTimespec(*a) + } + if m == nil { + ts[1] = unix.Timespec{Nsec: unix.UTIME_OMIT} + } else { + ts[1], _ = unix.TimeToTimespec(*m) + } return ts } diff --git a/internal/syscallcompat/thread_credentials_linux.go b/internal/syscallcompat/thread_credentials_linux.go new file mode 100644 index 0000000..b5ec6cd --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux.go @@ -0,0 +1,61 @@ +//go:build linux + +// golang.org/x/sys/unix commit +// https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51 +// changed unix.Setreuid/unix.Setregid functions to affect the whole thread, which is +// what gocryptfs does NOT want (https://github.com/rfjakob/gocryptfs/issues/893). +// The functions Setreuid/Setegid are copy-pasted from one commit before +// (9e1f76180b77a12eb07c82eb8e1ea8a7f8d202e7). +// +// Looking at the diff at https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51 +// we see that only two architectures, 386 and arm, use SYS_SETREUID32/SYS_SETREGID32 +// (see "man 2 setreuid" for why). +// All the others architectures use SYS_SETREUID/SYS_SETREGID. +// +// As of golang.org/x/sys/unix v0.30.0, Setgroups/setgroups is still per-thread, but +// it is likely that this will change, too. Setgroups/setgroups are copy-pasted from +// v0.30.0. The SYS_SETGROUPS32/SYS_SETGROUPS split is the same as for Setreuid. +// +// Note: _Gid_t is always uint32 on linux, so we can directly use uint32 for setgroups. +package syscallcompat + +import ( + "log" +) + +// Setgroups is like setgroups(2) but affects only the current thread +func Setgroups(gids []int) (err error) { + if len(gids) == 0 { + return setgroups(0, nil) + } + + a := make([]uint32, len(gids)) + for i, v := range gids { + a[i] = uint32(v) + } + return setgroups(len(a), &a[0]) +} + +// SetgroupsPanic calls Setgroups and panics on error +func SetgroupsPanic(gids []int) { + err := Setgroups(gids) + if err != nil { + log.Panic(err) + } +} + +// SetregidPanic calls Setregid and panics on error +func SetregidPanic(rgid int, egid int) { + err := Setregid(rgid, egid) + if err != nil { + log.Panic(err) + } +} + +// SetreuidPanic calls Setreuid and panics on error +func SetreuidPanic(ruid int, euid int) { + err := Setreuid(ruid, euid) + if err != nil { + log.Panic(err) + } +} diff --git a/internal/syscallcompat/thread_credentials_linux_32.go b/internal/syscallcompat/thread_credentials_linux_32.go new file mode 100644 index 0000000..69fffca --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux_32.go @@ -0,0 +1,40 @@ +//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 ( + "unsafe" + + "golang.org/x/sys/unix" +) + +// See thread_credentials_linux.go for docs + +// Setreuid is like setreuid(2) but affects only the current thread +func Setreuid(ruid int, euid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID32, uintptr(ruid), uintptr(euid), 0) + if e1 != 0 { + err = e1 + } + return +} + +// Setreuid is like setregid(2) but affects only the current thread +func Setregid(rgid int, egid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID32, uintptr(rgid), uintptr(egid), 0) + if e1 != 0 { + err = e1 + } + return +} + +func setgroups(n int, list *uint32) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS32, uintptr(n), uintptr(unsafe.Pointer(list)), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/syscallcompat/thread_credentials_linux_other.go b/internal/syscallcompat/thread_credentials_linux_other.go new file mode 100644 index 0000000..ab11c71 --- /dev/null +++ b/internal/syscallcompat/thread_credentials_linux_other.go @@ -0,0 +1,37 @@ +//go:build !((linux && 386) || (linux && arm)) + +package syscallcompat + +import ( + "unsafe" + + "golang.org/x/sys/unix" +) + +// See thread_credentials_linux.go for docs + +// Setreuid is like setreuid(2) but affects only the current thread +func Setreuid(ruid int, euid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREUID, uintptr(ruid), uintptr(euid), 0) + if e1 != 0 { + err = e1 + } + return +} + +// Setreuid is like setregid(2) but affects only the current thread +func Setregid(rgid int, egid int) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETREGID, uintptr(rgid), uintptr(egid), 0) + if e1 != 0 { + err = e1 + } + return +} + +func setgroups(n int, list *uint32) (err error) { + _, _, e1 := unix.RawSyscall(unix.SYS_SETGROUPS, uintptr(n), uintptr(unsafe.Pointer(list)), 0) + if e1 != 0 { + err = e1 + } + return +} diff --git a/internal/tlog/log.go b/internal/tlog/log.go index 62d791d..02c760c 100644 --- a/internal/tlog/log.go +++ b/internal/tlog/log.go @@ -73,7 +73,7 @@ func (l *toggledLogger) Printf(format string, v ...interface{}) { return } msg := trimNewline(fmt.Sprintf(format, v...)) - l.Logger.Printf(l.prefix + msg + l.postfix) + l.Logger.Print(l.prefix + msg + l.postfix) if l.Wpanic { l.Logger.Panic(wpanicMsg + msg) } @@ -120,7 +120,7 @@ func changePassword(args *argContainer) { tlog.Fatal.Println(err) os.Exit(exitcodes.WriteConf) } - tlog.Info.Printf(tlog.ColorGreen + "Password changed." + tlog.ColorReset) + tlog.Info.Println(tlog.ColorGreen + "Password changed." + tlog.ColorReset) } func main() { diff --git a/masterkey.go b/masterkey.go index d488441..c67c5d5 100644 --- a/masterkey.go +++ b/masterkey.go @@ -24,9 +24,9 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte { tlog.Fatal.Printf("Master key has length %d but we require length %d", len(key), cryptocore.KeyLen) os.Exit(exitcodes.MasterKey) } - tlog.Info.Printf("Using explicit master key.") + tlog.Info.Println("Using explicit master key.") if !fromStdin { - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println(tlog.ColorYellow + "THE MASTER KEY IS VISIBLE VIA \"ps ax\" AND MAY BE STORED IN YOUR SHELL HISTORY!\n" + "ONLY USE THIS MODE FOR EMERGENCIES" + tlog.ColorReset) } @@ -52,8 +52,8 @@ func handleArgsMasterkey(args *argContainer) (masterkey []byte) { } // "-zerokey" if args.zerokey { - tlog.Info.Printf("Using all-zero dummy master key.") - tlog.Info.Printf(tlog.ColorYellow + + tlog.Info.Println("Using all-zero dummy master key.") + tlog.Info.Println(tlog.ColorYellow + "ZEROKEY MODE PROVIDES NO SECURITY AT ALL AND SHOULD ONLY BE USED FOR TESTING." + tlog.ColorReset) return make([]byte, cryptocore.KeyLen) @@ -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 { @@ -285,6 +284,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f SharedStorage: args.sharedstorage, OneFileSystem: args.one_file_system, DeterministicNames: args.deterministic_names, + NoXattr: args.noxattr, } // confFile is nil when "-zerokey" or "-masterkey" was used if confFile != nil { @@ -316,9 +316,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 } @@ -409,7 +413,7 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { mOpts := &fuseOpts.MountOptions opts := make(map[string]string) if args.allow_other { - tlog.Info.Printf(tlog.ColorYellow + "The option \"-allow_other\" is set. Make sure the file " + + tlog.Info.Println(tlog.ColorYellow + "The option \"-allow_other\" is set. Make sure the file " + "permissions protect your data from unwanted access." + tlog.ColorReset) mOpts.AllowOther = true // Make the kernel check the file permissions for us @@ -469,6 +473,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/package-release-tarballs.bash b/package-release-tarballs.bash index fde214e..881bce0 100755 --- a/package-release-tarballs.bash +++ b/package-release-tarballs.bash @@ -94,4 +94,6 @@ fi package_source package_static_binary +export GOARCH=arm64 +package_static_binary signing_hint diff --git a/profiling/tinyfiles.bash b/profiling/tinyfiles.bash new file mode 100755 index 0000000..90820de --- /dev/null +++ b/profiling/tinyfiles.bash @@ -0,0 +1,33 @@ +#!/bin/bash -eu +# +# Create a tarball of 100k 1-byte files using reverse mode +# https://github.com/rfjakob/gocryptfs/issues/965 + +cd "$(dirname "$0")" + +T=$(mktemp -d) +mkdir "$T/a" "$T/b" + +../gocryptfs -init -reverse -quiet -scryptn 10 -extpass "echo test" "$@" "$T/a" + +# Cleanup trap +# shellcheck disable=SC2064 +trap "cd /; fusermount -u -z '$T/b'; rm -Rf '$T/a'" EXIT + +echo "Creating 100k 1-byte files" +SECONDS=0 +dd if=/dev/urandom bs=100k count=1 status=none | split --suffix-length=10 -b 1 - "$T/a/tinyfile." +echo "done, $SECONDS seconds" + +../gocryptfs -reverse -quiet -nosyslog -extpass "echo test" \ + -cpuprofile "$T/cprof" -memprofile "$T/mprof" \ + "$@" "$T/a" "$T/b" + +echo "Running tar under profiler..." +SECONDS=0 +tar -cf /dev/null "$T/b" +echo "done, $SECONDS seconds" + +echo +echo "Hint: go tool pprof ../gocryptfs $T/cprof" +echo " go tool pprof -alloc_space ../gocryptfs $T/mprof" diff --git a/test-without-openssl.bash b/test-without-openssl.bash index 3f9de9e..e596753 100755 --- a/test-without-openssl.bash +++ b/test-without-openssl.bash @@ -2,4 +2,4 @@ cd "$(dirname "$0")" -./test.bash -tags without_openssl "$@" +CGO_ENABLED=0 ./test.bash "$@" @@ -22,7 +22,6 @@ else fi cd "$(dirname "$0")" -export GO111MODULE=on MYNAME=$(basename "$0") TESTDIR=$TMPDIR/gocryptfs-test-parent-$UID mkdir -p "$TESTDIR" @@ -38,9 +37,9 @@ unmount_leftovers() { return $RET } -( # Prevent multiple parallel test.bash instances as this causes # all kinds of mayhem +exec 200> "$LOCKFILE" if ! command -v flock > /dev/null ; then echo "flock is not available, skipping" elif ! flock -n 200 ; then @@ -55,8 +54,8 @@ unmount_leftovers || true echo "$MYNAME: build-without-openssl.bash failed" exit 1 } -# Don't build with openssl if we were passed "-tags without_openssl" -if [[ "$*" != *without_openssl* ]] ; then +# Only build with openssl if cgo is enabled +if [[ $(go env CGO_ENABLED) -eq 1 ]] ; then ./build.bash fi @@ -68,15 +67,20 @@ else go vet ./... fi +if command -v staticcheck > /dev/null ; then + staticcheck ./... +else + echo "staticcheck not installed - skipping" +fi + if command -v shellcheck > /dev/null ; then - # SC2002 = useless cat. Does no harm, disable the check. - shellcheck -x -e SC2002 ./*.bash + shellcheck -x ./*.bash else echo "shellcheck not installed - skipping" fi echo -n "Testing on TMPDIR=$TMPDIR, filesystem: " -findmnt --noheadings --target "$TESTDIR" --output FSTYPE,OPTIONS || true +findmnt --noheadings --target "$TESTDIR" --output FSTYPE,OPTIONS || echo "?" EXTRA_ARGS="" if [[ $VERBOSE -eq 1 ]]; then @@ -110,18 +114,19 @@ if find internal -type f -name \*.go -print0 | xargs -0 grep "panic("; then exit 1 fi -# All functions from the commit msg in https://go-review.googlesource.com/c/go/+/210639 -if find . -type f -name \*.go -print0 | xargs -0 grep -E 'syscall.(Setegid|Seteuid|Setgroups|Setgid|Setregid|Setreuid|Setresgid|Setresuid|Setuid)\(' ; then - echo "$MYNAME: You probably want to use unix.Setgroups and friends. See the comments in OpenatUser() for why." +# Both syscall.Setreuid etc (since 2020, https://github.com/golang/go/commit/d1b1145cace8b968307f9311ff611e4bb810710c) +# and unix.Setreuid etc (since 2022, https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51) +# affect the whole process, not only the current thread, which is what we do NOT want. +if find . ! -path "./vendor/*" -type f -name \*.go -print0 | xargs -0 grep -v -E '^//' | + grep -E '(syscall|unix).(Setegid|Seteuid|Setgroups|Setgid|Setregid|Setreuid|Setresgid|Setresuid|Setuid)\(' ; then + echo "$MYNAME: This affects the whole process. Please use the syscallcompat wrappers instead." exit 1 fi -if find . -type f -name \*.go -print0 | xargs -0 grep '\.Creat('; then +if find . ! -path "./vendor/*" -type f -name \*.go -print0 | xargs -0 grep '\.Creat('; then # MacOS does not have syscall.Creat(). Creat() is equivalent to Open(..., O_CREAT|O_WRONLY|O_TRUNC, ...), # but usually you want O_EXCL instead of O_TRUNC because it is safer, so that's what we suggest # instead. echo "$MYNAME: Please use Open(..., O_CREAT|O_WRONLY|O_EXCL, ...) instead of Creat()! https://github.com/rfjakob/gocryptfs/issues/623" exit 1 fi - -) 200> "$LOCKFILE" diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index fb09338..01cc3b7 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "errors" "fmt" - "io/ioutil" "os" "os/exec" "strconv" @@ -174,7 +173,7 @@ func TestPasswd(t *testing.T) { // Add content test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test") file1 := mnt + "/file1" - err := ioutil.WriteFile(file1, []byte("somecontent"), 0600) + err := os.WriteFile(file1, []byte("somecontent"), 0600) if err != nil { t.Fatal(err) } @@ -186,7 +185,7 @@ func TestPasswd(t *testing.T) { testPasswd(t, dir) // Mount and verify test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo newpasswd") - content, err := ioutil.ReadFile(file1) + content, err := os.ReadFile(file1) if err != nil { t.Error(err) } else if string(content) != "somecontent" { @@ -201,12 +200,12 @@ func TestPasswd(t *testing.T) { // cp copies file at `src` to `dst`, overwriting // `dst` if it already exists. Calls t.Fatal on failure. func cp(t *testing.T, src string, dst string) { - conf, err := ioutil.ReadFile(src) + conf, err := os.ReadFile(src) if err != nil { t.Fatal(err) } syscall.Unlink(dst) - err = ioutil.WriteFile(dst, conf, 0600) + err = os.WriteFile(dst, conf, 0600) if err != nil { t.Fatal(err) } @@ -222,7 +221,7 @@ func TestPasswdMasterkey(t *testing.T) { mnt := dir + ".mnt" test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test") file1 := mnt + "/file1" - err := ioutil.WriteFile(file1, []byte("somecontent"), 0600) + err := os.WriteFile(file1, []byte("somecontent"), 0600) if err != nil { t.Fatal(err) } @@ -251,7 +250,7 @@ func TestPasswdMasterkey(t *testing.T) { } // Mount and verify test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo newpasswd") - content, err := ioutil.ReadFile(file1) + content, err := os.ReadFile(file1) if err != nil { t.Error(err) } else if string(content) != "somecontent" { @@ -270,7 +269,7 @@ func TestPasswdMasterkeyStdin(t *testing.T) { mnt := dir + ".mnt" test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test") file1 := mnt + "/file1" - err := ioutil.WriteFile(file1, []byte("somecontent"), 0600) + err := os.WriteFile(file1, []byte("somecontent"), 0600) if err != nil { t.Fatal(err) } @@ -300,7 +299,7 @@ func TestPasswdMasterkeyStdin(t *testing.T) { } // Mount and verify test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo newpasswd") - content, err := ioutil.ReadFile(file1) + content, err := os.ReadFile(file1) if err != nil { t.Fatal(err) } else if string(content) != "somecontent" { @@ -392,7 +391,7 @@ func TestNonempty(t *testing.T) { if err != nil { t.Fatal(err) } - err = ioutil.WriteFile(mnt+"/somefile", []byte("xyz"), 0600) + err = os.WriteFile(mnt+"/somefile", []byte("xyz"), 0600) if err != nil { t.Fatal(err) } @@ -586,7 +585,7 @@ func TestNoexec(t *testing.T) { content := `#!/bin/bash echo hello ` - err := ioutil.WriteFile(sh, []byte(content), 0755) + err := os.WriteFile(sh, []byte(content), 0755) if err != nil { t.Fatal(err) } @@ -697,13 +696,13 @@ func TestNotIdle(t *testing.T) { if err != nil { t.Fatal(err) } - err = ioutil.WriteFile(mnt+"/foo", []byte("foo"), 0600) + err = os.WriteFile(mnt+"/foo", []byte("foo"), 0600) if err != nil { t.Fatal(err) } // Read every 10 milliseconds for a total of 1 second for i := 1; i < 100; i++ { - _, err = ioutil.ReadFile(mnt + "/foo") + _, err = os.ReadFile(mnt + "/foo") if err != nil { t.Fatalf("iteration %d failed: %v", i, err) } @@ -784,7 +783,7 @@ func TestBadname(t *testing.T) { file := mnt + "/" + validFileName // Case 1: write one valid filename (empty content) - err := ioutil.WriteFile(file, nil, 0600) + err := os.WriteFile(file, nil, 0600) if err != nil { t.Fatal(err) } @@ -809,26 +808,26 @@ func TestBadname(t *testing.T) { } //Generate valid cipherdata for all cases for i := 0; i < len(contentCipher); i++ { - err := ioutil.WriteFile(file, []byte(fmt.Sprintf("Content Case %d.", i+1)), 0600) + err := os.WriteFile(file, []byte(fmt.Sprintf("Content Case %d.", i+1)), 0600) if err != nil { t.Fatal(err) } //save the cipher data for file operations in cipher dir - contentCipher[i], err = ioutil.ReadFile(dir + "/" + encryptedfilename) + contentCipher[i], err = os.ReadFile(dir + "/" + encryptedfilename) if err != nil { t.Fatal(err) } } //re-write content for case 1 - err = ioutil.WriteFile(file, []byte("Content Case 1."), 0600) + err = os.WriteFile(file, []byte("Content Case 1."), 0600) if err != nil { t.Fatal(err) } // Case 2: File with invalid suffix in plain name but valid cipher file file = mnt + "/" + validFileName + nametransform.BadnameSuffix - err = ioutil.WriteFile(file, []byte("Content Case 2."), 0600) + err = os.WriteFile(file, []byte("Content Case 2."), 0600) if err != nil { t.Fatal(err) } @@ -842,29 +841,29 @@ func TestBadname(t *testing.T) { // Case 3 is impossible: only BadnameSuffix would mean the cipher name is valid // Case 4: write invalid file which should be decodable - err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, contentCipher[3], 0600) + err = os.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, contentCipher[3], 0600) if err != nil { t.Fatal(err) } //Case 5: write invalid file which is not decodable (replace last 2 bytes with percent sign) - err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+"%%"+invalidSuffix, contentCipher[4], 0600) + err = os.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+"%%"+invalidSuffix, contentCipher[4], 0600) if err != nil { t.Fatal(err) } // Case 6: Multiple possible matches // generate two files with invalid cipher names which can both match the badname pattern - err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wPC"+invalidSuffix, contentCipher[5], 0600) + err = os.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wPC"+invalidSuffix, contentCipher[5], 0600) if err != nil { t.Fatal(err) } - err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wP"+invalidSuffix, contentCipher[5], 0600) + err = os.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wP"+invalidSuffix, contentCipher[5], 0600) if err != nil { t.Fatal(err) } // Case 7: Non-Matching badname pattern - err = ioutil.WriteFile(dir+"/"+encryptedfilename+"wrongPattern", contentCipher[6], 0600) + err = os.WriteFile(dir+"/"+encryptedfilename+"wrongPattern", contentCipher[6], 0600) if err != nil { t.Fatal(err) } @@ -894,7 +893,7 @@ func TestBadname(t *testing.T) { for _, name := range names { if name == searchstrings[0] { //Case 1: Test access - filebytes, err = ioutil.ReadFile(mnt + "/" + name) + filebytes, err = os.ReadFile(mnt + "/" + name) if err != nil { t.Fatal(err) } @@ -905,7 +904,7 @@ func TestBadname(t *testing.T) { } else if name == searchstrings[1] { //Case 2: Test Access - filebytes, err = ioutil.ReadFile(mnt + "/" + name) + filebytes, err = os.ReadFile(mnt + "/" + name) if err != nil { t.Fatal(err) } @@ -915,7 +914,7 @@ func TestBadname(t *testing.T) { } } else if name == searchstrings[3] { //Case 4: Test Access - filebytes, err = ioutil.ReadFile(mnt + "/" + name) + filebytes, err = os.ReadFile(mnt + "/" + name) if err != nil { t.Fatal(err) } @@ -925,7 +924,7 @@ func TestBadname(t *testing.T) { } } else if name == searchstrings[4] { //Case 5: Test Access - filebytes, err = ioutil.ReadFile(mnt + "/" + name) + filebytes, err = os.ReadFile(mnt + "/" + name) if err != nil { t.Fatal(err) } @@ -955,7 +954,7 @@ func TestPassfile(t *testing.T) { dir := test_helpers.InitFS(t) mnt := dir + ".mnt" passfile1 := mnt + ".1.txt" - ioutil.WriteFile(passfile1, []byte("test"), 0600) + os.WriteFile(passfile1, []byte("test"), 0600) test_helpers.MountOrFatal(t, dir, mnt, "-passfile="+passfile1) defer test_helpers.UnmountPanic(mnt) } @@ -966,8 +965,8 @@ func TestPassfileX2(t *testing.T) { mnt := dir + ".mnt" passfile1 := mnt + ".1.txt" passfile2 := mnt + ".2.txt" - ioutil.WriteFile(passfile1, []byte("te"), 0600) - ioutil.WriteFile(passfile2, []byte("st"), 0600) + os.WriteFile(passfile1, []byte("te"), 0600) + os.WriteFile(passfile2, []byte("st"), 0600) test_helpers.MountOrFatal(t, dir, mnt, "-passfile="+passfile1, "-passfile="+passfile2) defer test_helpers.UnmountPanic(mnt) } @@ -979,7 +978,7 @@ func TestInitNotEmpty(t *testing.T) { if err := os.Mkdir(dir, 0700); err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(dir+"/foo", nil, 0700); err != nil { + if err := os.WriteFile(dir+"/foo", nil, 0700); err != nil { t.Fatal(err) } cmd := exec.Command(test_helpers.GocryptfsBinary, "-init", "-extpass", "echo test", dir) @@ -1001,7 +1000,7 @@ func TestSharedstorage(t *testing.T) { defer test_helpers.UnmountPanic(mnt) foo1 := mnt + "/foo1" foo2 := mnt + "/foo2" - if err := ioutil.WriteFile(foo1, nil, 0755); err != nil { + if err := os.WriteFile(foo1, nil, 0755); err != nil { t.Fatal(err) } if err := os.Link(foo1, foo2); err != nil { @@ -1020,7 +1019,7 @@ func TestSharedstorage(t *testing.T) { } // Check that we we don't have stat caching. New length should show up // on the hard link immediately. - if err := ioutil.WriteFile(foo1, []byte("xxxxxx"), 0755); err != nil { + if err := os.WriteFile(foo1, []byte("xxxxxx"), 0755); err != nil { t.Fatal(err) } if err := syscall.Stat(foo2, &st2); err != nil { diff --git a/tests/cli/longnamemax_test.go b/tests/cli/longnamemax_test.go index e44a84e..357ede6 100644 --- a/tests/cli/longnamemax_test.go +++ b/tests/cli/longnamemax_test.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -38,7 +37,7 @@ func TestLongnamemax100(t *testing.T) { for l := 1; l <= 255; l++ { path := pDir + "/" + strings.Repeat("x", l) - if err := ioutil.WriteFile(path, nil, 0600); err != nil { + if err := os.WriteFile(path, nil, 0600); err != nil { t.Fatal(err) } matches, err := filepath.Glob(cDir + "/gocryptfs.longname.*") @@ -84,7 +83,7 @@ func TestLongnamemax100Reverse(t *testing.T) { for l := 1; l <= 255; l++ { path := backingDir + "/" + strings.Repeat("x", l) - if err := ioutil.WriteFile(path, nil, 0600); err != nil { + if err := os.WriteFile(path, nil, 0600); err != nil { t.Fatal(err) } matches, err := filepath.Glob(mntDir + "/gocryptfs.longname.*") diff --git a/tests/cli/xchacha_test.go b/tests/cli/xchacha_test.go index 7f24c8b..f0fe163 100644 --- a/tests/cli/xchacha_test.go +++ b/tests/cli/xchacha_test.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "io/ioutil" "os" "syscall" "testing" @@ -40,7 +39,7 @@ func TestXchacha(t *testing.T) { test_helpers.MountOrExit(cDir, pDir, "-extpass", "echo test") defer test_helpers.UnmountPanic(pDir) - if err := ioutil.WriteFile(pDir+"/1byte", []byte("x"), 0700); err != nil { + if err := os.WriteFile(pDir+"/1byte", []byte("x"), 0700); err != nil { t.Fatal(err) } var st syscall.Stat_t @@ -53,7 +52,7 @@ func TestXchacha(t *testing.T) { } // 1 MiB = 256 4kiB blocks - if err := ioutil.WriteFile(pDir+"/1MiB", make([]byte, 1024*1024), 0700); err != nil { + if err := os.WriteFile(pDir+"/1MiB", make([]byte, 1024*1024), 0700); err != nil { t.Fatal(err) } if err := syscall.Stat(cDir+"/1MiB", &st); err != nil { diff --git a/tests/cli/zerokey_test.go b/tests/cli/zerokey_test.go index e9b9c50..359651c 100644 --- a/tests/cli/zerokey_test.go +++ b/tests/cli/zerokey_test.go @@ -1,7 +1,6 @@ package cli import ( - "io/ioutil" "os" "os/exec" "testing" @@ -42,7 +41,7 @@ func TestZerokey(t *testing.T) { mnt := dir + ".mnt" test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test") file1 := mnt + "/file1" - err = ioutil.WriteFile(file1, []byte("somecontent"), 0600) + err = os.WriteFile(file1, []byte("somecontent"), 0600) if err != nil { t.Fatal(err) } @@ -50,7 +49,7 @@ func TestZerokey(t *testing.T) { // Mount using -zerokey and verify we get the same result test_helpers.MountOrFatal(t, dir, mnt, "-extpass", "echo test") - content, err := ioutil.ReadFile(file1) + content, err := os.ReadFile(file1) if err != nil { t.Error(err) } else if string(content) != "somecontent" { diff --git a/tests/defaults/acl_test.go b/tests/defaults/acl_test.go index 2ab5dc1..bddb5e5 100644 --- a/tests/defaults/acl_test.go +++ b/tests/defaults/acl_test.go @@ -1,14 +1,12 @@ package defaults import ( - "io/ioutil" "math/rand" "os" "os/exec" "path/filepath" "syscall" "testing" - "time" "golang.org/x/sys/unix" @@ -22,8 +20,6 @@ func TestCpA(t *testing.T) { fn1 := filepath.Join(test_helpers.TmpDir, t.Name()) fn2 := filepath.Join(test_helpers.DefaultPlainDir, t.Name()) - rand.Seed(int64(time.Now().Nanosecond())) - { // Need unrestricted umask old := syscall.Umask(000) @@ -35,7 +31,7 @@ func TestCpA(t *testing.T) { var modeWant os.FileMode = os.FileMode(rand.Int31n(0777+1) | 0400) // Create file outside mount - err := ioutil.WriteFile(fn1, nil, modeWant) + err := os.WriteFile(fn1, nil, modeWant) if err != nil { t.Fatal(err) } @@ -45,7 +41,7 @@ func TestCpA(t *testing.T) { t.Fatal(err) } if fi.Mode() != modeWant { - t.Errorf("ioutil.WriteFile created wrong permissions: want %o have %o", modeWant, fi.Mode()) + t.Errorf("os.WriteFile created wrong permissions: want %o have %o", modeWant, fi.Mode()) } // "cp -a" from outside to inside mount @@ -93,7 +89,7 @@ func TestAcl543(t *testing.T) { } // Set acl on file outside gocryptfs mount - err := ioutil.WriteFile(fn1, nil, modeWant) + err := os.WriteFile(fn1, nil, modeWant) if err != nil { t.Fatal(err) } @@ -117,7 +113,7 @@ func TestAcl543(t *testing.T) { } // Set acl on file inside gocryptfs mount - err = ioutil.WriteFile(fn2, nil, modeWant) + err = os.WriteFile(fn2, nil, modeWant) if err != nil { t.Fatal(err) } @@ -164,7 +160,7 @@ func TestAcl543(t *testing.T) { // Check that we handle zero-sized and undersized buffers correctly func TestXattrOverflow(t *testing.T) { fn := filepath.Join(test_helpers.DefaultPlainDir, t.Name()) - ioutil.WriteFile(fn, nil, 0600) + os.WriteFile(fn, nil, 0600) attr := "user.foo123" val := []byte("12341234") diff --git a/tests/defaults/diriv_test.go b/tests/defaults/diriv_test.go index 639b33d..48ba39a 100644 --- a/tests/defaults/diriv_test.go +++ b/tests/defaults/diriv_test.go @@ -1,7 +1,6 @@ package defaults import ( - "io/ioutil" "os" "sync" "sync/atomic" @@ -19,7 +18,7 @@ func TestDirIVRace(t *testing.T) { if err != nil { t.Fatal(err) } - err = ioutil.WriteFile(dir1+"/file", nil, 0600) + err = os.WriteFile(dir1+"/file", nil, 0600) if err != nil { t.Fatal(err) } @@ -31,7 +30,7 @@ func TestDirIVRace(t *testing.T) { t.Fatal(err) } file2 := dir2 + "/file" - err = ioutil.WriteFile(file2, nil, 0600) + err = os.WriteFile(file2, nil, 0600) if err != nil { t.Fatal(err) } 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..a19f079 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -3,8 +3,8 @@ package defaults import ( "bytes" + "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -175,7 +175,7 @@ func TestXfs124(t *testing.T) { func TestWrite0200File(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestWrite0200File" - err := ioutil.WriteFile(fn, nil, 0200) + err := os.WriteFile(fn, nil, 0200) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -211,7 +211,7 @@ func TestWrite0200File(t *testing.T) { // Now we return EOPNOTSUPP and mv is happy. func TestMvWarnings(t *testing.T) { fn := test_helpers.TmpDir + "/TestMvWarnings" - err := ioutil.WriteFile(fn, nil, 0600) + err := os.WriteFile(fn, nil, 0600) if err != nil { t.Fatalf("creating file failed: %v", err) } @@ -256,7 +256,7 @@ func TestMvWarningSymlink(t *testing.T) { // See TestMvWarnings. func TestCpWarnings(t *testing.T) { fn := test_helpers.TmpDir + "/TestCpWarnings" - err := ioutil.WriteFile(fn, []byte("foo"), 0600) + err := os.WriteFile(fn, []byte("foo"), 0600) if err != nil { t.Fatalf("creating file failed: %v", err) } @@ -506,3 +506,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/defaults/overlayfs_test.go b/tests/defaults/overlayfs_test.go index 8cb773d..32c0a90 100644 --- a/tests/defaults/overlayfs_test.go +++ b/tests/defaults/overlayfs_test.go @@ -4,7 +4,6 @@ package defaults import ( - "io/ioutil" "os" "strings" "testing" @@ -43,7 +42,7 @@ func TestRenameWhiteout(t *testing.T) { for _, n := range names { pSrc := test_helpers.DefaultPlainDir + "/" + n[0] pDst := test_helpers.DefaultPlainDir + "/" + n[1] - if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil { + if err := os.WriteFile(pSrc, nil, 0200); err != nil { t.Fatalf("creating empty file failed: %v", err) } err := unix.Renameat2(-1, pSrc, -1, pDst, flags) @@ -96,10 +95,10 @@ func TestRenameExchange(t *testing.T) { for _, n := range names { pSrc := test_helpers.DefaultPlainDir + "/" + n[0] pDst := test_helpers.DefaultPlainDir + "/" + n[1] - if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil { + if err := os.WriteFile(pSrc, nil, 0200); err != nil { t.Fatalf("creating empty file failed: %v", err) } - if err := ioutil.WriteFile(pDst, nil, 0200); err != nil { + if err := os.WriteFile(pDst, nil, 0200); err != nil { t.Fatalf("creating empty file failed: %v", err) } err := unix.Renameat2(-1, pSrc, -1, pDst, unix.RENAME_EXCHANGE) @@ -119,7 +118,7 @@ func TestRenameExchange(t *testing.T) { } } -// Looks like the FUSE protocol does support O_TMPFILE yet +// Looks like the FUSE protocol does not support O_TMPFILE yet func TestOTmpfile(t *testing.T) { p := test_helpers.DefaultPlainDir + "/" + t.Name() fd, err := unix.Openat(-1, p, unix.O_TMPFILE, 0600) diff --git a/tests/defaults/performance_test.go b/tests/defaults/performance_test.go index 11d827f..8bae5c1 100644 --- a/tests/defaults/performance_test.go +++ b/tests/defaults/performance_test.go @@ -4,7 +4,6 @@ package defaults import ( "fmt" "io" - "io/ioutil" "os" "testing" @@ -96,7 +95,7 @@ func createFiles(t *testing.B, count int, size int) { for i = 0; i < count; i++ { file := fmt.Sprintf("%s/%d", dir, i) if size > 0 { - err = ioutil.WriteFile(file, buf, 0666) + err = os.WriteFile(file, buf, 0666) } else { var fh *os.File fh, err = os.Create(file) diff --git a/tests/deterministic_names/deterministic_names_test.go b/tests/deterministic_names/deterministic_names_test.go index 00d80fc..330cfc3 100644 --- a/tests/deterministic_names/deterministic_names_test.go +++ b/tests/deterministic_names/deterministic_names_test.go @@ -4,7 +4,6 @@ package deterministic_names import ( "fmt" - "io/ioutil" "os" "path/filepath" "testing" @@ -70,7 +69,7 @@ func TestDeterministicNames(t *testing.T) { if err := os.RemoveAll(pDir + "/foo"); err != nil { t.Fatal(err) } - if err := ioutil.WriteFile(pDir+"/foo", nil, 0700); err != nil { + if err := os.WriteFile(pDir+"/foo", nil, 0700); err != nil { t.Fatal(err) } _, err = os.Stat(cDir + "/" + fooEncrypted) diff --git a/tests/example_filesystems/example_test_helpers.go b/tests/example_filesystems/example_test_helpers.go index c56d75a..34e5786 100644 --- a/tests/example_filesystems/example_test_helpers.go +++ b/tests/example_filesystems/example_test_helpers.go @@ -1,7 +1,6 @@ package example_filesystems import ( - "io/ioutil" "os" "path/filepath" "testing" @@ -15,7 +14,7 @@ const statusTxtContent = "It works!\n" func checkExampleFS(t *testing.T, dir string, rw bool) { // Read regular file statusFile := filepath.Join(dir, "status.txt") - contentBytes, err := ioutil.ReadFile(statusFile) + contentBytes, err := os.ReadFile(statusFile) if err != nil { t.Error(err) return @@ -68,7 +67,7 @@ func checkExampleFSrw(t *testing.T, dir string, rw bool) { "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "xxxxxxxxxxxxxxxxxxxxxxxx" - contentBytes, err := ioutil.ReadFile(filepath.Join(dir, longname)) + contentBytes, err := os.ReadFile(filepath.Join(dir, longname)) if err != nil { t.Error(err) return diff --git a/tests/fsck/.gitignore b/tests/fsck/.gitignore new file mode 100644 index 0000000..9c9febc --- /dev/null +++ b/tests/fsck/.gitignore @@ -0,0 +1,7 @@ +# Cannot have these "evil" files in git because they break "go install" +# https://github.com/golang/go/issues/28001 +# +# "malleable_base64/27AG8t-XZH7G9ou2OSD_z\ng" +# "malleable_base64/27AG8t-XZH7G9ou2OSD_z\rg" +# +malleable_base64/27AG8t-XZH7G9ou2OSD_z?g diff --git a/tests/fsck/fsck_test.go b/tests/fsck/fsck_test.go index b70fd49..de49819 100644 --- a/tests/fsck/fsck_test.go +++ b/tests/fsck/fsck_test.go @@ -50,6 +50,17 @@ func TestBrokenFsV14(t *testing.T) { } func TestMalleableBase64(t *testing.T) { + // Evil filenames. Cannot have them in git, because if we do, + // go install github.com/rfjakob/gocryptfs/v2@latest + // fails with + // g: malformed file path "tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z\rg": invalid char '\r' + // g: malformed file path "tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z\rg": invalid char '\r' + if err := os.WriteFile("malleable_base64/27AG8t-XZH7G9ou2OSD_z\ng", nil, 0644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile("malleable_base64/27AG8t-XZH7G9ou2OSD_z\rg", nil, 0644); err != nil { + t.Fatal(err) + } cmd := exec.Command(test_helpers.GocryptfsBinary, "-fsck", "-extpass", "echo test", "malleable_base64") outBin, err := cmd.CombinedOutput() out := string(outBin) diff --git a/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z
g b/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z
g deleted file mode 100644 index e69de29..0000000 --- a/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z g +++ /dev/null diff --git a/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z
g b/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z
g deleted file mode 100644 index e69de29..0000000 --- a/tests/fsck/malleable_base64/27AG8t-XZH7G9ou2OSD_z g +++ /dev/null diff --git a/tests/hkdf_sanity/sanity_test.go b/tests/hkdf_sanity/sanity_test.go index f221439..27a1085 100644 --- a/tests/hkdf_sanity/sanity_test.go +++ b/tests/hkdf_sanity/sanity_test.go @@ -4,7 +4,6 @@ package hkdf_sanity import ( - "io/ioutil" "os" "testing" @@ -15,7 +14,7 @@ func TestBrokenContent(t *testing.T) { cDir := "broken_content" pDir := test_helpers.TmpDir + "/" + cDir test_helpers.MountOrFatal(t, cDir, pDir, "-extpass", "echo test", "-wpanic=false") - _, err := ioutil.ReadFile(pDir + "/status.txt") + _, err := os.ReadFile(pDir + "/status.txt") if err == nil { t.Error("this should fail") } diff --git a/tests/issue893.sh b/tests/issue893.sh new file mode 100755 index 0000000..a1e7cdb --- /dev/null +++ b/tests/issue893.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# Reproducer for https://github.com/rfjakob/gocryptfs/issues/893 . +# Run this script as non-root against a root-mounted gocryptfs -allow_other. + +set -eu + +mountpoint $1 +cd $1 + +work() { + for i in $(seq 100) ; do + D=mtest.$BASHPID.$i/foo/bar/baz + mkdir -p $D + touch $D/foo $D/bar + echo AAAAAAAAAAAAAAAAAAAAA > $D/foo + rm $D/foo + mkdir $D/baz + done +} + +rm -Rf mtest.* +echo . + +work & +work & + +wait diff --git a/tests/matrix/dir_test.go b/tests/matrix/dir_test.go index 13d6712..e2ecf46 100644 --- a/tests/matrix/dir_test.go +++ b/tests/matrix/dir_test.go @@ -70,3 +70,27 @@ func TestHaveDotdot(t *testing.T) { t.Errorf("have=%q want=%q", have, want) } } + +// mkdir used to report the wrong file mode when creating a read-only +// director. +// https://github.com/rfjakob/gocryptfs/issues/964 +func Test555Dir(t *testing.T) { + dir1 := test_helpers.DefaultPlainDir + "/" + t.Name() + err := os.Mkdir(dir1, 0555) + if err != nil { + t.Fatal(err) + } + + // Avoid permission errors when cleaning up the directory later + defer syscall.Chmod(dir1, 0700) + + var st syscall.Stat_t + err = syscall.Stat(dir1, &st) + if err != nil { + t.Fatal(err) + } + have := st.Mode & 0777 + if have != 0555 { + t.Errorf("wrong mode. want 0555 have %04o", have) + } +} diff --git a/tests/matrix/main_test.go b/tests/matrix/main_test.go new file mode 100644 index 0000000..cf0b6c4 --- /dev/null +++ b/tests/matrix/main_test.go @@ -0,0 +1,121 @@ +// Tests run for (almost all) combinations of openssl, aessiv, plaintextnames. +// +// File reading, writing, modification, truncate, ... +// +// Runs all tests N times, for the combinations of different flags specified +// in the `matrix` variable. + +package matrix + +import ( + "flag" + "fmt" + "os" + "testing" + "time" + + "github.com/rfjakob/gocryptfs/v2/internal/stupidgcm" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// Several tests need to be aware if plaintextnames is active or not, so make this +// a global variable +var testcase testcaseMatrix + +var ctlsockPath string + +type testcaseMatrix struct { + plaintextnames bool + openssl string + aessiv bool + raw64 bool + extraArgs []string +} + +// isSet finds out if `extraArg` is set in `tc.extraArgs` +func (tc *testcaseMatrix) isSet(extraArg string) bool { + for _, v := range tc.extraArgs { + if v == extraArg { + return true + } + } + return false +} + +// This is the entry point for the tests +func TestMain(m *testing.M) { + var matrix = []testcaseMatrix{ + // Normal + {false, "auto", false, false, nil}, + {false, "true", false, false, nil}, + {false, "false", false, false, nil}, + // Plaintextnames + {true, "true", false, false, nil}, + {true, "false", false, false, nil}, + // AES-SIV (does not use openssl, no need to test permutations) + {false, "auto", true, false, nil}, + {true, "auto", true, false, nil}, + // Raw64 + {false, "auto", false, true, nil}, + // -serialize_reads + {false, "auto", false, false, []string{"-serialize_reads"}}, + {false, "auto", false, false, []string{"-sharedstorage"}}, + {false, "auto", false, false, []string{"-deterministic-names"}}, + // Test xchacha with and without openssl + {false, "true", false, true, []string{"-xchacha"}}, + {false, "false", false, true, []string{"-xchacha"}}, + } + + // Make "testing.Verbose()" return the correct value + flag.Parse() + var i int + for i, testcase = range matrix { + if testcase.openssl == "true" && stupidgcm.BuiltWithoutOpenssl { + continue + } + if testing.Verbose() { + fmt.Printf("matrix: testcase = %#v\n", testcase) + } + createDirIV := true + if testcase.plaintextnames { + createDirIV = false + } else if testcase.isSet("-deterministic-names") { + createDirIV = false + } + ctlsockPath = fmt.Sprintf("%s/ctlsock.%d", test_helpers.TmpDir, i) + test_helpers.ResetTmpDir(createDirIV) + opts := []string{"-zerokey", "-ctlsock", ctlsockPath} + //opts = append(opts, "-fusedebug") + opts = append(opts, fmt.Sprintf("-openssl=%v", testcase.openssl)) + opts = append(opts, fmt.Sprintf("-plaintextnames=%v", testcase.plaintextnames)) + opts = append(opts, fmt.Sprintf("-aessiv=%v", testcase.aessiv)) + opts = append(opts, fmt.Sprintf("-raw64=%v", testcase.raw64)) + opts = append(opts, testcase.extraArgs...) + test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, opts...) + before := test_helpers.ListFds(0, test_helpers.TmpDir) + t0 := time.Now() + r := m.Run() + if testing.Verbose() { + fmt.Printf("matrix[%d]: run took %v\n", i, time.Since(t0)) + } + // Catch fd leaks in the tests. NOTE: this does NOT catch leaks in + // the gocryptfs FUSE process, but only in the tests that access it! + // All fds that point outside TmpDir are not interesting (the Go test + // infrastucture creates temporary log files we don't care about). + after := test_helpers.ListFds(0, test_helpers.TmpDir) + if len(before) != len(after) { + fmt.Printf("fd leak in test process? before, after:\n%v\n%v\n", before, after) + os.Exit(1) + } + test_helpers.UnmountPanic(test_helpers.DefaultPlainDir) + if r != 0 { + fmt.Printf("TestMain: matrix[%d] = %#v failed\n", i, testcase) + os.Exit(r) + } + // The ctlsock file is deleted asynchronously after unmount. + // Ensure it is delete here so it does not race (and trip up) ResetTmpDir() of the next + // loop iteration. + os.Remove(ctlsockPath) + } + os.Exit(0) +} diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index 417e126..a2ec549 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -1,16 +1,8 @@ -// Tests run for (almost all) combinations of openssl, aessiv, plaintextnames. package matrix -// File reading, writing, modification, truncate, ... -// -// Runs all tests N times, for the combinations of different flags specified -// in the `matrix` variable. - import ( "bytes" - "flag" "fmt" - "io/ioutil" "math/rand" "os" "os/exec" @@ -20,109 +12,14 @@ import ( "sync" "syscall" "testing" - "time" "golang.org/x/sys/unix" - "github.com/rfjakob/gocryptfs/v2/internal/stupidgcm" + "github.com/rfjakob/gocryptfs/v2/ctlsock" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) -// Several tests need to be aware if plaintextnames is active or not, so make this -// a global variable -var testcase testcaseMatrix - -type testcaseMatrix struct { - plaintextnames bool - openssl string - aessiv bool - raw64 bool - extraArgs []string -} - -// isSet finds out if `extraArg` is set in `tc.extraArgs` -func (tc *testcaseMatrix) isSet(extraArg string) bool { - for _, v := range tc.extraArgs { - if v == extraArg { - return true - } - } - return false -} - -var matrix = []testcaseMatrix{ - // Normal - {false, "auto", false, false, nil}, - {false, "true", false, false, nil}, - {false, "false", false, false, nil}, - // Plaintextnames - {true, "true", false, false, nil}, - {true, "false", false, false, nil}, - // AES-SIV (does not use openssl, no need to test permutations) - {false, "auto", true, false, nil}, - {true, "auto", true, false, nil}, - // Raw64 - {false, "auto", false, true, nil}, - // -serialize_reads - {false, "auto", false, false, []string{"-serialize_reads"}}, - {false, "auto", false, false, []string{"-sharedstorage"}}, - {false, "auto", false, false, []string{"-deterministic-names"}}, - // Test xchacha with and without openssl - {false, "true", false, true, []string{"-xchacha"}}, - {false, "false", false, true, []string{"-xchacha"}}, -} - -// This is the entry point for the tests -func TestMain(m *testing.M) { - // Make "testing.Verbose()" return the correct value - flag.Parse() - var i int - for i, testcase = range matrix { - if testcase.openssl == "true" && stupidgcm.BuiltWithoutOpenssl { - continue - } - if testing.Verbose() { - fmt.Printf("matrix: testcase = %#v\n", testcase) - } - createDirIV := true - if testcase.plaintextnames { - createDirIV = false - } else if testcase.isSet("-deterministic-names") { - createDirIV = false - } - test_helpers.ResetTmpDir(createDirIV) - opts := []string{"-zerokey"} - //opts = append(opts, "-fusedebug") - opts = append(opts, fmt.Sprintf("-openssl=%v", testcase.openssl)) - opts = append(opts, fmt.Sprintf("-plaintextnames=%v", testcase.plaintextnames)) - opts = append(opts, fmt.Sprintf("-aessiv=%v", testcase.aessiv)) - opts = append(opts, fmt.Sprintf("-raw64=%v", testcase.raw64)) - opts = append(opts, testcase.extraArgs...) - test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, opts...) - before := test_helpers.ListFds(0, test_helpers.TmpDir) - t0 := time.Now() - r := m.Run() - if testing.Verbose() { - fmt.Printf("matrix: run took %v\n", time.Since(t0)) - } - // Catch fd leaks in the tests. NOTE: this does NOT catch leaks in - // the gocryptfs FUSE process, but only in the tests that access it! - // All fds that point outside TmpDir are not interesting (the Go test - // infrastucture creates temporary log files we don't care about). - after := test_helpers.ListFds(0, test_helpers.TmpDir) - if len(before) != len(after) { - fmt.Printf("fd leak in test process? before, after:\n%v\n%v\n", before, after) - os.Exit(1) - } - test_helpers.UnmountPanic(test_helpers.DefaultPlainDir) - if r != 0 { - fmt.Printf("TestMain: matrix[%d] = %#v failed\n", i, testcase) - os.Exit(r) - } - } - os.Exit(0) -} - // Write `n` random bytes to filename `fn`, read again, compare hash func testWriteN(t *testing.T, fn string, n int) string { file, err := os.Create(test_helpers.DefaultPlainDir + "/" + fn) @@ -327,7 +224,7 @@ func TestFileHoles(t *testing.T) { foo := []byte("foo") file.Write(foo) file.WriteAt(foo, 4096) - _, err = ioutil.ReadFile(fn) + _, err = os.ReadFile(fn) if err != nil { t.Error(err) } @@ -391,7 +288,7 @@ func TestRmwRace(t *testing.T) { // but it must not be // [oooooossss] - buf, _ := ioutil.ReadFile(fn) + buf, _ := os.ReadFile(fn) m := test_helpers.Md5hex(buf) goodMd5[m] = goodMd5[m] + 1 @@ -495,7 +392,7 @@ func TestNameLengths(t *testing.T) { } func TestLongNames(t *testing.T) { - fi, err := ioutil.ReadDir(test_helpers.DefaultCipherDir) + fi, err := os.ReadDir(test_helpers.DefaultCipherDir) if err != nil { t.Fatal(err) } @@ -607,7 +504,7 @@ func TestLongNames(t *testing.T) { t.Error(err) } // Check for orphaned files - fi, err = ioutil.ReadDir(test_helpers.DefaultCipherDir) + fi, err = os.ReadDir(test_helpers.DefaultCipherDir) if err != nil { t.Fatal(err) } @@ -740,7 +637,7 @@ func doTestUtimesNano(t *testing.T, path string) { // Set nanoseconds by path, normal file func TestUtimesNano(t *testing.T) { path := test_helpers.DefaultPlainDir + "/utimesnano" - err := ioutil.WriteFile(path, []byte("foobar"), 0600) + err := os.WriteFile(path, []byte("foobar"), 0600) if err != nil { t.Fatal(err) } @@ -788,7 +685,7 @@ func TestMagicNames(t *testing.T) { t.Logf("Testing n=%q", n) p := test_helpers.DefaultPlainDir + "/" + n // Create file - err := ioutil.WriteFile(p, []byte("xxxxxxx"), 0200) + err := os.WriteFile(p, []byte("xxxxxxx"), 0200) if err != nil { t.Fatalf("creating file %q failed: %v", n, err) } @@ -825,7 +722,7 @@ func TestMagicNames(t *testing.T) { syscall.Unlink(p) // Link target := test_helpers.DefaultPlainDir + "/linktarget" - err = ioutil.WriteFile(target, []byte("yyyyy"), 0600) + err = os.WriteFile(target, []byte("yyyyy"), 0600) if err != nil { t.Fatal(err) } @@ -982,3 +879,117 @@ func TestRootIno(t *testing.T) { t.Errorf("inode number of root dir is zero") } } + +// TestDirSize checks that we report the correct size for directories. +// +// https://github.com/rfjakob/gocryptfs/issues/951: +// "cipherSize 30: incomplete last block" after upgrade to 2.6.0 +func TestDirSize(t *testing.T) { + pDir := test_helpers.DefaultPlainDir + "/" + t.Name() + err := os.Mkdir(pDir, 0700) + if err != nil { + t.Fatal(err) + } + req := ctlsock.RequestStruct{ + EncryptPath: t.Name(), + } + resp := test_helpers.QueryCtlSock(t, ctlsockPath, req) + if resp.Result == "" { + t.Fatal(resp) + } + cDir := test_helpers.DefaultCipherDir + "/" + resp.Result + if err != nil { + t.Fatal(err) + } + + fstat := func(path string) (st syscall.Stat_t) { + fd, err := syscall.Open(path, syscall.O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + + err = syscall.Fstat(fd, &st) + if err != nil { + t.Fatal(err) + } + return st + } + + // Check that plaintext size is equal to ciphertext size and + // that stat and fstat gives the same result + compareSize := func() { + var pStat, cStat syscall.Stat_t + err = syscall.Stat(pDir, &pStat) + if err != nil { + t.Fatal(err) + } + pFstat := fstat(pDir) + + err = syscall.Stat(cDir, &cStat) + if err != nil { + t.Fatal(err) + } + cFstat := fstat(cDir) + + if pStat.Size != cStat.Size { + t.Errorf("stat size is different: pSt=%d cSt=%d", pStat.Size, cStat.Size) + } + if pStat.Size != pFstat.Size { + t.Errorf("stat size is different from fstat size: pStat=%d pFstat=%d", pStat.Size, pFstat.Size) + } + if pFstat.Size != cFstat.Size { + t.Errorf("fstat size is different: pSt=%d cSt=%d", pFstat.Size, cFstat.Size) + } + } + + for i := 0; i < 100; i++ { + // Grow the directory by creating new files with long names inside it + path := fmt.Sprintf("%s/%0100d", pDir, i) + err = os.WriteFile(path, nil, 0600) + if err != nil { + t.Fatal(err) + } + compareSize() + } +} + +// TestRenameExchangeOnGocryptfs tests the core RENAME_EXCHANGE functionality +// on a mounted gocryptfs filesystem +func TestRenameExchangeOnGocryptfs(t *testing.T) { + // Create two files with different content + file1 := filepath.Join(test_helpers.DefaultPlainDir, "file1.txt") + file2 := filepath.Join(test_helpers.DefaultPlainDir, "file2.txt") + content1 := []byte("Content of file 1") + content2 := []byte("Content of file 2") + + if err := os.WriteFile(file1, content1, 0644); err != nil { + t.Fatalf("Failed to create file1: %v", err) + } + if err := os.WriteFile(file2, content2, 0644); err != nil { + t.Fatalf("Failed to create file2: %v", err) + } + + // Use RENAME_EXCHANGE to atomically swap the files + err := syscallcompat.Renameat2(unix.AT_FDCWD, file1, unix.AT_FDCWD, file2, syscallcompat.RENAME_EXCHANGE) + if err != nil { + t.Fatalf("RENAME_EXCHANGE failed on gocryptfs: %v", err) + } + + // Verify the files were swapped + newContent1, err := os.ReadFile(file1) + if err != nil { + t.Fatalf("Failed to read file1 after exchange: %v", err) + } + newContent2, err := os.ReadFile(file2) + if err != nil { + t.Fatalf("Failed to read file2 after exchange: %v", err) + } + + if string(newContent1) != string(content2) { + t.Errorf("file1 content wrong after exchange. Expected: %s, Got: %s", content2, newContent1) + } + if string(newContent2) != string(content1) { + t.Errorf("file2 content wrong after exchange. Expected: %s, Got: %s", content1, newContent2) + } +} diff --git a/tests/matrix/symlink_darwin_test.go b/tests/matrix/symlink_darwin_test.go new file mode 100644 index 0000000..be28d9d --- /dev/null +++ b/tests/matrix/symlink_darwin_test.go @@ -0,0 +1,39 @@ +package matrix + +import ( + "os" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestOpenSymlinkDarwin checks that a symlink can be opened +// using O_SYMLINK. +func TestOpenSymlinkDarwin(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/TestOpenSymlink" + target := "/target/does/not/exist" + err := os.Symlink(target, path) + if err != nil { + t.Fatal(err) + } + fd, err := unix.Open(path, unix.O_RDONLY|unix.O_SYMLINK, 0) + if err != nil { + t.Fatal(err) + } + defer unix.Close(fd) + var st unix.Stat_t + if err := unix.Fstat(fd, &st); err != nil { + t.Fatal(err) + } + if st.Size != int64(len(target)) { + t.Errorf("wrong size: have=%d want=%d", st.Size, len(target)) + } + if err := unix.Unlink(path); err != nil { + t.Fatal(err) + } + if err := unix.Fstat(fd, &st); err != nil { + t.Error(err) + } +} diff --git a/tests/matrix/symlink_linux_test.go b/tests/matrix/symlink_linux_test.go new file mode 100644 index 0000000..fdb7051 --- /dev/null +++ b/tests/matrix/symlink_linux_test.go @@ -0,0 +1,47 @@ +package matrix + +import ( + "os" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestOpenSymlinkLinux checks that a symlink can be opened +// using O_PATH. +// Only works on Linux because is uses O_PATH and AT_EMPTY_PATH. +// MacOS has O_SYMLINK instead (see TestOpenSymlinkDarwin). +func TestOpenSymlinkLinux(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/TestOpenSymlink" + target := "/target/does/not/exist" + err := os.Symlink(target, path) + if err != nil { + t.Fatal(err) + } + how := unix.OpenHow{ + Flags: unix.O_PATH | unix.O_NOFOLLOW, + } + fd, err := unix.Openat2(unix.AT_FDCWD, path, &how) + if err != nil { + t.Fatal(err) + } + defer unix.Close(fd) + var st unix.Stat_t + if err := unix.Fstatat(fd, "", &st, unix.AT_EMPTY_PATH); err != nil { + t.Fatal(err) + } + if st.Size != int64(len(target)) { + t.Errorf("wrong size: have=%d want=%d", st.Size, len(target)) + } + if err := unix.Unlink(path); err != nil { + t.Fatal(err) + } + if err = unix.Fstatat(fd, "", &st, unix.AT_EMPTY_PATH); err != nil { + // That's a bug, but I have never heard of a use case that would break because of this. + // Also I don't see how to fix it, as gocryptfs does not get informed about the earlier + // Openat2(). + t.Logf("posix compliance issue: deleted symlink cannot be accessed: Fstatat: %v", err) + } +} diff --git a/tests/plaintextnames/file_holes_test.go b/tests/plaintextnames/file_holes_test.go index a17597a..7a356c0 100644 --- a/tests/plaintextnames/file_holes_test.go +++ b/tests/plaintextnames/file_holes_test.go @@ -7,7 +7,6 @@ import ( "os/exec" "syscall" "testing" - "time" "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" @@ -129,6 +128,8 @@ func doTestFileHoleCopy(t *testing.T, name string, writeOffsets []int64) { // The test runs with -plaintextnames because that makes it easier to manipulate // cipherdir directly. func TestFileHoleCopy(t *testing.T) { + t.Skip("TODO: find out why this fails on recent kernels") + // | hole | x | hole | x | hole | // truncate -s 50000 foo && dd if=/dev/zero of=foo bs=1 seek=10000 count=1 conv=notrunc && dd if=/dev/zero of=foo bs=1 seek=30000 count=1 conv=notrunc name := "c0" @@ -138,7 +139,6 @@ func TestFileHoleCopy(t *testing.T) { return } - rand.Seed(time.Now().UnixNano()) for k := 0; k < 100; k++ { c1 := make([]int64, 10) for i := range c1 { diff --git a/tests/plaintextnames/plaintextnames_test.go b/tests/plaintextnames/plaintextnames_test.go index 4de0730..a755deb 100644 --- a/tests/plaintextnames/plaintextnames_test.go +++ b/tests/plaintextnames/plaintextnames_test.go @@ -4,7 +4,6 @@ package plaintextnames import ( "fmt" - "io/ioutil" "os" "syscall" "testing" @@ -63,7 +62,7 @@ func TestDirIV(t *testing.T) { // else should work. func TestFiltered(t *testing.T) { filteredFile := pDir + "/gocryptfs.conf" - err := ioutil.WriteFile(filteredFile, []byte("foo"), 0777) + err := os.WriteFile(filteredFile, []byte("foo"), 0777) if err == nil { t.Errorf("should have failed but didn't") } @@ -71,11 +70,11 @@ func TestFiltered(t *testing.T) { if err == nil { t.Errorf("should have failed but didn't") } - err = ioutil.WriteFile(pDir+"/gocryptfs.diriv", []byte("foo"), 0777) + err = os.WriteFile(pDir+"/gocryptfs.diriv", []byte("foo"), 0777) if err != nil { t.Error(err) } - subDir, err := ioutil.TempDir(pDir, "") + subDir, err := os.MkdirTemp(pDir, "") if err != nil { t.Fatal(err) } diff --git a/tests/reverse/correctness_test.go b/tests/reverse/correctness_test.go index b335456..4028ac3 100644 --- a/tests/reverse/correctness_test.go +++ b/tests/reverse/correctness_test.go @@ -3,7 +3,6 @@ package reverse_test import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" "strings" @@ -72,7 +71,7 @@ func TestSymlinkDentrySize(t *testing.T) { } symlink := "a_symlink" - mnt, err := ioutil.TempDir(test_helpers.TmpDir, "reverse_mnt_") + mnt, err := os.MkdirTemp(test_helpers.TmpDir, "reverse_mnt_") if err != nil { t.Fatal(err) } @@ -111,7 +110,7 @@ func TestConfigMapping(t *testing.T) { if !test_helpers.VerifyExistence(t, c) { t.Errorf("%s missing", c) } - data, err := ioutil.ReadFile(c) + data, err := os.ReadFile(c) if err != nil { t.Fatal(err) } @@ -233,7 +232,7 @@ func Test0100Dir(t *testing.T) { t.Fatal(err) } file := dir + "/hello" - err = ioutil.WriteFile(file, []byte("hello"), 0600) + err = os.WriteFile(file, []byte("hello"), 0600) if err != nil { t.Fatal(err) } @@ -295,6 +294,23 @@ func TestSeekData(t *testing.T) { f.Close() } +// newWorkdir creates a new empty dir in dirA and returns the full path to it along +// with the corresponding encrypted path in dirB +func newWorkdir(t *testing.T) (workdirA, workdirB string) { + workdirA = dirA + "/" + t.Name() + if err := os.Mkdir(workdirA, 0700); err != nil { + t.Fatal(err) + } + // Find workdir in dirB (=encrypted view) + var st syscall.Stat_t + if err := syscall.Stat(workdirA, &st); err != nil { + t.Fatal(err) + } + workdirB = dirB + "/" + findIno(dirB, st.Ino) + t.Logf("newWorkdir: workdirA=%q workdirB=%q", workdirA, workdirB) + return +} + // gocryptfs.longname.*.name of hardlinked files should not appear hardlinked (as the // contents are different). // @@ -308,28 +324,22 @@ func TestHardlinkedLongname(t *testing.T) { t.Skip() } - workdir := dirA + "/" + t.Name() - if err := os.Mkdir(workdir, 0700); err != nil { + workdirA, workdirB := newWorkdir(t) + + long1 := workdirA + "/" + strings.Repeat("x", 200) + if err := os.WriteFile(long1, []byte("hello"), 0600); err != nil { t.Fatal(err) } - long1 := workdir + "/" + strings.Repeat("x", 200) - if err := ioutil.WriteFile(long1, []byte("hello"), 0600); err != nil { + var long1_stat syscall.Stat_t + if err := syscall.Stat(long1, &long1_stat); err != nil { t.Fatal(err) } - long2 := workdir + "/" + strings.Repeat("y", 220) + long2 := workdirA + "/" + strings.Repeat("y", 220) if err := syscall.Link(long1, long2); err != nil { t.Fatal(err) } - // Find workdir in encrypted view - var st syscall.Stat_t - if err := syscall.Stat(workdir, &st); err != nil { - t.Fatal(err) - } - cWorkdir := dirB + "/" + findIno(dirB, st.Ino) - t.Logf("workdir=%q cWorkdir=%q", workdir, cWorkdir) - - matches, err := filepath.Glob(cWorkdir + "/gocryptfs.longname.*.name") + matches, err := filepath.Glob(workdirB + "/gocryptfs.longname.*.name") if err != nil { t.Fatal(err) } @@ -352,3 +362,61 @@ func TestHardlinkedLongname(t *testing.T) { t.Errorf("Files %q have the same inode number - that's wrong!", matches) } } + +// With inode number reuse and hard links, we could have returned +// wrong data for gocryptfs.diriv and gocryptfs.xyz.longname files, respectively +// (https://github.com/rfjakob/gocryptfs/issues/802). +// +// Now that this is fixed, ensure that rsync and similar tools pick up the new +// correct files by advancing mtime and ctime by 10 seconds, which should be more +// than any filesytems' timestamp granularity (FAT32 has 2 seconds). +func TestMtimePlus10(t *testing.T) { + if plaintextnames { + t.Skip("plaintextnames mode does not have virtual files") + } + + workdirA, workdirB := newWorkdir(t) + + long := workdirA + "/" + strings.Repeat("x", 200) + if err := os.WriteFile(long, nil, 0600); err != nil { + t.Fatal(err) + } + long_stat, err := os.Stat(long) + if err != nil { + t.Fatal(err) + } + + workdirA_stat, err := os.Stat(workdirA) + if err != nil { + t.Fatal(err) + } + + // Find and check gocryptfs.longname.*.name + matches, err := filepath.Glob(workdirB + "/gocryptfs.longname.*.name") + if err != nil { + t.Fatal(err) + } + if len(matches) != 1 { + t.Fatal(matches) + } + name_stat, err := os.Stat(matches[0]) + if err != nil { + t.Fatal(err) + } + if name_stat.ModTime().Unix() != long_stat.ModTime().Unix()+10 { + t.Errorf(".name file should show mtime+10") + } + + // Check gocryptfs.diriv + if deterministic_names { + // No gocryptfs.diriv + return + } + diriv_stat, err := os.Stat(workdirB + "/gocryptfs.diriv") + if err != nil { + t.Fatal(err) + } + if diriv_stat.ModTime().Unix() != workdirA_stat.ModTime().Unix()+10 { + t.Errorf("diriv file should show mtime+10") + } +} diff --git a/tests/reverse/ctlsock_test.go b/tests/reverse/ctlsock_test.go index f59fa45..8f9d843 100644 --- a/tests/reverse/ctlsock_test.go +++ b/tests/reverse/ctlsock_test.go @@ -1,7 +1,7 @@ package reverse_test import ( - "io/ioutil" + "os" "syscall" "testing" @@ -28,7 +28,7 @@ func TestCtlSockPathOps(t *testing.T) { if plaintextnames { t.Skip("this only tests encrypted names") } - mnt, err := ioutil.TempDir(test_helpers.TmpDir, "reverse_mnt_") + mnt, err := os.MkdirTemp(test_helpers.TmpDir, "reverse_mnt_") if err != nil { t.Fatal(err) } @@ -74,7 +74,7 @@ func TestCtlSockCrash(t *testing.T) { if plaintextnames { t.Skip("this only tests encrypted names") } - mnt, err := ioutil.TempDir(test_helpers.TmpDir, "reverse_mnt_") + mnt, err := os.MkdirTemp(test_helpers.TmpDir, "reverse_mnt_") if err != nil { t.Fatal(err) } diff --git a/tests/reverse/exclude_test.go b/tests/reverse/exclude_test.go index f24ff2c..645d267 100644 --- a/tests/reverse/exclude_test.go +++ b/tests/reverse/exclude_test.go @@ -1,7 +1,8 @@ package reverse_test import ( - "io/ioutil" + "log" + "os" "path/filepath" "testing" @@ -19,36 +20,29 @@ func ctlsockEncryptPath(t *testing.T, sock string, path string) string { return response.Result } -// doTestExcludeTestFs runs exclude tests against the exclude_test_fs folder -func doTestExcludeTestFs(t *testing.T, flag string, patterns, visible, hidden []string) { - // Mount reverse fs - mnt, err := ioutil.TempDir(test_helpers.TmpDir, t.Name()) - if err != nil { - t.Fatal(err) - } - sock := mnt + ".sock" - cliArgs := []string{"-reverse", "-extpass", "echo test", "-ctlsock", sock} +// doTestExcludeTestFs runs exclude tests against the exclude_test_fs folder. +// flag is either "--exclude-wildcard" or "--exclude" +func doTestExcludeTestFs(t *testing.T, flag string, patterns []string, tree directoryTree) { + var extraArgs []string for _, v := range patterns { - cliArgs = append(cliArgs, flag, v) - } - if plaintextnames { - cliArgs = append(cliArgs, "-config", "exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames") + extraArgs = append(extraArgs, flag, v) } - test_helpers.MountOrFatal(t, "exclude_test_fs", mnt, cliArgs...) + // Mount reverse fs + backingDir, mnt, sock := newReverseFS(extraArgs) defer test_helpers.UnmountPanic(mnt) + tree.createOnDisk(backingDir) + // Get encrypted version of visible and hidden paths - cVisible := encryptExcludeTestPaths(t, sock, visible) - cHidden := encryptExcludeTestPaths(t, sock, hidden) + cVisible := encryptExcludeTestPaths(t, sock, tree.visible()) + cHidden := encryptExcludeTestPaths(t, sock, tree.hidden()) // Check that hidden paths are not there and visible paths are there for _, v := range cHidden { if test_helpers.VerifyExistence(t, mnt+"/"+v) { t.Errorf("File %q is visible, but should be hidden", v) } - if nametransform.IsLongContent(filepath.Base(v)) { - // TODO ??? - } + } for _, v := range cVisible { if !test_helpers.VerifyExistence(t, mnt+"/"+v) { @@ -74,6 +68,44 @@ func encryptExcludeTestPaths(t *testing.T, socket string, pRelPaths []string) (o return out } +type directoryTree struct { + visibleFiles []string + visibleDirs []string + hiddenFiles []string + hiddenDirs []string +} + +func (tr *directoryTree) visible() []string { + return append(tr.visibleDirs, tr.visibleFiles...) +} + +func (tr *directoryTree) hidden() []string { + return append(tr.hiddenDirs, tr.hiddenFiles...) +} + +func (tr *directoryTree) createOnDisk(baseDir string) { + dirs := append(tr.hiddenDirs, tr.visibleDirs...) + for _, d := range dirs { + err := os.MkdirAll(baseDir+"/"+d, 0700) + if err != nil { + log.Panic(err) + } + } + + files := append(tr.hiddenFiles, tr.visibleFiles...) + for _, f := range files { + d := filepath.Dir(f) + err := os.MkdirAll(baseDir+"/"+d, 0700) + if err != nil { + log.Panic(err) + } + err = os.WriteFile(baseDir+"/"+f, nil, 0600) + if err != nil { + log.Panic(err) + } + } +} + // TestExcludeTestFs runs exclude tests against the exclude_test_fs folder. func TestExcludeTestFs(t *testing.T) { // --exclude-wildcard patterns, gitignore syntax @@ -90,19 +122,22 @@ func TestExcludeTestFs(t *testing.T) { "dir1/**/exclude", // ** matches any number of directories "file3/", // pattern with trailing slash should not match a file } + var tree directoryTree // visible are plaintext paths that should be visible in the encrypted view - visible := []string{ + tree.visibleFiles = []string{ "file2", "dir1/longfile1" + x240, "dir1/longfile3" + x240, - "longdir1" + x240, "longdir1" + x240 + "/file1", "longdir2" + x240 + "/file", "longfile1" + x240, "file3", } + tree.visibleDirs = []string{ + "longdir1" + x240, + } // hidden are plaintext paths that should be hidden in the encrypted view - hidden := []string{ + tree.hiddenFiles = []string{ "bkp1~", "dir1/file1", "dir1/file2", @@ -111,20 +146,21 @@ func TestExcludeTestFs(t *testing.T) { "dir1/longfile2" + x240, "dir1/subdir1/exclude", "dir1/subdir1/subdir2/exclude", - "dir2", "dir2/file", "dir2/longdir1" + x240 + "/file", "dir2/longfile." + x240, - "dir2/subdir", "dir2/subdir/file", "file1", "longdir2" + x240 + "/bkp~", "longfile2" + x240, "longfile3" + x240, } - - doTestExcludeTestFs(t, "-exclude-wildcard", patterns, visible, hidden) - doTestExcludeTestFs(t, "-ew", patterns, visible, hidden) + tree.hiddenDirs = []string{ + "dir2", + "dir2/subdir", + } + doTestExcludeTestFs(t, "-exclude-wildcard", patterns, tree) + doTestExcludeTestFs(t, "-ew", patterns, tree) } // Exclude everything using "/*", then selectively include only dir1 using "!/dir1" @@ -135,23 +171,28 @@ func TestExcludeAllOnlyDir1(t *testing.T) { "*", "!/dir1", } + var tree directoryTree // visible are plaintext paths that should be visible in the encrypted view - visible := []string{ + tree.visibleDirs = []string{ "dir1", + } + tree.visibleFiles = []string{ "dir1/file1", } // hidden are plaintext paths that should be hidden in the encrypted view - hidden := []string{ + tree.hiddenDirs = []string{ "dir2", + "dir2/subdir", + } + tree.hiddenFiles = []string{ "dir2/file", "dir2/longdir1" + x240 + "/file", "dir2/longfile." + x240, - "dir2/subdir", "dir2/subdir/file", "file1", "longdir2" + x240 + "/bkp~", "longfile2" + x240, "longfile3" + x240, } - doTestExcludeTestFs(t, "-exclude-wildcard", patterns, visible, hidden) + doTestExcludeTestFs(t, "-exclude-wildcard", patterns, tree) } diff --git a/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf b/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf deleted file mode 100644 index 835d11c..0000000 --- a/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf +++ /dev/null @@ -1,21 +0,0 @@ -{ - "Creator": "gocryptfs v1.5-41-gf48b731-dirty", - "EncryptedKey": "FkACqloUeFZesem0UzRD3ezLXtPl8wIAxEHoIEfZxFdLMQeWOxqtw5xopJagDWE/GI1VFSUIrJIIIwwgMipmYA==", - "ScryptObject": { - "Salt": "UVfIgV31uj/voHWI4GqGwsTcbVKyYDOWvbleqJKhZbk=", - "N": 1024, - "R": 8, - "P": 1, - "KeyLen": 32 - }, - "Version": 2, - "FeatureFlags": [ - "GCMIV128", - "HKDF", - "DirIV", - "EMENames", - "LongNames", - "Raw64", - "AESSIV" - ] -} diff --git a/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames b/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames deleted file mode 100644 index 9cb762c..0000000 --- a/tests/reverse/exclude_test_fs/.gocryptfs.reverse.conf.plaintextnames +++ /dev/null @@ -1,18 +0,0 @@ -{ - "Creator": "gocryptfs v1.5-41-gf48b731-dirty", - "EncryptedKey": "wAmckZb7QsIv/GCdkhb5ep8TwJa44qhnswn5tbER6Tifk8TbUmkwBTceaTtYfHAnTQ48q9mnIlcN9cfbNe5oPw==", - "ScryptObject": { - "Salt": "o5XJ78TgG85zZXRnU55ZqHhKLbPge6jsyDiqrLvSqe0=", - "N": 1024, - "R": 8, - "P": 1, - "KeyLen": 32 - }, - "Version": 2, - "FeatureFlags": [ - "GCMIV128", - "HKDF", - "PlaintextNames", - "AESSIV" - ] -} diff --git a/tests/reverse/exclude_test_fs/bkp1~ b/tests/reverse/exclude_test_fs/bkp1~ deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/bkp1~ +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/exclude b/tests/reverse/exclude_test_fs/dir1/exclude deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/exclude +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/file1 b/tests/reverse/exclude_test_fs/dir1/file1 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/file1 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/file2 b/tests/reverse/exclude_test_fs/dir1/file2 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/file2 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ b/tests/reverse/exclude_test_fs/dir1/longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/longbkp1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~ +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/dir1/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/dir1/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/dir1/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/subdir1/exclude b/tests/reverse/exclude_test_fs/dir1/subdir1/exclude deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/subdir1/exclude +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir1/subdir1/subdir2/exclude b/tests/reverse/exclude_test_fs/dir1/subdir1/subdir2/exclude deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir1/subdir1/subdir2/exclude +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir2/file b/tests/reverse/exclude_test_fs/dir2/file deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir2/file +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir2/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file b/tests/reverse/exclude_test_fs/dir2/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir2/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir2/longfile.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/dir2/longfile.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir2/longfile.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/dir2/subdir/file b/tests/reverse/exclude_test_fs/dir2/subdir/file deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/dir2/subdir/file +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/file1 b/tests/reverse/exclude_test_fs/file1 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/file1 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/file2 b/tests/reverse/exclude_test_fs/file2 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/file2 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/file3 b/tests/reverse/exclude_test_fs/file3 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/file3 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file1 b/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file1 deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longdir1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file1 +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/bkp~ b/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/bkp~ deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/bkp~ +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file b/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longdir2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/file +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longfile1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longfile2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/exclude_test_fs/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx b/tests/reverse/exclude_test_fs/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx deleted file mode 100644 index e69de29..0000000 --- a/tests/reverse/exclude_test_fs/longfile3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +++ /dev/null diff --git a/tests/reverse/force_owner_test.go b/tests/reverse/force_owner_test.go index 6e47b50..aeb0a4e 100644 --- a/tests/reverse/force_owner_test.go +++ b/tests/reverse/force_owner_test.go @@ -1,7 +1,6 @@ package reverse_test import ( - "io/ioutil" "net/url" "os" "syscall" @@ -13,7 +12,7 @@ import ( func TestForceOwner(t *testing.T) { // Let's not explode with "TempDir: pattern contains path separator" myEscapedName := url.PathEscape(t.Name()) - mnt, err := ioutil.TempDir(test_helpers.TmpDir, myEscapedName) + mnt, err := os.MkdirTemp(test_helpers.TmpDir, myEscapedName) if err != nil { t.Fatal(err) } diff --git a/tests/reverse/inomap_test.go b/tests/reverse/inomap_test.go index ff78f4c..fadf6a5 100644 --- a/tests/reverse/inomap_test.go +++ b/tests/reverse/inomap_test.go @@ -85,7 +85,7 @@ func TestVirtualFileIno(t *testing.T) { if err != nil { t.Fatal(err) } - dirents, err := fd.Readdirnames(0) + _, err = fd.Readdirnames(0) if err != nil { t.Fatal(err) } @@ -104,7 +104,7 @@ func TestVirtualFileIno(t *testing.T) { if err != nil { t.Fatal(err) } - dirents, err = fd.Readdirnames(0) + dirents, err := fd.Readdirnames(0) if err != nil { t.Fatal(err) } diff --git a/tests/reverse/main_test.go b/tests/reverse/main_test.go index 2fc9e5e..6a3ed9b 100644 --- a/tests/reverse/main_test.go +++ b/tests/reverse/main_test.go @@ -2,7 +2,9 @@ package reverse_test import ( "bytes" + "flag" "fmt" + "log" "os" "testing" @@ -31,6 +33,8 @@ var dirC string func TestMain(m *testing.M) { var r int + flag.Parse() + testcases := []struct { plaintextnames bool deterministic_names bool @@ -40,23 +44,17 @@ func TestMain(m *testing.M) { {false, true}, } for i, tc := range testcases { - argsA := []string{"-reverse"} + // Fill the global vars plaintextnames, deterministic_names = tc.plaintextnames, tc.deterministic_names - if tc.plaintextnames { - argsA = append(argsA, "-plaintextnames") - } else if tc.deterministic_names { - argsA = append(argsA, "-deterministic-names") + if testing.Verbose() { + log.Printf("TestMain: plaintextnames=%v deterministic_names=%v", plaintextnames, deterministic_names) } - dirA = test_helpers.InitFS(nil, argsA...) - dirB = test_helpers.TmpDir + "/b" + + dirA, dirB, _ = newReverseFS(nil) dirC = test_helpers.TmpDir + "/c" - if err := os.Mkdir(dirB, 0700); err != nil { - panic(err) - } if err := os.Mkdir(dirC, 0700); err != nil { panic(err) } - test_helpers.MountOrExit(dirA, dirB, "-reverse", "-extpass", "echo test") test_helpers.MountOrExit(dirB, dirC, "-extpass", "echo test") r = m.Run() test_helpers.UnmountPanic(dirC) @@ -73,3 +71,24 @@ func TestMain(m *testing.M) { } os.Exit(r) } + +// newReverseFS creates and mounts a new, empty reverse filesystem. +func newReverseFS(extraMountArgs []string) (backingDir, mntDir, ctlsockPath string) { + args := []string{"-reverse"} + if plaintextnames { + args = append(args, "-plaintextnames") + } else if deterministic_names { + args = append(args, "-deterministic-names") + } + backingDir = test_helpers.InitFS(nil, args...) + mntDir = backingDir + ".mnt" + ctlsockPath = mntDir + ".sock" + mountArgs := []string{"-reverse", "-extpass", "echo test", "-ctlsock", ctlsockPath} + mountArgs = append(mountArgs, extraMountArgs...) + test_helpers.MountOrExit(backingDir, mntDir, mountArgs...) + + if testing.Verbose() { + log.Printf("newReverseFS: mounted %q on %q", backingDir, mntDir) + } + return +} diff --git a/tests/reverse/one_file_system_test.go b/tests/reverse/one_file_system_test.go index 61190ea..823cf62 100644 --- a/tests/reverse/one_file_system_test.go +++ b/tests/reverse/one_file_system_test.go @@ -1,7 +1,6 @@ package reverse_test import ( - "io/ioutil" "net/url" "os" "runtime" @@ -17,7 +16,7 @@ func TestOneFileSystem(t *testing.T) { } // Let's not explode with "TempDir: pattern contains path separator" myEscapedName := url.PathEscape(t.Name()) - mnt, err := ioutil.TempDir(test_helpers.TmpDir, myEscapedName) + mnt, err := os.MkdirTemp(test_helpers.TmpDir, myEscapedName) if err != nil { t.Fatal(err) } @@ -33,7 +32,7 @@ func TestOneFileSystem(t *testing.T) { // Copied from inomap const maxPassthruIno = 1<<48 - 1 - entries, err := ioutil.ReadDir(mnt) + entries, err := os.ReadDir(mnt) if err != nil { t.Fatal(err) } @@ -43,7 +42,11 @@ func TestOneFileSystem(t *testing.T) { // We are only interested in directories continue } - st := e.Sys().(*syscall.Stat_t) + info, err := e.Info() + if err != nil { + continue + } + st := info.Sys().(*syscall.Stat_t) // The inode numbers of files with a different device number are remapped // to something above maxPassthruIno if st.Ino > maxPassthruIno { diff --git a/tests/reverse/xattr_test.go b/tests/reverse/xattr_test.go index 8002604..b6a7b34 100644 --- a/tests/reverse/xattr_test.go +++ b/tests/reverse/xattr_test.go @@ -2,12 +2,14 @@ package reverse_test import ( "fmt" - "io/ioutil" + "os" "path/filepath" + "strings" "syscall" "testing" "github.com/pkg/xattr" + "golang.org/x/sys/unix" ) func xattrSupported(path string) bool { @@ -16,26 +18,21 @@ func xattrSupported(path string) bool { return true } err2 := err.(*xattr.Error) - if err2.Err == syscall.EOPNOTSUPP { - return false - } - return true + return err2.Err != syscall.EOPNOTSUPP } func TestXattrList(t *testing.T) { - t.Skip("TODO: not implemented yet in reverse mode") - if !xattrSupported(dirA) { t.Skip() } fnA := filepath.Join(dirA, t.Name()) - err := ioutil.WriteFile(fnA, nil, 0700) + err := os.WriteFile(fnA, nil, 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } val := []byte("xxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzz") num := 20 - var namesA map[string]string + namesA := map[string]string{} for i := 1; i <= num; i++ { attr := fmt.Sprintf("user.TestXattrList.%02d", i) err = xattr.LSet(fnA, attr, val) @@ -49,9 +46,14 @@ func TestXattrList(t *testing.T) { if err != nil { t.Fatal(err) } - var namesC map[string]string + namesC := map[string]string{} for _, n := range tmp { - namesC[n] = string(val) + if strings.HasPrefix(n, "security.") { + t.Logf("Ignoring xattr %q", n) + continue + } + v, _ := xattr.LGet(fnC, n) + namesC[n] = string(v) } if len(namesA) != len(namesC) { t.Errorf("wrong number of names, want=%d have=%d", len(namesA), len(namesC)) @@ -64,3 +66,18 @@ func TestXattrList(t *testing.T) { } } } + +// Shouldn't get EINVAL when querying the mountpoint. +func TestXattrGetMountpoint(t *testing.T) { + _, err := xattr.LGet(dirB, "user.foo453465324") + if err == nil { + return + } + e2 := err.(*xattr.Error) + if e2.Unwrap() == unix.EINVAL { + t.Errorf("LGet: %v", err) + } + // Let's see what LList says + _, err = xattr.LList(dirB) + t.Logf("LList: err=%v", err) +} diff --git a/tests/root_test/btrfs_test.go b/tests/root_test/btrfs_test.go new file mode 100644 index 0000000..898cd39 --- /dev/null +++ b/tests/root_test/btrfs_test.go @@ -0,0 +1,110 @@ +package root_test + +import ( + "os" + "os/exec" + "path/filepath" + "syscall" + "testing" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// createBtrfsImage creates a btrfs image file, formats it, and mounts it. +// Returns the mount path and a cleanup function. +func createBtrfsImage(t *testing.T) (mnt string, cleanup func()) { + t.Helper() + + if os.Getuid() != 0 { + t.Skip("must run as root") + } + + _, err := exec.LookPath("mkfs.btrfs") + if err != nil { + t.Skip("mkfs.btrfs not found, skipping test") + } + + img := filepath.Join(test_helpers.TmpDir, t.Name()+".img") + f, err := os.Create(img) + if err != nil { + t.Fatal(err) + } + defer f.Close() + // minimum size for each btrfs device is 114294784 + err = f.Truncate(200 * 1024 * 1024) + if err != nil { + t.Fatal(err) + } + + // Format as Btrfs + cmd := exec.Command("mkfs.btrfs", img) + out, err := cmd.CombinedOutput() + if err != nil { + t.Logf("%q", cmd.Args) + t.Log(string(out)) + t.Fatal(err) + } + + // Mount + mnt = img + ".mnt" + err = os.Mkdir(mnt, 0600) + if err != nil { + t.Fatal(err) + } + cmd = exec.Command("mount", img, mnt) + out, err = cmd.CombinedOutput() + if err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + cleanup = func() { + syscall.Unmount(mnt, 0) + syscall.Unlink(img) + } + return mnt, cleanup +} + +// TestBtrfsQuirks needs root permissions because it creates a loop disk +func TestBtrfsQuirks(t *testing.T) { + mnt, cleanup := createBtrfsImage(t) + defer cleanup() + + quirk := syscallcompat.DetectQuirks(mnt) + if quirk != syscallcompat.QuirkBtrfsBrokenFalloc { + t.Errorf("wrong quirk: %v", quirk) + } +} + +// TestBtrfsQuirksNoCow verifies that when the backing directory has +// the NOCOW attribute (chattr +C), the QuirkBtrfsBrokenFalloc quirk +// is NOT set, because fallocate works correctly with NOCOW. +func TestBtrfsQuirksNoCow(t *testing.T) { + mnt, cleanup := createBtrfsImage(t) + defer cleanup() + + _, err := exec.LookPath("chattr") + if err != nil { + t.Skip("chattr not found, skipping test") + } + + // Create a subdirectory with NOCOW attribute + nocowDir := filepath.Join(mnt, "nocow") + err = os.Mkdir(nocowDir, 0700) + if err != nil { + t.Fatal(err) + } + cmd := exec.Command("chattr", "+C", nocowDir) + out, err := cmd.CombinedOutput() + if err != nil { + t.Log(string(out)) + t.Fatal(err) + } + + quirk := syscallcompat.DetectQuirks(nocowDir) + if quirk&syscallcompat.QuirkBtrfsBrokenFalloc != 0 { + t.Errorf("QuirkBtrfsBrokenFalloc should not be set on NOCOW directory, got quirks: %v", quirk) + } +} diff --git a/tests/root_test/issue893_test.go b/tests/root_test/issue893_test.go new file mode 100644 index 0000000..2db0b19 --- /dev/null +++ b/tests/root_test/issue893_test.go @@ -0,0 +1,98 @@ +//go:build linux + +package root_test + +import ( + "fmt" + "os" + "sync" + "syscall" + "testing" + "time" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// gocryptfs v2.5.0 upgraded x/sys/unix and we found out that, since +// https://github.com/golang/sys/commit/d0df966e6959f00dc1c74363e537872647352d51 , +// unix.Setreuid() and friends now affect the whole process instead of only the +// current thread, breaking allow_other: https://github.com/rfjakob/gocryptfs/issues/893 +// +// Let's not have this happen again by testing it here. +func TestConcurrentUserOps(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("must run as root") + } + + var wg sync.WaitGroup + + oneStressor := func(tid int) { + defer wg.Done() + err := asUser(10000+tid, 20000+tid, nil, func() (err error) { + for i := 0; i < 100; i++ { + d := fmt.Sprintf("%s/tid%d.i%d/foo/bar/baz", test_helpers.DefaultPlainDir, tid, i) + if err = os.MkdirAll(d, 0700); err != nil { + return + } + if err = os.WriteFile(d+"/foo", nil, 0400); err != nil { + return + } + if err = os.WriteFile(d+"/bar", []byte("aaaaaaaaaaaaaaaaaaaaa"), 0400); err != nil { + return + } + if err = syscall.Unlink(d + "/foo"); err != nil { + return + } + if err = os.Mkdir(d+"/foo", 0700); err != nil { + return + } + } + return nil + }) + if err != nil { + t.Error(err) + } + } + + threads := 4 + wg.Add(threads) + for tid := 0; tid < threads; tid++ { + go oneStressor(tid) + } + wg.Wait() +} + +// Test that our root_test.asUser function works as expected under concurrency by +// similating a long-runnig operation with sleep(10ms). +// https://github.com/rfjakob/gocryptfs/issues/893 +func TestAsUserSleep(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("must run as root") + } + + var wg sync.WaitGroup + f := func(euid_want int) error { + euid_have := syscall.Geteuid() + if euid_want != euid_have { + return fmt.Errorf("wrong euid: want=%d have=%d", euid_want, euid_have) + } + time.Sleep(10 * time.Millisecond) + euid_have2 := syscall.Geteuid() + if euid_want != euid_have2 { + return fmt.Errorf("wrong euid: want=%d have2=%d", euid_want, euid_have2) + } + return nil + } + threads := 100 + wg.Add(threads) + for i := 0; i < threads; i++ { + go func(i int) { + defer wg.Done() + err := asUser(10000+i, 20000+i, nil, func() error { return f(10000 + i) }) + if err != nil { + t.Error(err) + } + }(i) + } + wg.Wait() +} diff --git a/tests/root_test/main_test.go b/tests/root_test/main_test.go new file mode 100644 index 0000000..d6d5cc4 --- /dev/null +++ b/tests/root_test/main_test.go @@ -0,0 +1,20 @@ +//go:build linux + +package root_test + +import ( + "os" + "testing" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +func TestMain(m *testing.M) { + test_helpers.ResetTmpDir(true) + os.Chmod(test_helpers.DefaultCipherDir, 0777) + test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, "-zerokey", "-allow_other") + r := m.Run() + test_helpers.UnmountPanic(test_helpers.DefaultPlainDir) + os.RemoveAll(test_helpers.TmpDir) + os.Exit(r) +} diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index 23b44d0..650d802 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -1,12 +1,10 @@ //go:build linux -// +build linux // Package root_test contains tests that need root // permissions to run package root_test import ( - "io/ioutil" "os" "os/exec" "path/filepath" @@ -17,8 +15,6 @@ import ( "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" - "golang.org/x/sys/unix" - "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) @@ -26,36 +22,23 @@ func asUser(uid int, gid int, supplementaryGroups []int, f func() error) error { runtime.LockOSThread() defer runtime.UnlockOSThread() - err := unix.Setgroups(supplementaryGroups) + err := syscallcompat.Setgroups(supplementaryGroups) if err != nil { return err } - defer func() { - err = unix.Setgroups(nil) - if err != nil { - panic(err) - } - }() - err = unix.Setregid(-1, gid) + defer syscallcompat.SetgroupsPanic(nil) + + err = syscallcompat.Setregid(-1, gid) if err != nil { return err } - defer func() { - err = unix.Setregid(-1, 0) - if err != nil { - panic(err) - } - }() - err = unix.Setreuid(-1, uid) + defer syscallcompat.SetregidPanic(-1, 0) + + err = syscallcompat.Setreuid(-1, uid) if err != nil { return err } - defer func() { - err = unix.Setreuid(-1, 0) - if err != nil { - panic(err) - } - }() + defer syscallcompat.SetreuidPanic(-1, 0) ret := f() @@ -67,13 +50,13 @@ func asUser(uid int, gid int, supplementaryGroups []int, f func() error) error { // // How to check: // ps -o tid,pid,euid,ruid,suid,egid,rgid,sgid,cmd -eL - err = unix.Setresuid(0, 0, 0) - if err != nil { - panic(err) + _, _, errno := syscall.RawSyscall(syscall.SYS_SETRESUID, uintptr(0), uintptr(0), uintptr(0)) + if errno != 0 { + panic(errno) } - err = unix.Setresgid(0, 0, 0) - if err != nil { - panic(err) + _, _, errno = syscall.RawSyscall(syscall.SYS_SETRESGID, uintptr(0), uintptr(0), uintptr(0)) + if errno != 0 { + panic(errno) } return ret @@ -83,16 +66,11 @@ func TestSupplementaryGroups(t *testing.T) { if os.Getuid() != 0 { t.Skip("must run as root") } - cDir := test_helpers.InitFS(t) - os.Chmod(cDir, 0755) - pDir := cDir + ".mnt" - test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test") - defer test_helpers.UnmountPanic(pDir) // We need an unrestricted umask syscall.Umask(0000) - dir1 := pDir + "/dir1" + dir1 := test_helpers.DefaultPlainDir + "/dir1" err := os.Mkdir(dir1, 0770) if err != nil { t.Fatal(err) @@ -224,7 +202,7 @@ func TestDiskFull(t *testing.T) { } t.Logf("sz1=%d, sz2=%d", sz1, sz2) - foo1, err := ioutil.ReadFile(mnt + "/foo1") + foo1, err := os.ReadFile(mnt + "/foo1") if err != nil { t.Fatal(err) } @@ -232,7 +210,7 @@ func TestDiskFull(t *testing.T) { t.Fail() } - foo2, err := ioutil.ReadFile(mnt + "/foo2") + foo2, err := os.ReadFile(mnt + "/foo2") if err != nil { t.Fatal(err) } @@ -252,7 +230,7 @@ func TestAcl(t *testing.T) { defer test_helpers.UnmountPanic(pDir) f1 := pDir + "/f1" - if err := ioutil.WriteFile(f1, []byte("hello world\n"), 000); err != nil { + if err := os.WriteFile(f1, []byte("hello world\n"), 000); err != nil { t.Fatal(err) } @@ -322,62 +300,11 @@ func TestAcl(t *testing.T) { } } -// TestBtrfsQuirks needs root permissions because it creates a loop disk -func TestBtrfsQuirks(t *testing.T) { - if os.Getuid() != 0 { - t.Skip("must run as root") - } - - img := filepath.Join(test_helpers.TmpDir, t.Name()+".img") - f, err := os.Create(img) - if err != nil { - t.Fatal(err) - } - defer f.Close() - // minimum size for each btrfs device is 114294784 - err = f.Truncate(200 * 1024 * 1024) - if err != nil { - t.Fatal(err) - } - - // Format as Btrfs - cmd := exec.Command("mkfs.btrfs", img) - out, err := cmd.CombinedOutput() - if err != nil { - t.Logf("%q", cmd.Args) - t.Log(string(out)) - t.Fatal(err) - } - - // Mount - mnt := img + ".mnt" - err = os.Mkdir(mnt, 0600) - if err != nil { - t.Fatal(err) - } - cmd = exec.Command("mount", img, mnt) - out, err = cmd.CombinedOutput() - if err != nil { - t.Log(string(out)) - t.Fatal(err) - } - defer syscall.Unlink(img) - defer syscall.Unmount(mnt, 0) - - quirk := syscallcompat.DetectQuirks(mnt) - if quirk != syscallcompat.QuirkBrokenFalloc { - t.Errorf("wrong quirk: %v", quirk) - } -} - func TestOverlay(t *testing.T) { if os.Getuid() != 0 { t.Skip("must run as root") } cDir := test_helpers.InitFS(t) - if syscallcompat.DetectQuirks(cDir)&syscallcompat.QuirkNoUserXattr != 0 { - t.Logf("No user xattrs! overlay mount will likely fail.") - } os.Chmod(cDir, 0755) pDir := cDir + ".mnt" test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test") @@ -400,3 +327,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" diff --git a/tests/test_helpers/helpers.go b/tests/test_helpers/helpers.go index 0d21548..c8cd151 100644 --- a/tests/test_helpers/helpers.go +++ b/tests/test_helpers/helpers.go @@ -5,7 +5,6 @@ import ( "crypto/md5" "encoding/hex" "fmt" - "io/ioutil" "log" "os" "os/exec" @@ -55,7 +54,7 @@ func doInit() { fmt.Printf("test_helpers: warning: testParentDir %q does not reside on ext4, we will miss failures caused by ino reuse\n", testParentDir) } var err error - TmpDir, err = ioutil.TempDir(testParentDir, "") + TmpDir, err = os.MkdirTemp(testParentDir, "") if err != nil { panic(err) } @@ -73,7 +72,7 @@ func doInit() { // *-- gocryptfs.diriv func ResetTmpDir(createDirIV bool) { // Try to unmount and delete everything - entries, err := ioutil.ReadDir(TmpDir) + entries, err := os.ReadDir(TmpDir) if err == nil { for _, e := range entries { d := filepath.Join(TmpDir, e.Name()) @@ -148,7 +147,7 @@ func InitFS(t *testing.T, extraArgs ...string) string { if t != nil { prefix = t.Name() + "." } - dir, err := ioutil.TempDir(TmpDir, prefix) + dir, err := os.MkdirTemp(TmpDir, prefix) if err != nil { if t != nil { t.Fatal(err) @@ -178,7 +177,7 @@ func InitFS(t *testing.T, extraArgs ...string) string { // Md5fn returns an md5 string for file "filename" func Md5fn(filename string) string { - buf, err := ioutil.ReadFile(filename) + buf, err := os.ReadFile(filename) if err != nil { fmt.Printf("ReadFile: %v\n", err) return "" @@ -199,7 +198,7 @@ func Md5hex(buf []byte) string { // 3) Size reported by Fstat() func VerifySize(t *testing.T, path string, want int) { // Read whole file - buf, err := ioutil.ReadFile(path) + buf, err := os.ReadFile(path) if err != nil { t.Errorf("ReadFile failed: %v", err) } else if len(buf) != want { @@ -299,7 +298,7 @@ func TestMkdirRmdir(t *testing.T, plainDir string) { func TestRename(t *testing.T, plainDir string) { file1 := plainDir + "/rename1" file2 := plainDir + "/rename2" - err := ioutil.WriteFile(file1, []byte("content"), 0777) + err := os.WriteFile(file1, []byte("content"), 0777) if err != nil { t.Error(err) return diff --git a/tests/test_helpers/mount_unmount.go b/tests/test_helpers/mount_unmount.go index 46c64c9..4abc432 100644 --- a/tests/test_helpers/mount_unmount.go +++ b/tests/test_helpers/mount_unmount.go @@ -187,7 +187,7 @@ func UnmountErr(dir string) (err error) { err = cmd.Run() if err == nil { if len(fdsNow) > len(fds)+maxCacheFds { - return fmt.Errorf("fd leak in gocryptfs process? pid=%d dir=%q, fds:\nold=%v \nnew=%v\n", pid, dir, fds, fdsNow) + return fmt.Errorf("fd leak in gocryptfs process? pid=%d dir=%q, fds:\nold=%v \nnew=%v", pid, dir, fds, fdsNow) } return nil } diff --git a/tests/xattr/xattr_fd_test.go b/tests/xattr/xattr_fd_test.go index f3586cf..ae53afc 100644 --- a/tests/xattr/xattr_fd_test.go +++ b/tests/xattr/xattr_fd_test.go @@ -6,7 +6,7 @@ package xattr_tests import ( - "io/ioutil" + "os" "syscall" "testing" @@ -18,7 +18,7 @@ import ( func TestFdXattr(t *testing.T) { attr := "user.foo" fn := test_helpers.DefaultPlainDir + "/TestFdXattr" - err := ioutil.WriteFile(fn, nil, 0700) + err := os.WriteFile(fn, nil, 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } diff --git a/tests/xattr/xattr_integration_test.go b/tests/xattr/xattr_integration_test.go index c968a47..e662828 100644 --- a/tests/xattr/xattr_integration_test.go +++ b/tests/xattr/xattr_integration_test.go @@ -10,7 +10,6 @@ import ( "bytes" "encoding/base64" "fmt" - "io/ioutil" "os" "strings" "syscall" @@ -31,7 +30,7 @@ func TestMain(m *testing.M) { // Write deterministic diriv so encrypted filenames are deterministic. os.Remove(test_helpers.DefaultCipherDir + "/gocryptfs.diriv") diriv := []byte("1234567890123456") - err := ioutil.WriteFile(test_helpers.DefaultCipherDir+"/gocryptfs.diriv", diriv, 0400) + err := os.WriteFile(test_helpers.DefaultCipherDir+"/gocryptfs.diriv", diriv, 0400) if err != nil { fmt.Println(err) os.Exit(1) @@ -92,7 +91,7 @@ func setGetRmList3(fn string, attr string, val []byte) error { // Test xattr set, get, rm on a regular file. func TestSetGetRmRegularFile(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestSetGetRmRegularFile" - err := ioutil.WriteFile(fn, []byte("12345"), 0700) + err := os.WriteFile(fn, []byte("12345"), 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -132,7 +131,7 @@ func TestSetGetRmDir(t *testing.T) { func TestXattrSetEmpty(t *testing.T) { attr := "user.foo" fn := test_helpers.DefaultPlainDir + "/TestXattrSetEmpty1" - err := ioutil.WriteFile(fn, nil, 0700) + err := os.WriteFile(fn, nil, 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -185,7 +184,7 @@ func TestXattrSetEmpty(t *testing.T) { func TestXattrList(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestXattrList" - err := ioutil.WriteFile(fn, nil, 0700) + err := os.WriteFile(fn, nil, 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -218,10 +217,7 @@ func xattrSupported(path string) bool { return true } err2 := err.(*xattr.Error) - if err2.Err == syscall.EOPNOTSUPP { - return false - } - return true + return err2.Err != syscall.EOPNOTSUPP } func TestBase64XattrRead(t *testing.T) { @@ -236,7 +232,7 @@ func TestBase64XattrRead(t *testing.T) { plainFn := test_helpers.DefaultPlainDir + "/" + fileName encryptedFn := test_helpers.DefaultCipherDir + "/" + encryptedFileName - err := ioutil.WriteFile(plainFn, nil, 0700) + err := os.WriteFile(plainFn, nil, 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -289,7 +285,7 @@ func TestBase64XattrRead(t *testing.T) { // Listing xattrs should work even when we don't have read access func TestList0000File(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestList0000File" - err := ioutil.WriteFile(fn, nil, 0000) + err := os.WriteFile(fn, nil, 0000) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -302,7 +298,7 @@ func TestList0000File(t *testing.T) { // Setting xattrs should work even when we don't have read access func TestSet0200File(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestSet0200File" - err := ioutil.WriteFile(fn, nil, 0200) + err := os.WriteFile(fn, nil, 0200) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -342,7 +338,7 @@ func TestSet0200Dir(t *testing.T) { func TestAcl(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/TestAcl" - err := ioutil.WriteFile(fn, nil, 0600) + err := os.WriteFile(fn, nil, 0600) if err != nil { t.Fatalf("creating empty file failed: %v", err) } @@ -374,7 +370,7 @@ func TestAcl(t *testing.T) { // https://github.com/rfjakob/gocryptfs/issues/627 func TestSlashInName(t *testing.T) { fn := test_helpers.DefaultPlainDir + "/" + t.Name() - err := ioutil.WriteFile(fn, []byte("12345"), 0700) + err := os.WriteFile(fn, []byte("12345"), 0700) if err != nil { t.Fatalf("creating empty file failed: %v", err) } |
