JVM Advent

The JVM Programming Advent Calendar

Peaceful and Bright Future of Integrity by Default in Java

As Java continues to evolve, its commitment to stability and robustness grows stronger. Recent and upcoming JDK releases reflect this approach by gradually enhancing boundaries of unsafe APIs: restricting dynamic agent loading, deprecating potentially unsafe memory-access methods, and preparing for stricter Java Native Interface (JNI) usage. Introducing the Foreign Function and Memory (FFM) API in JDK 22 simplified access to native code while offering a much safer approach to preserve Java‘s resilience, reflecting even more the shift to integrity by default. This article explores these changes that embody a thoughtful transition to a more predictable and reliable Java ecosystem.

Integrity in the Java Platform

In software, integrity refers to being whole and undivided, ensuring that the constructs from which we build programs are complete and sound. This means that the Java Platform’s specifications are exhaustive (completeness), and its implementations adhere strictly to them (soundness). This foundational integrity enables you to build your application logic with confidence upon the Java platform’s constructs.

The “Integrity by Default” JDK Enhancement Proposal (JEP) emphasizes the importance of protecting code and data against unwanted or unwise use. While encapsulation is the fundamental tool for establishing integrity, the Java Platform contains certain unsafe APIs that can undermine this expectation, potentially affecting the correctness, maintainability, scalability, security, and performance of your application or library:

  • The Instrumentation API allows agents to modify the bytecode of any method in any class. For example, an agent can rewrite the incrementByTwo method of the EvenCounter class and redirect the old value of x to a file before incrementing it.
  • The AccessibleObject::setAccessible(boolean) method enables reflection over fields and methods without regard to encapsulation boundaries. This method’s goal is to support object serialization and deserialization, but given its access, any code can utilize it to invoke the private methods of any class, read and write the private fields of any object, and even write final fields. 
  • The sun.misc.Unsafe class includes methods that can access private methods and fields, and write final fields, which disregarding encapsulation boundaries.
  • The Java Native Interface (JNI) enables native code to interact with Java objects. Native code can access private methods and fields, and write final fields, disregarding encapsulation boundaries.

To prevent misuse of these unsafe APIs, the proposal recommends restricting direct access to them so that libraries, frameworks, and tools cannot use them by default. Moreover, a series of JEPs gradually address each pain point when using unsafe APIs, providing migration alternatives and allowing you to override this default restriction when necessary.

Gradual Limitation of Access to Unsafe APIs And Alternatives

Organize to Disallow Dynamic Loading of Agents

Building upon the integrity theme, a JDK 21 enhancement (JEP 451) focused on the implications of dynamic loading of agents into a running Java Virtual Machine (JVM). Agents are a component introduced in JDK 5 and allow for the instrumentation of classes, enabling tools like profilers to monitor applications. For example, to debug a remote Java application you probably configured it with the -agentlib:jdwp option in order to enable at startup an agent built into the JVM. Under the hood, the javalauncher uses the Attach API that allows a tool launched with appropriate operating-system privileges to connect to a running JVM.

However, certain libraries exploited the Attach API to silently connect to the JVMs in which they run, load agents dynamically and gain code-altering superpowers. In consequence, dynamically loading agents poses risks to application integrity. Since JDK 21, the JVM issues warnings when agents are loaded dynamically to alert you of these risks, but also in order for you to prepare for a future release where such actions may be disallowed by default. This change does not affect the majority of tools that do not need to load agents dynamically.

💡 The gracious manner for libraries to employ an agent is to load it at startup with the -javaagent/-agentlib options.

This approach balances the demand for serviceability with the need to maintain your application integrity.

Act upon Unsafe Memory-Access Methods Usage

Throughout time, the sun.misc.Unsafe class has provided developers access to low-level operations, particularly for tasks like:

  •  Performing direct memory operations to achieve better performance.
  • Handling off-heap memory without the limitations of  ByteBuffer.
  • Executing atomic operations like compare-and-swap.

Yet, utilizing these methods comes with risks:

  • They can lead to undefined behavior, crashes, or worse performance because they bypass the JVM optimizations.
  • They expose low-level details of JVM internals, leading to compatibility issues across Java versions.
  • Their unsafe nature produces maintainability and security challenges.

Starting with Java 23, the JDK is phasing out the memory-access methods in sun.misc.Unsafe, due to the risks and limitations associated with these unsafe operations. Instead, the recommended alternatives are the VarHandle API (introduced in JDK 9) and the  Foreign Function and Memory API (introduced in JDK 22).  For example, you might have used Unsafe for atomic updates in case of on-heap memory operations. To avoid such a dangerous practice, you can migrate from using Unsafeto achieve the same with VarHandle:

/// Migration example from Unsafe
private static final Unsafe UNSAFE = ...;
private static final long OFFSET;

static {
    try {
       OFFSET = UNSAFE.objectFieldOffset(
             Point.class.getDeclaredField("x")
       );
    } catch (Exception ex) {
       throw new AssertionError(ex);
    }
}

private int x;

public boolean update(int newValue) {
   return UNSAFE.compareAndSwapInt(this, OFFSET, x, newValue);
}

/// Use VarHandle to achieve the same
private static final VarHandle HANDLE = MethodHandles.lookup()
        .findVarHandle(Point.class, "x", int.class);

public boolean update(int newValue) {
   return HANDLE.compareAndSet(this, x, newValue);
}

Similarly, in case you used Unsafe for off-heap memory operations, you can migrate to MemorySegment :

// Using Unsafe for off-heap memory
long address = UNSAFE.allocateMemory(1024);
UNSAFE.putInt(address, 42);
int value = UNSAFE.getInt(address);
UNSAFE.freeMemory(address);

// Using MemorySegment to achieve the same
try (Arena arena = Arena.ofShared()) {
    long byteSize = ValueLayout.JAVA_INT.byteSize();
    MemorySegment segment = arena.allocate(byteSize);
    segment.set(ValueLayout.JAVA_INT, 0, 42);
    int value = segment.get(ValueLayout.JAVA_INT, 0);
}

These standard APIs offer safe, performant replacements for most use cases, ensuring compatibility with modern and future Java versions.

💡 There are a few tools that can further help you identify dependencies on Unsafe:

  • Look after the compile-time warnings emitted by javac .
  • If you use JDK Flight Recorder (JFR) on the command line, the jdk.DeprecatedInvocation event is recorded whenever the profiled JVM invokes a terminally deprecated method.

With each JDK release, you can assess how the deprecation and removal of these methods will affect libraries by running with a new command line option: --sun-misc-unsafe-memory-access={allow|warn|debug|deny}.  The upcoming JDK releases will deprecate the memory-access methods in sun.misc.Unsafe in phases, currently envisioned as :

  1. JDK 23 introduced deprecation with warnings at compile and runtime (by default--sun-misc-unsafe-memory-access=allow).
  2. As of JDK 24, through JEP 498, runtime warnings become default (by default--sun-misc-unsafe-memory-access=warn).
  3. As of JDK 26 unsupported operations throw exceptions by default(by default--sun-misc-unsafe-memory-access=deny).
  4. After JDK 26 the methods are removed entirely and the JVM ignores --sun-misc-unsafe-memory-access option.

When you migrate from sun.misc.Unsafe memory access methods, please do not rely on unsupported JDK internals, as this will increase risks of breaking changes.

PreparE to Restrict the Use of JNI

The iconic enhancement of JDK 22 was the graduation to final feature of the  Foreign Function & Memory (FFM) API. This API facilitates Java programs to invoke code outside the JVM (foreign functions) and to safely access memory not managed by the JVM (foreign memory).

Yet, since JDK 1.1, the Java Native Interface (JNI) facilitated interoperability between Java code and native code. While helpful, JNI interactions can compromise application integrity:

  • Calling native code can lead to arbitrary undefined behavior, including JVM crashes. Native and Java code often exchange data through direct byte buffers, which are regions of memory not managed by the JVM’s garbage collector. If you end up using a byte buffer backed by an invalid region of memory, that will for sure cause an undefined behavior.
  • Native code can use JNI to access fields and call methods without any access checks by the JVM.
  • Native code may use certain JNI functions (like GetPrimitiveArrayCritical ) incorrectly, causing undesirable garbage collector behavior that can manifest during the program’s lifetime.

Native access restrictions pertain to the processes of loading and linking native libraries. Given this potential for undefined behavior and JVM crashes, application developers must be carefully enabling native access for specific Java code at startup. This action acknowledges the necessity to load and link native libraries, thereby lifting the imposed restrictions.

💡When a library utilizes JNI or the FFM API, its documentation should inform its users (e.g., application developers) about the need to enable native access. If you are the developer or deployer who uses such a library, you are responsible for allowing native access. For example, if your application code uses a library that requires --enable-native-access=ALL-UNNAMED option at runtime ,you should be aware that this lifts native access restrictions on JNI and the FFM API for all classes on the class path.

As the --enable-native-access=ALL-UNNAMED option is broad, you can minimize risk and enhance integrity by moving JAR files that use JNI or the FFM API to the module path. This strategy allows native access explicitly enabled for those JAR files rather than the entire class path. If you move a JAR file from the class path to the module path without being modularized, the Java runtime will treat it as an automatic module, naming it based on its filename.

If a module does not have native access enabled, any code in that module that tries to perform a restricted operation is considered illegal. The Java runtime’s response to such operations is governed by the --illegal-native-access command-line option and works as follows:

  • --illegal-native-access=allow permits the operation to proceed without any warnings or exceptions.
  • --illegal-native-access=warn allows the operation but issues a warning the first time illegal native access occurs in a particular module. Only one warning per module is issued and in JDK 24 this is  the default mode for --illegal-native-access .
  • --illegal-native-access=deny throws an IllegalCallerException  for every illegal native access operation.

In JNI, the JVM loads native libraries using the load and loadLibrary methods of the java.lang.Runtime class. The identically named methods in the java.lang.System class invoke the corresponding Runtime methods. Loading a native library is risky because it can execute native code through initialization functions defined in the library or via a JNI_OnLoad function invoked by the Java runtime. Due to these risks, JDK 24 restricts load and loadLibrary methods , similar to the SymbolLookup::libraryLookup methods in the FFM API.

Before JDK 24, If you enabled native access for one or more modules via the --enable-native-access option, the attempts to call restricted FFM methods from any other module would result in an IllegalCallerException. This behavior has been relaxed in JDK 24 to align the FFM API with JNI. So, illegal native access operations in the FFM API get the same treatment as in JNI, resulting in warnings rather than exceptions. To restore the previous behavior, you can use the following combination of options:

java --enable-native-access=Module1,... --illegal-native-access=deny ...

In order to prepare your code for upcoming changes, you should run existing code with deny value for --enable-native-access to identify any code that requires native access.

Conclusion

Java Platform’s commitment to integrity is evident through JDK enhancement proposals and with every improvement integrated in its releases. By introducing modern, safe alternatives while slowly deprecating unsafe features, the platform aligns with the evolving best practices in software development.

 

Author: Ana-Maria Mihalceanu

Ana is a Java Champion Alumni, Developer Advocate, guest author of the book “DevOps tools for Java Developers”, and a constant adopter of challenging technical scenarios involving Java-based frameworks and multiple cloud providers. She actively supports technical communities’ growth through knowledge sharing and enjoys curating content for conferences as a program committee member. To learn more about/from her, follow her on Twitter @ammbra1508 or on Mastodon @ammbra1508.mastodon.social.

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén