aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml6
-rwxr-xr-xDocumentation/MANPAGE-render.bash2
-rw-r--r--Documentation/MANPAGE.md34
-rw-r--r--Documentation/performance.txt5
-rw-r--r--Documentation/pjdfstest.md321
-rw-r--r--README.md3
-rwxr-xr-xbenchmark-reverse.bash4
-rwxr-xr-xbenchmark.bash4
-rwxr-xr-xbuild-without-openssl.bash4
-rwxr-xr-xbuild.bash4
-rw-r--r--cli_args.go6
-rw-r--r--cli_args_test.go12
-rwxr-xr-xcontrib/gocryptfs-maybe.bash2
-rwxr-xr-xcontrib/maxlen.bash2
-rwxr-xr-xcrossbuild.bash6
-rw-r--r--go.mod13
-rw-r--r--go.sum26
-rw-r--r--internal/configfile/config_file.go8
-rw-r--r--internal/configfile/feature_flags.go11
-rw-r--r--internal/cryptocore/hkdf.go15
-rw-r--r--internal/fido2/fido2.go2
-rw-r--r--internal/fusefrontend/args.go6
-rw-r--r--internal/fusefrontend/file.go53
-rw-r--r--internal/fusefrontend/file_allocate_truncate.go62
-rw-r--r--internal/fusefrontend/file_lock.go51
-rw-r--r--internal/fusefrontend/node.go27
-rw-r--r--internal/fusefrontend/node_helpers.go18
-rw-r--r--internal/fusefrontend/node_open_create.go29
-rw-r--r--internal/fusefrontend/node_prepare_syscall.go4
-rw-r--r--internal/fusefrontend/node_xattr.go18
-rw-r--r--internal/fusefrontend/node_xattr_darwin.go17
-rw-r--r--internal/fusefrontend/node_xattr_freebsd.go33
-rw-r--r--internal/fusefrontend/node_xattr_linux.go3
-rw-r--r--internal/fusefrontend/root_node.go31
-rw-r--r--internal/fusefrontend/statx_linux.go78
-rw-r--r--internal/fusefrontend_reverse/node_helpers.go16
-rw-r--r--internal/fusefrontend_reverse/node_xattr.go12
-rw-r--r--internal/fusefrontend_reverse/node_xattr_darwin.go3
-rw-r--r--internal/fusefrontend_reverse/node_xattr_freebsd.go17
-rw-r--r--internal/fusefrontend_reverse/node_xattr_linux.go3
-rw-r--r--internal/fusefrontend_reverse/root_node.go32
-rw-r--r--internal/nametransform/diriv.go18
-rw-r--r--internal/nametransform/names.go43
-rw-r--r--internal/nametransform/nfc_test.go29
-rwxr-xr-xinternal/siv_aead/benchmark.bash2
-rwxr-xr-xinternal/speed/benchmark.bash2
-rwxr-xr-xinternal/stupidgcm/benchmark.bash2
-rw-r--r--internal/syscallcompat/asuser_freebsd.go24
-rw-r--r--internal/syscallcompat/emulate.go2
-rw-r--r--internal/syscallcompat/emulate_test.go2
-rw-r--r--internal/syscallcompat/quirks.go7
-rw-r--r--internal/syscallcompat/quirks_darwin.go2
-rw-r--r--internal/syscallcompat/quirks_freebsd.go22
-rw-r--r--internal/syscallcompat/quirks_linux.go40
-rw-r--r--internal/syscallcompat/sys_common.go76
-rw-r--r--internal/syscallcompat/sys_darwin.go11
-rw-r--r--internal/syscallcompat/sys_freebsd.go165
-rw-r--r--internal/syscallcompat/sys_linux.go21
-rw-r--r--internal/syscallcompat/unix2syscall.go (renamed from internal/syscallcompat/unix2syscall_darwin.go)2
-rw-r--r--mount.go36
-rwxr-xr-xpackage-release-tarballs.bash2
-rwxr-xr-xprofiling/ls.bash4
-rwxr-xr-xprofiling/streaming-read.bash4
-rwxr-xr-xprofiling/streaming-write.bash4
-rwxr-xr-xprofiling/tar-extract.bash4
-rwxr-xr-xprofiling/tinyfiles.bash33
-rwxr-xr-xprofiling/write-trace.bash4
-rwxr-xr-xtest-without-openssl.bash4
-rwxr-xr-xtest.bash2
-rwxr-xr-xtests/canonical-benchmarks.bash3
-rw-r--r--tests/cli/cli_test.go19
-rw-r--r--tests/cluster/cluster_test.go159
-rw-r--r--tests/cluster/poc_test.go230
-rw-r--r--tests/defaults/main_test.go35
-rwxr-xr-xtests/dl-linux-tarball.bash4
-rw-r--r--tests/example_filesystems/example_test_helpers.go12
-rwxr-xr-xtests/fuse-unmount.bash5
-rw-r--r--tests/matrix/atime_darwin+freebsd.go (renamed from tests/matrix/atime_darwin.go)2
-rw-r--r--tests/matrix/main_test.go1
-rw-r--r--tests/matrix/matrix_test.go15
-rw-r--r--tests/matrix/statx_linux_test.go156
-rw-r--r--tests/matrix/symlink_darwin_test.go39
-rw-r--r--tests/matrix/symlink_linux_test.go47
-rw-r--r--tests/reverse/config_custom_test.go62
-rwxr-xr-xtests/reverse/linux-tarball-test.bash2
-rw-r--r--tests/reverse/xattr_test.go16
-rw-r--r--tests/root_test/btrfs_test.go65
-rwxr-xr-xtests/sshfs-benchmark.bash2
-rwxr-xr-xtests/stress_tests/extractloop.bash2
-rwxr-xr-xtests/stress_tests/fsstress-gocryptfs.bash2
-rwxr-xr-xtests/stress_tests/pingpong.bash2
-rwxr-xr-xtests/stress_tests/pjdfstest.bash34
-rw-r--r--tests/test_helpers/helpers.go2
-rw-r--r--tests/test_helpers/mount_unmount.go6
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
+```
diff --git a/README.md b/README.md
index c5228dc..f27a058 100644
--- a/README.md
+++ b/README.md
@@ -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!
+
![Folders side-by-side animation](Documentation/folders-side-by-side.gif)
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")"
diff --git a/build.bash b/build.bash
index 5e95c3e..e78188e 100755
--- a/build.bash
+++ b/build.bash
@@ -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
+
diff --git a/go.mod b/go.mod
index c608e6f..6ca1f2f 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index f0e7ea7..f8a2c2e 100644
--- a/go.sum
+++ b/go.sum
@@ -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 (
diff --git a/mount.go b/mount.go
index 7f72773..489ae31 100644
--- a/mount.go
+++ b/mount.go
@@ -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")"
diff --git a/test.bash b/test.bash
index d5da874..12e6b7f 100755
--- a/test.bash
+++ b/test.bash
@@ -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")