RFR: 8352728: InternalError loading java.security due to Windows parent folder permissions [v14]

Francisco Ferrari Bihurriet fferrari at openjdk.org
Wed Dec 10 23:03:04 UTC 2025


On Wed, 10 Dec 2025 17:46:38 GMT, Weijun Wang <weijun at openjdk.org> wrote:

>> If you prefer, I can leave the code as it is now, without applying the [suggested change](#discussion_r2594151432).
>
> I'm a little confused. Now that we have agreed to no longer revolve symlinks, `/tmp/a/b/../f2` should indeed be `/tmp/a/f2`. Since it does not exist, we simply fail. Why is this a problem? Did I miss anything?

Hmm, I also got confused for a moment, it seems important to make a distinction between file links and directory links, and how each platform handles each of them.

We are no longer resolving any type of link, here:

https://github.com/openjdk/jdk/blob/c33bf62c2831acefd90ec476fcfb6d853be873ee/src/java.base/share/classes/java/security/Security.java#L250-L258

`currentPath.resolveSibling(path)` is equivalent to `currentPath.getParent().resolve(path)` when `currentPath` has a parent.

If `currentPath` is `/tmp/a/b/f1` and `path` is `../f2`, after line 257, path will be `/tmp/a/b/../f2`.

In the [previous _Linux_ example](#discussion_r2607247993), `/tmp/a/b` is a **directory** symbolic link to `/tmp/x/y`, so `/tmp/a/b/../f2` still reads `/tmp/x/f2` (at the OS level) when opening the file.

<details>
<summary>Extension of that <em>Linux</em> example's <code>jshell</code> snippet showing how <code>/tmp/a/b/../f2</code> can be opened</summary>


jshell -<<'EOF'
Path current = Path.of("/tmp/a/b/f1");
Path included = current.resolveSibling("../f2");

Map<String, Path> opts = new LinkedHashMap<String, Path>();
opts.put("included", included);
opts.put("included.toRealPath()", included.toRealPath());
opts.put("included.toRealPath(LinkOption.NOFOLLOW_LINKS)",
        included.toRealPath(LinkOption.NOFOLLOW_LINKS));
opts.put("included.normalize()", included.normalize());

for (Map.Entry<String, Path> opt : opts.entrySet()) {
    System.out.println();
    System.out.println(opt.getKey() + " -> " + opt.getValue());
    Files.newInputStream(opt.getValue());
    System.out.println("succesfully opened");
}
EOF


Output:


included -> /tmp/a/b/../f2
succesfully opened

included.toRealPath() -> /tmp/x/f2
succesfully opened

included.toRealPath(LinkOption.NOFOLLOW_LINKS) -> /tmp/a/b/../f2
succesfully opened

included.normalize() -> /tmp/a/f2
Exception java.nio.file.NoSuchFileException: /tmp/a/f2
      at UnixException.translateToIOException (UnixException.java:92)
      at UnixException.rethrowAsIOException (UnixException.java:106)
      at UnixException.rethrowAsIOException (UnixException.java:111)
      at UnixFileSystemProvider.newByteChannel (UnixFileSystemProvider.java:261)
      at Files.newByteChannel (Files.java:380)
      at Files.newByteChannel (Files.java:432)
      at FileSystemProvider.newInputStream (FileSystemProvider.java:420)
      at Files.newInputStream (Files.java:160)
      at (#8:4)


</details>

With that observation, I tried to re-introduce 7abb62c069ad35f4ec34f6cd9b9f6d05febceecc, which is similar to this case, but in _Windows_ and with a **directory** junction ([not quite the same as a symbolic link](https://ss64.com/nt/mklink.html)). However, it's failing with:


Caused by: java.nio.file.NoSuchFileException: C:\Users\test\AppData\Local\Temp\JDK-8352728-tmp-3508915478448537696\jdk-parent-dir\jdk\conf\security\link-to-other-dir..\relatively.included.properties


Thus, the meaning of `parent-dir/link-to-other-dir/../file` is platform dependent.

What we stopped doing is the resolution of `currentPath` before line 257, so if `/tmp/a/b/f1` was a **file** symbolic link pointing somewhere else, it wouldn't be resolved before getting its parent to compute the relative `include`. **File** symbolic links is [what I previously tested with Git includes](#discussion_r2593849381).

For everyone's sanity, here is the same _Linux_ **directory** symbolic links example with Git includes, showing how Git behaves the same as the current `java.security.Security` code:


mkdir -p /tmp/x/y /tmp/a
echo -e '[include]\n	path = ../f2' >/tmp/x/y/f1
echo -e '[section]\n	key = "/tmp/x/f2 was included"' >/tmp/x/f2
ln -s /tmp/x/y /tmp/a/b

# Backup user's config and rewrite it including /tmp/a/b/f1 (-> /tmp/x/y/f1)
mv ~/.gitconfig{,.bak}
echo -e '[include]\n	path = /tmp/a/b/f1' >~/.gitconfig

# Check if file is included (ensure CWD is anything else)
git -C / config section.key

# Cleanup and restore user's config
rm -rf /tmp/x /tmp/a
mv ~/.gitconfig{.bak,}


Output:


/tmp/x/f2 was included


----

I like this behavior because it leaves the interpretation of the paths to the underlying platform, with the simplest API (just open the file).

Regarding how to display the paths in debugging logs, I'm ok with:

* Attempting `path.toRealPath(LinkOption.NOFOLLOW_LINKS)` (for this example, it would log `/tmp/a/b/../f2`)
* Attempting `path.toRealPath()` (for this example, it would log `/tmp/x/f2`)
    * Originally, I mistakenly thought this would be confusing, but it wouldn't, it's the ultimate path of the file being processed (I was thinking that logging something equivalent to `currentPath.toRealPath().resolveSibling(path)` would be confusing, but this would be equivalent to `currentPath.resolveSibling(path).toRealPath()` instead)
* Leaving the paths as they are (for this example, it would log `/tmp/a/b/../f2`)

But `path.normalize()` would produce inaccurate debugging output in _Linux_ (for this example, it would log `/tmp/a/f2` which doesn't exist).

I hope everything is clearer now, there are many subtle details.

-------------

PR Review Comment: https://git.openjdk.org/jdk/pull/24465#discussion_r2608505651


More information about the security-dev mailing list