aboutsummaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authormax2026-08-17 01:15:41 +0000
committerJakob Unterwurzacher2026-08-30 22:49:34 +0200
commit9c2130d6c2e54c45c6787fbc49a896a8544a5e47 (patch)
tree8a228b71212c0de93cc9ea60e9e53b5379569faa /tests
parent790362e8f8eabd1bdf75f98328daf94cf623c600 (diff)
fusefrontend: take ContentLock for truncate through node.SetattrHEADmaster
node.Setattr opens its own file handle and called truncate directly, without ContentLock. truncate ends in doWrite, which documents that the caller holds that lock, and file.Setattr and Allocate both take it. Besides leaving a path truncate unserialized against concurrent writes, this skipped the write-operation counter that ContentLock.Lock() increments. isConsecutiveWrite() reads that counter to notice foreign modifications, so an already-open handle kept assuming its next write appends, skipped writePadHole() and grew the file past a short last block. That block no longer decrypted, and every later operation on the file failed with EIO. Fixes #1024
Diffstat (limited to 'tests')
-rw-r--r--tests/defaults/main_test.go35
1 files changed, 35 insertions, 0 deletions
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)
+ }
+}