Skip to content

[GR-51046] Verify all objects reachable through build-time-initialized static fields - #14722

Open
graalvmbot wants to merge 1 commit into
masterfrom
dlano/GR-51046/predictable-buildtime-init
Open

graalvmbot wants to merge 1 commit into
masterfrom
dlano/GR-51046/predictable-buildtime-init

Conversation

@graalvmbot

Copy link
Copy Markdown
Collaborator

This PR adds --future-defaults=predictable-build-time-initialization.
By default, Native Image verifies that objects included in the executable's image heap are instances
of build-time initialized classes and are allowed in the image heap.
With this option, verification extends to any object referenced directly or indirectly by static
fields of build-time initialized classes that could become part of the image heap, regardless of
the results of reachability analysis.

Why?

Correctness of the build should not depend on reachability analysis.

User-facing changes

This is a breaking change introduced behind --future-defaults=predictable-build-time-initialization.

The verification works as follows:

  1. Object graph traversal. The graph includes every object referenced by a static field of a
    build-time initialized class and every object referenced transitively through instance fields or array elements.
  2. Values used. Reading an object from a field may involve transforming the field's value
    with a FieldValueTransformer, followed by replacing the object with an ObjectReplacer.
    Verification applies only to the resulting object.
    • Primitive values and null are ignored.
    • If a field value is delayed by a FieldValueTransformer, verification of that value is deferred until it becomes available.
      • The value must be available at beforeImageWrite at the latest.
  3. Verification. Every covered object must be an instance of a class
    initialized at build time and must satisfy Native Image's restrictions on objects allowed in the
    image heap.
    • These requirements apply even if the object will not be included in the executable.
  4. Failure behavior. If an object violates either requirement, the build fails. The diagnostic
    identifies the violation, provides a reference path from a build-time initialized class to the
    offending object, and suggests possible resolutions.

As a result, enabling this option can cause a previously successful build to fail even without
application code changes, for example: when an unused field was previously unchecked.

This feature does not change the contents of the executable: checking a field does not cause its
contents to be included.

Example

This example shows the difference in behavior.
The build-time initialized class Root holds a single static field that is not deemed reachable by the static analysis.
The field references, through a Wrapper, an instance of a run-time initialized class UnreachableValue.

class Root { // --initialize-at-build-time
    static final Wrapper unreachable = new Wrapper(new UnreachableValue());
}

class Wrapper { // --initialize-at-build-time
    final Object value;

    Wrapper(Object value) {
        this.value = value;
    }
}

class UnreachableValue { // --initialize-at-run-time
}

Previously, this program built successfully. However, an unrelated code change could make the
field reachable and cause the build to fail.

With --future-defaults=predictable-build-time-initialization, the build fails predictably even
while the field remains unreachable, with the following message:

Error: Objects were found accessible from static fields of build-time initialized classes, but cannot be persisted safely in the image heap.
This is not allowed for correctness reasons: all objects accessible from static fields of build-time initialized classes must be safe to persist in the image heap.

[1] An object of type 'UnreachableValue' was found accessible from static fields of build-time initialized classes.
    This type, however, is marked for initialization at image run time for the following reason: classes are initialized at run time by default.

    Found from the build-time initialized class 'Root' through the path 'Root.unreachable.value'.
    The object is held by class 'Wrapper'.

    Possible resolutions:
      1) If it is intended that objects of type 'UnreachableValue' are persisted in the image heap, add the following to the native-image arguments:
      
         '--initialize-at-build-time=UnreachableValue'
      
         Note that initializing new types can store additional objects in the heap.
         It is advised to check the static fields of 'UnreachableValue' to see if they are safe for build-time initialization, and that they do not contain any sensitive data that should not become part of the image.

      2) The object was found through the build-time initialized class 'Root'.
         If this class does not need to be initialized at build time, the safer approach is to initialize it at run time with:
      
         '--initialize-at-run-time=Root'
      
         This prevents the root's static state from being persisted in the image heap.

      To trace where the offending objects are instantiated, re-run with:
      
        '--trace-object-instantiation=UnreachableValue'

The error message identifies:

  • The violating type: UnreachableValue
  • The root where scanning began: Root, which was previously missing.
  • The path from the root to the violating object: Root.unreachable.value
  • Two possible resolutions:
    • Initialize Root at run time, which is the safer option.
    • Initialize UnreachableValue at build time.

The suggested resolution shows that initializing Root at run time safely prevents this exception
by preventing the root's static state from being persisted in the image heap.

Public API Changes

The public Feature API changes regarding FieldValueTransformers as follows:

  • BeforeAnalysisAccess.registerBeforeFieldValueAccessHandler(BiConsumer<BeforeFieldValueAccess, Class<?>>, Class<?>)
    registers a handler that can register field value transformers on demand, before field values are accessed.
    • The method's second argument specifies a base class. The handler is executed for the base class and its subtypes.
    • For each matching class, Native Image invokes the handler before accessing the first field
      declared by that class, passing the declaring class as the handler's second argument.
    • The handler itself must be registered before analysis starts.
  • BeforeFieldValueAccess is a new interface:
    • It provides registerFieldValueTransformer(Field, FieldValueTransformer), which the handler
      uses to register a transformer for a field before any field declared by the same class is accessed.
    • BeforeAnalysisAccess extends this interface because both interfaces share the method for
      registering field value transformers.
  • Breaking: With --future-defaults=predictable-build-time-initialization, registration of
    FieldValueTransformers can only be done before analysis starts or in a beforeFieldValueAccess handler.
    • This forbids registration during analysis or in a reachability handler.

These changes are required to allow a field's value to be accessed through a field value
transformer without affecting the reachability status of the field or its declaring class, and
therefore without requiring a reachability handler for that class to be invoked.
Accessing the value allows the verification described above without changing the image contents
determined by analysis.

Review Entry Points

  • PredictableBuildTimeInitializationFeature contains the main implementation. It is invoked from
    NativeImageGenerator after all beforeAnalysis callbacks have completed and class initialization
    configuration has been sealed.
  • HeapObjectReplacer, HeapObjectConstantReplacer, and their implementations distinguish
    replacements that require reachable objects or use internal operations to modify the image heap or hosted/
    analysis universe.
  • FieldValueTransformerRequiresUniverse marks transformers that use internal operations to
    modify the image heap or analysis/hosted universe.
  • PredictableBuildTimeInitializationLookupLayer and its associated type and field classes provide
    access to substituted types without creating new analysis types.
  • FieldValueInterceptionSupport now supports non-analysis fields and handles
    beforeFieldValueAccess callbacks.

Limitations

  • Verification of fields and objects that require analysis-dependent transformers (FieldValueTransformerRequiresUniverse) or
    replacers (HeapObjectConstantReplacer) still depends on reachability.
  • HeapObjectConstantReplacer is internal-only. If a user-implemented replacer modifies an
    unrelated class, it could still theoretically change the image contents. This limitation is
    acceptable because it limits the changes required in user code for this feature.
  • In layered builds, verification assumes that previous layers were built with
    --future-defaults=predictable-build-time-initialization and have already verified their roots.

@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Oct 19, 2026
@graalvmbot
graalvmbot force-pushed the dlano/GR-51046/predictable-buildtime-init branch 6 times, most recently from e7bb624 to 9a3a8a3 Compare October 22, 2026 18:00
@graalvmbot
graalvmbot force-pushed the dlano/GR-51046/predictable-buildtime-init branch from 9a3a8a3 to 5505643 Compare October 22, 2026 19:30

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants