diff options
33 files changed, 1419 insertions, 96 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb55979..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 }} 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 7e964e0..1d5d12a 100644 --- a/Documentation/performance.txt +++ b/Documentation/performance.txt @@ -77,6 +77,7 @@ v2.0.1-28-g49507ea 335 951 10.2 5.4 4.1 2.0 go1.25.4, Linux 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/cli_args.go b/cli_args.go index 707b453..52061e3 100644 --- a/cli_args.go +++ b/cli_args.go @@ -29,7 +29,7 @@ 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, noxattr bool // Mount options with opposites @@ -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") 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{ { @@ -1,6 +1,6 @@ module github.com/rfjakob/gocryptfs/v2 -go 1.24.0 +go 1.25.0 require ( github.com/aperturerobotics/jacobsa-crypto v1.1.0 @@ -10,8 +10,8 @@ require ( 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/text v0.31.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 ) @@ -29,17 +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/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +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/fusefrontend/args.go b/internal/fusefrontend/args.go index ec3d1c2..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 diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 64c6ca0..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 { @@ -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,7 +353,6 @@ 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 @@ -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_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/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/root_node.go b/internal/fusefrontend_reverse/root_node.go index 1a668af..461b25c 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -36,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 @@ -63,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 { @@ -74,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, @@ -92,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 } @@ -141,6 +156,11 @@ func (rn *RootNode) isExcludedPlain(pPath string) bool { 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) } @@ -148,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/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 52d4e0f..07eb30a 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -33,6 +33,11 @@ const ( // 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 index d88b1cf..0d28f3e 100644 --- a/internal/syscallcompat/sys_freebsd.go +++ b/internal/syscallcompat/sys_freebsd.go @@ -32,6 +32,11 @@ const ( // 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 diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index a850ba1..ffa5a97 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -35,6 +35,11 @@ const ( // 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 @@ -75,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) { @@ -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, @@ -329,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 { @@ -389,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 @@ -449,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"] = "" @@ -562,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/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 af93bc4..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. 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/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/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/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 { |
