diff --git a/src/hotspot/os/windows/os_windows.cpp b/src/hotspot/os/windows/os_windows.cpp index 59c1cae7de2..b6771719427 100644 --- a/src/hotspot/os/windows/os_windows.cpp +++ b/src/hotspot/os/windows/os_windows.cpp @@ -4626,6 +4626,63 @@ static void set_path_prefix(char* buf, LPCWSTR* prefix, int* prefix_off, bool* n } } +// This method checks if a wide path is actually a symbolic link +static bool is_symbolic_link(const wchar_t* wide_path) { + WIN32_FIND_DATAW fd; + HANDLE f = ::FindFirstFileW(wide_path, &fd); + if (f != INVALID_HANDLE_VALUE) { + const bool result = fd.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT && fd.dwReserved0 == IO_REPARSE_TAG_SYMLINK; + if (::FindClose(f) == 0) { + errno = ::GetLastError(); + log_debug(os)("is_symbolic_link() failed to FindClose: GetLastError->%ld.", errno); + } + return result; + } else { + errno = ::GetLastError(); + log_debug(os)("is_symbolic_link() failed to FindFirstFileW: GetLastError->%ld.", errno); + return false; + } +} + +// This method dereferences a symbolic link +static WCHAR* get_path_to_target(const wchar_t* wide_path) { + HANDLE hFile = CreateFileW(wide_path, GENERIC_READ, FILE_SHARE_READ, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (hFile == INVALID_HANDLE_VALUE) { + errno = ::GetLastError(); + log_debug(os)("get_path_to_target() failed to CreateFileW: GetLastError->%ld.", errno); + return nullptr; + } + + // Returned value includes the terminating null character. + const size_t target_path_size = ::GetFinalPathNameByHandleW(hFile, nullptr, 0, + FILE_NAME_NORMALIZED); + if (target_path_size == 0) { + errno = ::GetLastError(); + log_debug(os)("get_path_to_target() failed to GetFinalPathNameByHandleW: GetLastError->%ld.", errno); + return nullptr; + } + + WCHAR* path_to_target = NEW_C_HEAP_ARRAY(WCHAR, target_path_size, mtInternal); + + // The returned size is the length excluding the terminating null character. + const size_t res = ::GetFinalPathNameByHandleW(hFile, path_to_target, static_cast(target_path_size), + FILE_NAME_NORMALIZED); + if (res != target_path_size - 1) { + errno = ::GetLastError(); + log_debug(os)("get_path_to_target() failed to GetFinalPathNameByHandleW: GetLastError->%ld.", errno); + return nullptr; + } + + if (::CloseHandle(hFile) == 0) { + errno = ::GetLastError(); + log_debug(os)("get_path_to_target() failed to CloseHandle: GetLastError->%ld.", errno); + return nullptr; + } + + return path_to_target; +} + // Returns the given path as an absolute wide path in unc format. The returned path is null // on error (with err being set accordingly) and should be freed via os::free() otherwise. // additional_space is the size of space, in wchar_t, the function will additionally add to @@ -4694,15 +4751,35 @@ int os::stat(const char *path, struct stat *sbuf) { return -1; } - WIN32_FILE_ATTRIBUTE_DATA file_data;; - BOOL bret = ::GetFileAttributesExW(wide_path, GetFileExInfoStandard, &file_data); - os::free(wide_path); + const bool is_symlink = is_symbolic_link(wide_path); + WCHAR* path_to_target = nullptr; + if (is_symlink) { + path_to_target = get_path_to_target(wide_path); + if (path_to_target == nullptr) { + // it is a symbolic link, but we failed to resolve it, + // errno has been set in the call to get_path_to_target(), + // no need to overwrite it + os::free(wide_path); + return -1; + } + } + + WIN32_FILE_ATTRIBUTE_DATA file_data;; + BOOL bret = ::GetFileAttributesExW(is_symlink ? path_to_target : wide_path, GetFileExInfoStandard, &file_data); + + // if getting attributes failed, GetLastError should be called immediately after that if (!bret) { errno = ::GetLastError(); + log_debug(os)("os::stat() failed to GetFileAttributesExW: GetLastError->%ld.", errno); + os::free(wide_path); + os::free(path_to_target); return -1; } + os::free(wide_path); + os::free(path_to_target); + file_attribute_data_to_stat(sbuf, file_data); return 0; } @@ -4887,12 +4964,30 @@ int os::open(const char *path, int oflag, int mode) { errno = err; return -1; } - int fd = ::_wopen(wide_path, oflag | O_BINARY | O_NOINHERIT, mode); - os::free(wide_path); + const bool is_symlink = is_symbolic_link(wide_path); + WCHAR* path_to_target = nullptr; + + if (is_symlink) { + path_to_target = get_path_to_target(wide_path); + if (path_to_target == nullptr) { + // it is a symbolic link, but we failed to resolve it, + // errno has been set in the call to get_path_to_target(), + // no need to overwrite it + os::free(wide_path); + return -1; + } + } + + int fd = ::_wopen(is_symlink ? path_to_target : wide_path, oflag | O_BINARY | O_NOINHERIT, mode); + + // if opening files failed, GetLastError should be called immediately after that if (fd == -1) { errno = ::GetLastError(); + log_debug(os)("os::open() failed to _wopen: GetLastError->%ld.", errno); } + os::free(wide_path); + os::free(path_to_target); return fd; } diff --git a/test/hotspot/jtreg/runtime/LoadClass/TestSymlinkLoad.java b/test/hotspot/jtreg/runtime/LoadClass/TestSymlinkLoad.java new file mode 100644 index 00000000000..0be00658973 --- /dev/null +++ b/test/hotspot/jtreg/runtime/LoadClass/TestSymlinkLoad.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @summary JVM should be able to handle loading class via symlink on windows + * @requires vm.flagless + * @library /test/lib + * @run testng/othervm TestSymlinkLoad + */ + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import jdk.test.lib.compiler.CompilerUtils; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.util.FileUtils; +import org.testng.SkipException; +import org.testng.annotations.Test; + +public class TestSymlinkLoad { + + @Test + public void testSymlinkClassLoading() throws Exception { + Path sourceDir = Paths.get(System.getProperty("test.src"), "test-classes"); + + String subPath = "compiled"; + Path destDir = Paths.get(System.getProperty("test.classes"), subPath); + + CompilerUtils.compile(sourceDir, destDir); + + String bootCP = "-Xbootclasspath/a:" + destDir.toString(); + + String className = "Hello"; + + // try to load a class itself directly, i.e. not via a symlink + ProcessBuilder pb = ProcessTools.createLimitedTestJavaProcessBuilder( + bootCP, className); + + // make sure it runs as expected + OutputAnalyzer output = new OutputAnalyzer(pb.start()); + output.shouldContain("Hello World") + .shouldHaveExitValue(0); + + // create a symlink to the classfile in a subdir with a given name + Path classFile = Path.of(destDir + File.separator + className + ".class"); + final String subdir = "remote"; + final String pathToFolderForSymlink = destDir + File.separator + subdir + File.separator; + createLinkInSeparateFolder(pathToFolderForSymlink, classFile, className); + + // try to load class via its symlink, which is in a different directory + pb = ProcessTools.createLimitedTestJavaProcessBuilder( + bootCP + File.separator + subdir, className); + output = new OutputAnalyzer(pb.start()); + output.shouldContain("Hello World") + .shouldHaveExitValue(0); + + // remove the subdir + FileUtils.deleteFileTreeWithRetry(Path.of(pathToFolderForSymlink)); + } + + public static void createLinkInSeparateFolder(final String pathToFolderForSymlink, final Path target, final String className) throws IOException { + File theDir = new File(pathToFolderForSymlink); + if (!theDir.exists()) { + theDir.mkdirs(); + } + Path link = Paths.get(pathToFolderForSymlink, className + ".class"); + if (Files.exists(link)) { + Files.delete(link); + } + try { + Files.createSymbolicLink(link, target); + } catch (UnsupportedOperationException uoe) { + throw new SkipException("Symbolic link creation not supported.", uoe); + } catch (IOException ioe) { + throw new SkipException("Probably insufficient privileges to create symbolic links (Windows)", ioe); + } + } +}