Authored by Chris Woodham (Software Engineer at THG)
In our previous GC focused blog post we covered the structure of the Java heap on OpenJDK 17 and outlined how by default the fine-grained memory protection made possible by CHERI is not provided for Java objects. In this blog post we outline the changes our team has made to OpenJDK 17 in order to guarantee fine-grained memory protection for Java objects, which would in turn prevent a wide range of memory exploits and the associated patching efforts. Any allocator that guarantees exactly-constrained objects for CHERI platforms requires modification to comply with the demands of CHERI Concentrate, we cover these first before moving on to a summary of some of the many changes we have made that are specific to OpenJDK 17.
CHERI Concentrate
A CHERI capability is an 128-bit entity that contains a pointer (native address) alongside a range of metadata that can be used to provide both hardware-level and software-level security guarantees. Perhaps the most important parts of this metadata are the capability's bounds - the base and limit of the address range for which the capability can be dereferenced. However, on a 64-bit platform the pointer, base and limit of the capability are all 64-bit values and therefore together require 192 bits for storage. Taking into account the other metadata that a CHERI capability stores and the alignment requirements for pointer types, if the base and limit were stored as raw addresses then a capability would be a 256-bit fat pointer. Moving from 64-bit to 256-bit pointers would create very large memory overheads for programmes executed on CHERI platforms. As a result, a new capability compression scheme called CHERI Concentrate was pioneered for CHERI platforms.
CHERI Concentrate compresses the metadata of a capability into just 64-bits (plus an extra bit called the validity tag that is stored separately). This reduces the memory overhead of using CHERI capabilities and more information on CHERI Concentrate can be found here and here. However, there is a limitation associated with using compressed capabilities - not all pointer, base and limit combinations are representable within a CHERI capability. The way CHERI Concentrate has been applied on the Morello demonstrator means that for an object with bounds length <= 16,384 bytes (1024 words on Morello) any combination of pointer, base and limit is exactly representable. However, above this limit not all combinations are exactly representable and the bounds for a capability can become an overestimate (i.e. the bounds extend before the desired base and/or after the desired limit). Majority of allocations are small making this is a reasonable trade-off. For example, Project Lilliput found that for realistic Java workloads the average size of a Java object is within the region of 32 to 64 bytes (see a summary in this JEP Motivation Description and the experiment results for Project Lilliput).
The trade-off between memory overhead and bounds accuracy for large objects may be a worthwhile one to make, but it does lead to challenges when implementing an allocator on CHERI. For any allocator looking to guarantee exactly-constrained allocation bounds a couple of modifications are required for any allocation with a size greater than the exactly representable threshold:
- The length of the allocation must be a
cheri_representable_length- which may require the memory overhead of padding out the allocation to meet this requirement. - The start address (i.e. base) of the allocation may have to be aligned up to ensure that an exactly-representable base address is possible, again potentially creating memory overhead.
We will not go into these requirements in detail here (more detail can be found in Sections 7.5 and 7.6 of the Cheri C/C++ Programming Guide), but we have had to ensure that they are met by the Garbage Collectors in OpenJDK 17 that we have ported to run on CheriBSD on Morello. Alongside these two requirements common to all allocators guaranteeing exactly-constrained allocations on CHERI platforms, there are numerous OpenJDK specific changes that we have made in order to enhance the JVM with fine-grained memory protection, we will outline these in the next section.
Exactly-constrained Java object bounds for OpenJDK 17
Java object size vs Object heap size
The first of the changes needed for exactly-constrained object bounds within the JVM relates to the size of Java objects. In our OpenJDK 17 enhanced with exactly-constrained bounds all objects have two sizes:
-
Java object size - the size of the Java representation of the object. In OpenJDK 17 for CheriBSD on Morello, the Java object size is returned by
oopDesc::java_object_size. -
Object heap size - the distance between this object's
markWordand themarkWordof the next object on the Java heap (or if this object is the last object in the current space, then the object heap size is thecheri_representable_length(java_object_size)of the object's Java size). In OpenJDK 17 for CheriBSD on Morello, the object heap size is returned byoopDesc::sizeand this size is determined by three factors:- The size of the Java object representation
- Any padding of the object's Java object size needed to ensure it has exactly-representable bounds
- Any alignment up of the start address for the next object on the Java heap
These three factors that constitute the object's heap size can be seen below in Figure 1.

An implementation detail to mention here is the CheriNextObject struct that is present in Figure 1. This struct is used to store the number of HeapWords (which are 16-bytes for CheriBSD on Morello) that need to be skipped until you reach the markWord of the next object on the heap. These are the fields within our implementation of this struct
struct CheriNextObject {
size_t _heapWordsToNextObject;
int _cheriNextObjectMark;
int _mangled_space;
}
// Globally defined mark value
CheriNextObjectMark = 0x73737373;
When calculating the heap size of an object we jump to start_address + cheri_representable_length(java_object_size) - which is the address marked next_object on Figure 1. We then check the contents of next_object and:
if (next_object->_cheriNextObjectMark == CheriNextObjectMark) {
size = cheri_representable_length(java_object_size) + next_object->_heapWordsToNextObject;
} else {
size = cheri_representable_length(java_object_size);
}
Reduced maximum object size
OpenJDK 17 was designed with the characteristic that the size of a Java object must fit within a signed int. This design choice is built deep into many different components of the JDK and therefore is not something that can be modified without rewriting large swathes of the codebase. Working within the scope of this design decision necessarily means that the maximum Java object size is smaller on CheriBSD for Morello than for other platforms. 0x7FFFFFFF is the maximum value of a signed int, and for OpenJDK 17 on other platforms vs for CheriBSD on Morello:
// CheriBSD on Morello
java_object_size + padding_to_cheri_representable_length + max_align_up_of_next_object <= 0x7FFFFFFF
// Other platforms
java_object_size <= 0x7FFFFFFF
// Therefore - maximum java_object_size is smaller for CheriBSD on Morello
The size of an instance can change across its lifetime
Across non-CHERI platforms, all instances of the same non-array Class have the same heap size. But for CheriBSD on Morello, heap size is now an instance property for all array and non-array Classes. However, there is an even larger change in the premise of object size that was needed to implement exactly-constrained bounds for Java objects: for any Garbage Collector that moves objects, the heap size of an instance can change across its lifetime. This is because whenever a Garbage Collector (such as Serial or G1) copies objects to a new space, or compacts the surviving objects within a space, it changes the start addresses of the objects. As we outlined in a previous section, the heap size of an object is dependent on how much the start address of the next object on the heap has been aligned up. Therefore, moving an object, or allocating just beyond the current object, can change the object's size as is shown below in Figure 2.

Filler objects
Another modification we have made to OpenJDK is updating the approach to allocating and marking filler objects within the Java heap. Within OpenJDK 17, filler objects are used for two main purposes:
- Filling the remaining space within Thread Local Allocation Buffers (TLABs) so that the Java heap is parsable during a collection (for more info on this see Aleksey Skipilev's excellent blog).
- Inserting deadspace objects for compacting collectors to reduce the length of STW compaction pauses.
On non-CHERI platforms it is possible to insert a single array of the exact size that needs filling. However, when guaranteeing exactly-constrained object bounds for CheriBSD on Morello it may not be possible to exactly represent the bounds of the space that needs filling. Our solution was to fill the space with multiple objects whose size is equal to the maximum size with exactly-representable bounds regardless of start address (i.e. 16,384 bytes) and then if the remaining space is less than 16,384 bytes - fill the space with an object of this exact size. Our approach is outlined below in Figure 3.

Iterating the Java heap
Maintaining heap parsability (i.e. being able to iterate the objects on the Java heap without needing complex metadata to support the process) is important for the ease and efficiency of garbage collection activities. It is one of the reasons that filler objects are inserted into the Java heap (above). There are assumptions that are made within the OpenJDK 17 codebase with regards to iterating the heap that can be broken when exactly-constrained object bounds are enforced for CheriBSD on Morello. One example of this is the assumption that for any space that contains Java objects, the first object will start at the bottom of the space. However, with exactly-constrained bounds, if the first object in the space has had its start address aligned up then this assumption has been broken. As a result, we have introduced checks in a number of places within the garbage collection code to ensure the maintenance of heap parsability. These checks are similar to this example code below:
HeapWord* current_object = space->bottom();
CheriNextObject* potential_next_object = (CheriNextObject*) current_object;
// Check whether or not the first object has been aligned up
if (potential_next_object->_cheri_next_object_mark == CheriNextObjectMark) {
current_object += potential_next_object->_heap_words_to_next_object;
}
// continue ...
The benefits of exactly-constrained object bounds
The main benefit of running code on a CHERI platform is the hardware-level fine-grained memory protection that is provided. However, as outlined in another blog post these memory-safety guarantees are not provided by default for Java objects for OpenJDK 17 running on CheriBSD for Morello. This means that by default, CHERI only provides small additional memory-safety benefits when running Java programmes and common exploit attack vectors, such as buffer overflows, are still possible.
Whereas, with our enchanced OpenJDK that enforces exactly-constrained bounds for all Java objects, buffer overflows (and other memory-safety focused attack vectors) are protected against. As part of this project, our team re-injected CVE-2015-4843 (a buffer overflow exploit) in OpenJDK 17, which involved removing the software-level JDK patch that was introduced to protect against this specific exploit. With this weakness re-injected, the only protection against the buffer overflow exploit was our Garbage Collector that enforces exactly-constrained object bounds. In the section below is a diagram that provides a brief overview of the exploit and highlights that our enhanced, memory-secure JVM successfully protected against the exploit. In addition to protecting against this specific exploit, our memory-secure JVM can successfully protect against any buffer overflow on a Java array. Whereas, the software-level patch implemented in response to this CVE only protects against this specific vulnerability, meaning that more patching efforts will be required on an ongoing basis as new Java heap buffer overflow exploits arise.
More detail on our enchanced JVM successfully protecting against a buffer overflow exploit
If you would like to find out more about how our enhanced memory-secure JVM successfully protected against this CVE, please watch this fantastic demo that one of our team (Ellie Slater) put together.
For more information on CVE-2015-4843 (and other CVEs related to the JVM) please see the Phrack paper: Twenty years of Escaping the Java Sandbox.
Diagram with a brief outline of the exploit:
