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