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]

Reply via email to