Skip to content

Commit 17c7bde

Browse files
authored
Feature: Block write if file is in use (#338)
1 parent bca179a commit 17c7bde

12 files changed

Lines changed: 197 additions & 44 deletions

src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -414,9 +414,9 @@ private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, Effecti
414414
Files.createDirectories(ciphertextPath.getRawPath()); // suppresses FileAlreadyExists
415415
}
416416

417-
FileChannel ch = null;
418-
try {
419-
ch = openCryptoFiles.getOrCreate(ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
417+
FileChannel ch = null;
418+
try {
419+
ch = openCryptoFiles.getOrCreate(cleartextFilePath, ciphertextFilePath).newFileChannel(options, attrs); // might throw FileAlreadyExists
420420
if (options.writable()) {
421421
ciphertextPath.persistLongFileName();
422422
stats.incrementAccessesWritten();
@@ -618,7 +618,7 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
618618
// "the symbolic link itself, not the target of the link, is moved"
619619
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
620620
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
621-
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
621+
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), cleartextTarget)) {
622622
Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
623623
if (ciphertextTarget.isShortened()) {
624624
ciphertextTarget.persistLongFileName();
@@ -634,7 +634,7 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
634634
// we need to re-map the OpenCryptoFile entry.
635635
CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
636636
CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
637-
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
637+
try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), cleartextTarget)) {
638638
checkUsage(cleartextSource, ciphertextSource);
639639
checkUsage(cleartextTarget, ciphertextTarget);
640640
if (ciphertextTarget.isShortened()) {
@@ -742,4 +742,4 @@ void checkUsage(CryptoPath cleartextPath, CiphertextFilePath ciphertextPath) thr
742742
}
743743
}
744744

745-
}
745+
}

src/main/java/org/cryptomator/cryptofs/Symlinks.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttrib
4848
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW), readonlyFlag);
4949
ByteBuffer content = UTF_8.encode(target.toString());
5050
Files.createDirectory(ciphertextFilePath.getRawPath());
51-
openCryptoFiles.writeCiphertextFile(ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
51+
openCryptoFiles.writeCiphertextFile(cleartextPath, ciphertextFilePath.getSymlinkFilePath(), openOptions, content);
5252
ciphertextFilePath.persistLongFileName();
5353
}
5454

@@ -57,7 +57,7 @@ public CryptoPath readSymbolicLink(CryptoPath cleartextPath) throws IOException
5757
EffectiveOpenOptions openOptions = EffectiveOpenOptions.from(EnumSet.of(StandardOpenOption.READ), readonlyFlag);
5858
assertIsSymlink(cleartextPath, ciphertextSymlinkFile);
5959
try {
60-
ByteBuffer content = openCryptoFiles.readCiphertextFile(ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
60+
ByteBuffer content = openCryptoFiles.readCiphertextFile(cleartextPath, ciphertextSymlinkFile, openOptions, Constants.MAX_SYMLINK_LENGTH);
6161
return cleartextPath.getFileSystem().getPath(UTF_8.decode(content).toString());
6262
} catch (BufferUnderflowException e) {
6363
throw new NotLinkException(cleartextPath.toString(), null, "Unreasonably large symlink file");

src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,29 @@
22

33
import com.google.common.annotations.VisibleForTesting;
44
import com.google.common.base.Preconditions;
5+
import jakarta.inject.Inject;
56
import org.cryptomator.cryptofs.CryptoFileSystemStats;
7+
import org.cryptomator.cryptofs.CryptoPath;
68
import org.cryptomator.cryptofs.EffectiveOpenOptions;
9+
import org.cryptomator.cryptofs.event.FileIsInUseEvent;
10+
import org.cryptomator.cryptofs.event.FilesystemEvent;
711
import org.cryptomator.cryptofs.fh.BufferPool;
812
import org.cryptomator.cryptofs.fh.Chunk;
913
import org.cryptomator.cryptofs.fh.ChunkCache;
14+
import org.cryptomator.cryptofs.fh.CurrentOpenFileCleartextPath;
1015
import org.cryptomator.cryptofs.fh.CurrentOpenFilePath;
1116
import org.cryptomator.cryptofs.fh.ExceptionsDuringWrite;
1217
import org.cryptomator.cryptofs.fh.FileHeaderHolder;
1318
import org.cryptomator.cryptofs.fh.OpenFileModifiedDate;
1419
import org.cryptomator.cryptofs.fh.OpenFileSize;
20+
import org.cryptomator.cryptofs.inuse.FileAlreadyInUseException;
21+
import org.cryptomator.cryptofs.inuse.InUseManager;
22+
import org.cryptomator.cryptofs.inuse.StubInUseManager;
23+
import org.cryptomator.cryptofs.inuse.UseInfo;
1524
import org.cryptomator.cryptolib.api.Cryptor;
1625
import org.slf4j.Logger;
1726
import org.slf4j.LoggerFactory;
1827

19-
import jakarta.inject.Inject;
2028
import java.io.IOException;
2129
import java.nio.ByteBuffer;
2230
import java.nio.MappedByteBuffer;
@@ -55,10 +63,28 @@ public class CleartextFileChannel extends AbstractFileChannel {
5563
private final AtomicReference<Instant> lastModified;
5664
private final ExceptionsDuringWrite exceptionsDuringWrite;
5765
private final Consumer<FileChannel> closeListener;
66+
private final Consumer<FilesystemEvent> eventConsumer;
5867
private final CryptoFileSystemStats stats;
68+
private final InUseManager inUseManager;
69+
private final AtomicReference<CryptoPath> currentCleartextPath;
5970

6071
@Inject
61-
public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder fileHeaderHolder, ReadWriteLock readWriteLock, Cryptor cryptor, ChunkCache chunkCache, BufferPool bufferPool, EffectiveOpenOptions options, @OpenFileSize AtomicLong fileSize, @OpenFileModifiedDate AtomicReference<Instant> lastModified, @CurrentOpenFilePath AtomicReference<Path> currentPath, ExceptionsDuringWrite exceptionsDuringWrite, Consumer<FileChannel> closeListener, CryptoFileSystemStats stats) {
72+
public CleartextFileChannel(FileChannel ciphertextFileChannel, //
73+
FileHeaderHolder fileHeaderHolder, //
74+
ReadWriteLock readWriteLock, //
75+
Cryptor cryptor, //
76+
ChunkCache chunkCache, //
77+
BufferPool bufferPool, //
78+
EffectiveOpenOptions options, //
79+
@OpenFileSize AtomicLong fileSize, //
80+
@OpenFileModifiedDate AtomicReference<Instant> lastModified, //
81+
@CurrentOpenFilePath AtomicReference<Path> currentPath, //
82+
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
83+
ExceptionsDuringWrite exceptionsDuringWrite, //
84+
Consumer<FileChannel> closeListener, //
85+
Consumer<FilesystemEvent> eventConsumer, //
86+
CryptoFileSystemStats stats, //
87+
InUseManager inUseManager) {
6288
super(readWriteLock);
6389
this.ciphertextFileChannel = ciphertextFileChannel;
6490
this.fileHeaderHolder = fileHeaderHolder;
@@ -67,11 +93,14 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder
6793
this.bufferPool = bufferPool;
6894
this.options = options;
6995
this.currentFilePath = currentPath;
96+
this.currentCleartextPath = currentCleartextPath;
7097
this.fileSize = fileSize;
7198
this.lastModified = lastModified;
7299
this.exceptionsDuringWrite = exceptionsDuringWrite;
73100
this.closeListener = closeListener;
101+
this.eventConsumer = eventConsumer;
74102
this.stats = stats;
103+
this.inUseManager = inUseManager;
75104
if (options.append()) {
76105
position = fileSize.get();
77106
}
@@ -80,6 +109,23 @@ public CleartextFileChannel(FileChannel ciphertextFileChannel, FileHeaderHolder
80109
}
81110
}
82111

112+
@VisibleForTesting
113+
CleartextFileChannel(FileChannel ciphertextFileChannel, //
114+
FileHeaderHolder fileHeaderHolder, //
115+
ReadWriteLock readWriteLock, //
116+
Cryptor cryptor, //
117+
ChunkCache chunkCache, //
118+
BufferPool bufferPool, //
119+
EffectiveOpenOptions options, //
120+
AtomicLong fileSize, //
121+
AtomicReference<Instant> lastModified, //
122+
AtomicReference<Path> currentPath, //
123+
ExceptionsDuringWrite exceptionsDuringWrite, //
124+
Consumer<FileChannel> closeListener, //
125+
CryptoFileSystemStats stats) {
126+
this(ciphertextFileChannel, fileHeaderHolder, readWriteLock, cryptor, chunkCache, bufferPool, options, fileSize, lastModified, currentPath, new AtomicReference<>(null), exceptionsDuringWrite, closeListener, ignored -> {}, stats, new StubInUseManager());
127+
}
128+
83129
@Override
84130
public long size() throws IOException {
85131
assertOpen();
@@ -124,6 +170,17 @@ protected int readLocked(ByteBuffer dst, long position) throws IOException {
124170

125171
@Override
126172
protected int writeLocked(ByteBuffer src, long position) throws IOException {
173+
var path = currentFilePath.get();
174+
if (path != null && inUseManager.isInUseByOthers(path)) {
175+
var useInfo = inUseManager.getUseInfo(path).orElse(new UseInfo("UNKNOWN", Instant.now()));
176+
var cleartextPath = currentCleartextPath.get();
177+
if (cleartextPath != null) {
178+
eventConsumer.accept(new FileIsInUseEvent(cleartextPath, path, useInfo.owner(), useInfo.lastUpdated(), () -> inUseManager.ignoreInUse(path)));
179+
} else {
180+
LOG.warn("Unable to emit FileIsInUseEvent: Cleartext path is null. Ciphertext path is {}.", path);
181+
}
182+
throw new FileAlreadyInUseException(path);
183+
}
127184
long oldFileSize = fileSize.get();
128185
long written;
129186
if (position > oldFileSize) {
@@ -256,8 +313,8 @@ void persistLastModified() throws IOException {
256313
FileTime lastAccessTime = FileTime.from(Instant.now());
257314
var p = currentFilePath.get();
258315
if (p != null) {
259-
p.getFileSystem().provider()//
260-
.getFileAttributeView(p, BasicFileAttributeView.class)
316+
p.getFileSystem().provider() //
317+
.getFileAttributeView(p, BasicFileAttributeView.class) //
261318
.setTimes(lastModifiedTime, lastAccessTime, null);
262319
}
263320

@@ -328,7 +385,7 @@ long beginOfChunk(long cleartextPos) {
328385
protected void implCloseChannel() throws IOException {
329386
var closeActions = List.<CloseAction>of(this::flush, //
330387
super::implCloseChannel, //
331-
() -> closeListener.accept(ciphertextFileChannel),
388+
() -> closeListener.accept(ciphertextFileChannel), //
332389
ciphertextFileChannel::close, //
333390
this::tryPersistLastModified);
334391
tryAll(closeActions.iterator());
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package org.cryptomator.cryptofs.fh;
2+
3+
import jakarta.inject.Qualifier;
4+
5+
import java.lang.annotation.Documented;
6+
import java.lang.annotation.Retention;
7+
8+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
9+
10+
@Qualifier
11+
@Documented
12+
@Retention(RUNTIME)
13+
public @interface CurrentOpenFileCleartextPath {
14+
}

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.cryptomator.cryptofs.fh;
22

33
import jakarta.inject.Inject;
4+
import org.cryptomator.cryptofs.CryptoPath;
45
import org.cryptomator.cryptofs.EffectiveOpenOptions;
56
import org.cryptomator.cryptofs.ch.CleartextFileChannel;
67
import org.cryptomator.cryptofs.inuse.InUseManager;
@@ -33,6 +34,7 @@ public class OpenCryptoFile implements Closeable {
3334
private final FileHeaderHolder headerHolder;
3435
private final ChunkIO chunkIO;
3536
private final AtomicReference<Path> currentFilePath;
37+
private final AtomicReference<CryptoPath> currentCleartextPath;
3638
private final AtomicLong fileSize;
3739
private final OpenCryptoFileComponent component;
3840

@@ -42,22 +44,24 @@ public class OpenCryptoFile implements Closeable {
4244
@Inject
4345
public OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, //
4446
@CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, //
47+
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
4548
@OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component, //
4649
InUseManager inUseManager) {
47-
this(listener, cryptor, headerHolder, chunkIO, currentFilePath, fileSize, lastModified, component, inUseManager, UseToken.CLOSED_TOKEN);
50+
this(listener, cryptor, headerHolder, chunkIO, currentFilePath, fileSize, currentCleartextPath, lastModified, component, inUseManager, UseToken.CLOSED_TOKEN);
4851
}
4952

50-
5153
//for testing
5254
OpenCryptoFile(FileCloseListener listener, Cryptor cryptor, FileHeaderHolder headerHolder, ChunkIO chunkIO, //
5355
@CurrentOpenFilePath AtomicReference<Path> currentFilePath, @OpenFileSize AtomicLong fileSize, //
56+
@CurrentOpenFileCleartextPath AtomicReference<CryptoPath> currentCleartextPath, //
5457
@OpenFileModifiedDate AtomicReference<Instant> lastModified, OpenCryptoFileComponent component, //
5558
InUseManager inUseManager, UseToken token) {
5659
this.listener = listener;
5760
this.cryptor = cryptor;
5861
this.headerHolder = headerHolder;
5962
this.chunkIO = chunkIO;
6063
this.currentFilePath = currentFilePath;
64+
this.currentCleartextPath = currentCleartextPath;
6165
this.fileSize = fileSize;
6266
this.component = component;
6367
this.lastModified = lastModified;
@@ -180,6 +184,10 @@ public Path getCurrentFilePath() {
180184
return currentFilePath.get();
181185
}
182186

187+
public CryptoPath getCurrentCleartextPath() {
188+
return currentCleartextPath.get();
189+
}
190+
183191
/**
184192
* Updates the current ciphertext file path, if it is not already set to null (i.e., the openCryptoFile is deleted)
185193
*
@@ -190,10 +198,24 @@ public void updateCurrentFilePath(Path newFilePath) {
190198
if (newFilePath != null) {
191199
useToken.moveTo(newFilePath);
192200
} else {
201+
currentCleartextPath.set(null);
193202
useToken.close(); //encrypted file will be deleted, hence we can stop checking usage
194203
}
195204
}
196205

206+
/**
207+
* Updates the cleartext path if the file is not deleted (i.e., currentFilePath is not null).
208+
* Null input is ignored.
209+
*
210+
* @param cleartextPath new cleartext path, or null to skip update
211+
*/
212+
public void updateCurrentCleartextPath(CryptoPath cleartextPath) {
213+
if (cleartextPath == null) {
214+
return;
215+
}
216+
currentCleartextPath.getAndUpdate(p -> currentFilePath.get() == null ? p : cleartextPath);
217+
}
218+
197219
private synchronized void cleartextChannelClosed(FileChannel ciphertextFileChannel) {
198220
if (ciphertextFileChannel != null) {
199221
chunkIO.unregisterChannel(ciphertextFileChannel);

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import dagger.Module;
44
import dagger.Provides;
5+
import org.cryptomator.cryptofs.CryptoPath;
56

67
import java.io.IOException;
78
import java.nio.file.Files;
@@ -33,6 +34,13 @@ public AtomicReference<Path> provideCurrentPath(@OriginalOpenFilePath Path origi
3334
return new AtomicReference<>(originalPath);
3435
}
3536

37+
@Provides
38+
@OpenFileScoped
39+
@CurrentOpenFileCleartextPath
40+
public AtomicReference<CryptoPath> provideCurrentCleartextPath() {
41+
return new AtomicReference<>(null);
42+
}
43+
3644
@Provides
3745
@OpenFileScoped
3846
@OpenFileModifiedDate

src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFiles.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import jakarta.inject.Inject;
1212
import org.cryptomator.cryptofs.CryptoFileSystemScoped;
13+
import org.cryptomator.cryptofs.CryptoPath;
1314
import org.cryptomator.cryptofs.EffectiveOpenOptions;
1415

1516
import java.io.Closeable;
@@ -54,23 +55,30 @@ public Optional<OpenCryptoFile> get(Path ciphertextPath) {
5455
* Opens a file to {@link OpenCryptoFile#newFileChannel(EffectiveOpenOptions, java.nio.file.attribute.FileAttribute[]) retrieve a FileChannel}. If this file is already opened, a shared instance is returned.
5556
* Getting the file channel should be the next invocation, since the {@link OpenFileScoped lifecycle} of the OpenFile strictly depends on the lifecycle of the channel.
5657
*
58+
* @param cleartextPath Cleartext path of the file to open
5759
* @param ciphertextPath Path of the file to open
5860
* @return The opened file.
5961
* @see #get(Path)
6062
*/
63+
public OpenCryptoFile getOrCreate(CryptoPath cleartextPath, Path ciphertextPath) {
64+
OpenCryptoFile openFile = getOrCreate(ciphertextPath);
65+
openFile.updateCurrentCleartextPath(cleartextPath);
66+
return openFile;
67+
}
68+
6169
public OpenCryptoFile getOrCreate(Path ciphertextPath) {
6270
Path normalizedPath = ciphertextPath.toAbsolutePath().normalize();
6371
return openCryptoFiles.computeIfAbsent(normalizedPath, p -> openCryptoFileComponentFactory.create(p, openCryptoFiles::remove).openCryptoFile()); // computeIfAbsent is atomic, "create" is called at most once
6472
}
6573

66-
public void writeCiphertextFile(Path ciphertextPath, EffectiveOpenOptions openOptions, ByteBuffer contents) throws IOException {
67-
try (OpenCryptoFile f = getOrCreate(ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
74+
public void writeCiphertextFile(CryptoPath cleartextPath, Path ciphertextPath, EffectiveOpenOptions openOptions, ByteBuffer contents) throws IOException {
75+
try (OpenCryptoFile f = getOrCreate(cleartextPath, ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
6876
ch.write(contents);
6977
}
7078
}
7179

72-
public ByteBuffer readCiphertextFile(Path ciphertextPath, EffectiveOpenOptions openOptions, int maxBufferSize) throws BufferUnderflowException, IOException {
73-
try (OpenCryptoFile f = getOrCreate(ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
80+
public ByteBuffer readCiphertextFile(CryptoPath cleartextPath, Path ciphertextPath, EffectiveOpenOptions openOptions, int maxBufferSize) throws BufferUnderflowException, IOException {
81+
try (OpenCryptoFile f = getOrCreate(cleartextPath, ciphertextPath); FileChannel ch = f.newFileChannel(openOptions)) {
7482
if (ch.size() > maxBufferSize) {
7583
throw new BufferUnderflowException();
7684
}
@@ -101,11 +109,12 @@ public void delete(Path ciphertextPath) {
101109
*
102110
* @param src The ciphertext file path before the move
103111
* @param dst The ciphertext file path after the move
112+
* @param cleartextDst cleartext file path after the move
104113
* @return Utility to update OpenCryptoFile references.
105114
* @throws FileAlreadyExistsException Thrown if the destination file is an existing file that is currently opened.
106115
*/
107-
public TwoPhaseMove prepareMove(Path src, Path dst) throws FileAlreadyExistsException {
108-
return new TwoPhaseMove(src, dst);
116+
public TwoPhaseMove prepareMove(Path src, Path dst, CryptoPath cleartextDst) throws FileAlreadyExistsException {
117+
return new TwoPhaseMove(src, dst, cleartextDst);
109118
}
110119

111120
/**
@@ -125,13 +134,15 @@ public class TwoPhaseMove implements AutoCloseable {
125134

126135
private final Path src;
127136
private final Path dst;
137+
private final CryptoPath cleartextDst;
128138
private final OpenCryptoFile openCryptoFile;
129139
private boolean committed;
130140
private boolean rolledBack;
131141

132-
private TwoPhaseMove(Path src, Path dst) throws FileAlreadyExistsException {
142+
private TwoPhaseMove(Path src, Path dst, CryptoPath cleartextDst) throws FileAlreadyExistsException {
133143
this.src = Objects.requireNonNull(src);
134144
this.dst = Objects.requireNonNull(dst);
145+
this.cleartextDst = cleartextDst;
135146
try {
136147
// ConcurrentHashMap.compute is atomic:
137148
this.openCryptoFile = openCryptoFiles.compute(dst, (k, v) -> {
@@ -152,6 +163,7 @@ public void commit() {
152163
}
153164
if (openCryptoFile != null) {
154165
openCryptoFile.updateCurrentFilePath(dst);
166+
openCryptoFile.updateCurrentCleartextPath(cleartextDst);
155167
}
156168
openCryptoFiles.remove(src, openCryptoFile);
157169
committed = true;

0 commit comments

Comments
 (0)