From 174ab088bc62996daa50c68a3b9dd25947e10da1 Mon Sep 17 00:00:00 2001 From: Kevin Herron Date: Wed, 22 Jul 2026 15:47:00 -0700 Subject: [PATCH] Prevent type model compiler stack overflow Walk declarations iteratively so the configured depth limit is enforced before JVM stack exhaustion on platforms with smaller thread stacks. --- .../instantiation/TypeModelCompiler.java | 138 +++++++++++++----- 1 file changed, 105 insertions(+), 33 deletions(-) diff --git a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/nodes/instantiation/TypeModelCompiler.java b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/nodes/instantiation/TypeModelCompiler.java index f484db26a4..5bf4382bc5 100644 --- a/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/nodes/instantiation/TypeModelCompiler.java +++ b/opc-ua-sdk/sdk-server/src/main/java/org/eclipse/milo/opcua/sdk/server/nodes/instantiation/TypeModelCompiler.java @@ -12,9 +12,11 @@ import static java.util.Objects.requireNonNull; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; +import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -355,9 +357,59 @@ private void walkChildren( CompileContext ctx, Set pathNodeIds) { + Deque frames = new ArrayDeque<>(); + frames.push(childWalkFrame(sourceNodeId, parentPath, declaringTypeId, null, ctx)); + + while (!frames.isEmpty()) { + ChildWalkFrame frame = frames.peek(); + if (frame.hasNext()) { + Child child = frame.next(); + ChildExpansion expansion = + processChild( + child.reference, + child.targetId, + child.node, + frame.parentPath, + frame.declaringTypeId, + table, + ctx, + pathNodeIds); + + if (expansion != null) { + frames.push( + childWalkFrame( + expansion.targetId, expansion.path, expansion.declaringTypeId, expansion, ctx)); + } + } else { + frames.pop(); + + ChildExpansion expansion = frame.expansion; + if (expansion != null) { + pathNodeIds.remove(expansion.targetId); + + if (expansion.memberTypeId != null) { + expandMemberType( + expansion.declaration, + expansion.memberTypeId, + expansion.path, + expansion.targetId, + table, + ctx); + } + } + } + } + } + + private ChildWalkFrame childWalkFrame( + NodeId sourceNodeId, + BrowsePath parentPath, + NodeId declaringTypeId, + @Nullable ChildExpansion expansion, + CompileContext ctx) { + List refs = getReferences(sourceNodeId, ctx); - record Child(Reference reference, NodeId targetId, UaNode node) {} List children = new ArrayList<>(); for (Reference r : refs) { @@ -418,20 +470,10 @@ record Child(Reference reference, NodeId targetId, UaNode node) {} .thenComparing(c -> c.reference.getReferenceTypeId().toParseableString()) .thenComparing(c -> c.targetId.toParseableString())); - for (Child child : children) { - processChild( - child.reference, - child.targetId, - child.node, - parentPath, - declaringTypeId, - table, - ctx, - pathNodeIds); - } + return new ChildWalkFrame(parentPath, declaringTypeId, children, expansion); } - private void processChild( + private @Nullable ChildExpansion processChild( Reference reference, NodeId targetId, UaNode node, @@ -471,7 +513,7 @@ private void processChild( path, targetId)); } - return; + return null; } table.pathOccupants.put(path, targetId); @@ -502,7 +544,7 @@ private void processChild( path, targetId)); } - return; + return null; } if (ruleIds.isEmpty()) { @@ -520,7 +562,7 @@ private void processChild( + ")", path, targetId)); - return; + return null; } if (ruleRefs.size() > 1) { @@ -569,7 +611,7 @@ private void processChild( nodeClass + " declaration at " + path + " has no HasTypeDefinition reference", path, targetId)); - return; + return null; } if (memberTypeXnis.size() > 1) { @@ -587,7 +629,7 @@ private void processChild( + "; exactly one is required", path, targetId)); - return; + return null; } ExpandedNodeId memberTypeXni = memberTypeXnis.get(0); @@ -603,7 +645,7 @@ private void processChild( + " is not resolvable in this server", path, targetId)); - return; + return null; } } @@ -660,7 +702,7 @@ private void processChild( } } } - return; + return null; } if (path.depth() >= MAX_DEPTH) { @@ -670,7 +712,7 @@ private void processChild( "declaration hierarchy exceeds depth " + MAX_DEPTH + " at " + path, path, targetId)); - return; + return null; } if (!pathNodeIds.add(targetId)) { @@ -680,20 +722,13 @@ private void processChild( "declaration " + targetId + " reached again along its own path at " + path, path, targetId)); - return; - } - - try { - // Explicit on-declaration children first: they win over same-path member-type defaults - // (Part 3 §6.3.3.3). - walkChildren(targetId, path, declaringTypeId, table, ctx, pathNodeIds); - } finally { - pathNodeIds.remove(targetId); + return null; } - if (memberTypeId != null) { - expandMemberType(declaration, memberTypeId, path, targetId, table, ctx); - } + // Explicit on-declaration children are traversed before member-type expansion, so they win over + // same-path member-type defaults (Part 3 §6.3.3.3). The continuation also removes targetId from + // the active path after its children have been processed. + return new ChildExpansion(targetId, path, declaringTypeId, declaration, memberTypeId); } /** @@ -1696,6 +1731,43 @@ private static List dedupe(List diagnostics) { return List.copyOf(new LinkedHashSet<>(diagnostics)); } + private record Child(Reference reference, NodeId targetId, UaNode node) {} + + private record ChildExpansion( + NodeId targetId, + BrowsePath path, + NodeId declaringTypeId, + InstanceDeclaration declaration, + @Nullable NodeId memberTypeId) {} + + private static final class ChildWalkFrame { + final BrowsePath parentPath; + final NodeId declaringTypeId; + final List children; + final @Nullable ChildExpansion expansion; + + int nextChild; + + ChildWalkFrame( + BrowsePath parentPath, + NodeId declaringTypeId, + List children, + @Nullable ChildExpansion expansion) { + this.parentPath = parentPath; + this.declaringTypeId = declaringTypeId; + this.children = children; + this.expansion = expansion; + } + + boolean hasNext() { + return nextChild < children.size(); + } + + Child next() { + return children.get(nextChild++); + } + } + private static final class CompileContext { final Map memo = new HashMap<>(); final Set inProgress = new HashSet<>();