This is an automated email from the ASF dual-hosted git repository.

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fory.git


The following commit(s) were added to refs/heads/main by this push:
     new 6ddc4145d fix(java): cache accepted type checker classes (#3773)
6ddc4145d is described below

commit 6ddc4145d99c048900767aa87317a544155ef4d0
Author: Shawn Yang <[email protected]>
AuthorDate: Sun Jun 21 10:53:08 2026 +0530

    fix(java): cache accepted type checker classes (#3773)
    
    ## Why?
    
    
    
    ## What does this PR do?
    
    TypeChecker is invoked in multiple places for security concerns, this pr
    cache the result to only invoke once
    
    ## Related issues
    
    Closes #3769
    
    ## AI Contribution Checklist
    
    
    
    - [ ] Substantial AI assistance was used in this PR: `yes` / `no`
    - [ ] If `yes`, I included a completed [AI Contribution
    
Checklist](https://github.com/apache/fory/blob/main/AI_POLICY.md#9-contributor-checklist-for-ai-assisted-prs)
    in this PR description and the required `AI Usage Disclosure`.
    - [ ] If `yes`, my PR description includes the required `ai_review`
    summary and screenshot evidence of the final clean AI review results
    from both fresh reviewers on the current PR diff or current HEAD after
    the latest code changes.
    
    
    
    ## Does this PR introduce any user-facing change?
    
    
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
    
    ## Benchmark
---
 .../org/apache/fory/resolver/AllowListChecker.java | 16 ++++-
 .../org/apache/fory/resolver/ClassResolver.java    |  2 +-
 .../org/apache/fory/resolver/SharedRegistry.java   | 32 +++++++++
 .../org/apache/fory/resolver/TypeResolver.java     | 30 +++++++-
 .../apache/fory/resolver/AllowListCheckerTest.java | 84 ++++++++++++++++++++++
 5 files changed, 161 insertions(+), 3 deletions(-)

diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
index 4a3fa0f2a..704e6bd7f 100644
--- 
a/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
+++ 
b/java/fory-core/src/main/java/org/apache/fory/resolver/AllowListChecker.java
@@ -77,7 +77,13 @@ public class AllowListChecker implements TypeChecker {
   }
 
   public void setCheckLevel(CheckLevel checkLevel) {
-    this.checkLevel = checkLevel;
+    try {
+      lock.writeLock().lock();
+      this.checkLevel = checkLevel;
+      clearCheckerCache();
+    } finally {
+      lock.writeLock().unlock();
+    }
   }
 
   @Override
@@ -209,6 +215,7 @@ public class AllowListChecker implements TypeChecker {
       String prefix = classNameOrPrefix.substring(0, 
classNameOrPrefix.length() - 1);
       disallowListPrefix.add(prefix);
       for (ClassResolver classResolver : listeners.keySet()) {
+        classResolver.clearCheckerCacheForPrefix(prefix);
         try {
           classResolver.getJITContext().lock();
           // clear serializer may throw NullPointerException for field 
serialization.
@@ -220,6 +227,7 @@ public class AllowListChecker implements TypeChecker {
     } else {
       disallowList.add(classNameOrPrefix);
       for (ClassResolver classResolver : listeners.keySet()) {
+        classResolver.clearCheckerCacheForClass(classNameOrPrefix);
         try {
           classResolver.getJITContext().lock();
           // clear serializer may throw NullPointerException for field 
serialization.
@@ -244,6 +252,12 @@ public class AllowListChecker implements TypeChecker {
     }
   }
 
+  private void clearCheckerCache() {
+    for (ClassResolver classResolver : listeners.keySet()) {
+      classResolver.clearCheckerCache();
+    }
+  }
+
   @SuppressWarnings({"rawtypes", "unchecked"})
   private static class DisallowSerializer extends Serializer {
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
index 4104d023f..253c14b50 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/ClassResolver.java
@@ -1952,7 +1952,7 @@ public class ClassResolver extends TypeResolver {
           || extRegistry.registeredClassIdMap.get(cls) != null
           || shimDispatcher.contains(cls);
     } else {
-      return extRegistry.typeChecker.checkType(this, cls.getName());
+      return checkType(cls.getName());
     }
   }
 
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
index fa72d2f1f..d4ba499db 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/SharedRegistry.java
@@ -58,6 +58,7 @@ import org.apache.fory.type.DescriptorGrouper;
 public final class SharedRegistry {
   private static final int MAX_CACHED_ENCODED_META_STRINGS = 32768;
   private static final int MAX_CACHED_ENCODED_META_STRING_LENGTH = 2048;
+  private static final int MAX_CACHED_TYPE_CHECKER_CLASSES = 8192;
   private static final int MIN_REMOTE_TYPE_DEF_LIMIT = 8192;
 
   final ConcurrentIdentityMap<Class<?>, TypeDef> typeDefMap = new 
ConcurrentIdentityMap<>();
@@ -85,6 +86,14 @@ public final class SharedRegistry {
       new ConcurrentIdentityMap<>();
   final ConcurrentIdentityMap<Class<?>, Serializer<?>> 
registeredSerializerCache =
       new ConcurrentIdentityMap<>();
+  // Positive TypeChecker decisions are shared by class name across this 
registry's equivalent Fory
+  // instances. Do not include TypeResolver or TypeChecker identity in the 
key; SharedRegistry is
+  // already the sharing boundary for equivalent type-checking policy. Mutable 
checker owners must
+  // clear these entries when a policy change can make a previously accepted 
class forbidden. This
+  // cache is intentionally bounded; after it fills, duplicate TypeChecker 
calls are safer than
+  // retaining attacker-controlled class names without limit.
+  private final ConcurrentHashMap<String, Boolean> acceptedTypeCheckerClasses =
+      new ConcurrentHashMap<>();
   private final ConcurrentHashMap<Class<?>, ObjectInstantiator<?>> 
objectInstantiatorCache =
       new ConcurrentHashMap<>();
   private final ConcurrentHashMap<Class<?>, ObjectInstantiator<?>> 
objectStreamInstantiatorCache =
@@ -182,6 +191,29 @@ public final class SharedRegistry {
     return existing == null ? typeInfo : existing;
   }
 
+  boolean isTypeAccepted(String className) {
+    return acceptedTypeCheckerClasses.containsKey(className);
+  }
+
+  void markTypeAccepted(String className) {
+    if (acceptedTypeCheckerClasses.size() >= MAX_CACHED_TYPE_CHECKER_CLASSES) {
+      return;
+    }
+    acceptedTypeCheckerClasses.putIfAbsent(className, true);
+  }
+
+  void clearCheckerCache() {
+    acceptedTypeCheckerClasses.clear();
+  }
+
+  void clearCheckerCacheForClass(String className) {
+    acceptedTypeCheckerClasses.remove(className);
+  }
+
+  void clearCheckerCacheForPrefix(String prefix) {
+    acceptedTypeCheckerClasses.keySet().removeIf(className -> 
className.startsWith(prefix));
+  }
+
   TypeDef getOrCreateTypeDef(TypeDef typeDef) {
     long id = typeDef.getId();
     TypeDef existing = typeDefById.get(id);
diff --git 
a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java 
b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
index 87a18426e..b10498552 100644
--- a/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
+++ b/java/fory-core/src/main/java/org/apache/fory/resolver/TypeResolver.java
@@ -1345,7 +1345,7 @@ public abstract class TypeResolver {
     // Remote TypeDef/TypeMeta paths reach class materialization through this 
owner. Keep
     // name-level checks before Class.forName so rejected metadata cannot 
force arbitrary class
     // loading.
-    if (!extRegistry.typeChecker.checkType(this, className)) {
+    if (!checkType(className)) {
       throw new InsecureException(
           String.format("Class %s is forbidden for serialization.", 
className));
     }
@@ -2154,12 +2154,40 @@ public abstract class TypeResolver {
   }
 
   public void setTypeChecker(TypeChecker typeChecker) {
+    sharedRegistry.clearCheckerCache();
     extRegistry.typeChecker = typeChecker == null ? DEFAULT_TYPE_CHECKER : 
typeChecker;
     if (extRegistry.typeChecker instanceof AllowListChecker && this instanceof 
ClassResolver) {
       ((AllowListChecker) extRegistry.typeChecker).addListener((ClassResolver) 
this);
     }
   }
 
+  final boolean checkType(String className) {
+    TypeChecker typeChecker = extRegistry.typeChecker;
+    if (typeChecker == DEFAULT_TYPE_CHECKER) {
+      return true;
+    }
+    if (sharedRegistry.isTypeAccepted(className)) {
+      return true;
+    }
+    if (!typeChecker.checkType(this, className)) {
+      return false;
+    }
+    sharedRegistry.markTypeAccepted(className);
+    return true;
+  }
+
+  final void clearCheckerCache() {
+    sharedRegistry.clearCheckerCache();
+  }
+
+  final void clearCheckerCacheForClass(String className) {
+    sharedRegistry.clearCheckerCacheForClass(className);
+  }
+
+  final void clearCheckerCacheForPrefix(String prefix) {
+    sharedRegistry.clearCheckerCacheForPrefix(prefix);
+  }
+
   public void registerSerializerFactory(SerializerFactory serializerFactory) {
     
extRegistry.serializerFactories.add(Preconditions.checkNotNull(serializerFactory));
   }
diff --git 
a/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
 
b/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
index 5d9b9b35e..cdd929344 100644
--- 
a/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
+++ 
b/java/fory-core/src/test/java/org/apache/fory/resolver/AllowListCheckerTest.java
@@ -24,8 +24,10 @@ import static org.testng.Assert.*;
 import java.io.ByteArrayOutputStream;
 import java.io.PrintStream;
 import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.apache.fory.Fory;
 import org.apache.fory.ThreadSafeFory;
+import org.apache.fory.config.Language;
 import org.apache.fory.exception.InsecureException;
 import org.apache.fory.logging.LogLevel;
 import org.apache.fory.logging.LoggerFactory;
@@ -156,6 +158,84 @@ public class AllowListCheckerTest {
     }
   }
 
+  @Test
+  public void testTypeCheckerSingleCheck() {
+    Fory fory = 
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+    AtomicInteger checks = new AtomicInteger();
+    fory.getTypeResolver()
+        .setTypeChecker(
+            (resolver, className) -> {
+              if (className.equals(CheckedType.class.getName())) {
+                checks.incrementAndGet();
+              }
+              return true;
+            });
+
+    byte[] bytes = fory.serialize(new CheckedType());
+    CheckedType result = (CheckedType) fory.deserialize(bytes);
+
+    assertEquals(result.value, "test");
+    assertEquals(checks.get(), 1);
+  }
+
+  @Test
+  public void testTypeCheckerCacheClearedByDisallow() {
+    Fory fory = 
Fory.builder().withLanguage(Language.JAVA).requireClassRegistration(false).build();
+    AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.WARN);
+    fory.getTypeResolver().setTypeChecker(checker);
+
+    byte[] bytes = fory.serialize(new CheckedType());
+    checker.disallowClass(CheckedType.class.getName());
+
+    assertThrows(InsecureException.class, () -> fory.deserialize(bytes));
+  }
+
+  @Test
+  public void testTypeCheckerCacheSharedByRegistry() {
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    Fory writer =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .requireClassRegistration(false)
+            .withSharedRegistry(sharedRegistry)
+            .build();
+    Fory reader =
+        Fory.builder()
+            .withLanguage(Language.JAVA)
+            .requireClassRegistration(false)
+            .withSharedRegistry(sharedRegistry)
+            .build();
+    AtomicInteger checks = new AtomicInteger();
+    TypeChecker checker =
+        (resolver, className) -> {
+          if (className.equals(CheckedType.class.getName())) {
+            checks.incrementAndGet();
+          }
+          return true;
+        };
+    writer.getTypeResolver().setTypeChecker(checker);
+    reader.getTypeResolver().setTypeChecker(checker);
+
+    byte[] bytes = writer.serialize(new CheckedType());
+    CheckedType result = (CheckedType) reader.deserialize(bytes);
+
+    assertEquals(result.value, "test");
+    assertEquals(checks.get(), 1);
+  }
+
+  @Test
+  public void testTypeCheckerCacheLimit() {
+    SharedRegistry sharedRegistry = new SharedRegistry();
+    for (int i = 0; i < 8192; i++) {
+      String className = "test.C" + i;
+      sharedRegistry.markTypeAccepted(className);
+      assertTrue(sharedRegistry.isTypeAccepted(className));
+    }
+    sharedRegistry.markTypeAccepted("test.Overflow");
+
+    assertFalse(sharedRegistry.isTypeAccepted("test.Overflow"));
+  }
+
   @Test
   public void testThreadSafeFory() {
     AllowListChecker checker = new 
AllowListChecker(AllowListChecker.CheckLevel.STRICT);
@@ -172,4 +252,8 @@ public class AllowListCheckerTest {
     assertThrows(InsecureException.class, () -> fory.serialize(new 
AllowListCheckerTest()));
     assertThrows(InsecureException.class, () -> fory.deserialize(bytes));
   }
+
+  public static class CheckedType {
+    public String value = "test";
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to