For tracing filesystem accesses of Java applications, native tracing facilities are always the first choice. On Windows, use Process Monitor to trace I/O. On Linux, use strace. Other platforms provide similar facilities.
By tracing directly in Java, you can work around environment limitations. For example, strace is unavailable in a container that lacks the CAP_SYS_PTRACE
capability and the container host is not always accessible. Also, a potentially lighter-weight tracing mechanism comes handy for tracing in production environments.
To go the Java route, you can implement your own security manager by extending java.lang.SecurityManager
. This class provides checkRead
, checkWrite
, and checkDelete
methods that get called as soon as code attempts corresponding accesses.
A sample implementation:
public class TraceSecurityManager extends SecurityManager {
public void checkRead(String file) {
System.out.println("Read: " + file);
super.checkRead(file);
}
public void checkRead(String file, Object context) {
System.out.println("Read: " + file);
super.checkRead(file, context);
}
public void checkWrite(String file) {
System.out.println("Write: " + file);
super.checkWrite(file);
}
public void checkDelete(String file) {
System.out.println("Delete: " + file);
super.checkDelete(file);
}
}
For testing the sample, we use the Java compiler as our test subject. To enable the trace security manager, we set the appropriate system property and execute the command with a valid Java source file Test.java
:
$ java -Djava.security.manager=TraceSecurityManager com.sun.tools.javac.Main Test.java
Read: /home/user/com/sun/tools/javac/resources/spi/compilerProvider.class
Read: /home/user/com/sun/tools/javac/resources/compiler_en.properties
Read: /home/user/com/sun/tools/javac/resources/compiler_en_US.properties
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.RuntimePermission" "getenv.JDK_JAVAC_OPTIONS")
at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
at java.base/java.lang.System.getenv(System.java:999)
at jdk.compiler/com.sun.tools.javac.main.CommandLine.appendParsedEnvVariables(CommandLine.java:252)
at jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:99)
at jdk.compiler/com.sun.tools.javac.main.CommandLine.parse(CommandLine.java:123)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:215)
at jdk.compiler/com.sun.tools.javac.main.Main.compile(Main.java:170)
at jdk.compiler/com.sun.tools.javac.Main.compile(Main.java:57)
at jdk.compiler/com.sun.tools.javac.Main.main(Main.java:43)
The trace implementation works. We can even see class loading attempts. However, javac fails because permissions are missing. The reason is that, with the installation of the security manager via the system property, the default Java security policy is active and does not grant the required permission. To work around that, you can either provide a minimal custom policy or you can override the checkPermission
method with an empty implementation. In this case, I chose the minimal policy:
grant {
permission java.security.AllPermission "", "";
};
With the policy in place, we can retest:
$ java -Djava.security.policy=test.policy -Djava.security.manager=TraceSecurityManager com.sun.tools.javac.Main Test.java
Read: /home/user/com/sun/tools/javac/resources/spi/compilerProvider.class
Read: /home/user/com/sun/tools/javac/resources/compiler_en.properties
Read: /home/user/com/sun/tools/javac/resources/compiler_en_US.properties
Read: Test.java
Read: Test.java
Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/jfxrt.jar
Read: /home/user/META-INF/services/java.nio.file.spi.FileSystemProvider
Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
Read: /usr/lib/jvm/java-11-openjdk-11.0.10.0.9-1.el7_9.x86_64/lib/modules
Read: Test.java
Read: /home/user/Test.java
Read: ./.bash_logout
Read: /home/user/.bash_logout
Read: ./.bash_profile
Read: /home/user/.bash_profile
Read: ./.bashrc
Read: /home/user/.bashrc
[...]
Read: /home/user/com/sun/tools/javac/resources/spi/ctProvider.class
Read: /home/user/com/sun/tools/javac/resources/ct_en.properties
Read: /home/user/com/sun/tools/javac/resources/ct_en_US.properties
Read: /home/user/TraceSecurityManager.class
Read: /home/user/Test.class
Read: /home/user
Write: /home/user/Test.class
Read: Test.java
This time around, we got a full filesystem access trace of javac. The security manager can also be enabled at runtime, which is useful if you cannot control the Java command line for whatever reason:
System.setSecurityManager(new TraceSecurityManager());
In that particular case, a custom policy is not necessary because the default policy is not active.
Using a security manager to trace filesystem accesses is certainly not the best option as details are missing that might be relevant to your debugging scenario, but it’s a good compromise if you’re out of alternatives and need to get things done or if low-overhead tracing is required.