diff --git a/docs/source/5.0-Upgrade-Guide.md b/docs/source/5.0-Upgrade-Guide.md index 2e12b9afb..2eb08d1bd 100644 --- a/docs/source/5.0-Upgrade-Guide.md +++ b/docs/source/5.0-Upgrade-Guide.md @@ -84,7 +84,8 @@ cfg.CreateMap().MaxDepth(3); cfg.CreateMap().PreserveReferences(); ``` -Starting from 6.1.0 PreserveReferences is set automatically at config time whenever the recursion can be detected statically. If you're still getting `StackOverflowException`, open an issue with a full repro and we'll look into it. +Starting from 6.1.0 PreserveReferences is set automatically at config time whenever the recursion can be detected statically. +Starting from 16.x / MagicMapper patch, a default MaxDepth of 64 is also applied automatically, preventing a StackOverflowException from deeply nested (but non-circular) object graphs (see GHSA-rvv3-g6hj-g44x). If you need deeper mapping, call `.MaxDepth(n)` explicitly. To rely solely on object-identity caching without a depth limit, call `.PreserveReferences()` explicitly. ## UseDestinationValue diff --git a/docs/source/Configuration.md b/docs/source/Configuration.md index b8b1e9e8b..3e10ee212 100644 --- a/docs/source/Configuration.md +++ b/docs/source/Configuration.md @@ -224,4 +224,20 @@ Compilation times increase with the size of the execution plan and that depends You can set `MapAtRuntime` per member or `MaxExecutionPlanDepth` globally (the default is one, set it to zero). These will reduce the size of the execution plan by replacing the execution plan for a child object with a method call. The compilation will be faster, but the mapping itself might be slower. Search the repo for more details and use a profiler to better understand the effect. -Avoiding `PreserveReferences` and `MaxDepth` also helps. \ No newline at end of file +Avoiding `PreserveReferences` and `MaxDepth` also helps. + +## Circular and Self-Referential Types + +When MagicMapper detects a self-referential type mapping (e.g., `CreateMap()` where `Node` has a `Node` property), it automatically enables `PreserveReferences` to avoid re-mapping the same object instance. It also applies a default `MaxDepth` of **64** — matching System.Text.Json and Newtonsoft.Json — to prevent a Denial-of-Service condition from deeply nested object graphs (see GHSA-rvv3-g6hj-g44x). + +If your object graphs legitimately exceed 64 levels, increase the limit explicitly: + +```c# +cfg.CreateMap().MaxDepth(128); +``` + +To disable the depth limit entirely and rely solely on object-identity caching, call `.PreserveReferences()` explicitly: + +```c# +cfg.CreateMap().PreserveReferences(); +``` \ No newline at end of file diff --git a/src/UnitTests/Bug/DeepNestingStackOverflow.cs b/src/UnitTests/Bug/DeepNestingStackOverflow.cs new file mode 100644 index 000000000..1a1a7a5a4 --- /dev/null +++ b/src/UnitTests/Bug/DeepNestingStackOverflow.cs @@ -0,0 +1,47 @@ +namespace AutoMapper.UnitTests.Bug; + +public class DeepNestingStackOverflow +{ + class Circular { public Circular Self { get; set; } } + + // Verifies that mapping a deeply nested self-referential object does not + // crash the process with a StackOverflowException (GHSA-rvv3-g6hj-g44x). + // AutoMapper auto-applies a default MaxDepth of 64 (matching System.Text.Json + // and Newtonsoft.Json) when it detects a self-referential type mapping. + [Fact] + public void Mapping_deeply_nested_self_referential_object_should_not_stackoverflow() + { + var config = new MapperConfiguration(cfg => cfg.CreateMap()); + var mapper = config.CreateMapper(); + + var root = new Circular(); + var current = root; + for (int i = 0; i < 30_000; i++) + { + current.Self = new Circular(); + current = current.Self; + } + + // Should complete without crashing; mapping is truncated at default MaxDepth (64) + var result = mapper.Map(root); + result.ShouldNotBeNull(); + + int depth = 0; + current = result; + while (current.Self != null) + { + depth++; + current = current.Self; + } + depth.ShouldBeLessThanOrEqualTo(64); + } + + // Verifies that configuration validation does not detect the vulnerability — + // only the runtime mapping is affected, not the configuration itself. + [Fact] + public void AssertConfigurationIsValid_does_not_detect_deep_nesting_vulnerability() + { + var config = new MapperConfiguration(cfg => cfg.CreateMap()); + config.AssertConfigurationIsValid(); + } +}