Skip to content

Commit

Permalink
core: add check for infinite loops (#1588)
Browse files Browse the repository at this point in the history
If the `:call` operator is used recursively it can result
in an infinite loop. This change adds some sanity checks
for the call depth to quickly fail in these cases and
indicate that a loop was detected.
  • Loading branch information
brharrington authored Nov 10, 2023
1 parent bd7b959 commit 33bd114
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ case class Context(
variables: Map[String, Any],
initialVariables: Map[String, Any] = Map.empty,
frozenStack: List[Any] = Nil,
features: Features = Features.STABLE
features: Features = Features.STABLE,
callDepth: Int = 0
) {

require(callDepth >= 0, "call depth cannot be negative")

/**
* Remove the contents of the stack and push them onto the frozen stack. The variable
* state will also be cleared.
Expand All @@ -61,4 +64,14 @@ case class Context(
def unfreeze: Context = {
copy(stack = stack ::: frozenStack, frozenStack = Nil)
}

/** Increase the call depth for detecting deeply nested calls. */
def incrementCallDepth: Context = {
copy(callDepth = callDepth + 1)
}

/** Decrease the call depth for detecting deeply nested calls. */
def decrementCallDepth: Context = {
copy(callDepth = callDepth - 1)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,18 @@ case class Interpreter(vocabulary: List[Word]) {

@scala.annotation.tailrec
private def execute(s: Step): Context = {
if (s.context.callDepth > 10) {
// Prevent infinite loops. Operations like `:each` and `:map` to traverse a list are
// finite and will increase the depth by 1. The max call depth of 10 is arbitrary, but
// should be more than enough for legitimate use-cases. Testing 1M actual expressions, 3
// was the highest depth seen.
throw new IllegalStateException(s"looping detected")
}
if (s.program.isEmpty) s.context else execute(nextStep(s))
}

final def execute(program: List[Any], context: Context, unfreeze: Boolean = true): Context = {
val result = execute(Step(program, context))
val result = execute(Step(program, context.incrementCallDepth)).decrementCallDepth
if (unfreeze) result.unfreeze else result
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2014-2023 Netflix, Inc.
*
* Licensed 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.
*/
package com.netflix.atlas.core.stacklang

import munit.FunSuite

class LoopSuite extends FunSuite {

private val interpreter = Interpreter(StandardVocabulary.allWords)

test("infinite loop: fcall recursion") {
val e = intercept[IllegalStateException] {
interpreter.execute("loop,(,loop,:fcall,),:set,loop,:fcall")
}
assertEquals(e.getMessage, "looping detected")
}

test("infinite loop: call recursion") {
val e = intercept[IllegalStateException] {
interpreter.execute("loop,(,loop,:get,:call,),:set,loop,:get,:call")
}
assertEquals(e.getMessage, "looping detected")
}

test("infinite loop: nested") {
val e = intercept[IllegalStateException] {
interpreter.execute("a,(,b,:fcall,),:set,b,(,c,:fcall,),:set,c,(,a,:fcall,),:set,a,:fcall")
}
assertEquals(e.getMessage, "looping detected")
}
}

0 comments on commit 33bd114

Please sign in to comment.