diff options
94 files changed, 2227 insertions, 265 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 792b879..ac0c358 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,12 +24,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 0 # Make "git describe" work - name: Install Go ${{ matrix.go }} - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 with: go-version: ${{ matrix.go }} @@ -48,7 +48,7 @@ jobs: # Build & upload static binary - run: ./build-without-openssl.bash - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: gocryptfs ${{ github.sha }} static ${{ runner.arch }} binary, Go ${{ matrix.go }} path: gocryptfs diff --git a/Documentation/MANPAGE-render.bash b/Documentation/MANPAGE-render.bash index c141c1e..2f19355 100755 --- a/Documentation/MANPAGE-render.bash +++ b/Documentation/MANPAGE-render.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu cd "$(dirname "$0")" diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 64bbaa8..e306cf3 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -386,6 +386,32 @@ Only applicable to reverse mode. Limitation: Mounted single files (yes this is possible) are NOT hidden. +#### -readdirplus +Enable FUSE `READDIRPLUS` on Linux. It is disabled by default, so +attributes are fetched only when an application requests them. + +`READDIRPLUS` requests attributes for every entry during a directory +listing. This can improve metadata-heavy workloads such as `ls -l`, +but adds unnecessary work to names-only listings. With the default +cache timeouts (without `-sharedstorage`), enabling `READDIRPLUS` made +metadata-heavy listings 1.36x faster in a local 100,000-file directory +and 1.82x faster in a single run over a 1,000,000-file NFS directory. +It made names-only listings 6.5x and 18.2x slower respectively. + +The default also applies to reverse mode. Backup and synchronization +tools that inspect metadata for most entries may benefit from +`-readdirplus`. + +This option can also be combined with `-sharedstorage`. Because that +mode disables kernel attribute caching, whether `READDIRPLUS` helps +depends on the workload and backing storage. + +On platforms other than Linux, this option is accepted but has no +effect. + +For benchmarks and more details, see +https://github.com/rfjakob/gocryptfs/issues/1026 . + #### -rw, -ro Mount the filesystem read-write (`-rw`, default) or read-only (`-ro`). If both are specified, `-ro` takes precedence. @@ -457,6 +483,14 @@ Use specified config file instead of `CIPHERDIR/gocryptfs.conf`. Applies to: all actions that use a config file: mount, `-fsck`, `-passwd`, `-info`, `-init`. +In `-reverse` mode, this also changes what the encrypted view contains: by +default the config is exposed there as a virtual `gocryptfs.conf` (so a backup +of the encrypted view is self-contained), but with `-config` no `gocryptfs.conf` +is presented. Make sure to back up the config file (or the master key) from its +custom location separately, otherwise the encrypted data cannot be decrypted. +If the custom config file is located inside `CIPHERDIR`, it is hidden from the +encrypted view rather than exposed in encrypted form. + #### -cpuprofile string Write cpu profile to specified file. diff --git a/Documentation/performance.txt b/Documentation/performance.txt index 24265f5..1d5d12a 100644 --- a/Documentation/performance.txt +++ b/Documentation/performance.txt @@ -73,6 +73,11 @@ 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 +v2.6.1-71-g5b2881e7 449 964 10.5 5.6 2.4 2.6 use READDIR by default Results for EncFS for comparison (benchmark.bash -encfs): diff --git a/Documentation/pjdfstest.md b/Documentation/pjdfstest.md new file mode 100644 index 0000000..1c22694 --- /dev/null +++ b/Documentation/pjdfstest.md @@ -0,0 +1,321 @@ +# How to run + + $ sudo gocryptfs --allow_other a b + $ cd b + $ sudo prove -r /home/jakob/code/pjdfstest/test + +# Version Info + +https://github.com/pjd/pjdfstest @ 8a2adf0 + + $ gocryptfs --version + gocryptfs v2.6.1-46-gf76f8a2a; go-fuse v2.9.0; 2026-07-15 go1.25.4 linux/amd64 + +# Results + +``` +root@brikett:/var/tmp/g/b# prove -r /home/jakob/code/pjdfstest/tests +/home/jakob/code/pjdfstest/tests/chflags/00.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/01.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/02.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/03.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/04.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/05.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/06.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/07.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/08.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/09.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/10.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/11.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/12.t .......... ok +/home/jakob/code/pjdfstest/tests/chflags/13.t .......... ok +/home/jakob/code/pjdfstest/tests/chmod/00.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/01.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/02.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/03.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/04.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/05.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/06.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/07.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/08.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/09.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/10.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/11.t ............ ok +/home/jakob/code/pjdfstest/tests/chmod/12.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/00.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/01.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/02.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/03.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/04.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/05.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/06.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/07.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/08.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/09.t ............ ok +/home/jakob/code/pjdfstest/tests/chown/10.t ............ ok +/home/jakob/code/pjdfstest/tests/ftruncate/00.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/01.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/02.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/03.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/04.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/05.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/06.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/07.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/08.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/09.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/10.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/11.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/12.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/13.t ........ ok +/home/jakob/code/pjdfstest/tests/ftruncate/14.t ........ ok +/home/jakob/code/pjdfstest/tests/granular/00.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/01.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/02.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/03.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/04.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/05.t ......... ok +/home/jakob/code/pjdfstest/tests/granular/06.t ......... ok +/home/jakob/code/pjdfstest/tests/link/00.t ............. ok +/home/jakob/code/pjdfstest/tests/link/01.t ............. ok +/home/jakob/code/pjdfstest/tests/link/02.t ............. ok +/home/jakob/code/pjdfstest/tests/link/03.t ............. ok +/home/jakob/code/pjdfstest/tests/link/04.t ............. ok +/home/jakob/code/pjdfstest/tests/link/05.t ............. ok +/home/jakob/code/pjdfstest/tests/link/06.t ............. ok +/home/jakob/code/pjdfstest/tests/link/07.t ............. ok +/home/jakob/code/pjdfstest/tests/link/08.t ............. ok +/home/jakob/code/pjdfstest/tests/link/09.t ............. ok +/home/jakob/code/pjdfstest/tests/link/10.t ............. ok +/home/jakob/code/pjdfstest/tests/link/11.t ............. ok +/home/jakob/code/pjdfstest/tests/link/12.t ............. ok +/home/jakob/code/pjdfstest/tests/link/13.t ............. ok +/home/jakob/code/pjdfstest/tests/link/14.t ............. ok +/home/jakob/code/pjdfstest/tests/link/15.t ............. ok +/home/jakob/code/pjdfstest/tests/link/16.t ............. ok +/home/jakob/code/pjdfstest/tests/link/17.t ............. ok +/home/jakob/code/pjdfstest/tests/mkdir/00.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/01.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/02.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/03.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/04.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/05.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/06.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/07.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/08.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/09.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/10.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/11.t ............ ok +/home/jakob/code/pjdfstest/tests/mkdir/12.t ............ ok +/home/jakob/code/pjdfstest/tests/mkfifo/00.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/01.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/02.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/03.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/04.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/05.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/06.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/07.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/08.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/09.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/10.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/11.t ........... ok +/home/jakob/code/pjdfstest/tests/mkfifo/12.t ........... ok +/home/jakob/code/pjdfstest/tests/mknod/00.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/01.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/02.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/03.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/04.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/05.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/06.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/07.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/08.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/09.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/10.t ............ ok +/home/jakob/code/pjdfstest/tests/mknod/11.t ............ ok +/home/jakob/code/pjdfstest/tests/open/00.t ............. ok +/home/jakob/code/pjdfstest/tests/open/01.t ............. ok +/home/jakob/code/pjdfstest/tests/open/02.t ............. ok +/home/jakob/code/pjdfstest/tests/open/03.t ............. ok +/home/jakob/code/pjdfstest/tests/open/04.t ............. ok +/home/jakob/code/pjdfstest/tests/open/05.t ............. ok +/home/jakob/code/pjdfstest/tests/open/06.t ............. ok +/home/jakob/code/pjdfstest/tests/open/07.t ............. ok +/home/jakob/code/pjdfstest/tests/open/08.t ............. ok +/home/jakob/code/pjdfstest/tests/open/09.t ............. ok +/home/jakob/code/pjdfstest/tests/open/10.t ............. ok +/home/jakob/code/pjdfstest/tests/open/11.t ............. ok +/home/jakob/code/pjdfstest/tests/open/12.t ............. ok +/home/jakob/code/pjdfstest/tests/open/13.t ............. ok +/home/jakob/code/pjdfstest/tests/open/14.t ............. ok +/home/jakob/code/pjdfstest/tests/open/15.t ............. ok +/home/jakob/code/pjdfstest/tests/open/16.t ............. ok +/home/jakob/code/pjdfstest/tests/open/17.t ............. ok +/home/jakob/code/pjdfstest/tests/open/18.t ............. ok +/home/jakob/code/pjdfstest/tests/open/19.t ............. ok +/home/jakob/code/pjdfstest/tests/open/20.t ............. ok +/home/jakob/code/pjdfstest/tests/open/21.t ............. ok +/home/jakob/code/pjdfstest/tests/open/22.t ............. ok +/home/jakob/code/pjdfstest/tests/open/23.t ............. ok +/home/jakob/code/pjdfstest/tests/open/24.t ............. ok +/home/jakob/code/pjdfstest/tests/open/25.t ............. ok +/home/jakob/code/pjdfstest/tests/posix_fallocate/00.t .. ok +/home/jakob/code/pjdfstest/tests/rename/00.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/01.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/02.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/03.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/04.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/05.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/06.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/07.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/08.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/09.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/10.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/11.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/12.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/13.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/14.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/15.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/16.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/17.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/18.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/19.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/20.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/21.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/22.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/23.t ........... ok +/home/jakob/code/pjdfstest/tests/rename/24.t ........... ok +/home/jakob/code/pjdfstest/tests/rmdir/00.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/01.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/02.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/03.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/04.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/05.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/06.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/07.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/08.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/09.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/10.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/11.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/12.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/13.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/14.t ............ ok +/home/jakob/code/pjdfstest/tests/rmdir/15.t ............ ok +/home/jakob/code/pjdfstest/tests/symlink/00.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/01.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/02.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/03.t .......... Failed 2/6 subtests +/home/jakob/code/pjdfstest/tests/symlink/04.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/05.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/06.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/07.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/08.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/09.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/10.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/11.t .......... ok +/home/jakob/code/pjdfstest/tests/symlink/12.t .......... ok +/home/jakob/code/pjdfstest/tests/truncate/00.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/01.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/02.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/03.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/04.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/05.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/06.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/07.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/08.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/09.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/10.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/11.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/12.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/13.t ......... ok +/home/jakob/code/pjdfstest/tests/truncate/14.t ......... ok +/home/jakob/code/pjdfstest/tests/unlink/00.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/01.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/02.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/03.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/04.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/05.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/06.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/07.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/08.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/09.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/10.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/11.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/12.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/13.t ........... ok +/home/jakob/code/pjdfstest/tests/unlink/14.t ........... ok +/home/jakob/code/pjdfstest/tests/utimensat/00.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/01.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/02.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/03.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/04.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/05.t ........ Failed 1/16 subtests +/home/jakob/code/pjdfstest/tests/utimensat/06.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/07.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/08.t ........ ok +/home/jakob/code/pjdfstest/tests/utimensat/09.t ........ ok + +Test Summary Report +------------------- +/home/jakob/code/pjdfstest/tests/chown/00.t (Wstat: 0 Tests: 1280 Failed: 0) + TODO passed: 1054, 1058, 1064, 1069, 1073, 1079, 1084 + 1088, 1094, 1099, 1103, 1109, 1114, 1118 + 1124, 1129, 1133, 1139, 1144 +/home/jakob/code/pjdfstest/tests/symlink/03.t (Wstat: 0 Tests: 6 Failed: 2) + Failed tests: 1-2 +/home/jakob/code/pjdfstest/tests/utimensat/05.t (Wstat: 0 Tests: 16 Failed: 1) + Failed test: 7 +Files=237, Tests=8789, 191 wallclock secs ( 1.80 usr 0.39 sys + 27.57 cusr 36.00 csys = 65.76 CPU) +Result: FAIL +``` + +# Failure Details + +``` +root@brikett:/var/tmp/g/b# prove -v /home/jakob/code/pjdfstest/tests/symlink/03.t +/home/jakob/code/pjdfstest/tests/symlink/03.t .. +1..6 +not ok 1 - tried 'symlink 23ed68f75f1b5eb776df42af2ed2848b3d641179783607d2f1a1deee4edf8f014523df3e81aaaf3e456da127a5067bb0c0450c45c2cff7e7facbe7a832387d4/248b250d4ec8724d5b5d1ededef36f3c2f9c2224897bcd80b328f4d002fd57e6fb6cae83dfcb176c04df37a6b426a778e977102ee49d38f7088a473891d5d1f/bc22aebec731731d0095b92473ab1b0c72c17b64f111ee1c498506f339fe6673378a4015e102b699933f5b91489019cb1cc89d2bb357d41359f1ce0785f843f/c44e68c3caaada10b07d9a2552ac48f7cd67c2ba3b5afc36ef5efc85b6c007f9037559bf3afcbd04b811103be376e78ec53772fd99fc83e15312d4b599d3200/59fc1e2cbc929f4f243e07bc7168a62707a8a185184227b55f3993edc12acc67f7155ad9c6ef4bf119ecec3c8cbc9ba4397011b59a51510cd84820cce7cb73c/f381e07bc3152f9e3120b981c52576948cfd565874f015bb83061ab07bdaf3fe8c0f6564e7f998e0b7a09b45241ee1232b3294999d5c524cd8380725983b72f/1a56838452edf4d37102f7019c060c4e98c4600eff9160461fa7e0d4fbc45ba7d729334bfdec9f8b970aea5dd64435c6c32073a74126ec513a9509646f1c0db/0e1d55ff1e2b709439cf52209f505a10feba0cf1d5d760b4bad10335b3f5bdd436ae98f9811b65c928f13a9f39a81d074ef76ffaeb29f6541d18132786d424b/ef972f65a195add49c08f7bb706ac3ab1230ba719da5a9bc723f50bdaa127f37e7ea5289f6cc5b06eba272bd0919922a0c1cb1ac5ec6915674dd084023b209b/92de4c5fc2d65acd49bdfb3bce0c0adaf5a5f4c14bbc7a79f1921d3f66f0973b08a779876170859b8a75e822e0e99fb310680c32ce2d0ad559ed01d2fbd07fb/ddf2464d41afcd55c63ba9e864e0566587a22aad0da07d6bdd6a2a7a53f41bc62e5ddea1fd5fd9515cb4deb9a4353ae5356f2d5012f6a9038a545ba73c540e1/3f732b80482cfa788fdb60ddada000074210d210f5bcc815ef4d07249d3a253c02237c0734c82945e28ba6d3c1b8ca2371ecccbfd1626c6409f9b63f235ecef/02b98d5a6ef457fa2aab857c667b9e3bc2d4ceb171aa757628ac7b52afd75a475d1590fb5c9213736144dc069758469673fccd519f5c09b07dbf3c8f8230842/ede5fbbd47ff4d152c0cef6aaf28374a4efba8d0d2b80397b9f88d94429d1ddeb04599acbb422cb5a4be4f2a794c70e292abc7c80225b96cfe4794eb4f0ff59/7fdf2daf92bcd3fd64be10782398c9a45c9494c1e41dd0d2f225097f3f56ee0181f438277e2e2e64f9676e705e08ea6625b50df110078499a88c5e4100f2a9a/62fe5fb014fbc604474df5de2b51886a836768e5c4825e2f6276f2b4e80fe052375a31bfc88fe1c4b57cc494485865e6afbaa61b3ffb73d0367c30701dec221/e08f4bd362975dee4f870eddc9e6e94410e4b3a76c042118fb9437407646435bdbd4c86ec08b483dae9cc5319ad5536c04411505e964c59b6f8cde2c8f90d51/bcc1c2f4d3e29a5518cb0e3ed5231a9b2762c1521b25bd0e8fe4dd7ab297eb45ce954c1e7cb1c2c098331334332ac4e62ded1d68b39f2615d663ae960a415af/18848c974aa7a6ee669da70cbbaa8509c036f4a555a80b927b5e2490102bf056026c376ead077e85a8ed3a049fb1b043e7fe54f90f41e45a53262ca4f8e9235/83c012877dda294d7792ab2f4a9ae34df5b50b96528a0356f8fcae346da053c9958be756b4c0dd430dc667215253375dbd83591e408cff510943de7d04cbf1e/9ac097c625dd96155b680b9f5794bd45af747e8ae22b045ee1735ee04ee93b90cbd283b1f119ec777023682a003bded1d4521f561ec1f71c8f5033afb875d8c/89ba1562b64d626ce474369dc4999e68a6e6c3bd286de55a2a3a275b0dd6ce195eb33422fa323ddcbef6ae4954d4fae4b5cb4f2314a56ef0d2ab04154df941e/6b04ec713129cf1e2170c58f7e7d7deafa95f00770d43e3e468b1971be40d6ec525300db2ad5df8f4571483f9e4d98e073c1e444556939a57de6934e1c05a59/50d0980c223384cc720a2159bc811ba196ae1a0e659fbbd18471c56d1c4068c8a13c5629f95c2aa46d6cf87fdd8731529b1f5b19a2da76de5a300bb800b5510/9935ad60b4a1c6968938436e87f9467baeb879262ae556f7e4390cd5f8baf7f2c1315c43906033753e413e32322c2607b4bc8849e00bc06f776b878762b2f77/7829f7dcbd16a1b9ac8a6d9b9501a5903cd69bdd9f1b2c16336961ad9256b5a2c13014d4efd8e749f8dfcbe97f3545964eede49d8e7a6367ae836ca938c2e3d/603b1ebbddd410b01ac030a4b7d8fa1ea9ff8a98ef3690514a762aae2e0a199e9b18df8d9dae416c34df6d3cd08243a1a85e58ede14efd279d1a5fcce320945/dfe6ad9a58012d7a0209adb3cd5e4bb5fecb7a7437afd1fc8dda6b86a340ca50ebe8aaa7e408bdb4bc9f80e10673155a3f668fe5cc87888f037fc71be81ba05/6b2bd09ecfb50e52e6fcbc23e71becdb6cb3c07ac4fcd89553eaac16cc998e8bacc2d906299e990626100a6f7750c4d9c23d19e4b72db720a0f18a66a4db871/a9354c54f3cc39df1aaf9e46e385d8b755d5349489a6d6afc3a2d63dfa2e44227df6af8fb6b0bc6487e010630fc07b820ec033d4e44b6152ae8b6801baf918b/e4400c8deecd70b6d0b181e1f1ef1ba563361a86c236601b09212c41c25e1e9c843ae1c52fde9c61dc8f26092ea103d222f68be09daa0ca6b70763a916b3a70/c862a9e0203028b1c88453f3b4aef1b93d5272b15d786fb8acd311669492f345051208b9e0ec1e7340d7154c9ea072f63323a05731cff15092c7e47985d2cf4 pjdfstest_b377cf7fa7a840a8e67c1329e3a9dca7', + expected 0, got ENAMETOOLONG +not ok 2 - tried 'unlink pjdfstest_b377cf7fa7a840a8e67c1329e3a9dca7', expected 0, got ENOENT +ok 3 +ok 4 +ok 5 +ok 6 +Failed 2/6 subtests + +Test Summary Report +------------------- +/home/jakob/code/pjdfstest/tests/symlink/03.t (Wstat: 0 Tests: 6 Failed: 2) + Failed tests: 1-2 +Files=1, Tests=6, 1 wallclock secs ( 0.03 usr 0.01 sys + 0.69 cusr 0.78 csys = 1.51 CPU) +Result: FAIL +``` + +``` +root@brikett:/var/tmp/g/b# prove -v /home/jakob/code/pjdfstest/tests/utimensat/05.t +/home/jakob/code/pjdfstest/tests/utimensat/05.t .. +1..16 +ok 1 +ok 2 +ok 3 +ok 4 +ok 5 +ok 6 +not ok 7 - tried 'lstat pjdfstest_f1cd2b8794f82da55def3ffa9af7556f atime', expected 1960000000, got 1784144798 +ok 8 +ok 9 +ok 10 +ok 11 +ok 12 +ok 13 +ok 14 +ok 15 +ok 16 +Failed 1/16 subtests + +Test Summary Report +------------------- +/home/jakob/code/pjdfstest/tests/utimensat/05.t (Wstat: 0 Tests: 16 Failed: 1) + Failed test: 7 +Files=1, Tests=16, 0 wallclock secs ( 0.03 usr 0.00 sys + 0.04 cusr 0.07 csys = 0.14 CPU) +Result: FAIL +``` @@ -8,6 +8,9 @@ An encrypted overlay filesystem written in Go. Official website: https://nuetzlich.net/gocryptfs ([markdown source](https://github.com/rfjakob/gocryptfs-website/blob/master/docs/index.md)). +> [!WARNING] +> The website "gocryptfs.com" is not affiliated with the gocryptfs project! +  gocryptfs is built on top the excellent diff --git a/benchmark-reverse.bash b/benchmark-reverse.bash index fad6bfe..ca3bc9b 100755 --- a/benchmark-reverse.bash +++ b/benchmark-reverse.bash @@ -1,7 +1,9 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # Benchmark gocryptfs' reverse mode +set -eu + cd "$(dirname "$0")" MYNAME=$(basename "$0") source tests/fuse-unmount.bash diff --git a/benchmark.bash b/benchmark.bash index fb99c65..f3d8a20 100755 --- a/benchmark.bash +++ b/benchmark.bash @@ -1,8 +1,10 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # Run the set of "canonical" benchmarks that are shown on # https://nuetzlich.net/gocryptfs/comparison/ +set -eu + cd "$(dirname "$0")" MYNAME=$(basename "$0") source tests/fuse-unmount.bash diff --git a/build-without-openssl.bash b/build-without-openssl.bash index c09e7f3..388f823 100755 --- a/build-without-openssl.bash +++ b/build-without-openssl.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" @@ -1,4 +1,4 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # # Compile gocryptfs and bake the git version string of itself and the go-fuse # library into the binary. @@ -10,6 +10,8 @@ # SOURCE_DATE_EPOCH=1544192417 ./build.bash # . +set -eu + cd "$(dirname "$0")" # $0 does not work because we may have been sourced diff --git a/cli_args.go b/cli_args.go index e690e15..52061e3 100644 --- a/cli_args.go +++ b/cli_args.go @@ -29,9 +29,9 @@ type argContainer struct { debug, init, zerokey, fusedebug, openssl, passwd, fg, version, plaintextnames, quiet, nosyslog, wpanic, longnames, allow_other, reverse, aessiv, nonempty, raw64, - noprealloc, speed, hkdf, serialize_reads, hh, info, + noprealloc, readdirplus, 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, @@ -178,6 +178,7 @@ func parseCliOpts(osArgs []string) (args argContainer) { flagSet.BoolVar(&args.nonempty, "nonempty", false, "Allow mounting over non-empty directories") flagSet.BoolVar(&args.raw64, "raw64", true, "Use unpadded base64 for file names") flagSet.BoolVar(&args.noprealloc, "noprealloc", false, "Disable preallocation before writing") + flagSet.BoolVar(&args.readdirplus, "readdirplus", false, "Enable FUSE READDIRPLUS (Linux only)") flagSet.BoolVar(&args.speed, "speed", false, "Run crypto speed test") flagSet.BoolVar(&args.hkdf, "hkdf", true, "Use HKDF as an additional key derivation step") flagSet.BoolVar(&args.serialize_reads, "serialize_reads", false, "Try to serialize read operations") @@ -188,6 +189,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") diff --git a/cli_args_test.go b/cli_args_test.go index 4fb01ac..d7e28ab 100644 --- a/cli_args_test.go +++ b/cli_args_test.go @@ -157,6 +157,18 @@ func TestParseCliOpts(t *testing.T) { }...) o = defaultArgs + o.readdirplus = true + testcases = append(testcases, []testcaseContainer{ + { + i: []string{"gocryptfs", "-readdirplus"}, + o: o, + }, { + i: []string{"gocryptfs", "-o", "readdirplus"}, + o: o, + }, + }...) + + o = defaultArgs o.exclude = []string{"foo", "bar", "baz,boe"} testcases = append(testcases, []testcaseContainer{ { diff --git a/contrib/gocryptfs-maybe.bash b/contrib/gocryptfs-maybe.bash index daf3e60..d83dd91 100755 --- a/contrib/gocryptfs-maybe.bash +++ b/contrib/gocryptfs-maybe.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Conditionally try to mount a gocryptfs filesystem. If either # * CIPHERDIR/gocryptfs.conf does not exist OR diff --git a/contrib/maxlen.bash b/contrib/maxlen.bash index be5f7a6..4cf5802 100755 --- a/contrib/maxlen.bash +++ b/contrib/maxlen.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Find out the maximum supported filename length and print it. # diff --git a/crossbuild.bash b/crossbuild.bash index ff773ec..685674e 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Build on all supported architectures & operating systems @@ -31,3 +31,7 @@ time GOOS=darwin GOARCH=amd64 compile_tests # MacOS on Apple Silicon M1. GOOS=darwin GOARCH=arm64 build + +# FreeBSD +GOOS=freebsd GOARCH=amd64 build + @@ -1,16 +1,17 @@ module github.com/rfjakob/gocryptfs/v2 -go 1.24.0 +go 1.25.0 require ( github.com/aperturerobotics/jacobsa-crypto v1.1.0 - github.com/hanwen/go-fuse/v2 v2.8.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.45.0 - golang.org/x/sys v0.38.0 - golang.org/x/term v0.37.0 + golang.org/x/crypto v0.52.0 + golang.org/x/sys v0.45.0 + golang.org/x/term v0.43.0 + golang.org/x/text v0.37.0 ) @@ -2,8 +2,8 @@ github.com/aperturerobotics/jacobsa-crypto v1.1.0 h1:0hig54FMzU80OHrqSfqmj/W8Hyd 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.8.0 h1:wV8rG7rmCz8XHSOwBZhG5YcVqcYjkzivjmbaMafPlAs= -github.com/hanwen/go-fuse/v2 v2.8.0/go.mod h1:yE6D2PqWwm3CbYRxFXV9xUd8Md5d6NG0WBs5spCswmI= +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= @@ -20,8 +20,8 @@ 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= @@ -29,15 +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.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/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= 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/internal/configfile/config_file.go b/internal/configfile/config_file.go index 28a1ca5..ebd818e 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -31,7 +31,7 @@ type FIDO2Params struct { // FIDO2 credential CredentialID []byte // FIDO2 hmac-secret salt - HMACSalt []byte + HMACSalt []byte AssertOptions []string } @@ -118,9 +118,9 @@ func Create(args *CreateArgs) error { if len(args.Fido2CredentialID) > 0 { cf.setFeatureFlag(FlagFIDO2) cf.FIDO2 = &FIDO2Params{ - CredentialID: args.Fido2CredentialID, - HMACSalt: args.Fido2HmacSalt, - AssertOptions: args.Fido2AssertOptions, + CredentialID: args.Fido2CredentialID, + HMACSalt: args.Fido2HmacSalt, + AssertOptions: args.Fido2AssertOptions, } } // Catch bugs and invalid cli flag combinations early diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index d6627a5..d5bd6d4 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -1,5 +1,9 @@ package configfile +import ( + "slices" +) + type flagIota int const ( @@ -64,10 +68,5 @@ func isFeatureFlagKnown(flag string) bool { // IsFeatureFlagSet returns true if the feature flag "flagWant" is enabled. func (cf *ConfFile) IsFeatureFlagSet(flagWant flagIota) bool { flagString := knownFlags[flagWant] - for _, flag := range cf.FeatureFlags { - if flag == flagString { - return true - } - } - return false + return slices.Contains(cf.FeatureFlags, flagString) } 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/fido2/fido2.go b/internal/fido2/fido2.go index e08e589..f47795b 100644 --- a/internal/fido2/fido2.go +++ b/internal/fido2/fido2.go @@ -44,7 +44,7 @@ func callFidoCommand(command fidoCommand, assertOptions []string, device string, var args []string args = append(args, "-G") args = append(args, "-h") - for i := range assertOptions{ + for i := range assertOptions { args = append(args, "-t") args = append(args, assertOptions[i]) } diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go index 64a5923..84e34af 100644 --- a/internal/fusefrontend/args.go +++ b/internal/fusefrontend/args.go @@ -24,6 +24,10 @@ type Args struct { // location. If it is false, reverse mode maps ".gocryptfs.reverse.conf" // to "gocryptfs.conf" in the plaintext dir. ConfigCustom bool + // Config is the path to the config file. In reverse mode, a custom config + // file (-config) located inside Cipherdir is hidden from the encrypted view + // (https://github.com/rfjakob/gocryptfs/issues/1009). + Config string // NoPrealloc disables automatic preallocation before writing NoPrealloc bool // Exclude is a list of paths to make inaccessible, starting match at @@ -51,4 +55,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 afee158..353029c 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -14,6 +14,8 @@ import ( "sync" "syscall" + "golang.org/x/sys/unix" + "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" @@ -90,6 +92,13 @@ func (f *File) readFileID() ([]byte, error) { // and not only the header. A header-only file will be considered empty. // This makes File ID poisoning more difficult. readLen := contentenc.HeaderLen + 1 + if f.rootNode.args.SharedStorage { + // With -sharedstorage, we consider a header-only file as valid, because + // another gocryptfs process may have either: + // 1) just created the header, and not written further data yet. + // 2) truncated the file down to just the header. + readLen = contentenc.HeaderLen + } buf := make([]byte, readLen) n, err := f.fd.ReadAt(buf, 0) if err != nil { @@ -115,7 +124,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) { @@ -244,7 +253,23 @@ func (f *File) Read(ctx context.Context, buf []byte, off int64) (resultData fuse tlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.qIno.Ino, off, len(buf)) out, errno := f.doRead(buf[:0], uint64(off), uint64(len(buf))) if errno != 0 { - return nil, errno + // With -sharedstorage, when we get a decryption error, we lock the + // byte range and try again. + if !(f.rootNode.args.SharedStorage && errno == syscall.EIO) { + return nil, errno + } + blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(buf))) + alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) + if err := f.LockSharedStorage(unix.F_RDLCK, int64(alignedOffset), int64(alignedLength)); err != nil { + tlog.Warn.Printf("ino%d: FUSE Read: LockSharedStorage(F_RDLCK, %d, %d) failed: %v", f.qIno.Ino, alignedOffset, alignedLength, err) + return nil, fs.ToErrno(err) + } + defer f.UnlockSharedStorage(int64(alignedOffset), int64(alignedLength)) + + out, errno = f.doRead(buf[:0], uint64(off), uint64(len(buf))) + if errno != 0 { + return nil, errno + } } tlog.Debug.Printf("ino%d: Read: errno=%d, returning %d bytes", f.qIno.Ino, errno, len(out)) return fuse.ReadResultData(out), errno @@ -266,6 +291,12 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { // // If the file ID is not cached, read it from disk if f.fileTableEntry.ID == nil { + if err := f.LockSharedStorage(unix.F_WRLCK, 0, contentenc.HeaderLen); err != nil { + tlog.Warn.Printf("ino%d: doWrite: LockSharedStorage(F_WRLCK, %d, %d) failed: %v", f.qIno.Ino, 0, contentenc.HeaderLen, err) + return 0, fs.ToErrno(err) + } + defer f.UnlockSharedStorage(0, contentenc.HeaderLen) + var err error fileID, err := f.readFileID() // Write a new file header if the file is empty @@ -285,7 +316,19 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { // Handle payload data dataBuf := bytes.NewBuffer(data) blocks := f.rootNode.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) + cOff, lkLen := blocks[0].JointCiphertextRange(blocks) toEncrypt := make([][]byte, len(blocks)) + + // As we must write complete ciphertext blocks (except at EOF), non-overlapping + // plaintext writes can overlap in the ciphertext. + // And because overlapping writes can turn the data into data soup (see + // TestPoCTornWrite) we serialize them using fcntl locking. + if err := f.LockSharedStorage(unix.F_WRLCK, int64(cOff), int64(lkLen)); err != nil { + tlog.Warn.Printf("ino%d: LockSharedStorage(F_WRLCK, %d, %d) failed: %v", f.qIno.Ino, cOff, int64(lkLen), err) + return 0, fs.ToErrno(err) + } + defer f.UnlockSharedStorage(int64(cOff), int64(lkLen)) + for i, b := range blocks { blockData := dataBuf.Next(int(b.Length)) // Incomplete block -> Read-Modify-Write @@ -310,12 +353,11 @@ func (f *File) doWrite(data []byte, off int64) (uint32, syscall.Errno) { // Preallocate so we cannot run out of space in the middle of the write. // This prevents partially written (=corrupt) blocks. var err error - cOff := blocks[0].BlockCipherOff() // f.fd.WriteAt & syscallcompat.EnospcPrealloc take int64 offsets! 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) { @@ -413,9 +455,6 @@ func (f *File) Flush(ctx context.Context) syscall.Errno { } // Fsync: handles FUSE opcode FSYNC -// -// Unfortunately, as Node.Fsync is also defined and takes precedence, -// File.Fsync is never called at the moment. func (f *File) Fsync(ctx context.Context, flags uint32) (errno syscall.Errno) { f.fdLock.RLock() defer f.fdLock.RUnlock() diff --git a/internal/fusefrontend/file_allocate_truncate.go b/internal/fusefrontend/file_allocate_truncate.go index a3decf9..f4a078c 100644 --- a/internal/fusefrontend/file_allocate_truncate.go +++ b/internal/fusefrontend/file_allocate_truncate.go @@ -9,6 +9,10 @@ import ( "sync" "syscall" + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/contentenc" + "github.com/hanwen/go-fuse/v2/fs" "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" @@ -92,20 +96,62 @@ func (f *File) Allocate(ctx context.Context, off uint64, sz uint64, mode uint32) return f.truncateGrowFile(oldPlainSz, newPlainSz) } -// truncate - called from Setattr. +// truncate - called from node.Setattr and file.Setattr. +// +// The caller must hold f.fileTableEntry.ContentLock func (f *File) truncate(newSize uint64) (errno syscall.Errno) { var err error // Common case first: Truncate to zero if newSize == 0 { - err = syscall.Ftruncate(int(f.fd.Fd()), 0) - if err != nil { - tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err) - return fs.ToErrno(err) + if !f.rootNode.args.SharedStorage { + err = syscall.Ftruncate(int(f.fd.Fd()), 0) + if err != nil { + tlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.qIno.Ino, f.intFd(), err) + return fs.ToErrno(err) + } + // Truncate to zero kills the file header + f.fileTableEntry.ID = nil + return 0 + } else { + // Prevent reads and writes concurrent with the truncate operation. It's + // racy on tmpfs and ext4 ( https://lore.kernel.org/all/18e9fa0f-ec31-9107-459c-ae1694503f87@gmail.com/t/ ) + // as evident by TestOpenTruncate test failures. + err = f.LockSharedStorage(unix.F_WRLCK, 0, 0) + if err != nil { + return fs.ToErrno(err) + } + defer f.UnlockSharedStorage(0, 0) + + // With -sharedstorage, we keep the on-disk file header. + // Other mounts may have the file ID cached so we cannot mess with it. + + // The file must have a header if we have the file ID cached, + // so we can blindly truncate. + if f.fileTableEntry.ID != nil { + return fs.ToErrno(syscall.Ftruncate(int(f.fd.Fd()), contentenc.HeaderLen)) + } + // We don't have the file ID cached, so the file may be empty on disk. + // We don't want to grow it, as this will create an all-zero header. + fi, err := f.fd.Stat() + if err != nil { + return fs.ToErrno(err) + } + if fi.Size() == 0 { + // nothing to do + return 0 + } + if fi.Size() == contentenc.HeaderLen { + // nothing to do + return 0 + } else if fi.Size() > contentenc.HeaderLen { + return fs.ToErrno(syscall.Ftruncate(int(f.fd.Fd()), contentenc.HeaderLen)) + } else { + tlog.Warn.Printf("truncate i%d: partial header, size=%d", f.qIno.Ino, fi.Size()) + return syscall.EIO + } } - // Truncate to zero kills the file header - f.fileTableEntry.ID = nil - return 0 } + // We need the old file size to determine if we are growing or shrinking // the file oldSize, err := f.statPlainSize() diff --git a/internal/fusefrontend/file_lock.go b/internal/fusefrontend/file_lock.go new file mode 100644 index 0000000..c2965ae --- /dev/null +++ b/internal/fusefrontend/file_lock.go @@ -0,0 +1,51 @@ +package fusefrontend + +import ( + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// SharedStorageLock conveniently wraps F_OFD_SETLKW. +// It is a no-op unless args.SharedStorage is set. +// +// See https://man7.org/linux/man-pages/man2/fcntl.2.html -> "Open file description locks (non-POSIX)" +// +// lkType is one of: +// * unix.F_RDLCK (shared read lock) +// * unix.F_WRLCK (exclusive write lock) +// * unix.F_UNLCK (unlock) +// +// This function is a no-op if args.SharedStorage == false. +func (f *File) LockSharedStorage(lkType int16, lkStart int64, lkLen int64) (err error) { + if !f.rootNode.args.SharedStorage { + return nil + } + lk := unix.Flock_t{ + Type: lkType, + Whence: unix.SEEK_SET, + Start: lkStart, + Len: lkLen, + } + err = unix.FcntlFlock(uintptr(f.intFd()), syscallcompat.F_OFD_SETLK, &lk) + switch err { + case unix.EACCES, unix.EAGAIN: + tlog.Debug.Printf("LockSharedStorage: waiting for lock") + case nil: + return + } + for { + err = unix.FcntlFlock(uintptr(f.intFd()), syscallcompat.F_OFD_SETLKW, &lk) + if err == unix.EINTR { + tlog.Debug.Printf("LockSharedStorage: looping on EINTR") + continue + } + return + } +} + +// UnlockSharedStorage calls LockSharedStorage with unix.F_UNLCK. +func (f *File) UnlockSharedStorage(lkStart int64, lkLen int64) error { + return f.LockSharedStorage(unix.F_UNLCK, lkStart, lkLen) +} diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 95be48d..c531876 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -38,7 +38,7 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch ch = n.newChild(ctx, st, out) // Translate ciphertext size in `out.Attr.Size` to plaintext size - n.translateSize(dirfd, cName, &out.Attr) + out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size) rn := n.rootNode() if rn.args.ForceOwner != nil { @@ -116,7 +116,7 @@ func (n *Node) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) out.Attr.FromStat(st) // Translate ciphertext size in `out.Attr.Size` to plaintext size - n.translateSize(dirfd, cName, &out.Attr) + out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size) out: if rn.args.ForceOwner != nil { @@ -246,6 +246,8 @@ func (n *Node) Setattr(ctx context.Context, f fs.FileHandle, in *fuse.SetAttrIn, } f2 := f.(*File) defer f2.Release(ctx) + f2.fileTableEntry.ContentLock.Lock() + defer f2.fileTableEntry.ContentLock.Unlock() errno = syscall.Errno(f2.truncate(sz)) if errno != 0 { return errno @@ -371,7 +373,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o return } inode = n.newChild(ctx, st, out) - n.translateSize(dirfd, cName, &out.Attr) + out.Size = n.translateSize(dirfd, cName, out.Mode, out.Size) return inode, 0 } @@ -514,22 +516,3 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd } return 0 } - -// Fsync: handles FUSE opcodes FSYNC & FDIRSYNC -// -// Note: f is always set to nil by go-fuse -func (n *Node) Fsync(ctx context.Context, f fs.FileHandle, flags uint32) syscall.Errno { - dirfd, cName, errno := n.prepareAtSyscallMyself() - if errno != 0 { - return errno - } - defer syscall.Close(dirfd) - - fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) - if err != nil { - return fs.ToErrno(err) - } - defer syscall.Close(fd) - - return fs.ToErrno(syscall.Fsync(fd)) -} diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index e8fca80..3102275 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -53,18 +53,18 @@ func (n *Node) readlink(dirfd int, cName string) (out []byte, errno syscall.Errn return []byte(target), 0 } -// translateSize translates the ciphertext size in `out` into plaintext size. +// translateSize translates the ciphertext size cSize into plaintext size. // Handles regular files & symlinks (and finds out what is what by looking at -// `out.Mode`). -func (n *Node) translateSize(dirfd int, cName string, out *fuse.Attr) { - if out.IsRegular() { - rn := n.rootNode() - out.Size = rn.contentEnc.CipherSizeToPlainSize(out.Size) - } else if out.IsSymlink() { - // read and decrypt target +// mode). +func (n *Node) translateSize(dirfd int, cName string, mode uint32, cSize uint64) (pSize uint64) { + switch mode & syscall.S_IFMT { + case syscall.S_IFREG: + return n.rootNode().contentEnc.CipherSizeToPlainSize(cSize) + case syscall.S_IFLNK: target, _ := n.readlink(dirfd, cName) - out.Size = uint64(len(target)) + return uint64(len(target)) } + return cSize } // Path returns the relative plaintext path of this node 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 9021350..03194df 100644 --- a/internal/fusefrontend/node_prepare_syscall.go +++ b/internal/fusefrontend/node_prepare_syscall.go @@ -3,6 +3,7 @@ package fusefrontend import ( "syscall" + "github.com/rfjakob/gocryptfs/v2/internal/nametransform" "github.com/rfjakob/gocryptfs/v2/internal/tlog" "github.com/hanwen/go-fuse/v2/fs" @@ -73,8 +74,9 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy var err error iv, err = rn.nameTransform.ReadDirIVAt(dirfd) if err != nil { + tlog.Warn.Printf("prepareAtSyscall: could not read %s: %v", nametransform.DirIVFilename, err) syscall.Close(dirfd) - return -1, "", fs.ToErrno(err) + return -1, "", syscall.EIO } } rn.dirCache.Store(n, dirfd, iv) diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go index 8ff7bab..1470a2a 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -32,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 . @@ -77,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 @@ -102,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) { @@ -119,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 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_freebsd.go b/internal/fusefrontend/node_xattr_freebsd.go new file mode 100644 index 0000000..9698283 --- /dev/null +++ b/internal/fusefrontend/node_xattr_freebsd.go @@ -0,0 +1,33 @@ +package fusefrontend + +import ( + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" +) + +const noSuchAttributeError = unix.ENOATTR + +func filterXattrSetFlags(flags int) int { + return flags +} + +func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) { + // TODO + return nil, unix.EOPNOTSUPP +} + +func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags uint32) (errno unix.Errno) { + // TODO + return unix.EOPNOTSUPP +} + +func (n *Node) removeXAttr(cAttr string) (errno unix.Errno) { + // TODO + return unix.EOPNOTSUPP +} + +func (n *Node) listXAttr() (out []string, errno unix.Errno) { + // TODO + return nil, unix.EOPNOTSUPP +} 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/root_node.go b/internal/fusefrontend/root_node.go index 8464c5f..38d070d 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -1,7 +1,6 @@ package fusefrontend import ( - "os" "strings" "sync" "sync/atomic" @@ -91,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 @@ -104,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/statx_linux.go b/internal/fusefrontend/statx_linux.go new file mode 100644 index 0000000..f9f87cf --- /dev/null +++ b/internal/fusefrontend/statx_linux.go @@ -0,0 +1,78 @@ +package fusefrontend + +import ( + "context" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/inomap" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" +) + +var _ = (fs.NodeStatxer)((*Node)(nil)) +var _ = (fs.FileStatxer)((*File)(nil)) + +// Statx is the Linux statx equivalent of Getattr. +func (n *Node) Statx(ctx context.Context, f fs.FileHandle, flags uint32, mask uint32, out *fuse.StatxOut) (errno syscall.Errno) { + // If the kernel gives us a file handle, use it. Current Linux kernels do + // not send one with FUSE_STATX, but keep this for future compatibility. + if f != nil { + if fsx, ok := f.(fs.FileStatxer); ok { + return fsx.Statx(ctx, flags, mask, out) + } + } + + dirfd, cName, errno := n.prepareAtSyscallMyself() + if errno != 0 { + return errno + } + defer syscall.Close(dirfd) + + var st unix.Statx_t + err := syscallcompat.Statx(dirfd, cName, int(flags)|unix.AT_SYMLINK_NOFOLLOW, int(mask), &st) + if err != nil { + return fs.ToErrno(err) + } + + // fix inode number, size, owner + rn := n.rootNode() + st.Ino = rn.inoMap.Translate(inomap.NewQIno(unix.Mkdev(st.Dev_major, st.Dev_minor), 0, st.Ino)) + st.Size = rn.translateSize(dirfd, cName, uint32(st.Mode), st.Size) + if rn.args.ForceOwner != nil { + st.Uid = rn.args.ForceOwner.Uid + st.Gid = rn.args.ForceOwner.Gid + } + + out.FromStatx(&st) + return 0 +} + +// Statx returns statx information for an open backing file. Current Linux +// kernels do not send a file handle with FUSE_STATX, so this is not reached yet. +func (f *File) Statx(_ context.Context, flags uint32, mask uint32, out *fuse.StatxOut) syscall.Errno { + f.fdLock.RLock() + defer f.fdLock.RUnlock() + + var st unix.Statx_t + err := syscallcompat.Statx(f.intFd(), "", int(flags)|unix.AT_EMPTY_PATH, int(mask), &st) + if err != nil { + return fs.ToErrno(err) + } + + // fix inode number, size, owner + rn := f.rootNode + st.Ino = rn.inoMap.Translate(inomap.NewQIno(unix.Mkdev(st.Dev_major, st.Dev_minor), 0, st.Ino)) + if uint32(st.Mode)&syscall.S_IFMT == syscall.S_IFREG { + st.Size = rn.contentEnc.CipherSizeToPlainSize(st.Size) + } + if rn.args.ForceOwner != nil { + st.Uid = rn.args.ForceOwner.Uid + st.Gid = rn.args.ForceOwner.Gid + } + + out.FromStatx(&st) + return 0 +} diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 3165db6..dc8d928 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -17,13 +17,13 @@ import ( ) const ( - // File names are padded to 16-byte multiples, encrypted and - // base64-encoded. We can encode at most 176 bytes to stay below the 255 - // bytes limit: - // * base64(176 bytes) = 235 bytes - // * 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. +// File names are padded to 16-byte multiples, encrypted and +// base64-encoded. We can encode at most 176 bytes to stay below the 255 +// bytes limit: +// * base64(176 bytes) = 235 bytes +// * 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. ) // translateSize translates the ciphertext size in `out` into plaintext size. @@ -134,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 } diff --git a/internal/fusefrontend_reverse/node_xattr.go b/internal/fusefrontend_reverse/node_xattr.go index f4e3bda..b940339 100644 --- a/internal/fusefrontend_reverse/node_xattr.go +++ b/internal/fusefrontend_reverse/node_xattr.go @@ -25,6 +25,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 + } var data []byte // ACLs are passed through without encryption if isAcl(attr) { @@ -36,7 +40,7 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, } else { pAttr, err := rn.decryptXattrName(attr) if err != nil { - return 0, syscall.EINVAL + return 0, noSuchAttributeError } pData, errno := n.getXAttr(pAttr) if errno != 0 { @@ -56,11 +60,15 @@ func (n *Node) Getxattr(ctx context.Context, attr string, dest []byte) (uint32, // // 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 } - rn := n.rootNode() var buf bytes.Buffer for _, pName := range pNames { // ACLs are passed through without encryption diff --git a/internal/fusefrontend_reverse/node_xattr_darwin.go b/internal/fusefrontend_reverse/node_xattr_darwin.go index f5b58d9..6816a18 100644 --- a/internal/fusefrontend_reverse/node_xattr_darwin.go +++ b/internal/fusefrontend_reverse/node_xattr_darwin.go @@ -8,6 +8,9 @@ import ( "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 { diff --git a/internal/fusefrontend_reverse/node_xattr_freebsd.go b/internal/fusefrontend_reverse/node_xattr_freebsd.go new file mode 100644 index 0000000..949cf15 --- /dev/null +++ b/internal/fusefrontend_reverse/node_xattr_freebsd.go @@ -0,0 +1,17 @@ +package fusefrontend_reverse + +import ( + "golang.org/x/sys/unix" +) + +const noSuchAttributeError = unix.ENOATTR + +func (n *Node) getXAttr(cAttr string) (out []byte, errno unix.Errno) { + // TODO + return nil, unix.EOPNOTSUPP +} + +func (n *Node) listXAttr() (out []string, errno unix.Errno) { + // TODO + return nil, unix.EOPNOTSUPP +} diff --git a/internal/fusefrontend_reverse/node_xattr_linux.go b/internal/fusefrontend_reverse/node_xattr_linux.go index f6d04a5..3c574f5 100644 --- a/internal/fusefrontend_reverse/node_xattr_linux.go +++ b/internal/fusefrontend_reverse/node_xattr_linux.go @@ -9,6 +9,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 (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) { d, errno := n.prepareAtSyscall("") if errno != 0 { diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index 420ed22..461b25c 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" @@ -35,6 +36,11 @@ type RootNode struct { contentEnc *contentenc.ContentEnc // Tests whether a path is excluded (hidden) from the user. Used by -exclude. excluder ignore.IgnoreParser + // configPlainPath is the path of a custom config file (-config), relative to + // Cipherdir, set only when that file is located inside Cipherdir. Such a file + // is hidden from the encrypted view, otherwise it would leak in encrypted + // form (https://github.com/rfjakob/gocryptfs/issues/1009). + configPlainPath string // inoMap translates inode numbers from different devices to unique inode // numbers. inoMap *inomap.InoMap @@ -62,7 +68,7 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf var rootDev uint64 var st syscall.Stat_t var statErr error - var shortNameMax int + var shortNameMax = syscall.NAME_MAX if statErr = syscall.Stat(args.Cipherdir, &st); statErr != nil { tlog.Warn.Printf("Could not stat backing directory %q: %v", args.Cipherdir, statErr) if args.OneFileSystem { @@ -73,8 +79,10 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf rootDev = uint64(st.Dev) } - shortNameMax = n.GetLongNameMax() * 3 / 4 - shortNameMax = shortNameMax - shortNameMax%16 - 1 + if !args.PlaintextNames { + shortNameMax = n.GetLongNameMax() * 3 / 4 + shortNameMax = shortNameMax - shortNameMax%16 - 1 + } rn := &RootNode{ args: args, @@ -91,6 +99,14 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { rn.excluder = prepareExcluder(args) } + // A custom config file (-config) located inside Cipherdir must be hidden from + // the encrypted view, otherwise it leaks there in encrypted form (#1009). + if args.ConfigCustom && args.Config != "" { + if rel, err := filepath.Rel(args.Cipherdir, args.Config); err == nil && + rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) { + rn.configPlainPath = rel + } + } return rn } @@ -136,9 +152,15 @@ 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 } + // A custom config file (-config) inside Cipherdir is hidden from the + // encrypted view (https://github.com/rfjakob/gocryptfs/issues/1009). + if rn.configPlainPath != "" && pPath == rn.configPlainPath { + return true + } return rn.excluder != nil && rn.excluder.MatchesPath(pPath) } @@ -146,7 +168,7 @@ func (rn *RootNode) isExcludedPlain(pPath string) bool { // pDir is the relative plaintext path to the directory these entries are // from. The entries should be plaintext files. func (rn *RootNode) excludeDirEntries(d *dirfdPlus, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { - if rn.excluder == nil { + if rn.excluder == nil && rn.configPlainPath == "" { return entries } filtered = make([]fuse.DirEntry, 0, len(entries)) diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index 5dd4940..aaa65ee 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -83,16 +83,22 @@ func WriteDirIVAt(dirfd int) error { if !syscallcompat.IsENOSPC(err) { tlog.Warn.Printf("WriteDirIV: Write: %v", err) } - // Delete incomplete gocryptfs.diriv file - syscallcompat.Unlinkat(dirfd, DirIVFilename, 0) - return err + goto delete + } + err = f.Sync() + if err != nil { + tlog.Warn.Printf("WriteDirIV: Sync: %v", err) + goto delete } err = f.Close() if err != nil { tlog.Warn.Printf("WriteDirIV: Close: %v", err) - // Delete incomplete gocryptfs.diriv file - syscallcompat.Unlinkat(dirfd, DirIVFilename, 0) - return err + goto delete } return nil + +delete: + // Delete potentially incomplete gocryptfs.diriv file + syscallcompat.Unlinkat(dirfd, DirIVFilename, 0) + return err } 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/siv_aead/benchmark.bash b/internal/siv_aead/benchmark.bash index 40b57b3..400c134 100755 --- a/internal/siv_aead/benchmark.bash +++ b/internal/siv_aead/benchmark.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu diff --git a/internal/speed/benchmark.bash b/internal/speed/benchmark.bash index d2678a7..699ceb8 100755 --- a/internal/speed/benchmark.bash +++ b/internal/speed/benchmark.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu diff --git a/internal/stupidgcm/benchmark.bash b/internal/stupidgcm/benchmark.bash index 8681495..8319659 100755 --- a/internal/stupidgcm/benchmark.bash +++ b/internal/stupidgcm/benchmark.bash @@ -1,3 +1,3 @@ -#!/bin/bash +#!/usr/bin/env bash exec ../speed/benchmark.bash diff --git a/internal/syscallcompat/asuser_freebsd.go b/internal/syscallcompat/asuser_freebsd.go new file mode 100644 index 0000000..dfa8e18 --- /dev/null +++ b/internal/syscallcompat/asuser_freebsd.go @@ -0,0 +1,24 @@ +package syscallcompat + +import ( + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// asUser runs `f()` under the effective uid, gid, groups specified +// in `context`. +// +// If `context` is nil, `f()` is executed directly without switching user id. +// +// FreeBSD does not support changing uid/gid per thread. If context is not nil, +// an error is returned. +func asUser(f func() (int, error), context *fuse.Context) (int, error) { + if context == nil { + return f() + } + tlog.Warn.Printf("asUser: error, only nil context is supported\n") + return 0, unix.EOPNOTSUPP +} diff --git a/internal/syscallcompat/emulate.go b/internal/syscallcompat/emulate.go index 91b592b..435c579 100644 --- a/internal/syscallcompat/emulate.go +++ b/internal/syscallcompat/emulate.go @@ -1,3 +1,5 @@ +//go:build !freebsd + package syscallcompat import ( diff --git a/internal/syscallcompat/emulate_test.go b/internal/syscallcompat/emulate_test.go index 16383f2..907ba3a 100644 --- a/internal/syscallcompat/emulate_test.go +++ b/internal/syscallcompat/emulate_test.go @@ -1,3 +1,5 @@ +//go:build !freebsd + package syscallcompat import ( diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go index e30d605..36bcb9f 100644 --- a/internal/syscallcompat/quirks.go +++ b/internal/syscallcompat/quirks.go @@ -5,16 +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 ) -func logQuirk(s string) { +// 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_freebsd.go b/internal/syscallcompat/quirks_freebsd.go new file mode 100644 index 0000000..c340cea --- /dev/null +++ b/internal/syscallcompat/quirks_freebsd.go @@ -0,0 +1,22 @@ +package syscallcompat + +import ( + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// DetectQuirks decides if there are known quirks on the backing filesystem +// that need to be workarounded. +// +// Tested by tests/root_test.TestBtrfsQuirks +func DetectQuirks(cipherdir string) (q uint64) { + var st unix.Statfs_t + err := unix.Statfs(cipherdir, &st) + if err != nil { + tlog.Warn.Printf("DetectQuirks: Statfs on %q failed: %v", cipherdir, err) + return 0 + } + + return q +} diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index bcdcf07..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,10 +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 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/sys_common.go b/internal/syscallcompat/sys_common.go index 1aa6a6e..4f84d98 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,10 +135,10 @@ 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 { + if err == ENODATA { return nil, err } // Underlying fs does not support security.capabilities (example: tmpfs) @@ -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 0ebdd3b..07eb30a 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -24,9 +24,20 @@ const ( RENAME_NOREPLACE = unix.RENAME_EXCL RENAME_EXCHANGE = unix.RENAME_SWAP + ENODATA = unix.ENODATA + // Only exists on Linux. Define here to fix build failure, even though // we will never see this flag. RENAME_WHITEOUT = 1 << 30 + + // On Darwin we use O_SYMLINK which allows opening a symlink itself. + // On Linux, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_SYMLINK + + // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a + // flawed replacement. + F_OFD_SETLKW = unix.F_SETLKW + F_OFD_SETLK = unix.F_SETLK ) // Unfortunately fsetattrlist does not have a syscall wrapper yet. diff --git a/internal/syscallcompat/sys_freebsd.go b/internal/syscallcompat/sys_freebsd.go new file mode 100644 index 0000000..0d28f3e --- /dev/null +++ b/internal/syscallcompat/sys_freebsd.go @@ -0,0 +1,165 @@ +// Package syscallcompat wraps FreeBSD-specific syscalls +package syscallcompat + +import ( + "time" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +const ( + O_DIRECT = unix.O_DIRECT + + // O_PATH is supported on FreeBSD, but is missing from the sys/unix package + // FreeBSD-15.0 /usr/src/sys/sys/fcntl.h:135 + O_PATH = 0x00400000 + + // Only defined on Linux, but we can emulate the functionality on FreeBSD + // in Renameat2() below + RENAME_NOREPLACE = 0x1 + RENAME_EXCHANGE = 0x2 + RENAME_WHITEOUT = 0x4 + + // ENODATA is only defined on Linux, but FreeBSD provides ENOATTR + ENODATA = unix.ENOATTR + + // On FreeBSD, we only have O_NOFOLLOW. + OpenatFlagNofollowSymlink = unix.O_NOFOLLOW + + // For the utimensat syscall on FreeBSD + AT_EMPTY_PATH = 0x4000 + + // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a + // flawed replacement. + F_OFD_SETLKW = unix.F_SETLKW + F_OFD_SETLK = unix.F_SETLK +) + +// EnospcPrealloc is supposed to preallocate ciphertext space without +// changing the file size. This guarantees that we don't run out of +// space while writing a ciphertext block (that would corrupt the block). +// +// The fallocate syscall isn't supported on FreeBSD with the same semantics +// as Linux, in particular the _FALLOC_FL_KEEP_SIZE mode isn't supported. +func EnospcPrealloc(fd int, off int64, len int64) (err error) { + return nil +} + +// Fallocate wraps the posix_fallocate() syscall. +// Fallocate returns an error if mode is not 0 +func Fallocate(fd int, mode uint32, off int64, len int64) (err error) { + if mode != 0 { + tlog.Warn.Printf("Fallocate: unsupported mode\n") + return unix.EOPNOTSUPP + } + _, _, err = unix.Syscall(unix.SYS_POSIX_FALLOCATE, uintptr(fd), uintptr(off), uintptr(len)) + return err +} + +// Mknodat wraps the Mknodat syscall. +func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { + return unix.Mknodat(dirfd, path, mode, uint64(dev)) +} + +// 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) { + return unix.Dup3(oldfd, newfd, flags) +} + +// FchmodatNofollow is like Fchmodat but never follows symlinks. +func FchmodatNofollow(dirfd int, path string, mode uint32) (err error) { + return unix.Fchmodat(dirfd, path, mode, unix.AT_SYMLINK_NOFOLLOW) +} + +// 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). +// +// If `context` is nil, this function behaves like ordinary Lsetxattr. +func LsetxattrUser(path string, attr string, data []byte, flags int, context *fuse.Context) (err error) { + f := func() (int, error) { + err := unix.Lsetxattr(path, attr, data, flags) + return -1, err + } + _, err = asUser(f, context) + return err +} + +func timesToTimespec(a *time.Time, m *time.Time) []unix.Timespec { + ts := make([]unix.Timespec, 2) + 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 +} + +// FutimesNano syscall. +func FutimesNano(fd int, a *time.Time, m *time.Time) (err error) { + ts := timesToTimespec(a, m) + return unix.UtimesNanoAt(unix.AT_FDCWD, "", ts, AT_EMPTY_PATH) +} + +// UtimesNanoAtNofollow is like UtimesNanoAt but never follows symlinks. +// Retries on EINTR. +func UtimesNanoAtNofollow(dirfd int, path string, a *time.Time, m *time.Time) (err error) { + ts := timesToTimespec(a, m) + err = retryEINTR(func() error { + return unix.UtimesNanoAt(dirfd, path, ts, unix.AT_SYMLINK_NOFOLLOW) + }) + return err +} + +// Getdents syscall with "." and ".." filtered out. +func Getdents(fd int) ([]fuse.DirEntry, error) { + entries, _, err := emulateGetdents(fd) + return entries, err +} + +// GetdentsSpecial calls the Getdents syscall, +// with normal entries and "." / ".." split into two slices. +func GetdentsSpecial(fd int) (entries []fuse.DirEntry, entriesSpecial []fuse.DirEntry, err error) { + return emulateGetdents(fd) +} + +// Renameat2 does not exist on FreeBSD, so we have to wrap it here. +// Retries on EINTR. +// The RENAME_EXCHANGE and RENAME_WHITEOUT flags are not supported. +func Renameat2(olddirfd int, oldpath string, newdirfd int, newpath string, flags uint) (err error) { + if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE { + return unix.EINVAL + } + if flags&(RENAME_NOREPLACE|RENAME_EXCHANGE) == RENAME_NOREPLACE|RENAME_EXCHANGE { + return unix.EINVAL + } + + if flags&RENAME_NOREPLACE != 0 { + var st unix.Stat_t + err = unix.Fstatat(newdirfd, newpath, &st, 0) + if err == nil { + // Assume newpath is an existing file if we can stat() it. + // On Linux, RENAME_NOREPLACE fails with EEXIST if newpath + // already exists. + return unix.EEXIST + } + } + if flags&RENAME_EXCHANGE != 0 { + return unix.EINVAL + } + if flags&RENAME_WHITEOUT != 0 { + return unix.EINVAL + } + + return unix.Renameat(olddirfd, oldpath, newdirfd, newpath) +} diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index 19d2c56..ffa5a97 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -28,6 +28,18 @@ 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 + + // Only defined on Linux + ENODATA = unix.ENODATA + + // F_OFD_SETLKW only exists on Linux. On Darwin, fall back to F_SETLKW as a + // flawed replacement. + F_OFD_SETLKW = unix.F_OFD_SETLKW + F_OFD_SETLK = unix.F_OFD_SETLK ) var preallocWarn sync.Once @@ -68,6 +80,15 @@ func Mknodat(dirfd int, path string, mode uint32, dev int) (err error) { return syscall.Mknodat(dirfd, path, mode, dev) } +// Statx wraps the Statx syscall. +// Retries on EINTR. +func Statx(dirfd int, path string, flags int, mask int, st *unix.Statx_t) (err error) { + err = retryEINTR(func() error { + return unix.Statx(dirfd, path, flags, mask, st) + }) + 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) { diff --git a/internal/syscallcompat/unix2syscall_darwin.go b/internal/syscallcompat/unix2syscall.go index 5767a27..fa2e8c4 100644 --- a/internal/syscallcompat/unix2syscall_darwin.go +++ b/internal/syscallcompat/unix2syscall.go @@ -1,3 +1,5 @@ +//go:build darwin || freebsd + package syscallcompat import ( @@ -274,6 +274,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f PlaintextNames: args.plaintextnames, LongNames: args.longnames, ConfigCustom: args._configCustom, + Config: args.config, NoPrealloc: args.noprealloc, ForceOwner: args._forceOwner, Exclude: args.exclude, @@ -284,6 +285,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 { @@ -328,8 +330,11 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f // Init crypto backend cCore := cryptocore.New(masterkey, cryptoBackend, IVBits, args.hkdf) cEnc := contentenc.New(cCore, contentenc.DefaultBS) - nameTransform := nametransform.New(cCore.EMECipher, frontendArgs.LongNames, args.longnamemax, - args.raw64, []string(args.badname), frontendArgs.DeterministicNames) + var nameTransform *nametransform.NameTransform + if !args.plaintextnames { + nameTransform = nametransform.New(cCore.EMECipher, frontendArgs.LongNames, args.longnamemax, + args.raw64, []string(args.badname), frontendArgs.DeterministicNames) + } // After the crypto backend is initialized, // we can purge the master key from memory. for i := range masterkey { @@ -388,6 +393,7 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { // Enable go-fuse warnings fuseOpts.Logger = log.New(os.Stderr, "go-fuse: ", log.Lmicroseconds) fuseOpts.MountOptions = fuse.MountOptions{ + DisableReadDirPlus: !args.readdirplus, // Writes and reads are usually capped at 128kiB on Linux through // the FUSE_MAX_PAGES_PER_REQ kernel constant in fuse_i.h. Our // sync.Pool buffer pools are sized acc. to the default. Users may set @@ -448,9 +454,18 @@ func initGoFuse(rootNode fs.InodeEmbedder, args *argContainer) *fuse.Server { if runtime.GOOS == "darwin" { opts["volname"] = strings.Replace(path.Base(args.mountpoint), ",", "_", -1) } + underlyingFilesystemRo, err := isReadOnlyFilesystem(args.cipherdir) + if err != nil { + tlog.Debug.Printf("Error checking if cipherdir is on a read-only filesystem: %s", err) + } else if underlyingFilesystemRo && args.rw { + tlog.Fatal.Printf("Writeable mount explicitly requested but cipherdir %s is on a read-only filesystem, refusing.", args.cipherdir) + os.Exit(exitcodes.Usage) + } else if underlyingFilesystemRo && !args.ro { + tlog.Info.Printf("Cipherdir %s is on a read-only filesystem, mounting as read-only.", args.cipherdir) + } // The kernel enforces read-only operation, we just have to pass "ro". - // Reverse mounts are always read-only. - if args.ro || args.reverse { + // Reverse mounts and mounts with cipherdirs on read-only filesystems are always read-only. + if args.ro || args.reverse || underlyingFilesystemRo { opts["ro"] = "" } else if args.rw { opts["rw"] = "" @@ -561,3 +576,16 @@ func unmount(srv *fuse.Server, mountpoint string) { } } } + +const ( + ST_RDONLY = 0x1 +) + +func isReadOnlyFilesystem(path string) (bool, error) { + var stat syscall.Statfs_t + if err := syscall.Statfs(path, &stat); err != nil { + return false, err + } + + return (stat.Flags & ST_RDONLY) != 0, nil +} diff --git a/package-release-tarballs.bash b/package-release-tarballs.bash index 881bce0..3581008 100755 --- a/package-release-tarballs.bash +++ b/package-release-tarballs.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu diff --git a/profiling/ls.bash b/profiling/ls.bash index 35f5a39..334d8be 100755 --- a/profiling/ls.bash +++ b/profiling/ls.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" diff --git a/profiling/streaming-read.bash b/profiling/streaming-read.bash index 86ef942..138148c 100755 --- a/profiling/streaming-read.bash +++ b/profiling/streaming-read.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" diff --git a/profiling/streaming-write.bash b/profiling/streaming-write.bash index 6f3af56..3c29ee6 100755 --- a/profiling/streaming-write.bash +++ b/profiling/streaming-write.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" diff --git a/profiling/tar-extract.bash b/profiling/tar-extract.bash index f176368..e98100a 100755 --- a/profiling/tar-extract.bash +++ b/profiling/tar-extract.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" 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/profiling/write-trace.bash b/profiling/write-trace.bash index 31af492..8b7cec9 100755 --- a/profiling/write-trace.bash +++ b/profiling/write-trace.bash @@ -1,8 +1,10 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # # Write an execution trace of writing 100MB of data # to a new gocryptfs mount on /tmp +set -eu + cd "$(dirname "$0")" T=$(mktemp -d) diff --git a/test-without-openssl.bash b/test-without-openssl.bash index e596753..d2cd7e4 100755 --- a/test-without-openssl.bash +++ b/test-without-openssl.bash @@ -1,4 +1,6 @@ -#!/bin/bash -eu +#!/usr/bin/env bash + +set -eu cd "$(dirname "$0")" @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # test.bash runs the gocryptfs test suite against $TMPDIR, # or, if unset, /var/tmp. diff --git a/tests/canonical-benchmarks.bash b/tests/canonical-benchmarks.bash index 4c1a357..963fd6a 100755 --- a/tests/canonical-benchmarks.bash +++ b/tests/canonical-benchmarks.bash @@ -1,4 +1,4 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # # Run the set of "canonical" benchmarks that are shown on # https://nuetzlich.net/gocryptfs/comparison/ @@ -6,6 +6,7 @@ # # This is called by the top-level script "benchmark.bash". +set -eu MYNAME=$(basename "$0") diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 01cc3b7..472941d 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -991,6 +991,25 @@ func TestInitNotEmpty(t *testing.T) { } } +// TestReaddirplus checks that mounting and listing work with -readdirplus. +func TestReaddirplus(t *testing.T) { + dir := test_helpers.InitFS(t) + mnt := dir + ".mnt" + test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-readdirplus") + defer test_helpers.UnmountPanic(mnt) + + if err := os.WriteFile(mnt+"/file", nil, 0600); err != nil { + t.Fatal(err) + } + entries, err := os.ReadDir(mnt) + if err != nil { + t.Fatal(err) + } + if len(entries) != 1 || entries[0].Name() != "file" { + t.Fatalf("unexpected directory entries: %v", entries) + } +} + // TestSharedstorage checks that `-sharedstorage` shows stable inode numbers to // userspace despite having hard link tracking disabled func TestSharedstorage(t *testing.T) { diff --git a/tests/cluster/cluster_test.go b/tests/cluster/cluster_test.go index 2e969ce..70a2a02 100644 --- a/tests/cluster/cluster_test.go +++ b/tests/cluster/cluster_test.go @@ -7,44 +7,47 @@ package cluster_test import ( "bytes" + "errors" + "io" "math/rand" "os" "sync" + "syscall" "testing" "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) -// This test passes on XFS but fails on ext4 and tmpfs!!! +// With -sharedstorage (i.e. with fcntl byte-range locks) this test passes on all +// filesystems. Without, it passes on XFS but fails on ext4 and tmpfs. // // Quoting https://lists.samba.org/archive/samba-technical/2019-March/133050.html // // > It turns out that xfs respects POSIX w.r.t "atomic read/write" and // > this is implemented by taking a file-wide shared lock on every // > buffered read. -// > This behavior is unique to XFS on Linux and is not optional. -// > Other Linux filesystems only guaranty page level atomicity for -// > buffered read/write. +// +// Note that ext4 actually provides NO ATOMICITY AT ALL. +// Quoting https://stackoverflow.com/a/35256626 : +// +// > Linux 4.2.6 with ext4: update atomicity = 1 byte +// +// TestPoCTornWrite in this package confirms this. // // See also: -// * https://lore.kernel.org/linux-xfs/20190325001044.GA23020@dastard/ -// Dave Chinner: XFS is the only linux filesystem that provides this behaviour. +// - https://lore.kernel.org/linux-xfs/20190325001044.GA23020@dastard/ +// Dave Chinner: XFS is the only linux filesystem that provides this behaviour. func TestClusterConcurrentRW(t *testing.T) { - if os.Getenv("ENABLE_CLUSTER_TEST") != "1" { - t.Skipf("This test is disabled by default because it fails unless on XFS.\n" + - "Run it like this: ENABLE_CLUSTER_TEST=1 go test\n" + - "Choose a backing directory by setting TMPDIR.") - } - - const blocksize = 4096 - const fileSize = 25 * blocksize // 100 kiB + const fileSize = 100000 // arbitrary unaligned size with a partial block at the end + const writeSize = 5000 // arbitrary unaligned size that touches two ciphertext blocks + const readSize = 5000 cDir := test_helpers.InitFS(t) mnt1 := cDir + ".mnt1" mnt2 := cDir + ".mnt2" - test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0") + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") defer test_helpers.UnmountPanic(mnt1) - test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0") + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") defer test_helpers.UnmountPanic(mnt2) f1, err := os.Create(mnt1 + "/foo") @@ -68,12 +71,12 @@ func TestClusterConcurrentRW(t *testing.T) { const loops = 10000 writeThread := func(f *os.File) { defer wg.Done() - buf := make([]byte, blocksize) + buf := make([]byte, writeSize) for i := 0; i < loops; i++ { if t.Failed() { return } - off := rand.Int63n(fileSize / blocksize) + off := rand.Int63n(int64(fileSize - len(buf) - 1)) _, err := f.WriteAt(buf, off) if err != nil { t.Errorf("writeThread iteration %d: WriteAt failed: %v", i, err) @@ -83,13 +86,13 @@ func TestClusterConcurrentRW(t *testing.T) { } readThread := func(f *os.File) { defer wg.Done() - zeroBlock := make([]byte, blocksize) - buf := make([]byte, blocksize) + zeroBlock := make([]byte, readSize) + buf := make([]byte, len(zeroBlock)) for i := 0; i < loops; i++ { if t.Failed() { return } - off := rand.Int63n(fileSize / blocksize) + off := rand.Int63n(int64(fileSize - len(zeroBlock) - 1)) _, err := f.ReadAt(buf, off) if err != nil { t.Errorf("readThread iteration %d: ReadAt failed: %v", i, err) @@ -109,3 +112,117 @@ func TestClusterConcurrentRW(t *testing.T) { go readThread(f2) wg.Wait() } + +// Multiple hosts creating the same file at the same time could +// overwrite each other's file header, leading to data +// corruption. Passing "-sharedstorage" should prevent this. +func TestConcurrentCreate(t *testing.T) { + cDir := test_helpers.InitFS(t) + mnt1 := cDir + ".mnt1" + mnt2 := cDir + ".mnt2" + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt1) + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt2) + + var wg sync.WaitGroup + const loops = 10000 + + createOrOpen := func(path string) (f *os.File, err error) { + // Use the high-level os.Create/OpenFile instead of syscall.Open because we + // *want* Go's EINTR retry logic. glibc open(2) has similar logic. + f, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600) + if err == nil { + return + } + if !errors.Is(err, os.ErrExist) { + t.Logf("POSIX compliance issue: exclusive create failed with unexpected error: err=%v", errors.Unwrap(err)) + } + f, err = os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err == nil { + return + } + t.Logf("POSIX compliance issue: non-exlusive create failed with err=%v", errors.Unwrap(err)) + return + } + + workerThread := func(path string) { + defer wg.Done() + buf := make([]byte, 10) + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := createOrOpen(path) + if err != nil { + // retry + continue + } + defer f.Close() + _, err = f.WriteAt(buf, 0) + if err != nil { + t.Errorf("iteration %d: Pwrite: %v", i, err) + return + } + buf2 := make([]byte, len(buf)+1) + n, err := f.ReadAt(buf2, 0) + if err != nil && err != io.EOF { + t.Errorf("iteration %d: ReadAt: %v", i, err) + return + } + buf2 = buf2[:n] + if !bytes.Equal(buf, buf2) { + t.Errorf("iteration %d: corrupt data received: %x", i, buf2) + return + } + syscall.Unlink(path) + + // Close now (not only when the whole loop exists) to avoid exhausting fds. + // Double-close on happy path is harmless: "Close will return an error if it has already been called" + f.Close() + } + } + + wg.Add(2) + go workerThread(mnt1 + "/foo") + go workerThread(mnt2 + "/foo") + wg.Wait() +} + +// Check that opening with O_CREATE|O_TRUNC and writing always works +func TestOpenTruncate(t *testing.T) { + cDir := test_helpers.InitFS(t) + mnt1 := cDir + ".mnt1" + mnt2 := cDir + ".mnt2" + test_helpers.MountOrFatal(t, cDir, mnt1, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt1) + test_helpers.MountOrFatal(t, cDir, mnt2, "-extpass=echo test", "-wpanic=0", "-sharedstorage") + defer test_helpers.UnmountPanic(mnt2) + + var wg sync.WaitGroup + const loops = 100 + + writerThread := func(path string) { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + t.Logf("POSIX compliance issue: non-exlusive create failed with err=%v", errors.Unwrap(err)) + continue + } + _, err = f.WriteAt([]byte("foo"), 0) + if err != nil { + t.Errorf("iteration %d: WriteAt: %v", i, err) + } + f.Close() + } + } + + wg.Add(2) + go writerThread(mnt1 + "/foo") + go writerThread(mnt2 + "/foo") + wg.Wait() +} diff --git a/tests/cluster/poc_test.go b/tests/cluster/poc_test.go new file mode 100644 index 0000000..52b9ec9 --- /dev/null +++ b/tests/cluster/poc_test.go @@ -0,0 +1,230 @@ +package cluster + +// poc_test.go contains proof of concept tests for the byte-range locking logic. +// This goes directly to an underlying filesystem without going through gocryptfs. + +import ( + "bytes" + "errors" + "io" + "os" + "sync" + "sync/atomic" + "syscall" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/contentenc" + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// Check that byte-range locks work on an empty file +func TestPoCFcntlFlock(t *testing.T) { + path := test_helpers.TmpDir + "/" + t.Name() + + fd1, err := syscall.Open(path, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_EXCL, 0600) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd1) + + // F_OFD_SETLK locks on the same fd always succeed, so we have to + // open a 2nd time. + fd2, err := syscall.Open(path, syscall.O_RDWR, 0) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd2) + + lk := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: unix.SEEK_SET, + Start: 0, + Len: 0, + } + err = unix.FcntlFlock(uintptr(fd1), syscallcompat.F_OFD_SETLK, &lk) + if err != nil { + t.Fatal(err) + } + err = unix.FcntlFlock(uintptr(fd2), syscallcompat.F_OFD_SETLK, &lk) + if err == nil { + t.Fatal("double-lock succeeded but should have failed") + } +} + +// See if we can get garbage data when the file header is read and written concurrently. +// We should get either 0 bytes or 18 correct bytes. +func TestPoCHeaderCreation(t *testing.T) { + path := test_helpers.TmpDir + "/" + t.Name() + var wg sync.WaitGroup + // I ran this with 10000 iteration and no problems to be seen. Let's not waste too + // much testing time. + const loops = 100 + + var stats struct { + readOk int64 + readEmpty int64 + writes int64 + } + + writeBuf := []byte("123456789012345678") + if len(writeBuf) != contentenc.HeaderLen { + t.Fatal("BUG wrong header length") + } + + writerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0600) + if err != nil { + t.Errorf("BUG: this should not happen: open err=%v", err) + return + } + // Do like gocryptfs does and prealloc the 18 bytes + err = syscallcompat.EnospcPrealloc(int(f.Fd()), 0, contentenc.HeaderLen) + if err != nil { + t.Error(err) + } + + _, err = f.WriteAt(writeBuf, 0) + if err != nil { + t.Errorf("iteration %d: Pwrite: %v", i, err) + } + atomic.AddInt64(&stats.writes, 1) + f.Close() + syscall.Unlink(path) + } + } + + readerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + f, err := os.OpenFile(path, os.O_RDONLY, 0600) + if errors.Is(err, os.ErrNotExist) { + continue + } + if err != nil { + t.Error(err) + return + } + readBuf := make([]byte, contentenc.HeaderLen) + _, err = f.ReadAt(readBuf, 0) + if errors.Is(err, io.EOF) { + atomic.AddInt64(&stats.readEmpty, 1) + goto close + } + if err != nil { + t.Errorf("iteration %d: ReadAt: %v", i, err) + goto close + } + if !bytes.Equal(writeBuf, readBuf) { + t.Errorf("iteration %d: corrupt data received: %x", i, readBuf) + goto close + } + atomic.AddInt64(&stats.readOk, 1) + close: + f.Close() + } + } + + wg.Add(2) + go writerThread() + go readerThread() + wg.Wait() + + t.Logf("readEmpty=%d readOk=%d writes=%d", stats.readEmpty, stats.readOk, stats.writes) +} + +// TestPoCTornWrite simulates what TestConcurrentCreate does. +// +// Fails on ext4, quoting https://stackoverflow.com/a/35256626 : +// > Linux 4.2.6 with ext4: update atomicity = 1 byte +// +// Passes on XFS. +func TestPoCTornWrite(t *testing.T) { + if os.Getenv("ASSUME_XFS") != "1" { + t.Skipf("This test is disabled by default because it fails unless on XFS.\n" + + "Run it like this: ASSUME_XFS=1 go test -run TestPoCTornWrite\n" + + "Choose a backing directory by setting TMPDIR.") + } + doTestPoCTornWrite(t, false) +} + +// Same as TestPoCTornWrite but uses fcntl byte range locks +func TestPoCTornWriteLocked(t *testing.T) { + doTestPoCTornWrite(t, true) +} + +func doTestPoCTornWrite(t *testing.T, locking bool) { + path := test_helpers.TmpDir + "/" + t.Name() + var wg sync.WaitGroup + const loops = 10000 + + writerThread := func() { + defer wg.Done() + for i := 0; i < loops; i++ { + if t.Failed() { + return + } + + f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0600) + if err != nil { + t.Errorf("BUG: this should not happen: open err=%v", err) + return + } + + // Write + blockData := bytes.Repeat([]byte{byte(i)}, 42) + if locking { + lk := unix.Flock_t{ + Type: unix.F_WRLCK, + Whence: unix.SEEK_SET, + Start: 0, + Len: int64(len(blockData)), + } + if err := unix.FcntlFlock(uintptr(f.Fd()), syscallcompat.F_OFD_SETLKW, &lk); err != nil { + t.Error(err) + return + } + // No need to unlock, lock is implicitely dropped on fd close + } + if _, err = f.WriteAt(blockData, 0); err != nil { + t.Errorf("iteration %d: WriteAt: %v", i, err) + return + } + + // Readback and verify + readBuf := make([]byte, 100) + if n, err := f.ReadAt(readBuf, 0); err == io.EOF { + readBuf = readBuf[:n] + } else if err != nil { + t.Error(err) + return + } + if len(readBuf) != len(blockData) { + t.Error("wrong length") + return + } + for _, v := range readBuf { + if v != readBuf[0] { + t.Errorf("iteration %d: inconsistent block: %x", i, readBuf) + return + } + } + f.Close() + } + } + + wg.Add(2) + go writerThread() + go writerThread() + wg.Wait() +} diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index a19f079..237dbd1 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -554,3 +554,38 @@ func TestSeekDir(t *testing.T) { t.Error("Seek did not have any effect") } } + +// Regression test for https://github.com/rfjakob/gocryptfs/issues/1024 +// +// truncate(2) goes through node.Setattr, which opens its own file handle. That +// handle must take ContentLock like file.Setattr does: the lock doubles as the +// global write-operation counter that isConsecutiveWrite() uses to notice that +// somebody else changed the file. Without it, an already-open handle keeps +// believing its next write appends, skips writePadHole(), and leaves the last +// block short while the file grows past it - the block then fails to decrypt. +func TestConcurrentTruncateViaPath(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/" + t.Name() + f, err := os.Create(path) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + b := make([]byte, 4096) + _, err = f.Write(b) + if err != nil { + t.Fatal(err) + } + // Truncate via path used to not increment writeOpCount... + if err = os.Truncate(path, 8); err != nil { + t.Fatal(err) + } + // ...which means this write will not call writePadHole. + if _, err = f.Write([]byte("foo")); err != nil { + t.Fatal(err) + } + // First block is corrupt now. + if _, err = f.ReadAt(b, 0); err != nil { + t.Fatal(err) + } +} diff --git a/tests/dl-linux-tarball.bash b/tests/dl-linux-tarball.bash index 03c0e7d..3c325db 100755 --- a/tests/dl-linux-tarball.bash +++ b/tests/dl-linux-tarball.bash @@ -1,8 +1,10 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # # This script checks the size of /tmp/linux-3.0.tar.gz and downloads # a fresh copy if the size is incorrect or the file is missing. +set -eu + URL=https://cdn.kernel.org/pub/linux/kernel/v3.0/linux-3.0.tar.gz TGZ=/tmp/linux-3.0.tar.gz diff --git a/tests/example_filesystems/example_test_helpers.go b/tests/example_filesystems/example_test_helpers.go index 34e5786..5e38721 100644 --- a/tests/example_filesystems/example_test_helpers.go +++ b/tests/example_filesystems/example_test_helpers.go @@ -27,26 +27,26 @@ func checkExampleFS(t *testing.T, dir string, rw bool) { symlink := filepath.Join(dir, "rel") target, err := os.Readlink(symlink) if err != nil { - t.Error(err) + t.Errorf("relative symlink: Readlink: %v", err) return } if target != "status.txt" { - t.Errorf("Unexpected link target: %s\n", target) + t.Errorf("relative symlink: Unexpected link target: %s\n", target) } // Read absolute symlink symlink = filepath.Join(dir, "abs") target, err = os.Readlink(symlink) if err != nil { - t.Error(err) + t.Errorf("absolute symlink: Readlink: %v", err) return } if target != "/a/b/c/d" { - t.Errorf("Unexpected link target: %s\n", target) + t.Errorf("absolute symlink: Unexpected link target: %s\n", target) } if rw { // Test directory operations - test_helpers.TestRename(t, dir) - test_helpers.TestMkdirRmdir(t, dir) + t.Run("TestRename", func(t *testing.T) { test_helpers.TestRename(t, dir) }) + t.Run("TestMkdirRmdir", func(t *testing.T) { test_helpers.TestMkdirRmdir(t, dir) }) } } diff --git a/tests/fuse-unmount.bash b/tests/fuse-unmount.bash index b36f28c..02c6e4c 100755 --- a/tests/fuse-unmount.bash +++ b/tests/fuse-unmount.bash @@ -1,10 +1,13 @@ -#!/bin/bash -eu +#!/usr/bin/env bash # # Compatibility wrapper around "fusermount" on Linux and "umount" on # Mac OS X and friends. # # This script can be sourced or executed directly. # + +set -eu + fuse-unmount() { local MYNAME=$(basename "$BASH_SOURCE") if [[ $# -eq 0 ]] ; then diff --git a/tests/matrix/atime_darwin.go b/tests/matrix/atime_darwin+freebsd.go index 5f89c69..43db0d5 100644 --- a/tests/matrix/atime_darwin.go +++ b/tests/matrix/atime_darwin+freebsd.go @@ -1,3 +1,5 @@ +//go:build darwin || freebsd + package matrix import ( diff --git a/tests/matrix/main_test.go b/tests/matrix/main_test.go index cf0b6c4..126adaf 100644 --- a/tests/matrix/main_test.go +++ b/tests/matrix/main_test.go @@ -59,6 +59,7 @@ func TestMain(m *testing.M) { {false, "auto", false, true, nil}, // -serialize_reads {false, "auto", false, false, []string{"-serialize_reads"}}, + {false, "auto", false, false, []string{"-readdirplus"}}, {false, "auto", false, false, []string{"-sharedstorage"}}, {false, "auto", false, false, []string{"-deterministic-names"}}, // Test xchacha with and without openssl diff --git a/tests/matrix/matrix_test.go b/tests/matrix/matrix_test.go index a2ec549..2f097fb 100644 --- a/tests/matrix/matrix_test.go +++ b/tests/matrix/matrix_test.go @@ -993,3 +993,18 @@ func TestRenameExchangeOnGocryptfs(t *testing.T) { t.Errorf("file2 content wrong after exchange. Expected: %s, Got: %s", content1, newContent2) } } + +func TestUnlinkedO_SYNC(t *testing.T) { + path := test_helpers.DefaultPlainDir + "/" + t.Name() + fd, err := syscall.Open(path, syscall.O_CREAT|syscall.O_RDWR|syscall.O_SYNC, 0600) + if err != nil { + t.Fatal(err) + } + defer syscall.Close(fd) + if err = os.Remove(path); err != nil { + t.Fatal(err) + } + if _, err = syscall.Write(fd, make([]byte, 10)); err != nil { + t.Error(err) + } +} diff --git a/tests/matrix/statx_linux_test.go b/tests/matrix/statx_linux_test.go new file mode 100644 index 0000000..4d62663 --- /dev/null +++ b/tests/matrix/statx_linux_test.go @@ -0,0 +1,156 @@ +package matrix + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" + "time" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/ctlsock" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +const testStatxMask = unix.STATX_BASIC_STATS | unix.STATX_BTIME + +func statxAt(t *testing.T, dirfd int, path string, flags int) unix.Statx_t { + t.Helper() + var st unix.Statx_t + if err := unix.Statx(dirfd, path, flags, testStatxMask, &st); err != nil { + t.Fatal(err) + } + return st +} + +func encryptedPath(t *testing.T, plainPath string) string { + t.Helper() + resp := test_helpers.QueryCtlSock(t, ctlsockPath, ctlsock.RequestStruct{ + EncryptPath: plainPath, + }) + if resp.Result == "" { + t.Fatal(resp) + } + return filepath.Join(test_helpers.DefaultCipherDir, resp.Result) +} + +func requireFuseStatx(t *testing.T) { + t.Helper() + data, err := os.ReadFile("/proc/sys/kernel/osrelease") + if err != nil { + t.Fatal(err) + } + parts := strings.SplitN(strings.TrimSpace(string(data)), ".", 3) + if len(parts) < 2 { + t.Skipf("cannot parse kernel release %q", data) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + t.Skipf("cannot parse kernel release %q: %v", data, err) + } + minorString := parts[1] + if i := strings.IndexFunc(minorString, func(r rune) bool { + return r < '0' || r > '9' + }); i >= 0 { + minorString = minorString[:i] + } + minor, err := strconv.Atoi(minorString) + if err != nil { + t.Skipf("cannot parse kernel release %q: %v", data, err) + } + if major < 6 || major == 6 && minor < 6 { + t.Skip("FUSE_STATX requires Linux 6.6 or newer") + } +} + +func checkBtime(t *testing.T, plainPath string, cipherPath string, flags int) unix.Statx_t { + t.Helper() + cipherSt := statxAt(t, unix.AT_FDCWD, cipherPath, flags) + if cipherSt.Mask&unix.STATX_BTIME == 0 { + t.Skip("backing filesystem does not report STATX_BTIME") + } + plainSt := statxAt(t, unix.AT_FDCWD, plainPath, flags) + if plainSt.Mask&unix.STATX_BTIME == 0 { + t.Fatalf("mounted filesystem did not report STATX_BTIME: mask=%#x", plainSt.Mask) + } + if plainSt.Btime.Sec != cipherSt.Btime.Sec || plainSt.Btime.Nsec != cipherSt.Btime.Nsec { + t.Errorf("birth time mismatch: plain=%d.%09d cipher=%d.%09d", + plainSt.Btime.Sec, plainSt.Btime.Nsec, + cipherSt.Btime.Sec, cipherSt.Btime.Nsec) + } + return plainSt +} + +func TestStatxBtime(t *testing.T) { + requireFuseStatx(t) + + t.Run("root", func(t *testing.T) { + checkBtime(t, test_helpers.DefaultPlainDir, test_helpers.DefaultCipherDir, unix.AT_SYMLINK_NOFOLLOW) + }) + + t.Run("regular", func(t *testing.T) { + const content = "statx birth time" + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.WriteFile(plainPath, []byte(content), 0600); err != nil { + t.Fatal(err) + } + cipherPath := encryptedPath(t, relPath) + + before := checkBtime(t, plainPath, cipherPath, unix.AT_SYMLINK_NOFOLLOW) + if before.Size != uint64(len(content)) { + t.Errorf("wrong plaintext size: have=%d want=%d", before.Size, len(content)) + } + + // Check user-visible AT_EMPTY_PATH behavior. Current Linux kernels do + // not send the file handle in FUSE_STATX, so this still reaches + // Node.Statx rather than File.Statx. + f, err := os.Open(plainPath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + fdSt := statxAt(t, int(f.Fd()), "", unix.AT_EMPTY_PATH) + if fdSt.Mask&unix.STATX_BTIME == 0 { + t.Fatalf("statx on open file did not report STATX_BTIME: mask=%#x", fdSt.Mask) + } + if fdSt.Btime.Sec != before.Btime.Sec || fdSt.Btime.Nsec != before.Btime.Nsec { + t.Errorf("statx on open file returned different birth time: path=%d.%09d fd=%d.%09d", + before.Btime.Sec, before.Btime.Nsec, fdSt.Btime.Sec, fdSt.Btime.Nsec) + } + + now := time.Now().Add(-time.Hour) + if err := os.Chtimes(plainPath, now, now); err != nil { + t.Fatal(err) + } + after := checkBtime(t, plainPath, cipherPath, unix.AT_SYMLINK_NOFOLLOW) + if after.Btime.Sec != before.Btime.Sec || after.Btime.Nsec != before.Btime.Nsec { + t.Errorf("birth time changed with mtime: before=%d.%09d after=%d.%09d", + before.Btime.Sec, before.Btime.Nsec, after.Btime.Sec, after.Btime.Nsec) + } + }) + + t.Run("directory", func(t *testing.T) { + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.Mkdir(plainPath, 0700); err != nil { + t.Fatal(err) + } + checkBtime(t, plainPath, encryptedPath(t, relPath), unix.AT_SYMLINK_NOFOLLOW) + }) + + t.Run("symlink", func(t *testing.T) { + const target = "/target/does/not/exist" + relPath := strings.ReplaceAll(t.Name(), "/", "_") + plainPath := filepath.Join(test_helpers.DefaultPlainDir, relPath) + if err := os.Symlink(target, plainPath); err != nil { + t.Fatal(err) + } + st := checkBtime(t, plainPath, encryptedPath(t, relPath), unix.AT_SYMLINK_NOFOLLOW) + if st.Size != uint64(len(target)) { + t.Errorf("wrong symlink size: have=%d want=%d", st.Size, len(target)) + } + }) +} 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/reverse/config_custom_test.go b/tests/reverse/config_custom_test.go new file mode 100644 index 0000000..a7883af --- /dev/null +++ b/tests/reverse/config_custom_test.go @@ -0,0 +1,62 @@ +package reverse_test + +import ( + "os" + "os/exec" + "testing" + + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// TestConfigCustomInsideCipherdir verifies that a custom config file (-config) +// located inside CIPHERDIR is hidden from the encrypted reverse view instead of +// leaking there in encrypted form. +// +// Regression test for https://github.com/rfjakob/gocryptfs/issues/1009 +func TestConfigCustomInsideCipherdir(t *testing.T) { + backingDir, err := os.MkdirTemp(test_helpers.TmpDir, t.Name()+".") + if err != nil { + t.Fatal(err) + } + // A regular file that must stay visible in the encrypted view. + if err := os.WriteFile(backingDir+"/secret.txt", []byte("hello"), 0600); err != nil { + t.Fatal(err) + } + // The custom config file lives *inside* the backing dir. + cfg := backingDir + "/my-custom.conf" + + // Init reverse fs with the config at the custom location. + initArgs := []string{"-q", "-init", "-reverse", "-extpass", "echo test", "-scryptn=10", "-config", cfg} + if plaintextnames { + initArgs = append(initArgs, "-plaintextnames") + } else if deterministic_names { + initArgs = append(initArgs, "-deterministic-names") + } + initArgs = append(initArgs, backingDir) + cmd := exec.Command(test_helpers.GocryptfsBinary, initArgs...) + cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr + if err := cmd.Run(); err != nil { + t.Fatalf("init failed: %v", err) + } + + mnt := backingDir + ".mnt" + sock := mnt + ".sock" + test_helpers.MountOrFatal(t, backingDir, mnt, "-reverse", "-extpass", "echo test", + "-config", cfg, "-ctlsock", sock) + defer test_helpers.UnmountPanic(mnt) + + // The custom config file must NOT show up (encrypted) in the view. + cCfg := ctlsockEncryptPath(t, sock, "my-custom.conf") + if test_helpers.VerifyExistence(t, mnt+"/"+cCfg) { + t.Errorf("custom config %q is exposed in the encrypted view (as %q), but should be hidden", cfg, cCfg) + } + // With a custom config file, no virtual gocryptfs.conf is presented. + if test_helpers.VerifyExistence(t, mnt+"/gocryptfs.conf") { + t.Errorf("gocryptfs.conf should not be present in the view when -config is used") + } + // Regular files must remain visible. + cSecret := ctlsockEncryptPath(t, sock, "secret.txt") + if !test_helpers.VerifyExistence(t, mnt+"/"+cSecret) { + t.Errorf("regular file secret.txt (as %q) is missing from the encrypted view", cSecret) + } +} diff --git a/tests/reverse/linux-tarball-test.bash b/tests/reverse/linux-tarball-test.bash index 4054c29..27afa23 100755 --- a/tests/reverse/linux-tarball-test.bash +++ b/tests/reverse/linux-tarball-test.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu diff --git a/tests/reverse/xattr_test.go b/tests/reverse/xattr_test.go index 406d055..b6a7b34 100644 --- a/tests/reverse/xattr_test.go +++ b/tests/reverse/xattr_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/pkg/xattr" + "golang.org/x/sys/unix" ) func xattrSupported(path string) bool { @@ -65,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 index 4f2527a..898cd39 100644 --- a/tests/root_test/btrfs_test.go +++ b/tests/root_test/btrfs_test.go @@ -12,12 +12,20 @@ import ( "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) -// TestBtrfsQuirks needs root permissions because it creates a loop disk -func TestBtrfsQuirks(t *testing.T) { +// 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 { @@ -31,10 +39,6 @@ func TestBtrfsQuirks(t *testing.T) { } // Format as Btrfs - _, err = exec.LookPath("mkfs.btrfs") - if err != nil { - t.Skip("mkfs.btrfs not found, skipping test") - } cmd := exec.Command("mkfs.btrfs", img) out, err := cmd.CombinedOutput() if err != nil { @@ -44,7 +48,7 @@ func TestBtrfsQuirks(t *testing.T) { } // Mount - mnt := img + ".mnt" + mnt = img + ".mnt" err = os.Mkdir(mnt, 0600) if err != nil { t.Fatal(err) @@ -55,11 +59,52 @@ func TestBtrfsQuirks(t *testing.T) { t.Log(string(out)) t.Fatal(err) } - defer syscall.Unlink(img) - defer syscall.Unmount(mnt, 0) + + 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.QuirkBrokenFalloc { + 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/sshfs-benchmark.bash b/tests/sshfs-benchmark.bash index 4695f8d..2421f20 100755 --- a/tests/sshfs-benchmark.bash +++ b/tests/sshfs-benchmark.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash set -eu diff --git a/tests/stress_tests/extractloop.bash b/tests/stress_tests/extractloop.bash index 1f78a5e..714d2d7 100755 --- a/tests/stress_tests/extractloop.bash +++ b/tests/stress_tests/extractloop.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Mount a gocryptfs filesystem somewhere on /tmp, then run two parallel # infinite loops inside that do the following: diff --git a/tests/stress_tests/fsstress-gocryptfs.bash b/tests/stress_tests/fsstress-gocryptfs.bash index e6c3281..7e3f160 100755 --- a/tests/stress_tests/fsstress-gocryptfs.bash +++ b/tests/stress_tests/fsstress-gocryptfs.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Mount a gocryptfs filesystem in /var/tmp and run fsstress against it # in an infinite loop, only exiting on errors. diff --git a/tests/stress_tests/pingpong.bash b/tests/stress_tests/pingpong.bash index d0d21b3..4fd5ff2 100755 --- a/tests/stress_tests/pingpong.bash +++ b/tests/stress_tests/pingpong.bash @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # Mounts two gocryptfs filesystems, "ping" and "pong" and moves the # linux-3.0 kernel tree back and forth between them, checking integrity diff --git a/tests/stress_tests/pjdfstest.bash b/tests/stress_tests/pjdfstest.bash new file mode 100755 index 0000000..a786e41 --- /dev/null +++ b/tests/stress_tests/pjdfstest.bash @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Mount a gocryptfs filesystem in /var/tmp and run pjdfstest against it + +set -eu + +export TMPDIR=${TMPDIR:-/var/tmp} + +cd "$(dirname "$0")" +MYNAME=$(basename "$0") +source ../fuse-unmount.bash + +pjdfstest_dir=$(realpath ../../../pjdfstest) +if [[ ! -x $pjdfstest_dir/pjdfstest ]] +then + echo "$MYNAME: pjdfstest binary not found at $pjdfstest_dir/pjdfstest" + echo "Please clone and build https://github.com/pjd/pjdfstest" + exit 1 +fi + +# Backing directory + mountpoint +DIR=$(mktemp -d "$TMPDIR/$MYNAME.XXX") +MNT="$DIR.mnt" +mkdir "$MNT" + +../../gocryptfs -q -init -extpass "echo test" -scryptn=10 "$DIR" +sudo ../../gocryptfs -q -extpass "echo test" -nosyslog -allow_other "$DIR" "$MNT" +cd "$MNT" + +# Cleanup trap +trap "cd /tmp ; fuse-unmount -z $MNT" EXIT + +echo "Starting pjdfstest" +sudo prove -r "$pjdfstest_dir/tests"
\ No newline at end of file diff --git a/tests/test_helpers/helpers.go b/tests/test_helpers/helpers.go index c8cd151..fd2ea9c 100644 --- a/tests/test_helpers/helpers.go +++ b/tests/test_helpers/helpers.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "path/filepath" + "strings" "syscall" "testing" @@ -146,6 +147,7 @@ func InitFS(t *testing.T, extraArgs ...string) string { prefix := "x." if t != nil { prefix = t.Name() + "." + prefix = strings.ReplaceAll(prefix, "/", "_") } dir, err := os.MkdirTemp(TmpDir, prefix) if err != nil { diff --git a/tests/test_helpers/mount_unmount.go b/tests/test_helpers/mount_unmount.go index 4abc432..3927dd5 100644 --- a/tests/test_helpers/mount_unmount.go +++ b/tests/test_helpers/mount_unmount.go @@ -35,7 +35,11 @@ type mountInfo struct { // Contrary to InitFS(), you MUST passt "-extpass=echo test" (or another way for // getting the master key) explicitly. func Mount(c string, p string, showOutput bool, extraArgs ...string) error { - args := []string{"-q", "-wpanic", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())} + args := []string{"-q", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())} + // We are warning-free on Linux, but not (yet) on other OS's + if runtime.GOOS == "linux" { + args = append(args, "-wpanic") + } args = append(args, extraArgs...) if _, isset := os.LookupEnv("FUSEDEBUG"); isset { fmt.Println("FUSEDEBUG is set, enabling -fusedebug") |
