Search before asking
Version
- Fory: 1.2.0.
- Component: Java runtime.
- JDK: reproduced in the hidden generated serializer path used on JDK 25. The application-facing failure is normal Fory serialization of a nested object graph.
Component(s)
Java
Minimal reproduce step
The regression was observed after adding an explicit allow-all TypeChecker to this existing setup:
private static final TypeChecker ALLOW_ALL_TYPES = (resolver, className) -> true;
Fory.builder()
.withXlang(false)
.requireClassRegistration(false)
.withRefTracking(true)
.withAsyncCompilation(true)
.withCompatible(false)
.withTypeChecker(ALLOW_ALL_TYPES)
.build();
The same test scenario passed without .withTypeChecker(ALLOW_ALL_TYPES) and failed with it. The TypeChecker does not seem to be the field-access bug itself; it changes the configured class-checking path enough to expose the async generated serializer callback failure.
The real-world shape is an ordinary class with nested object fields. The user model does not declare a hidden field. The hidden class is Fory's generated serializer class, for example a generated ContainerPayloadForyCodec_0/0x... class. That generated serializer has ordinary Serializer fields such as serializer and serializer1, and async JIT swaps those fields when nested field serializers finish compiling.
A focused regression test can be added as java/fory-core/src/test/java/org/apache/fory/builder/ForyHiddenSerializerFieldTest.java:
package org.apache.fory.builder;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import org.apache.fory.Fory;
import org.apache.fory.config.ForyBuilder;
import org.apache.fory.reflect.ReflectionUtils;
import org.apache.fory.resolver.TypeChecker;
import org.apache.fory.serializer.Serializer;
import org.testng.annotations.Test;
public class ForyHiddenSerializerFieldTest {
private static final TypeChecker ALLOW_ALL_TYPES = (resolver, className) -> true;
@Test(timeOut = 60000)
public void testAsyncCompilationSwitchAllowAllTypes() throws InterruptedException {
ForyBuilder builder =
Fory.builder()
.withXlang(false)
.requireClassRegistration(false)
.withRefTracking(true)
.withAsyncCompilation(true)
.withCompatible(false)
.withTypeChecker(ALLOW_ALL_TYPES);
Fory fory = builder.build();
ContainerPayload value =
new ContainerPayload(new NestedPayload(1, "name"), new PayloadDetails("category", true));
assertRoundTrip(fory, value);
Class<?>[] nestedTypes = {NestedPayload.class, PayloadDetails.class};
for (Class<?> cls : nestedTypes) {
while (!(fory.getTypeResolver().getSerializer(cls) instanceof Generated)) {
Thread.sleep(1000);
}
}
while (fory.getJITContext().hasJITResult(NestedPayload.class)) {
Thread.sleep(10);
}
while (fory.getJITContext().hasJITResult(PayloadDetails.class)) {
Thread.sleep(10);
}
Serializer<ContainerPayload> serializer =
fory.getTypeResolver().getSerializer(ContainerPayload.class);
assertTrue(ReflectionUtils.getObjectFieldValue(serializer, "serializer") instanceof Generated);
assertTrue(ReflectionUtils.getObjectFieldValue(serializer, "serializer1") instanceof Generated);
assertRoundTrip(fory, value);
}
private static void assertRoundTrip(Fory fory, ContainerPayload value) {
ContainerPayload roundTrip =
(ContainerPayload) fory.deserialize(fory.serialize(value));
assertEquals(roundTrip.nestedPayload.id, value.nestedPayload.id);
assertEquals(roundTrip.nestedPayload.name, value.nestedPayload.name);
assertEquals(roundTrip.details.category, value.details.category);
assertEquals(roundTrip.details.enabled, value.details.enabled);
}
public static final class ContainerPayload {
public NestedPayload nestedPayload;
public PayloadDetails details;
public ContainerPayload() {}
public ContainerPayload(NestedPayload nestedPayload, PayloadDetails details) {
this.nestedPayload = nestedPayload;
this.details = details;
}
}
public static final class NestedPayload {
public int id;
public String name;
public NestedPayload() {}
public NestedPayload(int id, String name) {
this.id = id;
this.name = name;
}
}
public static final class PayloadDetails {
public String category;
public boolean enabled;
public PayloadDetails() {}
public PayloadDetails(String category, boolean enabled) {
this.category = category;
this.enabled = enabled;
}
}
}
Run the test with debug output enabled:
ENABLE_FORY_DEBUG_OUTPUT=1 mvn -pl fory-core -am \
-Dtest=org.apache.fory.builder.ForyHiddenSerializerFieldTest \
-Dsurefire.failIfNoSpecifiedTests=false test
This matches the observed failure shape: Fory first creates a serializer for the parent object, then async-compiles serializers for nested field types. When a nested serializer JIT completes, Generated.GeneratedSerializer#registerJITNotifyCallback updates the parent generated serializer's Serializer field through ReflectionUtils.setObjectFieldValue.
What did you expect to see?
Adding an allow-all TypeChecker should not change serialization behavior compared with the implicit allow-all behavior when class registration is disabled. Hidden generated serializer classes should be usable by the async serializer JIT callback path.
What did you see instead?
The parent object itself is not hidden. The failure happens when Fory updates a Serializer field declared by the hidden generated serializer class. The base Unsafe-backed field accessor calls Unsafe.objectFieldOffset for that field, which the JDK rejects:
java.lang.UnsupportedOperationException: can't get field offset on a hidden class:
org.apache.fory.serializer.Serializer
...ContainerPayloadForyCodec_0/0x000000....serializer1
at sun.misc.Unsafe.objectFieldOffset
at org.apache.fory.reflect.InstanceFieldAccessors$InstanceAccessor.fieldOffset
at org.apache.fory.reflect.ReflectionUtils.setObjectFieldValue
at org.apache.fory.builder.Generated$GeneratedSerializer$1.onNotifyResult
at org.apache.fory.builder.JITContext.lambda$registerSerializerJITCallback$0
The original failure can then cascade into a secondary null-list failure in JITContext callback bookkeeping:
java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because the return value of "java.util.Map.get(Object)" is null
at org.apache.fory.builder.JITContext.lambda$registerSerializerJITCallback$0
Anything Else?
Disabling async compilation avoids this callback path and is a practical workaround. The runtime should still avoid Unsafe offsets for hidden-class fields and should handle callback failures without cascading.
Are you willing to submit a PR?
Search before asking
Version
Component(s)
Java
Minimal reproduce step
The regression was observed after adding an explicit allow-all
TypeCheckerto this existing setup:The same test scenario passed without
.withTypeChecker(ALLOW_ALL_TYPES)and failed with it. TheTypeCheckerdoes not seem to be the field-access bug itself; it changes the configured class-checking path enough to expose the async generated serializer callback failure.The real-world shape is an ordinary class with nested object fields. The user model does not declare a hidden field. The hidden class is Fory's generated serializer class, for example a generated
ContainerPayloadForyCodec_0/0x...class. That generated serializer has ordinarySerializerfields such asserializerandserializer1, and async JIT swaps those fields when nested field serializers finish compiling.A focused regression test can be added as
java/fory-core/src/test/java/org/apache/fory/builder/ForyHiddenSerializerFieldTest.java:Run the test with debug output enabled:
ENABLE_FORY_DEBUG_OUTPUT=1 mvn -pl fory-core -am \ -Dtest=org.apache.fory.builder.ForyHiddenSerializerFieldTest \ -Dsurefire.failIfNoSpecifiedTests=false testThis matches the observed failure shape: Fory first creates a serializer for the parent object, then async-compiles serializers for nested field types. When a nested serializer JIT completes,
Generated.GeneratedSerializer#registerJITNotifyCallbackupdates the parent generated serializer'sSerializerfield throughReflectionUtils.setObjectFieldValue.What did you expect to see?
Adding an allow-all
TypeCheckershould not change serialization behavior compared with the implicit allow-all behavior when class registration is disabled. Hidden generated serializer classes should be usable by the async serializer JIT callback path.What did you see instead?
The parent object itself is not hidden. The failure happens when Fory updates a
Serializerfield declared by the hidden generated serializer class. The base Unsafe-backed field accessor callsUnsafe.objectFieldOffsetfor that field, which the JDK rejects:The original failure can then cascade into a secondary null-list failure in
JITContextcallback bookkeeping:Anything Else?
Disabling async compilation avoids this callback path and is a practical workaround. The runtime should still avoid Unsafe offsets for hidden-class fields and should handle callback failures without cascading.
Are you willing to submit a PR?