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 13eda46e4 feat(dart): add configurable deserialization size guardrails
(#3434)
13eda46e4 is described below
commit 13eda46e40503c41425ba58d05ebad50c586f524
Author: Yash Agarwal <[email protected]>
AuthorDate: Sat Feb 28 16:48:21 2026 +0530
feat(dart): add configurable deserialization size guardrails (#3434)
## Why?
Untrusted payloads can encode arbitrarily large collection/binary
lengths,
causing pre-allocation of huge buffers and OOM crashes before any
elements are even deserialized.
## What does this PR do?
- Adds two config options `maxBinarySize` and `maxCollectionSize`
to `ForyConfig` (defaults to 1000000 Collection Size and 64MB for
BinarySize).
- Threads `ForyConfig` into `DeserializationContext` so serializers can
access limits at read time.
- Adds `InvalidDataException` following existing exception conventions.
- Enforces `maxCollectionSize` in `ListSerializer`, `SetSerializer`, and
`MapSerializer` before pre-allocation.
- Enforces `maxBinarySize` in `NumericArraySerializer` for
`ObjType.BINARY`
before buffer copy.
- Adds 15 test cases covering within-limit, at-limit, exceeded, empty,
and default scenarios for lists, sets, maps, and binary.
## Related issues
Closes #3415.
## Does this PR introduce any user-facing change?
- [x] Does this PR introduce any public API change?
- [ ] Does this PR introduce any binary protocol compatibility change?
## Benchmark
N/A — guard checks are a single null-check + comparison per
collection/binary read; no measurable performance impact.
---
.../test/config_test/size_guard_test.dart | 199 +++++++++++++++++++++
dart/packages/fory/lib/src/config/fory_config.dart | 7 +
.../fory/lib/src/deserialization_context.dart | 3 +
.../fory/lib/src/deserialization_dispatcher.dart | 1 +
.../fory/lib/src/exception/fory_exception.dart | 11 ++
dart/packages/fory/lib/src/fory_impl.dart | 4 +
.../fory/lib/src/serializer/array_serializer.dart | 7 +
.../collection/list/list_serializer.dart | 6 +
.../serializer/collection/map/map_serializer.dart | 6 +
.../serializer/collection/set/set_serializer.dart | 6 +
10 files changed, 250 insertions(+)
diff --git a/dart/packages/fory-test/test/config_test/size_guard_test.dart
b/dart/packages/fory-test/test/config_test/size_guard_test.dart
new file mode 100644
index 000000000..a52ed2b56
--- /dev/null
+++ b/dart/packages/fory-test/test/config_test/size_guard_test.dart
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+library;
+
+import 'dart:typed_data';
+import 'package:fory/fory.dart';
+import 'package:fory/src/exception/fory_exception.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('maxCollectionSize guard check', () {
+ test('list within limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize([1, 2, 3]);
+
+ final foryRead = Fory(maxCollectionSize: 10);
+ final result = foryRead.deserialize(bytes);
+ expect(result, equals([1, 2, 3]));
+ });
+
+ test('list exceeding limit throws InvalidDataException', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize([1, 2, 3, 4, 5]);
+
+ final foryRead = Fory(maxCollectionSize: 3);
+ expect(
+ () => foryRead.deserialize(bytes),
+ throwsA(isA<InvalidDataException>()),
+ );
+ });
+
+ test('list at exact limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize([1, 2, 3]);
+
+ final foryRead = Fory(maxCollectionSize: 3);
+ final result = foryRead.deserialize(bytes);
+ expect(result, equals([1, 2, 3]));
+ });
+
+ test('empty list always deserializes successfully', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize(<int>[]);
+
+ final foryRead = Fory(maxCollectionSize: 0);
+ final result = foryRead.deserialize(bytes);
+ expect(result, equals([]));
+ });
+
+ test('map exceeding limit throws InvalidDataException', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize({'a': 1, 'b': 2, 'c': 3});
+
+ final foryRead = Fory(maxCollectionSize: 2);
+ expect(
+ () => foryRead.deserialize(bytes),
+ throwsA(isA<InvalidDataException>()),
+ );
+ });
+
+ test('map within limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize({'a': 1, 'b': 2});
+
+ final foryRead = Fory(maxCollectionSize: 10);
+ final result = foryRead.deserialize(bytes);
+ expect(result, equals({'a': 1, 'b': 2}));
+ });
+
+ test('set exceeding limit throws InvalidDataException', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize({1, 2, 3, 4, 5});
+
+ final foryRead = Fory(maxCollectionSize: 2);
+ expect(
+ () => foryRead.deserialize(bytes),
+ throwsA(isA<InvalidDataException>()),
+ );
+ });
+
+ test('set within limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final bytes = foryWrite.serialize({1, 2, 3});
+
+ final foryRead = Fory(maxCollectionSize: 10);
+ final result = foryRead.deserialize(bytes);
+ expect(result, equals({1, 2, 3}));
+ });
+
+ test('default maxCollectionSize allows normal sizes', () {
+ final foryWrite = Fory();
+ final largeList = List.generate(1000, (i) => i);
+ final bytes = foryWrite.serialize(largeList);
+
+ final foryRead = Fory();
+ final result = foryRead.deserialize(bytes) as List;
+ expect(result.length, 1000);
+ });
+ });
+
+ group('maxBinarySize guard check', () {
+ test('binary within limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final data = Uint8List.fromList([1, 2, 3, 4, 5]);
+ final bytes = foryWrite.serialize(data);
+
+ final foryRead = Fory(maxBinarySize: 10);
+ final result = foryRead.deserialize(bytes) as Uint8List;
+ expect(result, equals(data));
+ });
+
+ test('binary exceeding limit throws InvalidDataException', () {
+ final foryWrite = Fory();
+ final data = Uint8List.fromList([1, 2, 3, 4, 5]);
+ final bytes = foryWrite.serialize(data);
+
+ final foryRead = Fory(maxBinarySize: 3);
+ expect(
+ () => foryRead.deserialize(bytes),
+ throwsA(isA<InvalidDataException>()),
+ );
+ });
+
+ test('binary at exact limit deserializes successfully', () {
+ final foryWrite = Fory();
+ final data = Uint8List.fromList([1, 2, 3]);
+ final bytes = foryWrite.serialize(data);
+
+ final foryRead = Fory(maxBinarySize: 3);
+ final result = foryRead.deserialize(bytes) as Uint8List;
+ expect(result, equals(data));
+ });
+
+ test('empty binary always deserializes successfully', () {
+ final foryWrite = Fory();
+ final data = Uint8List(0);
+ final bytes = foryWrite.serialize(data);
+
+ final foryRead = Fory(maxBinarySize: 0);
+ final result = foryRead.deserialize(bytes) as Uint8List;
+ expect(result, equals(data));
+ });
+
+ test('default maxBinarySize allows normal sizes', () {
+ final foryWrite = Fory();
+ final data = Uint8List.fromList(List.generate(1000, (i) => i % 256));
+ final bytes = foryWrite.serialize(data);
+
+ final foryRead = Fory();
+ final result = foryRead.deserialize(bytes) as Uint8List;
+ expect(result.length, 1000);
+ });
+ });
+
+ group('combined guard check', () {
+ test('both limits enforced independently', () {
+ final foryWrite = Fory();
+
+ final listBytes = foryWrite.serialize([1, 2, 3, 4, 5]);
+ final binaryBytes =
+ foryWrite.serialize(Uint8List.fromList([1, 2, 3, 4, 5]));
+
+ final foryRead = Fory(maxCollectionSize: 3, maxBinarySize: 10);
+
+ // Collection exceeds limit
+ expect(
+ () => foryRead.deserialize(listBytes),
+ throwsA(isA<InvalidDataException>()),
+ );
+
+ // Binary within limit
+ final result = foryRead.deserialize(binaryBytes) as Uint8List;
+ expect(result.length, 5);
+ });
+
+ test('default values are applied', () {
+ final config = ForyConfig();
+ expect(config.maxCollectionSize, ForyConfig.defaultMaxCollectionSize);
+ expect(config.maxBinarySize, ForyConfig.defaultMaxBinarySize);
+ });
+ });
+}
diff --git a/dart/packages/fory/lib/src/config/fory_config.dart
b/dart/packages/fory/lib/src/config/fory_config.dart
index 73c3b6f12..8c92ae34f 100644
--- a/dart/packages/fory/lib/src/config/fory_config.dart
+++ b/dart/packages/fory/lib/src/config/fory_config.dart
@@ -23,6 +23,11 @@ final class ForyConfig {
final bool basicTypesRefIgnored;
final bool timeRefIgnored;
final bool stringRefIgnored;
+ final int maxBinarySize;
+ final int maxCollectionSize;
+
+ static const int defaultMaxBinarySize = 64 * 1024 * 1024;
+ static const int defaultMaxCollectionSize = 1000000;
const ForyConfig({
this.compatible = false,
@@ -30,5 +35,7 @@ final class ForyConfig {
this.basicTypesRefIgnored = true,
this.timeRefIgnored = true,
this.stringRefIgnored = false,
+ this.maxBinarySize = defaultMaxBinarySize,
+ this.maxCollectionSize = defaultMaxCollectionSize,
});
}
diff --git a/dart/packages/fory/lib/src/deserialization_context.dart
b/dart/packages/fory/lib/src/deserialization_context.dart
index c98560153..764a18731 100644
--- a/dart/packages/fory/lib/src/deserialization_context.dart
+++ b/dart/packages/fory/lib/src/deserialization_context.dart
@@ -17,6 +17,7 @@
* under the License.
*/
+import 'package:fory/src/config/fory_config.dart';
import 'package:fory/src/deserialization_dispatcher.dart';
import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart';
import 'package:fory/src/resolver/deserialization_ref_resolver.dart';
@@ -26,6 +27,7 @@ import 'package:fory/src/runtime_context.dart';
import 'package:fory/src/collection/stack.dart';
final class DeserializationContext extends Pack {
+ final ForyConfig config;
final HeaderBrief header;
final DeserializationDispatcher deserializationDispatcher;
@@ -38,6 +40,7 @@ final class DeserializationContext extends Pack {
const DeserializationContext(
super.structHashResolver,
super.getTagByDartType,
+ this.config,
this.header,
this.deserializationDispatcher,
this.refResolver,
diff --git a/dart/packages/fory/lib/src/deserialization_dispatcher.dart
b/dart/packages/fory/lib/src/deserialization_dispatcher.dart
index ede6f8d9c..c9e38febd 100644
--- a/dart/packages/fory/lib/src/deserialization_dispatcher.dart
+++ b/dart/packages/fory/lib/src/deserialization_dispatcher.dart
@@ -58,6 +58,7 @@ class DeserializationDispatcher {
DeserializationContext deserializationContext = DeserializationContext(
StructHashResolver.inst,
typeResolver.getRegisteredTag,
+ conf,
header,
this,
DeserializationRefResolver.getOne(conf.ref),
diff --git a/dart/packages/fory/lib/src/exception/fory_exception.dart
b/dart/packages/fory/lib/src/exception/fory_exception.dart
index 3d0dafcc9..71f20e0ac 100644
--- a/dart/packages/fory/lib/src/exception/fory_exception.dart
+++ b/dart/packages/fory/lib/src/exception/fory_exception.dart
@@ -29,3 +29,14 @@ abstract class ForyException extends Error {
return buf.toString();
}
}
+
+class InvalidDataException extends ForyException {
+ final String message;
+
+ InvalidDataException(this.message);
+
+ @override
+ void giveExceptionMessage(StringBuffer buf) {
+ buf.write(message);
+ }
+}
diff --git a/dart/packages/fory/lib/src/fory_impl.dart
b/dart/packages/fory/lib/src/fory_impl.dart
index d2298ad46..141d91076 100644
--- a/dart/packages/fory/lib/src/fory_impl.dart
+++ b/dart/packages/fory/lib/src/fory_impl.dart
@@ -42,6 +42,8 @@ final class Fory {
bool basicTypesRefIgnored = true,
bool timeRefIgnored = true,
bool stringRefIgnored = false,
+ int maxBinarySize = ForyConfig.defaultMaxBinarySize,
+ int maxCollectionSize = ForyConfig.defaultMaxCollectionSize,
}) : this.fromConfig(
ForyConfig(
compatible: compatible,
@@ -49,6 +51,8 @@ final class Fory {
basicTypesRefIgnored: basicTypesRefIgnored,
timeRefIgnored: timeRefIgnored,
stringRefIgnored: stringRefIgnored,
+ maxBinarySize: maxBinarySize,
+ maxCollectionSize: maxCollectionSize,
),
);
diff --git a/dart/packages/fory/lib/src/serializer/array_serializer.dart
b/dart/packages/fory/lib/src/serializer/array_serializer.dart
index e164afa15..6ad67ded5 100644
--- a/dart/packages/fory/lib/src/serializer/array_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/array_serializer.dart
@@ -18,7 +18,9 @@
*/
import 'dart:typed_data';
+import 'package:fory/src/const/types.dart';
import 'package:fory/src/deserialization_context.dart';
+import 'package:fory/src/exception/fory_exception.dart';
import 'package:fory/src/memory/byte_reader.dart';
import 'package:fory/src/memory/byte_writer.dart';
import 'package:fory/src/serialization_context.dart';
@@ -60,6 +62,11 @@ abstract base class NumericArraySerializer<T extends num>
@override
TypedDataList<T> read(ByteReader br, int refId, DeserializationContext pack)
{
int numBytes = br.readVarUint32Small7();
+ if (objType == ObjType.BINARY && numBytes > pack.config.maxBinarySize) {
+ throw InvalidDataException(
+ 'Binary size $numBytes exceeds maxBinarySize
${pack.config.maxBinarySize}. '
+ 'The input data may be malicious, or need to increase the
maxBinarySize when creating Fory.');
+ }
int length = numBytes ~/ bytesPerNum;
if (isLittleEndian || bytesPerNum == 1) {
// Fast path: direct memory copy on little-endian or for single-byte
types
diff --git
a/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart
b/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart
index 55e75c43d..67bb80f0d 100644
--- a/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/collection/list/list_serializer.dart
@@ -20,6 +20,7 @@
import 'package:fory/src/const/ref_flag.dart';
import 'package:fory/src/const/types.dart';
import 'package:fory/src/deserialization_context.dart';
+import 'package:fory/src/exception/fory_exception.dart';
import 'package:fory/src/memory/byte_reader.dart';
import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart';
import 'package:fory/src/serializer/collection/iterable_serializer.dart';
@@ -33,6 +34,11 @@ abstract base class ListSerializer extends
IterableSerializer {
@override
List read(ByteReader br, int refId, DeserializationContext pack) {
int num = br.readVarUint32Small7();
+ if (num > pack.config.maxCollectionSize) {
+ throw InvalidDataException(
+ 'List size $num exceeds maxCollectionSize
${pack.config.maxCollectionSize}. '
+ 'The input data may be malicious, or need to increase the
maxCollectionSize when creating Fory.');
+ }
TypeSpecWrap? elemWrap = pack.typeWrapStack.peek?.param0;
List list = newList(
num,
diff --git
a/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart
b/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart
index 27e698a16..8176b8716 100644
--- a/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/collection/map/map_serializer.dart
@@ -18,6 +18,7 @@
*/
import 'package:fory/src/deserialization_context.dart';
+import 'package:fory/src/exception/fory_exception.dart';
import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart';
import 'package:fory/src/const/types.dart';
import 'package:fory/src/memory/byte_reader.dart';
@@ -51,6 +52,11 @@ abstract base class MapSerializer<T extends Map<Object?,
Object?>>
@override
T read(ByteReader br, int refId, DeserializationContext pack) {
int remaining = br.readVarUint32Small7();
+ if (remaining > pack.config.maxCollectionSize) {
+ throw InvalidDataException(
+ 'Map size $remaining exceeds maxCollectionSize
${pack.config.maxCollectionSize}. '
+ 'The input data may be malicious, or need to increase the
maxCollectionSize when creating Fory.');
+ }
T map = newMap(remaining);
if (writeRef) {
pack.refResolver.setRefTheLatestId(map);
diff --git
a/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart
b/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart
index a0622c1a5..b2612f29d 100644
--- a/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart
+++ b/dart/packages/fory/lib/src/serializer/collection/set/set_serializer.dart
@@ -28,6 +28,7 @@ library;
import 'package:fory/src/const/ref_flag.dart';
import 'package:fory/src/const/types.dart';
import 'package:fory/src/deserialization_context.dart';
+import 'package:fory/src/exception/fory_exception.dart';
import 'package:fory/src/memory/byte_reader.dart';
import 'package:fory/src/meta/spec_wraps/type_spec_wrap.dart';
import 'package:fory/src/serializer/collection/iterable_serializer.dart';
@@ -41,6 +42,11 @@ abstract base class SetSerializer extends IterableSerializer
{
@override
Set read(ByteReader br, int refId, DeserializationContext pack) {
int num = br.readVarUint32Small7();
+ if (num > pack.config.maxCollectionSize) {
+ throw InvalidDataException(
+ 'Set size $num exceeds maxCollectionSize
${pack.config.maxCollectionSize}. '
+ 'The input data may be malicious, or need to increase the
maxCollectionSize when creating Fory.');
+ }
TypeSpecWrap? elemWrap = pack.typeWrapStack.peek?.param0;
Set set = newSet(
elemWrap == null || elemWrap.nullable,
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]