diff --git a/src/java.base/share/classes/java/security/Security.java b/src/java.base/share/classes/java/security/Security.java index 6969fe8a8e142..1152849046d22 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 @@ -34,6 +34,7 @@ import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; @@ -112,7 +113,7 @@ private enum LoadingMode {OVERRIDE, APPEND} private static Path currentPath; - private static final Set activePaths = new HashSet<>(); + private static final List activePaths = new ArrayList<>(); static void loadAll() { // first load the master properties file to @@ -253,7 +254,10 @@ static void loadInclude(String propFile) { "from a non-regular properties file " + "(e.g. HTTP served file)"); } - path = currentPath.resolveSibling(path); + // We perform symlinks resolution on currentPath under the + // rationale that the original file writer is the one who + // decided where the relative includes should resolve. + path = resolve(currentPath).resolveSibling(path); } loadFromPath(path, LoadingMode.APPEND); } catch (IOException | InvalidPathException e) { @@ -262,20 +266,37 @@ static void loadInclude(String propFile) { } } + private static Path resolve(Path path) { + // 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. + try { + return path.toFile().getCanonicalFile().toPath(); + } catch (IOException e) { + throw new InternalError("Cannot resolve path", e); + } + } + + private static void checkCyclicInclude(Path path) { + for (Path activePath : activePaths) { + try { + if (Files.isSameFile(path, activePath)) { + throw new InternalError( + "Cyclic include of '" + path + "'"); + } + } catch (IOException ignore) {} + } + } + 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(); - } - if (activePaths.contains(path)) { - throw new InternalError("Cyclic include of '" + path + "'"); } try (InputStream is = Files.newInputStream(path)) { + checkCyclicInclude(path); reset(mode); Path previousPath = currentPath; currentPath = isRegularFile ? path : null; @@ -285,7 +306,7 @@ private static void loadFromPath(Path path, LoadingMode mode) props.load(is); debugLoad(false, path); } finally { - activePaths.remove(path); + activePaths.removeLast(); currentPath = previousPath; } } @@ -304,6 +325,7 @@ private static void loadFromUrl(URL url, LoadingMode mode) private static void debugLoad(boolean start, Object source) { if (sdebug != null) { int level = activePaths.isEmpty() ? 1 : activePaths.size(); + source = source instanceof Path path ? resolve(path) : source; sdebug.println((start ? ">".repeat(level) + " starting to process " : "<".repeat(level) + " finished processing ") + source); diff --git a/test/jdk/java/security/Security/ConfigFileTest.java b/test/jdk/java/security/Security/ConfigFileTest.java index caf657005e1ba..d30bb0a7dff90 100644 --- a/test/jdk/java/security/Security/ConfigFileTest.java +++ b/test/jdk/java/security/Security/ConfigFileTest.java @@ -394,8 +394,9 @@ static void testCannotIncludeCycles(Executor ex, FilesManager filesMgr) masterFile.addRelativeInclude(file0); ex.setMasterFile(masterFile); - ex.assertError( - "InternalError: Cyclic include of '" + masterFile.path + "'"); + ex.assertError("Cyclic include"); + ex.getOutputAnalyzer().stderrShouldMatch("\\QInternalError: Cyclic " + + "include of '\\E[^']+\\Q" + masterFile.fileName + "'\\E"); } static void testCannotIncludeURL(Executor ex, FilesManager filesMgr) diff --git a/test/jdk/java/security/Security/ConfigFileTestAnonymousPipes.sh b/test/jdk/java/security/Security/ConfigFileTestAnonymousPipes.sh new file mode 100644 index 0000000000000..518a304ba65f4 --- /dev/null +++ b/test/jdk/java/security/Security/ConfigFileTestAnonymousPipes.sh @@ -0,0 +1,61 @@ +# +# 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. +# + +# @test +# @summary Ensures the java executable is able to load extra security +# properties files from anonymous pipes (non-regular files). +# @bug 8352728 +# @requires os.family == "linux" +# @run shell/timeout=30 ConfigFileTestAnonymousPipes.sh + +if [ -z "${TESTJAVA}" ]; then + JAVA=java +else + JAVA="${TESTJAVA}/bin/java" +fi + +TEST_PROP="ConfigFileTestAnonymousPipes.property.name=PROPERTY_VALUE" + +function check_java() { + local java_output java_exit_code + java_output="$("${JAVA}" ${TESTVMOPTS} ${TESTJAVAOPTS} -XshowSettings:security:properties \ + -Djava.security.debug=properties "$@" -version 2>&1)" + java_exit_code=$? + if [ ${java_exit_code} -ne 0 ] || ! grep -qF "${TEST_PROP}" <<<"${java_output}"; then + echo "TEST FAILED (java exit code: ${java_exit_code})" + echo "${java_output}" + exit 1 + fi +} + +# https://www.gnu.org/software/bash/manual/bash.html#Pipelines +echo "Extra properties from pipeline" +echo "${TEST_PROP}" | check_java -Djava.security.properties=/dev/stdin || exit 1 + +# https://www.gnu.org/software/bash/manual/bash.html#Process-Substitution +echo "Extra properties from process-substitution, include other from pipeline" +echo "${TEST_PROP}" | check_java -Djava.security.properties=<(echo "include /dev/stdin") || exit 1 + +echo "TEST PASS - OK" +exit 0 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"); + } +}