RFR: 8352728: InternalError loading java.security due to Windows parent folder permissions
Francisco Ferrari Bihurriet
fferrari at openjdk.org
Thu Apr 10 00:25:51 UTC 2025
Hi, this is a proposal to fix 8352728.
The main idea is to replace [`java.nio.file.Path::toRealPath`](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/nio/file/Path.html#toRealPath(java.nio.file.LinkOption...)) by [`java.io.File::getCanonicalPath`](https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/io/File.html#getCanonicalPath()) for path canonicalization purposes. The rationale behind this decision is the following:
1. In _Windows_, `File::getCanonicalPath` handles restricted permissions in parent directories. Contrarily, `Path::toRealPath` fails with `AccessDeniedException`.
2. In _Linux_, `File::getCanonicalPath` handles non-regular files (e.g. `/dev/stdin`). Contrarily, `Path::toRealPath` fails with `NoSuchFileException`.
#### Windows Case
@martinuy and I tracked down the `File::getCanonicalPath` vs `Path::toRealPath` behaviour differences in _Windows_. Both methods end up calling the [`FindFirstFileW`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilew) API inside a loop for each parent directory in the path, until they include the leaf:
* [`File::getCanonicalPath`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/share/classes/java/io/File.java#L618 "src/java.base/share/classes/java/io/File.java:618") goes through the following stack into `FindFirstFileW`:
* [`WinNTFileSystem::canonicalize`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/java/io/WinNTFileSystem.java#L473 "src/java.base/windows/classes/java/io/WinNTFileSystem.java:473")
* [`WinNTFileSystem::canonicalize0`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libjava/WinNTFileSystem_md.c#L288 "src/java.base/windows/native/libjava/WinNTFileSystem_md.c:288")
* [`wcanonicalize`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libjava/canonicalize_md.c#L233 "src/java.base/windows/native/libjava/canonicalize_md.c:233") (here is the loop)
* If `FindFirstFileW` fails with `ERROR_ACCESS_DENIED`, `lastErrorReportable` is consulted, the error is [considered non-reportable](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libjava/canonicalize_md.c#L139 "src/java.base/windows/native/libjava/canonicalize_md.c:139") and the iteration is stopped [here](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libjava/canonicalize_md.c#L246-L250 "src/java.base/windows/native/libjava/canonicalize_md.c:246-250"). This may leave a partially normalized path, but it doesn't stop the processing, allowing [later symlinks resolution](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/java/io/WinNTFileSystem.java#L476 "src/java.base/windows/classes/java/io/WinNTFileSystem.java:476").
* [`Path::toRealPath`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/share/classes/java/nio/file/Path.java#L804 "src/java.base/share/classes/java/nio/file/Path.java:804") goes through the following stack into `FindFirstFileW`:
* [`WindowsPath::toRealPath`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/sun/nio/fs/WindowsPath.java#L907 "src/java.base/windows/classes/sun/nio/fs/WindowsPath.java:907")
* [`WindowsLinkSupport::getRealPath`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java#L255 "src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java:255") (here is the loop)
* [`WindowsNativeDispatcher::FindFirstFile`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/sun/nio/fs/WindowsNativeDispatcher.java#L182 "src/java.base/windows/classes/sun/nio/fs/WindowsNativeDispatcher.java:182")
* [`WindowsNativeDispatcher::FindFirstFile0`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libnio/fs/WindowsNativeDispatcher.c#L330 "src/java.base/windows/native/libnio/fs/WindowsNativeDispatcher.c:330")
* If `FindFirstFileW` fails with `ERROR_ACCESS_DENIED`, a `WindowsException` is [immediately thrown](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/native/libnio/fs/WindowsNativeDispatcher.c#L341 "src/java.base/windows/native/libnio/fs/WindowsNativeDispatcher.c:341"), then caught and [rethrown as an `IOException`](https://github.com/openjdk/jdk/blob/jdk-24-ga/src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java#L280 "src/java.base/windows/classes/sun/nio/fs/WindowsLinkSupport.java:280") (in particular `AccessDeniedException`). This not only stops the iteration but also makes the `Path::toRealPath` call fail.
NOTE: In cases in which `File::getCanonicalPath` gives a partially normalized path due to lack of permissions, the impact on cycle detection should be negligible: any include that leads to infinite recursion will revisit the exact same path at some point (even if not normalized).
#### Testing
The proposed `ConfigFileTestDirPermissions` test is passing, and no regressions have been found in `test/jdk/java/security/Security/ConfigFileTest.java` (_Windows_ and _Linux_).
Also, the [GitHub Actions testing run (`tier1` on various platforms)](https://github.com/franferrax/jdk/actions/runs/14363107070) has passed.
#### Testing Appendix
I could not make a fully automated symlinks resolution test in _Windows_, so I'm posting here a _PowerShell_ extended version of `ConfigFileTestDirPermissions`. This test requires user interaction, to accept _UAC_ elevation when creating the symlink. To run it, just paste the whole snippet in a non-elevated _PowerShell_ terminal at the root of a built `jdk` repository.
<details>
<summary>ConfigFileTestDirPermissionsEx PowerShell test</summary>
function ConfigFileTestDirPermissionsEx {
# Ensures java.security is loaded and symlinks are resolved in Windows,
# even when the user does not have permissions on a parent directory.
# Make sure we run non-elevated
$user = [Security.Principal.WindowsIdentity]::GetCurrent()
$adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator
$principal = New-Object Security.Principal.WindowsPrincipal($user)
if ($principal.IsInRole($adminRole)) {
throw "Must run non-elevated!"
}
$originalJdk = Get-Item -ErrorAction SilentlyContinue "build/*/images/jdk"
# Make sure a built JDK image is found
if (![System.IO.Directory]::Exists($originalJdk.FullName)) {
throw "Could not find a built image, must run from the jdk repo root"
}
# Create temporary directory
$tempDirName = "JDK-8352728-tmp-" + (New-Guid).ToString("N")
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) $tempDirName
New-Item $tempDir -ItemType Directory | Out-Null
try {
# Copy the jdk to a different directory
$jdk = Join-Path $tempDir "jdk-parent-dir/jdk"
Copy-Item -Recurse $originalJdk $jdk
# Create an extra.properties file with a relative include in it
$include = Join-Path $tempDir "relatively.included.properties"
$testProperty = "test.property.name=test_property_value"
Out-File -Encoding ascii $include -InputObject $testProperty
$extra = Join-Path $tempDir "extra.properties"
$content = "include " + (Split-Path -Leaf $include)
Out-File -Encoding ascii $extra -InputObject $content
# Create a symlink to extra.properties, from the jdk directory
$mainPropsDir = Join-Path $jdk "conf/security"
$mainProps = Join-Path $mainPropsDir "java.security"
$link = Join-Path $mainPropsDir "link.to.extra.properties"
Start-Process -Wait -Verb RunAs -WindowStyle Hidden "cmd.exe" @(
"/c", "mklink", $link, $extra
)
# Include link.to.extra.properties from java.security
$content = "`ninclude " + (Split-Path -Leaf $link)
Out-File -Encoding ascii -Append $mainProps -InputObject $content
# Remove current user permissions from jdk-parent-dir
$parent = Split-Path -Parent $jdk
$newAcl = New-Object System.Security.AccessControl.DirectorySecurity
$newAcl.SetAccessRule((New-Object `
System.Security.AccessControl.FileSystemAccessRule(
$user.Name, "FullControl", "Deny"
)
))
$originalAcl = Get-Acl $parent
Set-Acl $parent $newAcl
try {
# Make sure the permissions are affecting the current user
$java = Join-Path $jdk "bin/java.exe"
$stderrFile = Join-Path $tempDir "StandardError.txt"
$realPath = Join-Path $tempDir "RealPath.java"
Out-File -Encoding ascii $realPath -InputObject @"
public final class RealPath {
public static void main(String[] args) throws Exception {
java.nio.file.Path.of(args[0]).toRealPath();
}
}
"@
$proc = Start-Process -Wait -WindowStyle Hidden -PassThru `
-RedirectStandardError $stderrFile $java @(
$realPath, $mainProps
)
$stderrContent = Get-Content $stderrFile
if ($proc.ExitCode -eq 0) {
throw "Directory should affect the user, expected to fail"
}
if (($stderrContent -match "AccessDeniedException").Length -eq 0) {
throw "Failure was not an AccessDeniedException"
}
# Execute the copied jdk, ensuring java.security.Security is
# loaded (i.e. use -XshowSettings:security:properties)
$proc = Start-Process -Wait -WindowStyle Hidden -PassThru `
-RedirectStandardError $stderrFile $java @(
"-Djava.security.debug=properties",
"-XshowSettings:security:properties",
"-version"
)
$stderrContent = Get-Content $stderrFile
Write-Output $stderrContent
if ($proc.ExitCode -ne 0) {
throw "Execution failed"
}
if (($stderrContent -match $testProperty).Length -eq 0) {
throw "Expected '$testProperty' property not found"
}
Write-Output "TEST PASS - OK"
} finally {
Set-Acl $parent $originalAcl
}
} finally {
Remove-Item -Recurse -Force $tempDir
}
}
ConfigFileTestDirPermissionsEx
</details>
-------------
Commit messages:
- 8352728: InternalError loading java.security due to Windows parent folder permissions
Changes: https://git.openjdk.org/jdk/pull/24465/files
Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=24465&range=00
Issue: https://bugs.openjdk.org/browse/JDK-8352728
Stats: 102 lines in 2 files changed: 97 ins; 4 del; 1 mod
Patch: https://git.openjdk.org/jdk/pull/24465.diff
Fetch: git fetch https://git.openjdk.org/jdk.git pull/24465/head:pull/24465
PR: https://git.openjdk.org/jdk/pull/24465
More information about the security-dev
mailing list