mandrean opened a new issue, #3790:
URL: https://github.com/apache/fory/issues/3790
### Search before asking
- [x] I searched the open issues and found no similar issue.
### 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:
```java
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`:
```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:
```bash
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:
```text
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:
```text
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?
- [x] I'm willing to submit a PR!
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]