Skip to content

8356645: Javac should utilize new ZIP file system read-only access mode #25639

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

david-beaumont
Copy link
Contributor

@david-beaumont david-beaumont commented Jun 4, 2025

This PR seeks to integrate the new ZipFileSystem "accessMode" parameter to open internal ZIP/JAR files as read only, to act as defense in-depth against accidental modification.

Note that this currently also propagates the (currently undocumented) "zipinfo-time" parameter to several other places where ZIP/JAR files are opened, which is likely to improve performance. This was discussed and is expected to be safe (but it's something to be careful about).
This will, of course, be thoroughly tested before integration.

It also unifies several places to use a common helper method to obtain the environment map, adds more comments, and changes a small number of affected tests.

I'm also happy to update the original bug description to include the timestamp related changes as necessary.


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-8356645: Javac should utilize new ZIP file system read-only access mode (Enhancement - P4)

Reviewing

Using git

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

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

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 25639

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

Using diff file

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

Using Webrev

Link to Webrev Comment

Copy link
Contributor Author

@david-beaumont david-beaumont left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some pre-review comments.

* file-system from a multi-release JAR (or
* {@code null} to ignore release versioning).
*/
public static Map<String, ?> readOnlyJarFSEnv(String releaseVersion) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named this after the getJarFSProvider() method above, since they are used in close proximity, but would be happy to consider other names.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit uneasy about having the method static, as this class's API is normally instance methods. I think that an instance of FSInfo (fsInfo) is available in both JavacFileManager and Locations, so it would be better to simply have an instance method. (The static field is OK, that's not an API.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting idea since it permits per instance changes (e.g. due to flags/environment) later.
However there's no instance (yet) in VerifyCTSymClassFiles. I could mint one though.
Thoughts?

// or if non "*.jar" files are on the classpath.
this.fileSystem = FileSystems.newFileSystem(archivePath, env, (ClassLoader)null);
// or if non "*.jar" files are on the classpath. If this is not a ZIP/JAR file then it
// will ignore ZIP specific parameters in env, and may not end up being read-only.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment just to pull out the fact that we might not be able to promise that all ArchiveContainer instances are backed by a read-only file system, and the compiler still has a duty not to attempt any modification.

@@ -1463,7 +1460,7 @@ private Pair<String,Path> inferModuleName(Path p) {
log.error(Errors.LocnCantReadFile(p));
return null;
}
fs = jarFSProvider.newFileSystem(p, Collections.emptyMap());
fs = jarFSProvider.newFileSystem(p, FSInfo.readOnlyJarFSEnv(null));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to consider either commenting the null here (e.g. readOnlyJarFSEnv(/* releaseVersion */ null)), or adding a no-arg version of the method in FSInfo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a fallback path, so don't think we should add a no-arg version to promote skipping the release version.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just readOnlyJarFSEnv(null) is OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left it using readOnlyJarFSEnv(null)

@@ -96,6 +94,14 @@ public PlatformDescription getPlatformTrusted(String platformName) {

private static final String[] symbolFileLocation = { "lib", "ct.sym" };

// These must match attributes defined in ZipFileSystem.java.
private static final Map<String, ?> CT_SYM_ZIP_ENV = Map.of(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't use FSInfo here, even though it's mostly the same logic, because the symbol file is a ZIP, not a JAR, so naming gets a little muddled, and in this case there's no runtime info, so I can just make a constant map.

@@ -117,7 +123,7 @@ public PlatformDescription getPlatformTrusted(String platformName) {
SUPPORTED_JAVA_PLATFORM_VERSIONS = new TreeSet<>(NUMERICAL_COMPARATOR);
Path ctSymFile = findCtSym();
if (Files.exists(ctSymFile)) {
try (FileSystem fs = FileSystems.newFileSystem(ctSymFile, (ClassLoader)null);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several places in existing code pass "(ClassLoader) null", which by inspection makes no difference compared to simply not passing the parameter, so I took the decision to remove it for neatness.

fs = FileSystems.newFileSystem(file, CT_SYM_ZIP_ENV);
// If for any reason this was not opened from a ZIP file,
// then the resulting file system would not be read-only.
assert fs.isReadOnly();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if an assert is the right thing here or if this should be an always-on runtime check (it can be provoked by having a non-ZIP symbol file, but it's not sanctioned in any way).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically don't use assert in javac anymore, as it can be disabled. Either the condition is important, then we typically use com.sun.tools.javac.util.Assert.check, or we don't do the check. I think using Assert here would be appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm .... question:

The other place using FSInfo static method was the cySym verifier. And this is the cySym code. So it feels like it would be good to be consistent between both these cases. So is this a place for FSInfo as well, or not?

The advantage of FSInfo is that you can get the JAR provider (which we know is the ZIP provider too) and not have to worry about mismatches between given file and "discovered" file system type. You just only use the JAR provider and you're done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the FSInfo use in the test, just not worth the dependency.

@@ -160,7 +161,7 @@ List<Path> getTestFilePaths() throws IOException {
List<Path> getTestZipPaths() throws IOException {
if (zipfs == null) {
Path testZip = createSourceZip();
zipfs = FileSystems.newFileSystem(testZip);
zipfs = FileSystems.newFileSystem(testZip, Map.of("accessMode", "readOnly"));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly necessary, but seems like a good idea to use read-only for testing.

import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;

public class ClassRefDupInConstantPoolTest {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was failing and needed to be compiling its own class file rather than reading the one in the test library.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class file is from this test file instead of the test libraries, and this pattern of shipping additional classes in the same compilation unit for class file handling in tests is common in jdk/classfile and other packages. Could the clean directive be the culprit?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible. I remember having to change this when I prototyped the new code a while ago, but I'm not sure I remember exactly why. I can try reverting it if the original style is acceptable (personally I prefer to compile on-the-fly since it can test the compiler with different options - which is probably why I changed it originally).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also for compile on-the-fly, I usually use InMemoryJavaCompiler and ByteCodeLoader available with /test/lib library. InMemoryJavaCompiler conveniently returns a list of all class files, and its one-class-file version throws an exception if a compilation unit generates more than one class, which is not usually checked by manual invocations to javac task.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new tests, I would often prefer to use on-the-fly compilation (although javac has its own frameworks to do that, like the ToolBox used here).

For existing tests, I generally prefer to restrict changes to a minimum (although opinions may differ on that).

But here, I wonder: why is the test failing? I didn't see anything in the patch that would seem to be an obvious cause of a breakage of a test like this. So, I think it deserves a good look to find out what's the cause of the test failing, rather than just adjusting the test to pass.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I revert to the original code, I get:

java.lang.NullPointerException: Cannot invoke "java.io.InputStream.readAllBytes()" because the return value of "java.lang.Class.getResourceAsStream(String)" is null
	at ClassRefDupInConstantPoolTest.main(ClassRefDupInConstantPoolTest.java:40)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
	at java.base/java.lang.reflect.Method.invoke(Method.java:565)
	at com.sun.javatest.regtest.agent.MainWrapper$MainTask.run(MainWrapper.java:138)
	at java.base/java.lang.Thread.run(Thread.java:1474)

JavaTest Message: Test threw exception: java.lang.NullPointerException: Cannot invoke "java.io.InputStream.readAllBytes()" because the return value of "java.lang.Class.getResourceAsStream(String)" is null
JavaTest Message: shutting down test

when running via IntelliJ, but it passes when running via make. So there's some fragility with the environment.

I probably didn't realize originally that this was conditionally failing, so just changed it on the assumption that it was failing everywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have just verified that syncing back to master causes this test to fail when run via IntelliJ, so it's been "broken" for a while (whether it is at fault, or something in the way IntelliJ runs tests is at fault, I'm not sure). I'll revert it in this PR then, since it's not related to my change (I guess I just stumbled over the break and thought it was, so fixed it). Thanks for getting me to dig deeper.

try (FileSystem fs = FileSystems.newFileSystem(ctSym)) {
// Expected to always be a ZIP filesystem.
try (FileSystem fs = FileSystems.newFileSystem(ctSym, FSInfo.readOnlyJarFSEnv(null))) {
// Check that the file system is read only (not true if not a zip file system).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where I'm not 100% sure if it should be an assert, or a runtime exception/error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jtreg runs tests with -ea, so either works fine.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me: not relying on assert is preferred, esp. in tests. I.e. what is here currently seems good to me.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But you would be okay with this assert, or just happy to shrug and leave it as read-write if it somehow wasn't a ZIP file system? Alternately we could do what I see done elsewhere and extract the ZIP file system provider and use it directly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this test is verifying ct.sym is sensible, and we can extend that to make sure the file can be opened in the correct way. I.e., I would keep the check at this place as you did.

@bridgekeeper
Copy link

bridgekeeper bot commented Jun 4, 2025

👋 Welcome back david-beaumont! 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 Jun 4, 2025

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

@openjdk openjdk bot added the rfr Pull request is ready for review label Jun 4, 2025
@openjdk
Copy link

openjdk bot commented Jun 4, 2025

@david-beaumont The following label will be automatically applied to this pull request:

  • compiler

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.

@mlbridge
Copy link

mlbridge bot commented Jun 4, 2025

Webrevs

env.put("zipinfo-time", "false");

// Common parameters for opening ZIP/JAR file systems in Javac.
Map<String, ?> env = FSInfo.readOnlyJarFSEnv(multiReleaseValue);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this will now cause .zip files to be treated the same as .jar files when multiReleaseValue ≠ null (previously, this was ignored for .zip files)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's an excellent point. I will be sure to ask people if that looks acceptable and revert to the original behaviour otherwise. Thank you for spotting it and saying something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer to no send the multi release value for non-jar files - possibly there's no observable difference at this time, but the multi-release jar is for jars only.

(Also the comment above the variable seems a bit superfluous.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Though the more times I write readOnlyJarFSEnv(null) the more I think a no-arg version of that method might carry its weight.

Copy link
Contributor

@lahodaj lahodaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall seems like a reasonable direction. Please see some comments inline.

* file-system from a multi-release JAR (or
* {@code null} to ignore release versioning).
*/
public static Map<String, ?> readOnlyJarFSEnv(String releaseVersion) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit uneasy about having the method static, as this class's API is normally instance methods. I think that an instance of FSInfo (fsInfo) is available in both JavacFileManager and Locations, so it would be better to simply have an instance method. (The static field is OK, that's not an API.)

env.put("zipinfo-time", "false");

// Common parameters for opening ZIP/JAR file systems in Javac.
Map<String, ?> env = FSInfo.readOnlyJarFSEnv(multiReleaseValue);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would prefer to no send the multi release value for non-jar files - possibly there's no observable difference at this time, but the multi-release jar is for jars only.

(Also the comment above the variable seems a bit superfluous.)

@@ -1463,7 +1460,7 @@ private Pair<String,Path> inferModuleName(Path p) {
log.error(Errors.LocnCantReadFile(p));
return null;
}
fs = jarFSProvider.newFileSystem(p, Collections.emptyMap());
fs = jarFSProvider.newFileSystem(p, FSInfo.readOnlyJarFSEnv(null));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think just readOnlyJarFSEnv(null) is OK.

fs = FileSystems.newFileSystem(file, CT_SYM_ZIP_ENV);
// If for any reason this was not opened from a ZIP file,
// then the resulting file system would not be read-only.
assert fs.isReadOnly();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We typically don't use assert in javac anymore, as it can be disabled. Either the condition is important, then we typically use com.sun.tools.javac.util.Assert.check, or we don't do the check. I think using Assert here would be appropriate.

try (FileSystem fs = FileSystems.newFileSystem(ctSym)) {
// Expected to always be a ZIP filesystem.
try (FileSystem fs = FileSystems.newFileSystem(ctSym, FSInfo.readOnlyJarFSEnv(null))) {
// Check that the file system is read only (not true if not a zip file system).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me: not relying on assert is preferred, esp. in tests. I.e. what is here currently seems good to me.

import java.lang.classfile.*;
import java.lang.classfile.constantpool.*;

public class ClassRefDupInConstantPoolTest {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For new tests, I would often prefer to use on-the-fly compilation (although javac has its own frameworks to do that, like the ToolBox used here).

For existing tests, I generally prefer to restrict changes to a minimum (although opinions may differ on that).

But here, I wonder: why is the test failing? I didn't see anything in the patch that would seem to be an obvious cause of a breakage of a test like this. So, I think it deserves a good look to find out what's the cause of the test failing, rather than just adjusting the test to pass.

@openjdk
Copy link

openjdk bot commented Jun 18, 2025

@david-beaumont Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. See OpenJDK Developers’ Guide for more information.

@david-beaumont
Copy link
Contributor Author

david-beaumont commented Jun 18, 2025

Apologies for the recent force push. I'm having to switch laptops and this is coming from a different local repo.

@@ -56,6 +57,8 @@ public static void main(String... args) throws IOException, URISyntaxException {
t.checkClassFiles();
}

private final FSInfo fsInfo = FSInfo.instance(new Context());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is actually right unless we make the other cySym code use FSInfo. Happy to hear people's thoughts on this. One benefit here is that we know we've matched the expected ZIP file with the ZIP file system provider, so no need to worry if the environment was going to be honoured.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed both case (test and code) to NOT use FSInfo now. The dependency and extra code (esp. now the method is an instance method) just doesn't seem worth it.

@david-beaumont
Copy link
Contributor Author

Opened https://bugs.openjdk.org/browse/JDK-8360022 for the weird failing test thing (it was @cleanup, good call whoever suggested that!).

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

Successfully merging this pull request may close these issues.

4 participants