diff --git a/src/java.base/share/classes/java/security/Security.java b/src/java.base/share/classes/java/security/Security.java index 6969fe8a8e142..1675d69bd4af2 100644 --- a/src/java.base/share/classes/java/security/Security.java +++ b/src/java.base/share/classes/java/security/Security.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 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 @@ -253,6 +253,10 @@ static void loadInclude(String propFile) { "from a non-regular properties file " + "(e.g. HTTP served file)"); } + // Inside loadFromPath, we have performed symlinks + // resolution on currentFile under the rationale that + // the original file writer is the one who decided + // where the relative includes should resolve. path = currentPath.resolveSibling(path); } loadFromPath(path, LoadingMode.APPEND); @@ -265,13 +269,14 @@ static void loadInclude(String propFile) { private static void loadFromPath(Path path, LoadingMode mode) throws IOException { boolean isRegularFile = Files.isRegularFile(path); - if (isRegularFile) { - path = path.toRealPath(); - } else if (Files.isDirectory(path)) { + if (!isRegularFile && Files.isDirectory(path)) { throw new IOException("Is a directory"); - } else { - path = path.toAbsolutePath(); } + // JDK-8352728: we prefer java.io.File::getCanonicalFile over + // java.nio.file.Path::toRealPath because the former is more + // fault-tolerant, since the canonical form of a pathname is + // specified to exist even for nonexistent/inaccessible files. + path = path.toFile().getCanonicalFile().toPath(); if (activePaths.contains(path)) { throw new InternalError("Cyclic include of '" + path + "'"); } diff --git a/test/jdk/java/security/Security/ConfigFileTestDirPermissions.java b/test/jdk/java/security/Security/ConfigFileTestDirPermissions.java new file mode 100644 index 0000000000000..754507791f0bb --- /dev/null +++ b/test/jdk/java/security/Security/ConfigFileTestDirPermissions.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025, Red Hat, Inc. + * + * 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. + */ + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.util.FileUtils; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.AclEntry; +import java.nio.file.attribute.AclEntryType; +import java.nio.file.attribute.AclFileAttributeView; +import java.util.List; + +import static java.nio.file.StandardOpenOption.APPEND; + +/* + * @test + * @summary Ensures java.security is loadable in Windows and filesystem + * soft-links are resolved, even when the user does not have permissions + * on one of the parent directories. + * @bug 8352728 + * @requires os.family == "windows" + * @library /test/lib + * @run main ConfigFileTestDirPermissions + */ + +public class ConfigFileTestDirPermissions { + private static final String LF = System.lineSeparator(); + private static final String TEST_PROPERTY = + "test.property.name=test_property_value"; + + // Unlike symbolic links, directory junctions do not require elevation + private static void createJunction(Path target, Path link) + throws IOException, InterruptedException { + if (!Files.isDirectory(target)) { + throw new IOException("The target must be a directory: " + target); + } + int exitCode = + new ProcessBuilder("cmd", "/c", "MKLINK", "/J", link.toString(), + target.toString()).inheritIO().start().waitFor(); + if (exitCode != 0) { + throw new IOException("Unexpected exit code: " + exitCode); + } + } + + public static void main(String[] args) throws Exception { + Path temp = Files.createTempDirectory("JDK-8352728-tmp-"); + // We will create the following directories structure: + // + // 📁 JDK-8352728-tmp-*/ + // ├─🔒 jdk-parent-dir/ (ACL with REMOVED-PERMISSIONS) + // │ └─📁 jdk/ + // │ ├─📁 conf/ + // │ │ ├─📁 security/ + // │ │ │ ├─📄 java.security + // │ │ │ │ 📝 include link-to-other-dir/other.properties + // │ │ │ ├─🔗 link-to-other-dir/ ⟹ 📁 JDK-8352728-tmp-*/other-dir + // │ │ │ └─... (JUNCTION) + // │ │ └─... + // │ └─... + // ├─📁 other-dir/ + // │ └─📄 other.properties + // │ 📝 include ../relatively.included.properties + // └─📄 relatively.included.properties + // 📝 test.property.name=test_property_value + try { + // Copy the jdk to a different directory + Path originalJdk = Path.of(System.getProperty("test.jdk")); + Path jdk = temp.resolve("jdk-parent-dir", "jdk"); + Files.createDirectories(jdk); + FileUtils.copyDirectory(originalJdk, jdk); + + // Create a properties file with a relative include in it + Path otherDir = temp.resolve("other-dir"); + Files.createDirectories(otherDir); + Path other = otherDir.resolve("other.properties"); + Path included = temp.resolve("relatively.included.properties"); + Files.writeString(included, TEST_PROPERTY + LF); + Files.writeString(other, + "include ../" + included.getFileName() + LF); + + // Create a junction to the properties file dir, from the jdk dir + Path javaSec = jdk.resolve("conf", "security", "java.security"); + Path linkDir = javaSec.resolveSibling("link-to-other-dir"); + createJunction(otherDir, linkDir); + + // Include the properties file from java.security (through the link) + Files.writeString(javaSec, + LF + "include " + linkDir.getFileName() + "/" + + other.getFileName() + LF, APPEND); + + // Remove current user permissions from jdk-parent-dir + Path parent = jdk.getParent(); + AclFileAttributeView view = Files.getFileAttributeView(parent, + AclFileAttributeView.class); + List originalAcl = List.copyOf(view.getAcl()); + view.setAcl(List.of(AclEntry.newBuilder().setType(AclEntryType.DENY) + .setPrincipal(Files.getOwner(parent)).build())); + + try { + // Make sure the permissions are affecting the current user + try { + jdk.toRealPath(); + throw new jtreg.SkippedException("Must run non-elevated!"); + } catch (IOException expected) { } + + // Execute the copied jdk, ensuring java.security.Security is + // loaded (i.e. use -XshowSettings:security:properties) + ProcessTools.executeProcess(new ProcessBuilder( + jdk.resolve("bin", "java.exe").toString(), + "-Djava.security.debug=properties", + "-XshowSettings:security:properties", + "-version")) + .shouldHaveExitValue(0) + .shouldContain(TEST_PROPERTY); + } finally { + view.setAcl(originalAcl); + } + } finally { + FileUtils.deleteFileTreeUnchecked(temp); + } + + System.out.println("TEST PASS - OK"); + } +}