Skip to content

Commit b9e075c

Browse files
authored
Warn when deleting symlinks targeting locations outside temp dir (#4306)
Resolves #4303.
1 parent 686e49f commit b9e075c

File tree

5 files changed

+105
-12
lines changed

5 files changed

+105
-12
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc

+3-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ repository on GitHub.
4545
[[release-notes-5.12.0-RC2-junit-jupiter-new-features-and-improvements]]
4646
==== New Features and Improvements
4747

48-
* ❓
48+
* The `@TempDir` now warns when deleting symlinks that target locations outside the
49+
temporary directory to signal that the target file or directory is _not_ deleted, only
50+
the link to it.
4951

5052

5153
[[release-notes-5.12.0-RC2-junit-vintage]]

junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@
8484
* — when the test method or class has finished execution — JUnit will
8585
* attempt to clean up the temporary directory by recursively deleting all files
8686
* and directories in the temporary directory and, finally, the temporary directory
87-
* itself. In case deletion of a file or directory fails, an {@link IOException}
88-
* will be thrown that will cause the test or test class to fail.
87+
* itself. Symbolic and other types of links, such as junctions on Windows, are
88+
* not followed. A warning is logged when deleting a link that targets a
89+
* location outside the temporary directory. In case deletion of a file or
90+
* directory fails, an {@link IOException} will be thrown that will cause the
91+
* test or test class to fail.
8992
*
9093
* <p>The {@link #cleanup} attribute allows you to configure the {@link CleanupMode}.
9194
* If the cleanup mode is set to {@link CleanupMode#NEVER NEVER}, the temporary

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java

+27-8
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ private SortedMap<Path, IOException> deleteAllFilesAndDirectories(FileOperations
404404
@Override
405405
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
406406
LOGGER.trace(() -> "preVisitDirectory: " + dir);
407-
if (isLink(dir)) {
407+
if (isLinkWithTargetOutsideTempDir(dir)) {
408+
warnAboutLinkWithTargetOutsideTempDir("link", dir);
408409
delete(dir);
409410
return SKIP_SUBTREE;
410411
}
@@ -414,12 +415,6 @@ public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) th
414415
return CONTINUE;
415416
}
416417

417-
private boolean isLink(Path dir) throws IOException {
418-
// While `Files.walkFileTree` does not follow symbolic links, it may follow other links
419-
// such as "junctions" on Windows
420-
return !dir.toRealPath().startsWith(rootRealPath);
421-
}
422-
423418
@Override
424419
public FileVisitResult visitFileFailed(Path file, IOException exc) {
425420
LOGGER.trace(exc, () -> "visitFileFailed: " + file);
@@ -432,8 +427,11 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) {
432427
}
433428

434429
@Override
435-
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) {
430+
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
436431
LOGGER.trace(() -> "visitFile: " + file);
432+
if (Files.isSymbolicLink(file) && isLinkWithTargetOutsideTempDir(file)) {
433+
warnAboutLinkWithTargetOutsideTempDir("symbolic link", file);
434+
}
437435
delete(file);
438436
return CONTINUE;
439437
}
@@ -445,6 +443,27 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
445443
return CONTINUE;
446444
}
447445

446+
private boolean isLinkWithTargetOutsideTempDir(Path path) {
447+
// While `Files.walkFileTree` does not follow symbolic links, it may follow other links
448+
// such as "junctions" on Windows
449+
try {
450+
return !path.toRealPath().startsWith(rootRealPath);
451+
}
452+
catch (IOException e) {
453+
LOGGER.trace(e,
454+
() -> "Failed to determine real path for " + path + "; assuming it is not a link");
455+
return false;
456+
}
457+
}
458+
459+
private void warnAboutLinkWithTargetOutsideTempDir(String linkType, Path file) throws IOException {
460+
Path realPath = file.toRealPath();
461+
LOGGER.warn(() -> String.format(
462+
"Deleting %s from location inside of temp dir (%s) "
463+
+ "to location outside of temp dir (%s) but not the target file/directory",
464+
linkType, file, realPath));
465+
}
466+
448467
private void delete(Path path) {
449468
try {
450469
fileOperations.delete(path);

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java

+60
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import java.lang.reflect.Method;
4444
import java.lang.reflect.Parameter;
4545
import java.nio.file.FileSystem;
46+
import java.nio.file.Files;
4647
import java.nio.file.Path;
4748
import java.util.Optional;
4849
import java.util.logging.Level;
@@ -395,6 +396,65 @@ void onSuccessWithNoException(Class<?> elementType, @TrackLogRecords LogRecordLi
395396
.noneMatch(m -> m.startsWith("Skipping cleanup of temp dir"));
396397
}
397398

399+
@DisplayName("deletes symbolic links targeting directory inside temp dir")
400+
@ParameterizedTest
401+
@ElementTypeSource
402+
@DisabledOnOs(WINDOWS)
403+
void deletesSymbolicLinksTargetingDirInsideTempDir(Class<?> elementType,
404+
@TrackLogRecords LogRecordListener listener) throws IOException {
405+
406+
reset(factory);
407+
408+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
409+
extensionContext);
410+
var rootDir = closeablePath.get();
411+
assertThat(rootDir).isDirectory();
412+
413+
var subDir = createDirectory(rootDir.resolve("subDir"));
414+
Files.createFile(subDir.resolve("file"));
415+
Files.createSymbolicLink(rootDir.resolve("symbolicLink"), subDir);
416+
417+
closeablePath.close();
418+
419+
verify(factory).close();
420+
assertThat(rootDir).doesNotExist();
421+
assertThat(listener.stream(Level.WARNING)).map(LogRecord::getMessage).isEmpty();
422+
423+
}
424+
425+
@DisplayName("deletes symbolic links targeting directory outside temp dir")
426+
@ParameterizedTest
427+
@ElementTypeSource
428+
@DisabledOnOs(WINDOWS)
429+
void deletesSymbolicLinksTargetingDirOutsideTempDir(Class<?> elementType,
430+
@TrackLogRecords LogRecordListener listener) throws IOException {
431+
432+
reset(factory);
433+
434+
closeablePath = TempDirectory.createTempDir(factory, ON_SUCCESS, elementType, elementContext,
435+
extensionContext);
436+
var rootDir = closeablePath.get();
437+
assertThat(rootDir).isDirectory();
438+
439+
var directoryOutsideTempDir = createTempDirectory("junit-");
440+
try {
441+
var symbolicLink = createSymbolicLink(rootDir.resolve("symbolicLink"), directoryOutsideTempDir);
442+
443+
closeablePath.close();
444+
445+
verify(factory).close();
446+
assertThat(rootDir).doesNotExist();
447+
assertThat(directoryOutsideTempDir).isDirectory();
448+
assertThat(listener.stream(Level.WARNING)) //
449+
.map(LogRecord::getMessage) //
450+
.contains(("Deleting symbolic link from location inside of temp dir (%s) "
451+
+ "to location outside of temp dir (%s) but not the target file/directory").formatted(
452+
symbolicLink, directoryOutsideTempDir.toRealPath()));
453+
}
454+
finally {
455+
Files.deleteIfExists(directoryOutsideTempDir);
456+
}
457+
}
398458
}
399459

400460
static class TestCase {

jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.io.IOException;
2525
import java.nio.file.Files;
2626
import java.nio.file.Path;
27+
import java.util.logging.Level;
28+
import java.util.logging.LogRecord;
2729

2830
import org.junit.jupiter.api.AfterAll;
2931
import org.junit.jupiter.api.MethodOrderer;
@@ -32,9 +34,11 @@
3234
import org.junit.jupiter.api.Test;
3335
import org.junit.jupiter.api.TestMethodOrder;
3436
import org.junit.jupiter.api.condition.EnabledOnOs;
37+
import org.junit.jupiter.api.fixtures.TrackLogRecords;
3538
import org.junit.jupiter.api.io.CleanupMode;
3639
import org.junit.jupiter.api.io.TempDir;
3740
import org.junit.jupiter.engine.AbstractJupiterTestEngineTests;
41+
import org.junit.platform.commons.logging.LogRecordListener;
3842
import org.junit.platform.launcher.LauncherDiscoveryRequest;
3943

4044
/**
@@ -470,7 +474,8 @@ void deletesBrokenJunctions(@TempDir Path dir) throws Exception {
470474
}
471475

472476
@Test
473-
void doesNotFollowJunctions(@TempDir Path tempDir) throws IOException {
477+
void doesNotFollowJunctions(@TempDir Path tempDir, @TrackLogRecords LogRecordListener listener)
478+
throws IOException {
474479
var outsideDir = Files.createDirectory(tempDir.resolve("outside"));
475480
var testFile = Files.writeString(outsideDir.resolve("test.txt"), "test");
476481

@@ -485,6 +490,10 @@ void doesNotFollowJunctions(@TempDir Path tempDir) throws IOException {
485490

486491
assertThat(outsideDir).exists();
487492
assertThat(testFile).exists();
493+
assertThat(listener.stream(Level.WARNING)) //
494+
.map(LogRecord::getMessage) //
495+
.anyMatch(m -> m.matches(
496+
"Deleting link from location inside of temp dir \\(.+\\) to location outside of temp dir \\(.+\\) but not the target file/directory"));
488497
}
489498

490499
@SuppressWarnings("JUnitMalformedDeclaration")

0 commit comments

Comments
 (0)