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]

Reply via email to