Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8352728: InternalError loading java.security due to Windows parent folder permissions #24465

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

franferrax
Copy link
Contributor

@franferrax franferrax commented Apr 5, 2025

Hi, this is a proposal to fix 8352728.

The main idea is to replace java.nio.file.Path::toRealPath by java.io.File::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 API inside a loop for each parent directory in the path, until they include the leaf:

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) 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.

ConfigFileTestDirPermissionsEx PowerShell test
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

Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Issue

  • JDK-8352728: InternalError loading java.security due to Windows parent folder permissions (Bug - P4)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/24465/head:pull/24465
$ git checkout pull/24465

Update a local copy of the PR:
$ git checkout pull/24465
$ git pull https://git.openjdk.org/jdk.git pull/24465/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 24465

View PR using the GUI difftool:
$ git pr show -t 24465

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/24465.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Apr 5, 2025

👋 Welcome back fferrari! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Apr 5, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk
Copy link

openjdk bot commented Apr 5, 2025

@franferrax The following label will be automatically applied to this pull request:

  • security

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@franferrax franferrax marked this pull request as ready for review April 10, 2025 00:21
@openjdk openjdk bot added the rfr Pull request is ready for review label Apr 10, 2025
@mlbridge
Copy link

mlbridge bot commented Apr 10, 2025

Webrevs

@AlanBateman
Copy link
Contributor

AlanBateman commented Apr 10, 2025

I don't think this change should be integrated before more investigation. I think start by finding out why this code is using toRealPath. For the two cases listed, it looks like toRealPath is correctly failing for the first, but for /dev/stdin then please bring it to nio-dev to discuss how special devices should be handled by that method.

@franferrax
Copy link
Contributor Author

Hi @AlanBateman.

I don't think this change should be integrated before more investigation.

Ok, makes sense.

I think start by finding out why this code is using toRealPath.

The usage of Path::toRealPath was introduced by the 8319332: Security properties files inclusion proposal for the following reasons:

  1. Weak reason: detect cyclic re-inclusion through an alias of the same file (e.g. symlink, or alternative case in case-insensitive filesystems). This would only make cycle detection trigger earlier, but is not strictly necessary (infinite recursion will lead to path repetition even if not normalized).
  2. Weak reason: resolve a relative path passed through -Djava.security.properties=relative.props against the current working directory (stack: loadAll, loadExtra, loadExtraHelper, loadExtraFromPath, loadFromPath). This resolution could be done with Path::toAbsolutePath, but this case is also subject to the 3ʳᵈ reason.
  3. Strong reason: resolve symlinks, so that properties files use their original path to resolve relative includes. The rationale behind this is that the writer of the original properties file is the one who reasoned where relative includes should resolve to. On the other hand, the writer of the symlink just wants to use the original file with all its includes, without having to replicate anything else. This case is exercised by the PowerShell test on this PR's description.

For the two cases listed, it looks like toRealPath is correctly failing for the first […]

Yes, that was our impression, and that's why we are not proposing any fix to Path::toRealPath: there's nothing wrong with failing if normalization is not complete. But File::getCanonicalPath takes a best-effort approach that is more suitable to our needs.

[…] but for /dev/stdin then please bring it to nio-dev to discuss how special devices should be handled by that method.

I will investigate the Linux case, I had skipped it because File::getCanonicalPath looked like the only alternative on Windows, while it is also working on Linux.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rfr Pull request is ready for review security [email protected]
Development

Successfully merging this pull request may close these issues.

2 participants