From 85c8b5b304aea60aacf45ac690a321b3dd705df9 Mon Sep 17 00:00:00 2001 From: metagn Date: Fri, 6 Dec 2024 14:00:59 +0300 Subject: [PATCH] track call depth separately from loop count in VM (#24512) refs #24503 Infinite recursions currently are not tracked separately from infinite loops, because they also increase the loop counter. However the max infinite loop count is very high by default (10 million) and does not reliably catch infinite recursions before consuming a lot of memory. So to protect against infinite recursions, we separately track call depth, and add a separate option for the maximum call depth, much lower than the maximum iteration count by default (2000, the same as `nimCallDepthLimit`). --------- Co-authored-by: Andreas Rumpf (cherry picked from commit 6f4106bf5d409c6eb41e72dfbef80c566f2739d0) --- compiler/commands.nim | 6 ++++++ compiler/options.nim | 2 ++ compiler/vm.nim | 12 ++++++++++++ compiler/vmdef.nim | 5 ++++- doc/advopt.txt | 1 + tests/vm/tinfiniterecursion.nim | 9 +++++++++ 6 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 tests/vm/tinfiniterecursion.nim diff --git a/compiler/commands.nim b/compiler/commands.nim index cbf915ca67..d59b8cd1dd 100644 --- a/compiler/commands.nim +++ b/compiler/commands.nim @@ -919,6 +919,12 @@ proc processSwitch*(switch, arg: string, pass: TCmdLinePass, info: TLineInfo; discard parseSaturatedNatural(arg, value) if not value > 0: localError(conf, info, "maxLoopIterationsVM must be a positive integer greater than zero") conf.maxLoopIterationsVM = value + of "maxcalldepthvm": + expectArg(conf, switch, arg, pass, info) + var value: int = 2_000 + discard parseSaturatedNatural(arg, value) + if value <= 0: localError(conf, info, "maxCallDepthVM must be a positive integer greater than zero") + conf.maxCallDepthVM = value of "errormax": expectArg(conf, switch, arg, pass, info) # Note: `nim check` (etc) can overwrite this. diff --git a/compiler/options.nim b/compiler/options.nim index d6f6727977..5fdd359f2e 100644 --- a/compiler/options.nim +++ b/compiler/options.nim @@ -383,6 +383,7 @@ type warnCounter*: int errorMax*: int maxLoopIterationsVM*: int ## VM: max iterations of all loops + maxCallDepthVM*: int ## VM: max call depth isVmTrace*: bool configVars*: StringTableRef symbols*: StringTableRef ## We need to use a StringTableRef here as defined @@ -598,6 +599,7 @@ proc newConfigRef*(): ConfigRef = arguments: "", suggestMaxResults: 10_000, maxLoopIterationsVM: 10_000_000, + maxCallDepthVM: 2_000, vmProfileData: newProfileData(), spellSuggestMax: spellSuggestSecretSauce, currentConfigDir: "" diff --git a/compiler/vm.nim b/compiler/vm.nim index f26d41324f..778583a31d 100644 --- a/compiler/vm.nim +++ b/compiler/vm.nim @@ -516,6 +516,8 @@ const errIllegalConvFromXtoY = "illegal conversion from '$1' to '$2'" errTooManyIterations = "interpretation requires too many iterations; " & "if you are sure this is not a bug in your code, compile with `--maxLoopIterationsVM:number` (current value: $1)" + errCallDepthExceeded = "maximum call depth for the VM exceeded; " & + "if you are sure this is not a bug in your code, compile with `--maxCallDepthVM:number` (current value: $1)" errFieldXNotFound = "node lacks field: " @@ -590,6 +592,7 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = let newPc = c.cleanUpOnReturn(tos) # Perform any cleanup action before returning if newPc < 0: + inc(c.callDepth) pc = tos.comesFrom let retVal = regs[0] tos = tos.next @@ -1445,6 +1448,14 @@ proc rawExecute(c: PCtx, start: int, tos: PStackFrame): TFullReg = newFrame.slots[i] = regs[rb+i] if isClosure: newFrame.slots[rc] = TFullReg(kind: rkNode, node: regs[rb].node[1]) + if c.callDepth <= 0: + if allowInfiniteRecursion in c.features: + c.callDepth = c.config.maxCallDepthVM + else: + msgWriteln(c.config, "stack trace: (most recent call last)", {msgNoUnitSep}) + stackTraceAux(c, tos, pc) + globalError(c.config, c.debug[pc], errCallDepthExceeded % $c.config.maxCallDepthVM) + dec(c.callDepth) tos = newFrame updateRegsAlias # -1 for the following 'inc pc' @@ -2311,6 +2322,7 @@ proc execute(c: PCtx, start: int): PNode = proc execProc*(c: PCtx; sym: PSym; args: openArray[PNode]): PNode = c.loopIterations = c.config.maxLoopIterationsVM + c.callDepth = c.config.maxCallDepthVM if sym.kind in routineKinds: if sym.typ.paramsLen != args.len: result = nil diff --git a/compiler/vmdef.nim b/compiler/vmdef.nim index bfa589402a..3c39661127 100644 --- a/compiler/vmdef.nim +++ b/compiler/vmdef.nim @@ -201,6 +201,7 @@ type TSandboxFlag* = enum ## what the evaluation engine should allow allowCast, ## allow unsafe language feature: 'cast' allowInfiniteLoops ## allow endless loops + allowInfiniteRecursion ## allow infinite recursion TSandboxFlags* = set[TSandboxFlag] TSlotKind* = enum # We try to re-use slots in a smart way to @@ -257,7 +258,7 @@ type mode*: TEvalMode features*: TSandboxFlags traceActive*: bool - loopIterations*: int + loopIterations*, callDepth*: int comesFromHeuristic*: TLineInfo # Heuristic for better macro stack traces callbacks*: seq[VmCallback] callbackIndex*: Table[string, int] @@ -294,6 +295,7 @@ proc newCtx*(module: PSym; cache: IdentCache; g: ModuleGraph; idgen: IdGenerator PCtx(code: @[], debug: @[], globals: newNode(nkStmtListExpr), constants: newNode(nkStmtList), types: @[], prc: PProc(blocks: @[]), module: module, loopIterations: g.config.maxLoopIterationsVM, + callDepth: g.config.maxCallDepthVM, comesFromHeuristic: unknownLineInfo, callbacks: @[], callbackIndex: initTable[string, int](), errorFlag: "", cache: cache, config: g.config, graph: g, idgen: idgen) @@ -301,6 +303,7 @@ proc refresh*(c: PCtx, module: PSym; idgen: IdGenerator) = c.module = module c.prc = PProc(blocks: @[]) c.loopIterations = c.config.maxLoopIterationsVM + c.callDepth = c.config.maxCallDepthVM c.idgen = idgen proc reverseName(s: string): string = diff --git a/doc/advopt.txt b/doc/advopt.txt index 7c739ca723..042c387989 100644 --- a/doc/advopt.txt +++ b/doc/advopt.txt @@ -165,6 +165,7 @@ Advanced options: --verbosity:0|1|2|3 set Nim's verbosity level (1 is default) --errorMax:N stop compilation after N errors; 0 means unlimited --maxLoopIterationsVM:N set max iterations for all VM loops + --maxCallDepthVM:N set max call depth in the VM --experimental:$1 enable experimental language feature --legacy:$2 diff --git a/tests/vm/tinfiniterecursion.nim b/tests/vm/tinfiniterecursion.nim new file mode 100644 index 0000000000..6ccdc107d2 --- /dev/null +++ b/tests/vm/tinfiniterecursion.nim @@ -0,0 +1,9 @@ +proc foo(x: int) = + if x < 0: + echo "done" + else: + foo(x + 1) #[tt.Error + ^ maximum call depth for the VM exceeded; if you are sure this is not a bug in your code, compile with `--maxCallDepthVM:number` (current value: 2000)]# + +static: + foo(1)