Apache Fory: Denial of Service (DoS) due to Deserialization of Untrusted malicious large Data
Description
A vulnerability in Apache Fory allows a remote attacker to cause a Denial of Service (DoS). The issue stems from the insecure deserialization of untrusted data. An attacker can supply a large, specially crafted data payload that, when processed, consumes an excessive amount of CPU resources during the deserialization process. This leads to CPU exhaustion, rendering the application or system using the Apache Fory library unresponsive and unavailable to legitimate users.
Users of Apache Fory are strongly advised to upgrade to version 0.12.2 or later to mitigate this vulnerability. Developers of libraries and applications that depend on Apache Fory should update their dependency requirements to Apache Fory 0.12.2 or later and release new versions of their software.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Insecure deserialization in Apache Fory allows remote attackers to cause CPU exhaustion and Denial of Service via crafted payloads.
Vulnerability
CVE-2025-59328 is a denial-of-service vulnerability in Apache Fory, a multi-language serialization framework. The root cause is insecure deserialization of untrusted data: when the library processes a large, specially crafted payload, it consumes excessive CPU resources during deserialization, leading to CPU exhaustion. This is a classic resource exhaustion issue triggered by untrusted input [1][2].
Exploitation
The vulnerability is remotely exploitable without authentication. An attacker can send a malicious payload to any application or service that deserializes untrusted data using Apache Fory. The attack surface includes network-facing services that accept serialized data in Fory format. No special privileges are required beyond the ability to submit data for deserialization [2].
Impact
Successful exploitation results in denial of service: the targeted application or system becomes unresponsive and unavailable to legitimate users. The impact is limited to availability; there is no indication of data compromise or code execution. However, the DoS can disrupt critical services relying on Apache Fory [1][2].
Mitigation
Apache has addressed the vulnerability by introducing a configurable deserialization depth limit in commit c3b4d3fe389e38495aeb8301d75da1ab658f0c6e, which is part of the pull request #2578 [3][4]. Users must upgrade to Apache Fory version 0.12.2 or later. Developers of dependent libraries should update their dependency constraints accordingly and release new versions [1][2].
- GitHub - apache/fory: A blazingly fast multi-language serialization framework for idiomatic domain objects, schema IDL, and cross-language data exchange.
- NVD - CVE-2025-59328
- feat(java): support limit deserialization depth by chaokunyang · Pull Request #2578 · apache/fory
- feat(java): support limit deserialization depth (#2578) · apache/fory@c3b4d3f
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.apache.fory:fory-coreMaven | < 0.12.2 | 0.12.2 |
Affected products
2- Apache Software Foundation/Apache Foryv5Range: 0.5.0
Patches
1c3b4d3fe389efeat(java): support limit deserialization depth (#2578)
16 files changed · +334 −153
docs/guide/java_serialization_guide.md+33 −23 modified@@ -108,28 +108,30 @@ public class Example { ## ForyBuilder options -| Option Name | Description | Default Value | -| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | -| `timeRefIgnored` | Whether to ignore reference tracking of all time types registered in `TimeSerializers` and subclasses of those types when ref tracking is enabled. If ignored, ref tracking of every time type can be enabled by invoking `Fory#registerSerializer(Class, Serializer)`. For example, `fory.registerSerializer(Date.class, new DateSerializer(fory, true))`. Note that enabling ref tracking should happen before serializer codegen of any types which contain time fields. Otherwise, those fields will still skip ref tracking. | `true` | -| `compressInt` | Enables or disables int compression for smaller size. | `true` | -| `compressLong` | Enables or disables long compression for smaller size. | `true` | -| `compressString` | Enables or disables string compression for smaller size. | `false` | -| `classLoader` | The classloader should not be updated; Fory caches class metadata. Use `LoaderBinding` or `ThreadSafeFory` for classloader updates. | `Thread.currentThread().getContextClassLoader()` | -| `compatibleMode` | Type forward/backward compatibility config. Also Related to `checkClassVersion` config. `SCHEMA_CONSISTENT`: Class schema must be consistent between serialization peer and deserialization peer. `COMPATIBLE`: Class schema can be different between serialization peer and deserialization peer. They can add/delete fields independently. [See more](#class-inconsistency-and-class-version-check). | `CompatibleMode.SCHEMA_CONSISTENT` | -| `checkClassVersion` | Determines whether to check the consistency of the class schema. If enabled, Fory checks, writes, and checks consistency using the `classVersionHash`. It will be automatically disabled when `CompatibleMode#COMPATIBLE` is enabled. Disabling is not recommended unless you can ensure the class won't evolve. | `false` | -| `checkJdkClassSerializable` | Enables or disables checking of `Serializable` interface for classes under `java.*`. If a class under `java.*` is not `Serializable`, Fory will throw an `UnsupportedOperationException`. | `true` | -| `registerGuavaTypes` | Whether to pre-register Guava types such as `RegularImmutableMap`/`RegularImmutableList`. These types are not public API, but seem pretty stable. | `true` | -| `requireClassRegistration` | Disabling may allow unknown classes to be deserialized, potentially causing security risks. | `true` | -| `suppressClassRegistrationWarnings` | Whether to suppress class registration warnings. The warnings can be used for security audit, but may be annoying, this suppression will be enabled by default. | `true` | -| `metaShareEnabled` | Enables or disables meta share mode. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | -| `scopedMetaShareEnabled` | Scoped meta share focuses on a single serialization process. Metadata created or identified during this process is exclusive to it and is not shared with by other serializations. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | -| `metaCompressor` | Set a compressor for meta compression. Note that the passed MetaCompressor should be thread-safe. By default, a `Deflater` based compressor `DeflaterMetaCompressor` will be used. Users can pass other compressor such as `zstd` for better compression rate. | `DeflaterMetaCompressor` | -| `deserializeNonexistentClass` | Enables or disables deserialization/skipping of data for non-existent classes. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | -| `codeGenEnabled` | Disabling may result in faster initial serialization but slower subsequent serializations. | `true` | -| `asyncCompilationEnabled` | If enabled, serialization uses interpreter mode first and switches to JIT serialization after async serializer JIT for a class is finished. | `false` | -| `scalaOptimizationEnabled` | Enables or disables Scala-specific serialization optimization. | `false` | -| `copyRef` | When disabled, the copy performance will be better. But fory deep copy will ignore circular and shared reference. Same reference of an object graph will be copied into different objects in one `Fory#copy`. | `true` | -| `serializeEnumByName` | When Enabled, fory serialize enum by name instead of ordinal. | `false` | +| Option Name | Description | Default Value | +| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| `timeRefIgnored` | Whether to ignore reference tracking of all time types registered in `TimeSerializers` and subclasses of those types when ref tracking is enabled. If ignored, ref tracking of every time type can be enabled by invoking `Fory#registerSerializer(Class, Serializer)`. For example, `fory.registerSerializer(Date.class, new DateSerializer(fory, true))`. Note that enabling ref tracking should happen before serializer codegen of any types which contain time fields. Otherwise, those fields will still skip ref tracking. | `true` | +| `compressInt` | Enables or disables int compression for smaller size. | `true` | +| `compressLong` | Enables or disables long compression for smaller size. | `true` | +| `compressString` | Enables or disables string compression for smaller size. | `false` | +| `classLoader` | The classloader should not be updated; Fory caches class metadata. Use `LoaderBinding` or `ThreadSafeFory` for classloader updates. | `Thread.currentThread().getContextClassLoader()` | +| `compatibleMode` | Type forward/backward compatibility config. Also Related to `checkClassVersion` config. `SCHEMA_CONSISTENT`: Class schema must be consistent between serialization peer and deserialization peer. `COMPATIBLE`: Class schema can be different between serialization peer and deserialization peer. They can add/delete fields independently. [See more](#class-inconsistency-and-class-version-check). | `CompatibleMode.SCHEMA_CONSISTENT` | +| `checkClassVersion` | Determines whether to check the consistency of the class schema. If enabled, Fory checks, writes, and checks consistency using the `classVersionHash`. It will be automatically disabled when `CompatibleMode#COMPATIBLE` is enabled. Disabling is not recommended unless you can ensure the class won't evolve. | `false` | +| `checkJdkClassSerializable` | Enables or disables checking of `Serializable` interface for classes under `java.*`. If a class under `java.*` is not `Serializable`, Fory will throw an `UnsupportedOperationException`. | `true` | +| `registerGuavaTypes` | Whether to pre-register Guava types such as `RegularImmutableMap`/`RegularImmutableList`. These types are not public API, but seem pretty stable. | `true` | +| `requireClassRegistration` | Disabling may allow unknown classes to be deserialized, potentially causing security risks. | `true` | +| `requireClassRegistration` | Set max depth for deserialization, when depth exceeds, an exception will be thrown. This can be used to refuse deserialization DDOS attack. | `50` | + +| `suppressClassRegistrationWarnings` | Whether to suppress class registration warnings. The warnings can be used for security audit, but may be annoying, this suppression will be enabled by default. | `true` | +| `metaShareEnabled` | Enables or disables meta share mode. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | +| `scopedMetaShareEnabled` | Scoped meta share focuses on a single serialization process. Metadata created or identified during this process is exclusive to it and is not shared with by other serializations. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | +| `metaCompressor` | Set a compressor for meta compression. Note that the passed MetaCompressor should be thread-safe. By default, a `Deflater` based compressor `DeflaterMetaCompressor` will be used. Users can pass other compressor such as `zstd` for better compression rate. | `DeflaterMetaCompressor` | +| `deserializeNonexistentClass` | Enables or disables deserialization/skipping of data for non-existent classes. | `true` if `CompatibleMode.Compatible` is set, otherwise false. | +| `codeGenEnabled` | Disabling may result in faster initial serialization but slower subsequent serializations. | `true` | +| `asyncCompilationEnabled` | If enabled, serialization uses interpreter mode first and switches to JIT serialization after async serializer JIT for a class is finished. | `false` | +| `scalaOptimizationEnabled` | Enables or disables Scala-specific serialization optimization. | `false` | +| `copyRef` | When disabled, the copy performance will be better. But fory deep copy will ignore circular and shared reference. Same reference of an object graph will be copied into different objects in one `Fory#copy`. | `true` | +| `serializeEnumByName` | When Enabled, fory serialize enum by name instead of ordinal. | `false` | ## Advanced Usage @@ -1167,7 +1169,9 @@ Custom memory allocators are useful for: - **Debugging**: Add logging or tracking to monitor memory usage - **Off-heap Memory**: Integrate with off-heap memory management systems -### Security & Class Registration +### Security + +#### Class Registration `ForyBuilder#requireClassRegistration` can be used to disable class registration, this will allow to deserialize objects unknown types, @@ -1217,6 +1221,12 @@ simplify the customization of class check mechanism. You can use this checker or implement more sophisticated checker by yourself. +#### Limit max deserization depth + +Fory also provides a `ForyBuilder#withMaxDepth` to limit max deserialization depth. The default max depth is 50. + +If max depth is reached, Fory will throw `ForyException`. This can be used to prevent malicious data from causing stack overflow or other issues. + ### Register class by name Register class by id will have better performance and smaller space overhead. But in some cases, management for a bunch
java/fory-core/src/main/java/org/apache/fory/builder/BaseObjectCodecBuilder.java+16 −6 modified@@ -1636,13 +1636,13 @@ protected Expression deserializeForNotNull( obj = deserializeForMap(buffer, typeRef, serializer, invokeHint); } else { if (serializer != null) { - return new Invoke(serializer, "read", OBJECT_TYPE, buffer); + return read(serializer, buffer, OBJECT_TYPE); } if (isMonomorphic(cls)) { serializer = getOrCreateSerializer(cls); Class<?> returnType = ReflectionUtils.getReturnType(getRawType(serializer.type()), "read"); - obj = new Invoke(serializer, "read", TypeRef.of(returnType), buffer); + obj = read(serializer, buffer, TypeRef.of(returnType)); } else { obj = readForNotNullNonFinal(buffer, typeRef, serializer); } @@ -1651,13 +1651,24 @@ protected Expression deserializeForNotNull( } } + protected Expression read(Expression serializer, Expression buffer, TypeRef<?> returnType) { + Class<?> type = returnType.getRawType(); + Expression read = new Invoke(serializer, "read", returnType, buffer); + if (ReflectionUtils.isMonomorphic(type) && !TypeUtils.hasExpandableLeafs(type)) { + return read; + } + read = uninline(read); + return new ListExpression( + new Invoke(foryRef, "incReadDepth"), read, new Invoke(foryRef, "decDepth"), read); + } + protected Expression readForNotNullNonFinal( Expression buffer, TypeRef<?> typeRef, Expression serializer) { if (serializer == null) { Expression classInfo = readClassInfo(getRawType(typeRef), buffer); serializer = inlineInvoke(classInfo, "getSerializer", SERIALIZER_TYPE); } - return new Invoke(serializer, "read", OBJECT_TYPE, buffer); + return read(serializer, buffer, OBJECT_TYPE); } /** @@ -1693,7 +1704,7 @@ protected Expression deserializeForCollection( new If( supportHook, new ListExpression(collection, hookRead), - new Invoke(serializer, "read", OBJECT_TYPE, buffer), + read(serializer, buffer, OBJECT_TYPE), false); if (invokeHint != null && invokeHint.genNewMethod) { invokeHint.add(buffer); @@ -1969,8 +1980,7 @@ chunkHeader, cast(bitand(sizeAndHeader2, ofInt(0xff)), PRIMITIVE_INT_TYPE)), expressions.add(chunksLoop, newMap); // first newMap to create map, last newMap as expr value Expression map = inlineInvoke(serializer, "onMapRead", OBJECT_TYPE, expressions); - Expression action = - new If(supportHook, map, new Invoke(serializer, "read", OBJECT_TYPE, buffer), false); + Expression action = new If(supportHook, map, read(serializer, buffer, OBJECT_TYPE), false); if (invokeHint != null && invokeHint.genNewMethod) { invokeHint.add(buffer); invokeHint.add(serializer);
java/fory-core/src/main/java/org/apache/fory/builder/CodecBuilder.java+1 −1 modified@@ -95,7 +95,7 @@ public abstract class CodecBuilder { protected final boolean isRecord; protected final boolean isInterface; private final Set<String> duplicatedFields; - protected Reference foryRef = new Reference(FORY_NAME, TypeRef.of(Fory.class)); + protected Reference foryRef = Reference.fieldRef(FORY_NAME, TypeRef.of(Fory.class)); public static final Reference recordComponentDefaultValues = new Reference("recordComponentDefaultValues", OBJECT_ARRAY_TYPE); protected final Map<String, Reference> fieldMap = new HashMap<>();
java/fory-core/src/main/java/org/apache/fory/builder/ObjectCodecOptimizer.java+1 −1 modified@@ -121,7 +121,7 @@ private void buildGroups() { MutableTuple3.of( new ArrayList<>(descriptorGrouper.getFinalDescriptors()), 5, finalReadGroups), MutableTuple3.of( - new ArrayList<>(descriptorGrouper.getOtherDescriptors()), 5, otherReadGroups), + new ArrayList<>(descriptorGrouper.getOtherDescriptors()), 4, otherReadGroups), MutableTuple3.of( new ArrayList<>(descriptorGrouper.getOtherDescriptors()), 9, otherWriteGroups)); for (MutableTuple3<List<Descriptor>, Integer, List<List<Descriptor>>> decs : groups) {
java/fory-core/src/main/java/org/apache/fory/config/Config.java+7 −0 modified@@ -63,6 +63,7 @@ public class Config implements Serializable { private final boolean deserializeNonexistentEnumValueAsNull; private final boolean serializeEnumByName; private final int bufferSizeLimitBytes; + private final int maxDepth; public Config(ForyBuilder builder) { name = builder.name; @@ -101,6 +102,7 @@ public Config(ForyBuilder builder) { deserializeNonexistentEnumValueAsNull = builder.deserializeNonexistentEnumValueAsNull; serializeEnumByName = builder.serializeEnumByName; bufferSizeLimitBytes = builder.bufferSizeLimitBytes; + maxDepth = builder.maxDepth; } /** Returns the name for Fory serialization. */ @@ -357,4 +359,9 @@ public int getConfigHash() { } return configHash; } + + /** Returns max depth for deserialization, when depth exceeds, an exception will be thrown. */ + public int maxDepth() { + return maxDepth; + } }
java/fory-core/src/main/java/org/apache/fory/config/ForyBuilder.java+12 −0 modified@@ -38,6 +38,7 @@ import org.apache.fory.serializer.TimeSerializers; import org.apache.fory.serializer.collection.GuavaCollectionSerializers; import org.apache.fory.util.GraalvmSupport; +import org.apache.fory.util.Preconditions; /** Builder class to config and create {@link Fory}. */ // Method naming style for this builder: @@ -86,6 +87,7 @@ public final class ForyBuilder { boolean serializeEnumByName = false; int bufferSizeLimitBytes = 128 * 1024; MetaCompressor metaCompressor = new DeflaterMetaCompressor(); + int maxDepth = 50; public ForyBuilder() {} @@ -348,6 +350,16 @@ public ForyBuilder withAsyncCompilation(boolean asyncCompilation) { return this; } + /** + * Set max depth for deserialization, when depth exceeds, an exception will be thrown. Default max + * depth is 50. + */ + public ForyBuilder withMaxDepth(int maxDepth) { + Preconditions.checkArgument(maxDepth >= 2, "maxDepth must >= 2 but got %s", maxDepth); + this.maxDepth = maxDepth; + return this; + } + /** Whether enable scala-specific serialization optimization. */ public ForyBuilder withScalaOptimizationEnabled(boolean enableScalaOptimization) { this.scalaOptimizationEnabled = enableScalaOptimization;
java/fory-core/src/main/java/org/apache/fory/Fory.java+30 −26 modified@@ -40,6 +40,7 @@ import org.apache.fory.exception.CopyException; import org.apache.fory.exception.DeserializationException; import org.apache.fory.exception.ForyException; +import org.apache.fory.exception.InsecureException; import org.apache.fory.exception.SerializationException; import org.apache.fory.io.ForyInputStream; import org.apache.fory.io.ForyReadableChannel; @@ -126,6 +127,7 @@ public final class Fory implements BaseFory { private Iterator<MemoryBuffer> outOfBandBuffers; private boolean peerOutOfBandEnabled; private int depth; + private final int maxDepth; private int copyDepth; private final boolean copyRefTracking; private final IdentityMap<Object, Object> originToCopyMap; @@ -141,6 +143,7 @@ public Fory(ForyBuilder builder, ClassLoader classLoader) { this.shareMeta = config.isMetaShareEnabled(); compressInt = config.compressInt(); longEncoding = config.longEncoding(); + maxDepth = config.maxDepth(); if (refTracking) { this.refResolver = new MapRefResolver(); } else { @@ -653,17 +656,6 @@ private void writeData(MemoryBuffer buffer, ClassInfo classInfo, Object obj) { case ClassResolver.STRING_CLASS_ID: stringSerializer.writeJavaString(buffer, (String) obj); break; - case ClassResolver.ARRAYLIST_CLASS_ID: - depth++; - arrayListSerializer.write(buffer, (ArrayList) obj); - depth--; - break; - case ClassResolver.HASHMAP_CLASS_ID: - depth++; - hashMapSerializer.write(buffer, (HashMap) obj); - depth--; - break; - // TODO(add fastpath for other types) default: depth++; classInfo.getSerializer().write(buffer, obj); @@ -1024,7 +1016,7 @@ public Object readNullable(MemoryBuffer buffer, ClassInfoHolder classInfoHolder) /** Class should be read already. */ public Object readData(MemoryBuffer buffer, ClassInfo classInfo) { - depth++; + incReadDepth(); Serializer<?> serializer = classInfo.getSerializer(); Object read = serializer.read(buffer); depth--; @@ -1055,19 +1047,8 @@ private Object readDataInternal(MemoryBuffer buffer, ClassInfo classInfo) { return buffer.readFloat64(); case ClassResolver.STRING_CLASS_ID: return stringSerializer.readJavaString(buffer); - case ClassResolver.ARRAYLIST_CLASS_ID: - depth++; - Object list = arrayListSerializer.read(buffer); - depth--; - return list; - case ClassResolver.HASHMAP_CLASS_ID: - depth++; - Object map = hashMapSerializer.read(buffer); - depth--; - return map; - // TODO(add fastpath for other types) default: - depth++; + incReadDepth(); Object read = classInfo.getSerializer().read(buffer); depth--; return read; @@ -1112,7 +1093,7 @@ public Object xreadNonRef(MemoryBuffer buffer) { } public Object xreadNonRef(MemoryBuffer buffer, Serializer<?> serializer) { - depth++; + incReadDepth(); Object o = serializer.xread(buffer); depth--; return o; @@ -1142,7 +1123,7 @@ public Object xreadNonRef(MemoryBuffer buffer, ClassInfo classInfo) { return buffer.readFloat64(); // TODO(add fastpath for other types) default: - depth++; + incReadDepth(); Object o = classInfo.getSerializer().xread(buffer); depth--; return o; @@ -1682,6 +1663,29 @@ public void incDepth(int diff) { this.depth += diff; } + public void incDepth() { + this.depth += 1; + } + + public void decDepth() { + this.depth -= 1; + } + + public void incReadDepth() { + if ((this.depth += 1) > maxDepth) { + throwReadDepthExceedException(); + } + } + + private void throwReadDepthExceedException() { + throw new InsecureException( + String.format( + "Read depth exceed max depth %s, " + + "the deserialization data may be malicious. If it's not malicious, " + + "please increase max read depth by ForyBuilder#withMaxDepth(largerDepth)", + maxDepth)); + } + public void incCopyDepth(int diff) { this.copyDepth += diff; }
java/fory-core/src/main/java/org/apache/fory/resolver/XtypeResolver.java+4 −4 modified@@ -647,19 +647,19 @@ private void throwUnexpectTypeIdException(long xtypeId) { } private ClassInfo getListClassInfo() { - fory.incDepth(1); + fory.incReadDepth(); GenericType genericType = generics.nextGenericType(); - fory.incDepth(-1); + fory.decDepth(); if (genericType != null) { return getOrBuildClassInfo(genericType.getCls()); } return xtypeIdToClassMap.get(Types.LIST); } private ClassInfo getGenericClassInfo() { - fory.incDepth(1); + fory.incReadDepth(); GenericType genericType = generics.nextGenericType(); - fory.incDepth(-1); + fory.decDepth(); if (genericType != null) { return getOrBuildClassInfo(genericType.getCls()); }
java/fory-core/src/main/java/org/apache/fory/serializer/AbstractObjectSerializer.java+8 −4 modified@@ -89,15 +89,17 @@ static Object readFinalObjectFieldValue( boolean isFinal, MemoryBuffer buffer) { Serializer<Object> serializer = fieldInfo.classInfo.getSerializer(); + binding.incReadDepth(); Object fieldValue; boolean nullable = fieldInfo.nullable; if (isFinal) { if (!fieldInfo.trackingRef) { - return binding.readNullable(buffer, serializer, nullable); + fieldValue = binding.readNullable(buffer, serializer, nullable); + } else { + // whether tracking ref is recorded in `fieldInfo.serializer`, so it's still + // consistent with jit serializer. + fieldValue = binding.readRef(buffer, serializer); } - // whether tracking ref is recorded in `fieldInfo.serializer`, so it's still - // consistent with jit serializer. - fieldValue = binding.readRef(buffer, serializer); } else { if (serializer.needToWriteRef()) { int nextReadRefId = refResolver.tryPreserveRefId(buffer); @@ -112,13 +114,15 @@ static Object readFinalObjectFieldValue( if (nullable) { byte headFlag = buffer.readByte(); if (headFlag == Fory.NULL_FLAG) { + binding.decDepth(); return null; } } typeResolver.readClassInfo(buffer, fieldInfo.classInfo); fieldValue = serializer.read(buffer); } } + binding.decDepth(); return fieldValue; }
java/fory-core/src/main/java/org/apache/fory/serializer/collection/CollectionLikeSerializer.java+2 −2 modified@@ -662,7 +662,7 @@ private <T extends Collection> void readSameTypeElements( int flags, T collection, int numElements) { - fory.incDepth(1); + fory.incReadDepth(); if ((flags & CollectionFlags.TRACKING_REF) == CollectionFlags.TRACKING_REF) { for (int i = 0; i < numElements; i++) { collection.add(binding.readRef(buffer, serializer)); @@ -682,7 +682,7 @@ private <T extends Collection> void readSameTypeElements( } } } - fory.incDepth(-1); + fory.decDepth(); } /** Read elements whose type are different. */
java/fory-core/src/main/java/org/apache/fory/serializer/collection/MapLikeSerializer.java+16 −8 modified@@ -689,11 +689,13 @@ public long readJavaNullChunk( if (keySerializer == null) { key = readNonEmptyValueFromNullChunk(buffer, trackKeyRef, true); } else { + fory.incReadDepth(); if (trackKeyRef) { key = binding.readRef(buffer, keySerializer); } else { key = binding.read(buffer, keySerializer); } + fory.decDepth(); } } else { key = binding.readRef(buffer, keyClassInfoReadCache); @@ -729,11 +731,13 @@ private void readNullKeyChunk( if (valueSerializer == null) { value = readNonEmptyValueFromNullChunk(buffer, trackValueRef, false); } else { + fory.incReadDepth(); if (trackValueRef) { value = binding.readRef(buffer, valueSerializer); } else { value = binding.read(buffer, valueSerializer); } + fory.decDepth(); } } else { value = binding.readRef(buffer, valueClassInfoReadCache); @@ -753,15 +757,15 @@ private Object readNonEmptyValueFromNullChunk( } GenericType type = isKey ? genericType.getTypeParameter0() : genericType.getTypeParameter1(); generics.pushGenericType(type); - fory.incDepth(1); Serializer<?> serializer = type.getSerializer(typeResolver); Object v; + fory.incReadDepth(); if (trackRef) { v = binding.readRef(buffer, serializer); } else { v = binding.read(buffer, serializer); } - fory.incDepth(-1); + fory.decDepth(); generics.popGenericType(); return v; } @@ -781,8 +785,10 @@ public long readNullChunkKVFinalNoRef( if (!valueHasNull) { return (size << 8) | chunkHeader; } else { + fory.incReadDepth(); Object key = binding.read(buffer, keySerializer); map.put(key, null); + fory.decDepth(); } } else { readNullKeyChunk(buffer, map, chunkHeader, valueSerializer, valueHasNull); @@ -815,6 +821,7 @@ private long readJavaChunk( if (!valueIsDeclaredType) { valueSerializer = typeResolver.readClassInfo(buffer, valueClassInfoReadCache).getSerializer(); } + fory.incReadDepth(); for (int i = 0; i < chunkSize; i++) { Object key = trackKeyRef @@ -827,6 +834,7 @@ private long readJavaChunk( map.put(key, value); size--; } + fory.decDepth(); return size > 0 ? (size << 8) | buffer.readUnsignedByte() : 0; } @@ -866,28 +874,28 @@ private long readJavaChunkGeneric( if (keyGenericType.hasGenericParameters() || valueGenericType.hasGenericParameters()) { for (int i = 0; i < chunkSize; i++) { generics.pushGenericType(keyGenericType); - fory.incDepth(1); + fory.incReadDepth(); Object key = trackKeyRef ? binding.readRef(buffer, keySerializer) : binding.read(buffer, keySerializer); - fory.incDepth(-1); + fory.decDepth(); generics.popGenericType(); generics.pushGenericType(valueGenericType); - fory.incDepth(1); + fory.incReadDepth(); Object value = trackValueRef ? binding.readRef(buffer, valueSerializer) : binding.read(buffer, valueSerializer); - fory.incDepth(-1); + fory.decDepth(); generics.popGenericType(); map.put(key, value); size--; } } else { for (int i = 0; i < chunkSize; i++) { // increase depth to avoid read wrong outer generic type - fory.incDepth(1); + fory.incReadDepth(); Object key = trackKeyRef ? binding.readRef(buffer, keySerializer) @@ -896,7 +904,7 @@ private long readJavaChunkGeneric( trackValueRef ? binding.readRef(buffer, valueSerializer) : binding.read(buffer, valueSerializer); - fory.incDepth(-1); + fory.decDepth(); map.put(key, value); size--; }
java/fory-core/src/main/java/org/apache/fory/serializer/SerializationBinding.java+58 −50 modified@@ -34,63 +34,88 @@ // If it's used in other packages in fory, duplicate it in those packages. @SuppressWarnings({"rawtypes", "unchecked"}) // noinspection Duplicates -interface SerializationBinding { - <T> void writeRef(MemoryBuffer buffer, T obj); +abstract class SerializationBinding { + protected final Fory fory; + protected final RefResolver refResolver; - <T> void writeRef(MemoryBuffer buffer, T obj, Serializer<T> serializer); + SerializationBinding(Fory fory) { + this.fory = fory; + this.refResolver = fory.getRefResolver(); + } + + abstract <T> void writeRef(MemoryBuffer buffer, T obj); + + abstract <T> void writeRef(MemoryBuffer buffer, T obj, Serializer<T> serializer); - void writeRef(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); + abstract void writeRef(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); - void writeRef(MemoryBuffer buffer, Object obj, ClassInfo classInfo); + abstract void writeRef(MemoryBuffer buffer, Object obj, ClassInfo classInfo); - void writeNonRef(MemoryBuffer buffer, Object obj); + abstract void writeNonRef(MemoryBuffer buffer, Object obj); - void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfo classInfo); + abstract void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfo classInfo); - void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); + abstract void writeNonRef(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); - void writeNullable(MemoryBuffer buffer, Object obj); + abstract void writeNullable(MemoryBuffer buffer, Object obj); - void writeNullable(MemoryBuffer buffer, Object obj, Serializer serializer); + abstract void writeNullable(MemoryBuffer buffer, Object obj, Serializer serializer); - void writeNullable(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); + abstract void writeNullable(MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder); - void writeNullable(MemoryBuffer buffer, Object obj, ClassInfo classInfo); + abstract void writeNullable(MemoryBuffer buffer, Object obj, ClassInfo classInfo); - void writeNullable( + abstract void writeNullable( MemoryBuffer buffer, Object obj, ClassInfoHolder classInfoHolder, boolean nullable); - void writeNullable(MemoryBuffer buffer, Object obj, Serializer serializer, boolean nullable); + abstract void writeNullable( + MemoryBuffer buffer, Object obj, Serializer serializer, boolean nullable); - void writeContainerFieldValue(MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo); + abstract void writeContainerFieldValue( + MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo); - void write(MemoryBuffer buffer, Serializer serializer, Object value); + abstract void write(MemoryBuffer buffer, Serializer serializer, Object value); - Object read(MemoryBuffer buffer, Serializer serializer); + abstract Object read(MemoryBuffer buffer, Serializer serializer); - <T> T readRef(MemoryBuffer buffer, Serializer<T> serializer); + abstract <T> T readRef(MemoryBuffer buffer, Serializer<T> serializer); - Object readRef(MemoryBuffer buffer, GenericTypeField field); + abstract Object readRef(MemoryBuffer buffer, GenericTypeField field); - Object readRef(MemoryBuffer buffer, ClassInfoHolder classInfoHolder); + abstract Object readRef(MemoryBuffer buffer, ClassInfoHolder classInfoHolder); - Object readRef(MemoryBuffer buffer); + abstract Object readRef(MemoryBuffer buffer); - Object readNonRef(MemoryBuffer buffer); + abstract Object readNonRef(MemoryBuffer buffer); - Object readNonRef(MemoryBuffer buffer, ClassInfoHolder classInfoHolder); + abstract Object readNonRef(MemoryBuffer buffer, ClassInfoHolder classInfoHolder); - Object readNonRef(MemoryBuffer buffer, GenericTypeField field); + abstract Object readNonRef(MemoryBuffer buffer, GenericTypeField field); - Object readNullable(MemoryBuffer buffer, Serializer<Object> serializer); + abstract Object readNullable(MemoryBuffer buffer, Serializer<Object> serializer); - Object readNullable(MemoryBuffer buffer, Serializer<Object> serializer, boolean nullable); + abstract Object readNullable( + MemoryBuffer buffer, Serializer<Object> serializer, boolean nullable); - Object readContainerFieldValue(MemoryBuffer buffer, GenericTypeField field); + abstract Object readContainerFieldValue(MemoryBuffer buffer, GenericTypeField field); - Object readContainerFieldValueRef(MemoryBuffer buffer, GenericTypeField fieldInfo); + abstract Object readContainerFieldValueRef(MemoryBuffer buffer, GenericTypeField fieldInfo); - int preserveRefId(int refId); + public int preserveRefId(int refId) { + return refResolver.preserveRefId(refId); + } + + void incReadDepth() { + fory.incReadDepth(); + } + + void incDepth() { + fory.incDepth(); + } + + void decDepth() { + fory.decDepth(); + } static SerializationBinding createBinding(Fory fory) { if (fory.isCrossLanguage()) { @@ -100,15 +125,12 @@ static SerializationBinding createBinding(Fory fory) { } } - final class JavaSerializationBinding implements SerializationBinding { - private final Fory fory; + static final class JavaSerializationBinding extends SerializationBinding { private final ClassResolver classResolver; - private final RefResolver refResolver; JavaSerializationBinding(Fory fory) { - this.fory = fory; + super(fory); classResolver = fory.getClassResolver(); - refResolver = fory.getRefResolver(); } @Override @@ -290,23 +312,14 @@ public void writeContainerFieldValue( MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo) { fory.writeNonRef(buffer, fieldValue, classInfo); } - - @Override - public int preserveRefId(int refId) { - return refResolver.preserveRefId(refId); - } } - final class XlangSerializationBinding implements SerializationBinding { - - private final Fory fory; + static final class XlangSerializationBinding extends SerializationBinding { private final XtypeResolver xtypeResolver; - private final RefResolver refResolver; XlangSerializationBinding(Fory fory) { - this.fory = fory; + super(fory); xtypeResolver = fory.getXtypeResolver(); - refResolver = fory.getRefResolver(); } @Override @@ -500,10 +513,5 @@ public void writeContainerFieldValue( MemoryBuffer buffer, Object fieldValue, ClassInfo classInfo) { fory.xwriteData(buffer, classInfo, fieldValue); } - - @Override - public int preserveRefId(int refId) { - return refResolver.preserveRefId(refId); - } } }
java/fory-core/src/main/java/org/apache/fory/type/TypeUtils.java+75 −0 modified@@ -20,6 +20,7 @@ package org.apache.fory.type; import java.lang.reflect.Array; +import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; @@ -30,12 +31,27 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.sql.Date; +import java.sql.Time; import java.sql.Timestamp; +import java.time.Duration; import java.time.Instant; import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.MonthDay; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.Period; +import java.time.Year; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; import java.util.Collection; +import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -48,6 +64,8 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.Set; +import java.util.TimeZone; +import java.util.WeakHashMap; import java.util.stream.Collectors; import org.apache.fory.collection.IdentityMap; import org.apache.fory.collection.Tuple2; @@ -834,4 +852,61 @@ public static String qualifiedName(String pkg, String className) { return pkg + "." + className; } } + + private static Set<Class<?>> leafTypes = new HashSet<>(); + + static { + leafTypes.addAll(sortedPrimitiveClasses); + leafTypes.addAll(sortedBoxedClasses); + leafTypes.add(String.class); + leafTypes.addAll( + Arrays.asList( + java.util.Date.class, + java.sql.Date.class, + Time.class, + Timestamp.class, + LocalDate.class, + LocalTime.class, + LocalDateTime.class, + Instant.class, + Duration.class, + ZoneId.class, + ZoneOffset.class, + ZonedDateTime.class, + Year.class, + YearMonth.class, + MonthDay.class, + Period.class, + OffsetTime.class, + OffsetDateTime.class, + Calendar.class, + GregorianCalendar.class, + TimeZone.class)); + } + + private static final WeakHashMap<Class<?>, Boolean> hasExpandableLeafsCache = new WeakHashMap<>(); + + public static synchronized boolean hasExpandableLeafs(Class<?> cls) { + return hasExpandableLeafsCache.computeIfAbsent( + cls, + k -> { + if (cls.isEnum()) { + return false; + } + if (leafTypes.contains(cls)) { + return false; + } + List<Field> fields = ReflectionUtils.getFields(cls, true); + if (fields.isEmpty()) { + return false; + } + for (Field field : fields) { + Class<?> fieldType = field.getType(); + if (!leafTypes.contains(fieldType) && !fieldType.isEnum()) { + return true; + } + } + return false; + }); + } }
java/fory-core/src/test/java/org/apache/fory/ForyTestBase.java+1 −28 modified@@ -187,34 +187,7 @@ public static Object[][] javaForyConfig() { .requireClassRegistration(false) .suppressClassRegistrationWarnings(true) .build() - }, - { - Fory.builder() - .withLanguage(Language.JAVA) - .withRefTracking(false) - .withCodegen(false) - .requireClassRegistration(false) - .suppressClassRegistrationWarnings(true) - .build() - }, - { - Fory.builder() - .withLanguage(Language.JAVA) - .withRefTracking(true) - .withCodegen(true) - .requireClassRegistration(false) - .suppressClassRegistrationWarnings(true) - .build() - }, - { - Fory.builder() - .withLanguage(Language.JAVA) - .withRefTracking(false) - .withCodegen(true) - .requireClassRegistration(false) - .suppressClassRegistrationWarnings(true) - .build() - }, + } }; }
java/fory-core/src/test/java/org/apache/fory/ForyTest.java+39 −0 modified@@ -35,6 +35,7 @@ import java.sql.Timestamp; import java.time.Instant; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; @@ -73,6 +74,7 @@ import org.apache.fory.test.bean.BeanA; import org.apache.fory.test.bean.Struct; import org.apache.fory.type.Descriptor; +import org.apache.fory.type.TypeUtils; import org.apache.fory.util.DateTimeUtils; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -690,4 +692,41 @@ public void testDeserializeJavaObjectWrongType() { Assert.assertThrows( DeserializationException.class, () -> fory.deserializeJavaObject(bytes, Struct2.class)); } + + private Object maxDepthData() { + List<Object> list = new ArrayList<>(); + for (int i = 0; i < 6; i++) { + List<Object> list1 = new ArrayList<>(); + list1.add("abc"); + list1.add(list); + list = list1; + } + return list; + } + + @Test + public void testMaxDepth() { + byte[] bytes = Fory.builder().requireClassRegistration(false).build().serialize(maxDepthData()); + Fory fory = + Fory.builder().requireClassRegistration(false).withName("fory1").withMaxDepth(3).build(); + assertThrows(InsecureException.class, () -> fory.deserialize(bytes)); + } + + @AllArgsConstructor + static class MaxDepth { + int f1; + Object f2; + } + + @Test + public void testMaxDepthCodegen() { + assertTrue(TypeUtils.hasExpandableLeafs(MaxDepth.class)); + MaxDepth maxDepth = + new MaxDepth( + 1, new MaxDepth(2, new MaxDepth(3, new MaxDepth(4, new MaxDepth(5, maxDepthData()))))); + byte[] bytes = Fory.builder().requireClassRegistration(false).build().serialize(maxDepth); + Fory fory = + Fory.builder().requireClassRegistration(false).withName("fory2").withMaxDepth(3).build(); + assertThrows(InsecureException.class, () -> fory.deserialize(bytes)); + } }
java/fory-core/src/test/java/org/apache/fory/type/TypeUtilsTest.java+31 −0 modified@@ -20,9 +20,14 @@ package org.apache.fory.type; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; import com.google.common.collect.ImmutableList; +import com.google.common.primitives.Primitives; import java.lang.reflect.Type; +import java.time.Duration; +import java.time.Instant; import java.util.AbstractList; import java.util.ArrayList; import java.util.Arrays; @@ -39,6 +44,7 @@ import org.apache.fory.reflect.TypeRef; import org.apache.fory.test.bean.BeanA; import org.apache.fory.test.bean.BeanB; +import org.apache.fory.test.bean.Foo; import org.testng.Assert; import org.testng.annotations.Test; @@ -289,4 +295,29 @@ public void getAllTypeArguments() { assertEquals(allTypeArguments.size(), 3); assertEquals(allTypeArguments.get(2).getRawType(), BeanA.class); } + + static class SingleBasicFieldStruct { + int f1; + } + + static class SingleExpandableFieldStruct { + Object f1; + } + + @Test + public void testHasExpandableLeafs() { + for (Class<?> type : Primitives.allPrimitiveTypes()) { + assertFalse(TypeUtils.hasExpandableLeafs(type)); + } + for (Class<?> type : Primitives.allWrapperTypes()) { + assertFalse(TypeUtils.hasExpandableLeafs(type)); + } + assertFalse(TypeUtils.hasExpandableLeafs(Instant.class)); + assertFalse(TypeUtils.hasExpandableLeafs(Duration.class)); + assertFalse(TypeUtils.hasExpandableLeafs(Foo.class)); + assertTrue(TypeUtils.hasExpandableLeafs(BeanB.class)); + assertTrue(TypeUtils.hasExpandableLeafs(BeanA.class)); + assertFalse(TypeUtils.hasExpandableLeafs(SingleBasicFieldStruct.class)); + assertTrue(TypeUtils.hasExpandableLeafs(SingleExpandableFieldStruct.class)); + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- fory.apache.org/security/mitrevendor-advisory
- github.com/advisories/GHSA-5hmf-8wx5-4qq3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-59328ghsaADVISORY
- www.openwall.com/lists/oss-security/2025/09/15/1ghsaWEB
- fory.apache.org/securityghsaWEB
- github.com/apache/fory/commit/c3b4d3fe389e38495aeb8301d75da1ab658f0c6eghsaWEB
- github.com/apache/fory/pull/2578ghsaWEB
- github.com/apache/fory/releases/tag/v0.12.2ghsaWEB
News mentions
0No linked articles in our index yet.