Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions src/java.base/share/classes/java/security/Security.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -112,7 +113,7 @@ private enum LoadingMode {OVERRIDE, APPEND}

private static Path currentPath;

private static final Set<Path> activePaths = new HashSet<>();
private static final List<Path> activePaths = new ArrayList<>();

static void loadAll() {
// first load the master properties file to
Expand Down Expand Up @@ -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) {
Expand All @@ -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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seanjmullan If this change is integrated, can we create a follow-up on issue to replace it? I strongly disagree with the changes in this PR, we should not be using File::getCanonicalFile here to work around an issue with an inaccessible parent directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just wanted to note that I also dislike this change, but is the only way I found to support relative includes in Windows UWP Java applications.

I'm open to other suggestions, including going back to Path::toRealPath and dropping that use-case (while still fixing the original JDK-8352728 issue), even though that isn't my preference.

@AlanBateman: do you disagree with other changes in this PR? I'm not comfortable integrating something that we haven't agreed upon.

} 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) {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure you want to ignore this - seems better to let this propagate and be thrown as an InternalError.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can make this an InternalError, the most common failure case is one of the two files nonexistence. So before proceeding I want to make sure you are aware that this would make the following filesystem race-condition noticeable:

  1. File A is included, OpenJDK starts reading it
  2. File A is deleted by and administrator who is changing the settings
    • But OpenJDK keeps it open, this is possible in Linux
  3. File B is included, OpenJDK wants to check for a circular inclusion
  4. Files.isSameFile(path, activePath) throws IOException when path is file B and activePath is file A (now deleted)
  5. IOException isn't ignored but wrapped in an InternalError and thrown

Current code wouldn't fail in this scenario, although I recognize it's a corner case. I decided to ignore the exception under the assumption that Files.isSameFile(x, y) can be treated as false in this context for cases in which either x or y is nonexistent.

}
}

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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can a directory ever be a regular file? If not, you don't need the !isRegularFile check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A directory is not a regular file, but we need isRegularFile later here:

currentPath = isRegularFile ? path : null;

So !isRegularFile is part of the condition to save the posible Files::isDirectory I/O operations in the most likely case (we are including a regular file).

Previous code was also saving this I/O:

if (isRegularFile) {
path = path.toRealPath();
} else if (Files.isDirectory(path)) {
throw new IOException("Is a directory");
} else {

I would prefer not to, but I can remove the condition and inline Files::isRegularFile on line 302:

currentPath = Files.isRegularFile(path) ? path : null;

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;
Expand All @@ -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;
}
}
Expand All @@ -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);
Expand Down
5 changes: 3 additions & 2 deletions test/jdk/java/security/Security/ConfigFileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
61 changes: 61 additions & 0 deletions test/jdk/java/security/Security/ConfigFileTestAnonymousPipes.sh
Original file line number Diff line number Diff line change
@@ -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
147 changes: 147 additions & 0 deletions test/jdk/java/security/Security/ConfigFileTestDirPermissions.java
Original file line number Diff line number Diff line change
@@ -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<AclEntry> 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");
}
}