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