This is an automated email from the ASF dual-hosted git repository.

jackylee-ch pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new 85a7e09199 [GLUTEN-12401][CORE] Use Path.toRealPath() for JniLibLoader 
symlink resolution (#12402)
85a7e09199 is described below

commit 85a7e09199df8709817db69e9831481886903029
Author: YangJie <[email protected]>
AuthorDate: Wed Jul 1 18:42:54 2026 +0800

    [GLUTEN-12401][CORE] Use Path.toRealPath() for JniLibLoader symlink 
resolution (#12402)
---
 .../java/org/apache/gluten/jni/JniLibLoader.java   |  26 ++--
 .../org/apache/gluten/jni/JniLibLoaderTest.java    | 134 +++++++++++++++++++++
 2 files changed, 152 insertions(+), 8 deletions(-)

diff --git a/gluten-core/src/main/java/org/apache/gluten/jni/JniLibLoader.java 
b/gluten-core/src/main/java/org/apache/gluten/jni/JniLibLoader.java
index 816f0eb829..dcb024b006 100644
--- a/gluten-core/src/main/java/org/apache/gluten/jni/JniLibLoader.java
+++ b/gluten-core/src/main/java/org/apache/gluten/jni/JniLibLoader.java
@@ -43,16 +43,26 @@ public class JniLibLoader {
     this.workDir = workDir;
   }
 
-  private static String toRealPath(String libPath) {
-    String realPath = libPath;
+  /**
+   * Returns the canonical, fully-dereferenced absolute path for the given 
library path.
+   *
+   * <p>Delegates to {@link Path#toRealPath} so symbolic-link chains whose 
stored targets are
+   * relative (e.g. {@code lib.so -> lib.so.1 -> lib.so.1.2.3}) and 
symbolic-link cycles are handled
+   * by the JDK rather than a hand-rolled loop that resolved {@code 
readSymbolicLink} results
+   * against the process working directory and could spin forever on a cycle.
+   *
+   * <p>Package-private for testing.
+   */
+  static String toRealPath(String libPath) {
     try {
-      while (Files.isSymbolicLink(Paths.get(realPath))) {
-        realPath = Files.readSymbolicLink(Paths.get(realPath)).toString();
-      }
+      Path realPath = Paths.get(libPath).toRealPath();
       LOG.info("Read real path {} for libPath {}", realPath, libPath);
-      return realPath;
-    } catch (Throwable th) {
-      throw new GlutenException("Error to read real path for libPath: " + 
libPath, th);
+      return realPath.toString();
+    } catch (Exception e) {
+      // Wrap any operational failure (IOException, InvalidPathException, 
SecurityException,
+      // NPE, ...) as GlutenException so callers see a consistent type. Error 
subclasses
+      // (OOME, StackOverflowError, ...) are intentionally allowed to 
propagate.
+      throw new GlutenException("Error to read real path for libPath: " + 
libPath, e);
     }
   }
 
diff --git 
a/gluten-core/src/test/java/org/apache/gluten/jni/JniLibLoaderTest.java 
b/gluten-core/src/test/java/org/apache/gluten/jni/JniLibLoaderTest.java
new file mode 100644
index 0000000000..8e1de9029c
--- /dev/null
+++ b/gluten-core/src/test/java/org/apache/gluten/jni/JniLibLoaderTest.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.jni;
+
+import org.apache.gluten.exception.GlutenException;
+
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Locale;
+
+/** Tests for the symlink resolution path of {@link 
JniLibLoader#toRealPath(String)}. */
+public class JniLibLoaderTest {
+
+  @Rule public TemporaryFolder tempFolder = new TemporaryFolder();
+
+  @Before
+  public void assumeSymlinkSupport() throws Exception {
+    // Skip cleanly on filesystems / platforms that disallow symlinks or 
refuse to resolve them
+    // (FAT/SMB, Windows without privilege, some FUSE mounts, ...). The probe 
mirrors what the
+    // tests actually exercise — create a relative-target symlink AND walk it 
via toRealPath —
+    // so an environment that permits one but not the other still skips 
instead of red-failing.
+    File probeDir = tempFolder.newFolder("symlink-probe");
+    try {
+      Path target = probeDir.toPath().resolve("target");
+      Files.createFile(target);
+      Path link = probeDir.toPath().resolve("link");
+      Files.createSymbolicLink(link, Paths.get("target"));
+      link.toRealPath();
+    } catch (UnsupportedOperationException | SecurityException | 
java.io.IOException e) {
+      Assume.assumeNoException("filesystem does not support symbolic-link 
resolution", e);
+    }
+  }
+
+  @Test
+  public void toRealPathReturnsCanonicalAbsoluteForRegularFile() throws 
Exception {
+    // Force the input and the canonical real path to differ on every 
platform: address the file
+    // via a symlinked parent directory. A regression that returned the input 
verbatim would
+    // therefore fail on Linux (where /tmp is not a symlink) and not just on 
macOS (where /var is).
+    File realDir = tempFolder.newFolder("real-dir");
+    File regular = new File(realDir, "plain.so");
+    Assert.assertTrue("seed file created", regular.createNewFile());
+    Path linkDir = tempFolder.getRoot().toPath().resolve("link-dir");
+    Files.createSymbolicLink(linkDir, realDir.toPath());
+    String input = linkDir.resolve("plain.so").toString();
+
+    String resolved = JniLibLoader.toRealPath(input);
+
+    Assert.assertNotEquals(
+        "toRealPath must canonicalize the parent directory, not return the 
input verbatim",
+        input,
+        resolved);
+    Assert.assertEquals(
+        "regular file path should canonicalize to its real path",
+        regular.toPath().toRealPath().toString(),
+        resolved);
+  }
+
+  @Test
+  public void toRealPathResolvesRelativeSymlinkAgainstLinkParent() throws 
Exception {
+    // Versioned-library symlink chain:
+    //   <dir>/libfoo.so -> libfoo.so.1 -> libfoo.so.1.2.3 (regular file)
+    File dir = tempFolder.newFolder("nested-dir");
+    File realFile = new File(dir, "libfoo.so.1.2.3");
+    Assert.assertTrue("seed file created", realFile.createNewFile());
+
+    Path versioned = dir.toPath().resolve("libfoo.so.1");
+    Files.createSymbolicLink(versioned, Paths.get("libfoo.so.1.2.3"));
+    Path soname = dir.toPath().resolve("libfoo.so");
+    Files.createSymbolicLink(soname, Paths.get("libfoo.so.1"));
+
+    String resolved = JniLibLoader.toRealPath(soname.toString());
+
+    // Compare to the JDK-canonical real path of the underlying file. Do NOT 
re-canonicalize the
+    // returned string — a regression that returned the unresolved symlink 
path would otherwise be
+    // hidden by the comparison side following the link again.
+    Assert.assertEquals(
+        "relative symlinks must resolve against the link's parent, not the 
process CWD",
+        realFile.toPath().toRealPath().toString(),
+        resolved);
+  }
+
+  @Test
+  public void toRealPathRejectsSymlinkCycle() throws Exception {
+    // a -> b, b -> a — pre-fix the loop ran forever; the JDK throws 
FileSystemLoopException now.
+    File dir = tempFolder.newFolder("cycle-dir");
+    Path a = dir.toPath().resolve("a");
+    Path b = dir.toPath().resolve("b");
+    Files.createSymbolicLink(a, Paths.get("b"));
+    Files.createSymbolicLink(b, Paths.get("a"));
+
+    try {
+      JniLibLoader.toRealPath(a.toString());
+      Assert.fail("symlink cycle must not silently resolve to a real path");
+    } catch (GlutenException expected) {
+      // The cycle-detection contract: the JDK reports the loop. Linux maps 
ELOOP to
+      // FileSystemLoopException; macOS (current openjdk) surfaces a plain 
FileSystemException
+      // with "Too many levels of symbolic links" in the message. Accept 
either, but reject any
+      // other FileSystemException subclass (NoSuchFileException, 
AccessDeniedException, ...)
+      // so this test doesn't quietly degrade into "any FS failure passes".
+      Throwable cause = expected.getCause();
+      String causeMsg = cause == null ? "" : 
String.valueOf(cause.getMessage());
+      boolean reportsLoop =
+          cause instanceof java.nio.file.FileSystemLoopException
+              || (cause instanceof java.nio.file.FileSystemException
+                  && causeMsg
+                      .toLowerCase(Locale.ROOT)
+                      .contains("too many levels of symbolic links"));
+      Assert.assertTrue("cause should report a symlink loop, got: " + cause, 
reportsLoop);
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to