Skip to content

Commit

Permalink
track call depth separately from loop count in VM (#24512)
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
metagn and Araq authored Dec 6, 2024
1 parent c086114 commit 6f4106b
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 1 deletion.
6 changes: 6 additions & 0 deletions compiler/commands.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions compiler/options.nim
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,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
Expand Down Expand Up @@ -600,6 +601,7 @@ proc newConfigRef*(): ConfigRef =
arguments: "",
suggestMaxResults: 10_000,
maxLoopIterationsVM: 10_000_000,
maxCallDepthVM: 2_000,
vmProfileData: newProfileData(),
spellSuggestMax: spellSuggestSecretSauce,
currentConfigDir: ""
Expand Down
12 changes: 12 additions & 0 deletions compiler/vm.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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: "


Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion compiler/vmdef.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -294,13 +295,15 @@ 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)

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 =
Expand Down
1 change: 1 addition & 0 deletions doc/advopt.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions tests/vm/tinfiniterecursion.nim
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 6f4106b

Please sign in to comment.