Skip to content
6 changes: 6 additions & 0 deletions docs/changelog/95705.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pr: 95705
summary: Avoid stack overflow while parsing mapping
area: Mapping
type: bug
issues:
- 52098
Original file line number Diff line number Diff line change
Expand Up @@ -189,4 +189,40 @@ public boolean isFromDynamicTemplate() {
return true;
}
}

public MappingParserContext createObjectContext() {
return new ObjectParserContext(this);
}

static class ObjectParserContext extends MappingParserContext {
private final long depthLimit;
private int curDepth = 0;

private ObjectParserContext(MappingParserContext in) {
super(
in.similarityLookupService,
in.typeParsers,
in.runtimeFieldParsers,
in.indexVersionCreated,
in.clusterTransportVersion,
in.searchExecutionContextSupplier,
in.scriptCompiler,
in.indexAnalyzers,
in.indexSettings,
in.idFieldMapper
);
depthLimit = in.getIndexSettings().getMappingDepthLimit();
}

void incrementDepth() throws MapperParsingException {
curDepth++;
if (curDepth > depthLimit) {
throw new MapperParsingException("Limit of mapping depth [" + depthLimit + "] has been exceeded");
}
}

void decrementDepth() throws MapperParsingException {
curDepth--;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,6 @@ public ObjectMapper build(MapperBuilderContext context) {
}

public static class TypeParser implements Mapper.TypeParser {

@Override
public boolean supportsVersion(Version indexCreatedVersion) {
return true;
Expand All @@ -181,6 +180,11 @@ public boolean supportsVersion(Version indexCreatedVersion) {
@Override
public Mapper.Builder parse(String name, Map<String, Object> node, MappingParserContext parserContext)
throws MapperParsingException {
if (parserContext instanceof MappingParserContext.ObjectParserContext objectParserContext) {
objectParserContext.incrementDepth(); // throws MapperParsingException if depth limit is exceeded
} else {
parserContext = parserContext.createObjectContext();
}
Explicit<Boolean> subobjects = parseSubobjects(node);
ObjectMapper.Builder builder = new Builder(name, subobjects);
for (Iterator<Map.Entry<String, Object>> iterator = node.entrySet().iterator(); iterator.hasNext();) {
Expand All @@ -191,6 +195,9 @@ public Mapper.Builder parse(String name, Map<String, Object> node, MappingParser
iterator.remove();
}
}
if (parserContext instanceof MappingParserContext.ObjectParserContext objectParserContext) {
objectParserContext.decrementDepth();
}
return builder;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.elasticsearch.index.analysis.IndexAnalyzers;
import org.elasticsearch.index.analysis.NamedAnalyzer;
import org.elasticsearch.index.mapper.MapperService.MergeReason;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xcontent.XContentFactory;

import java.io.IOException;
import java.util.ArrayList;
Expand All @@ -32,6 +34,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static org.elasticsearch.index.mapper.MapperService.INDEX_MAPPING_DEPTH_LIMIT_SETTING;
import static org.elasticsearch.test.ListMatcher.matchesList;
import static org.elasticsearch.test.MapMatcher.assertMap;
import static org.hamcrest.Matchers.containsString;
Expand Down Expand Up @@ -345,4 +348,65 @@ public void testTooManyDimensionFields() {
})));
assertThat(e.getMessage(), containsString("Limit of total dimension fields [" + max + "] has been exceeded"));
}

public void testDeeplyNestedMapping() throws Exception {
final int maxDepth = INDEX_MAPPING_DEPTH_LIMIT_SETTING.get(Settings.EMPTY).intValue();
{
// test that the depth limit is enforced for object field
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc").startObject("properties");
for (int i = 0; i < maxDepth + 5; i++) {
builder.startObject("obj" + i);
builder.startObject("properties");
}
builder.startObject("foo").field("type", "keyword").endObject();
for (int i = 0; i < maxDepth + 5; i++) {
builder.endObject();
builder.endObject();
}
builder.endObject().endObject().endObject();

MapperParsingException exc = expectThrows(
MapperParsingException.class,
() -> createMapperService(Settings.builder().put(getIndexSettings()).build(), builder)
);
assertThat(exc.getMessage(), containsString("Limit of mapping depth [" + maxDepth + "] has been exceeded"));
}

{
// test that the limit is per individual field, so several object fields don't trip the limit
XContentBuilder builder = XContentFactory.jsonBuilder().startObject().startObject("_doc").startObject("properties");
for (int i = 0; i < maxDepth - 3; i++) {
builder.startObject("obj" + i);
builder.startObject("properties");
}

for (int i = 0; i < 2; i++) {
builder.startObject("sub_obj1" + i);
builder.startObject("properties");
}
builder.startObject("foo").field("type", "keyword").endObject();
for (int i = 0; i < 2; i++) {
builder.endObject();
builder.endObject();
}

for (int i = 0; i < 2; i++) {
builder.startObject("sub_obj2" + i);
builder.startObject("properties");
}
builder.startObject("foo2").field("type", "keyword").endObject();
for (int i = 0; i < 2; i++) {
builder.endObject();
builder.endObject();
}

for (int i = 0; i < maxDepth - 3; i++) {
builder.endObject();
builder.endObject();
}
builder.endObject().endObject().endObject();

createMapperService(Settings.builder().put(getIndexSettings()).build(), builder);
}
}
}