Skip to content

Commit 7ffc8a8

Browse files
committed
Additional symlink loop check for disable_advanced_recursive_link_checks
If the (non default) system property `-Dorg.eclipse.core.resources.disable_advanced_recursive_link_checks"` is set, expensive advanced symlink checks are disabled to avoid performance / OOM issues. This is a known tradeoff between correctness and performance. This commit adds one more "symlink loop" use case detected by the "non advanced" recursive symlink checks code path.
1 parent fe3597d commit 7ffc8a8

3 files changed

Lines changed: 87 additions & 10 deletions

File tree

resources/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/localstore/UnifiedTree.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,7 +520,7 @@ private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
520520
if (disable_advanced_recursive_link_checks) {
521521
// Multiple ../ backwards links can go outside the project tree
522522
if (linkTarget != null) {
523-
if (isRecursiveBackwardsLink(realParentPath, linkTarget)) {
523+
if (isRecursiveBackwardsLink(parent, realParentPath, linkTarget)) {
524524
return true;
525525
}
526526
// If link is outside the project tree, consider as non recursive
@@ -561,20 +561,24 @@ private boolean isRecursiveLink(IFileStore parentStore, IFileInfo localInfo) {
561561
}
562562

563563
/**
564-
* @param realParentPath real parent path object obtained as a result
565-
* of @code{Path.toRealPath()}
566-
* @param linkTarget the link target path as a string, may be relative or
567-
* absolute
564+
* @param parent the parent path object (may not be a real path), coming
565+
* from {@link #isRecursiveLink(IFileStore, IFileInfo)}
566+
* @param realParentPath real parent path object obtained as a result of
567+
* <code>Path.toRealPath()</code>
568+
* @param linkTarget the link target path as a string, may be relative or
569+
* absolute
568570
* @return true if the given target points backwards recursively to the given
569571
* parent path
570572
* @throws IOException
571573
*/
572-
private static boolean isRecursiveBackwardsLink(Path realParentPath, String linkTarget)
574+
private static boolean isRecursiveBackwardsLink(Path parent, Path realParentPath, String linkTarget)
573575
throws IOException {
574576
// Cheap test first: literal target points to the literal parent
575577
Path normalizedLink = realParentPath.resolve(linkTarget).normalize();
576578
if (realParentPath.startsWith(normalizedLink)) {
577579
return true;
580+
} else if (parent.startsWith(normalizedLink)) {
581+
return true;
578582
}
579583
// Next check costs more time because it does real IO when resolving paths
580584
Path realTarget = normalizedLink.toRealPath();

resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/localstore/SymlinkResourceTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ protected void createGithubBug2220Structure(IFileStore rootDir) throws CoreExcep
173173
@Test
174174
public void testBug232426() throws Exception {
175175
assumeTrue(canCreateSymLinks(), "only relevant for platforms supporting symbolic links");
176+
assumeTrue(UnifiedTree.isAdvancedRecursiveLinkChecksEnabled(), "Will hang the simple implementation");
176177

177178
IProject project = getWorkspace().getRoot().getProject("Project");
178179
createInWorkspace(project);
@@ -199,7 +200,7 @@ public void testBug232426() throws Exception {
199200
public boolean visit(IResource resource) {
200201
resourceCount++;
201202
// We have 1 root + 4 folders + 5 elements --> 10 elements to visit at most
202-
assertTrue(resourceCount <= 10);
203+
assertTrue(resourceCount <= 10, "Expected max 10 elements to visit, got: " + resourceCount);
203204
return true;
204205
}
205206
});

resources/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/resources/regression/Bug_185247_recursiveLinks.java

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.eclipse.core.resources.IProject;
2828
import org.eclipse.core.resources.IProjectDescription;
2929
import org.eclipse.core.resources.IResource;
30+
import org.eclipse.core.resources.IResourceVisitor;
3031
import org.eclipse.core.resources.ResourcesPlugin;
3132
import org.eclipse.core.runtime.IPath;
3233
import org.eclipse.core.tests.resources.util.WorkspaceResetExtension;
@@ -184,7 +185,76 @@ public void test5_linkParentDirectoyTwiceWithAbsolutePath(boolean useAdvancedLin
184185
runTest(createSymlinks, useAdvancedLinkCheck);
185186
}
186187

187-
private void runTest(CreateTestProjectStructure createSymlinks, boolean useAdvancedLinkCheck)
188+
/**
189+
* Test project structure:
190+
*
191+
* <pre>
192+
* project root
193+
* |-- directory
194+
* |
195+
* |- directory_1/
196+
* | |
197+
* | |- directory_1_1/
198+
* | |
199+
* | |- directory_1_1_1/
200+
* | |
201+
* | |- link_directory_2 -> ../../../directory_2/
202+
* |
203+
* |- directory_2/
204+
* |
205+
* |- directory_2_2/
206+
* |
207+
* |- link_directory_1 -> ../../directory_1/
208+
* </pre>
209+
*/
210+
@ParameterizedTest
211+
@ValueSource(booleans = { false, true })
212+
public void test6_linkGrandparentTwice(boolean useAdvancedLinkCheck) throws Exception {
213+
CreateTestProjectStructure createSymlinks = directory -> {
214+
215+
// directory_1/directory_1_1/directory_1_1_1/
216+
File directory1 = new File(directory, "directory_1");
217+
File directory1_1 = new File(directory1, "directory_1_1");
218+
File directory1_1_1 = new File(directory1_1, "directory_1_1_1");
219+
createDirectory(directory1_1_1);
220+
221+
// directory_2/directory_2_2/
222+
File directory2 = new File(directory, "directory_2");
223+
File directory2_2 = new File(directory2, "directory_2_2");
224+
createDirectory(directory2_2);
225+
226+
// link_directory_2 -> ../../../directory_2/ (from directory_1_1_1, goes up to
227+
// root directory)
228+
createSymlink(directory1_1_1, "link_directory_2", "../../../directory_2/");
229+
230+
// link_directory_1 -> ../../directory_1/ (from directory_2_2, goes up to
231+
// root directory)
232+
createSymlink(directory2_2, "link_directory_1", "../../directory_1/");
233+
};
234+
235+
IProject project = runTest(createSymlinks, useAdvancedLinkCheck);
236+
final boolean originalValue = UnifiedTree.isAdvancedRecursiveLinkChecksEnabled();
237+
UnifiedTree.enableAdvancedRecursiveLinkChecks(useAdvancedLinkCheck);
238+
try {
239+
// visiting the project should not visit forever
240+
project.accept(new IResourceVisitor() {
241+
int resourceCount = 0;
242+
243+
@Override
244+
public boolean visit(IResource resource) {
245+
resourceCount++;
246+
System.out.println(resourceCount + " visited: " + resource.getFullPath());
247+
assertTrue(resourceCount <= 13, "Expected max 13 elements to visit, got: " + resourceCount);
248+
return true;
249+
}
250+
});
251+
} finally {
252+
UnifiedTree.enableAdvancedRecursiveLinkChecks(originalValue);
253+
}
254+
255+
}
256+
257+
private IProject runTest(CreateTestProjectStructure createSymlinks, boolean useAdvancedLinkCheck)
188258
throws MalformedURLException, Exception {
189259
final boolean originalValue = UnifiedTree.isAdvancedRecursiveLinkChecksEnabled();
190260
try {
@@ -199,7 +269,8 @@ private void runTest(CreateTestProjectStructure createSymlinks, boolean useAdvan
199269

200270
URI projectRootLocation = URIUtil.toURI((projectRoot));
201271
// refreshing the project with recursive symlinks should not hang
202-
importProjectAndRefresh(projectName, projectRootLocation);
272+
IProject project = importProjectAndRefresh(projectName, projectRootLocation);
273+
return project;
203274
} finally {
204275
UnifiedTree.enableAdvancedRecursiveLinkChecks(originalValue);
205276
}
@@ -215,9 +286,10 @@ void createSymlink(File directory, String linkName, String linkTarget) throws IO
215286
createSymLink(directory, linkName, linkTarget, isDir);
216287
}
217288

218-
private void importProjectAndRefresh(String projectName, URI projectRootLocation) throws Exception {
289+
private IProject importProjectAndRefresh(String projectName, URI projectRootLocation) throws Exception {
219290
IProject project = importTestProject(projectName, projectRootLocation);
220291
project.refreshLocal(IResource.DEPTH_INFINITE, createTestMonitor());
292+
return project;
221293
}
222294

223295
private IProject importTestProject(String projectName, URI projectRootLocation) throws Exception {

0 commit comments

Comments
 (0)