From c4d894315fd8eb0d84a3d4784a11733bdcfc75d0 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 22 Sep 2026 06:17:43 +0000 Subject: [PATCH] fix: guard excessive function call depth --- docs/CURRENT_STATUS.md | 9 ++++++--- src/claro.c | 4 +++- tests/40_call_depth_guard.claro | 9 +++++++++ tests/40_call_depth_guard.out | 1 + 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 tests/40_call_depth_guard.claro create mode 100644 tests/40_call_depth_guard.out diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 6a85b44..7e95689 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -13,18 +13,21 @@ This file is the beginner-safe status map for the current package. It separates The v1.18.26 review identified missing-argument reads in standard-library built-ins. The current runtime now validates required argument counts centrally before any builtin indexes `args[]`. Covered calls include `math.abs`, `math.clamp`, `random.seed`, `random.int`, text helpers, CSV helpers, path helpers, and collection helpers. Missing arguments produce a beginner-facing `needs N arguments` runtime error. -Focused regression coverage: `tests/38_builtin_arity.claro` and `tests/39_random_inverted_range.claro`. +Focused regression coverage: `tests/38_builtin_arity.claro`, `tests/39_random_inverted_range.claro`, and `tests/40_call_depth_guard.claro`. The runtime now rejects inverted `random.int` ranges before modulo arithmetic with: `random.int needs the lower bound to be less than or equal to the upper bound.` This prevents invalid ranges from producing incorrect values or a divide-by-zero signal. +User-defined function and object-method calls now have a documented maximum active call depth of 256. Exceeding it produces: `Claro function call depth exceeded the safe limit of 256. Simplify the recursion or add a stopping condition.` Ordinary non-recursive beginner programs are unaffected. + Verified in this checkout on 2026-09-22: -- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-h2-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 /tmp/claro-h2-asan tests/39_random_inverted_range.claro`: expected diagnostic; no AddressSanitizer or UndefinedBehaviorSanitizer report. +- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-call-depth-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 /tmp/claro-call-depth-asan tests/40_call_depth_guard.claro`: expected diagnostic; no AddressSanitizer or UndefinedBehaviorSanitizer report. +- `make -s all`: rebuilt both `claro` and `claro.exe` from current source. - `./claro test`: `PASS: 0 failure(s)`. - `./claro doctor`: all checks `OK`. - `./claro validate`: validation passed. -This slice does not yet fix recursion depth, memory cleanup, `LASTEXIT`, HTTP buffering/status handling, or other review findings. Claro remains a trusted-script interpreter, not a sandbox. +This slice does not yet fix memory cleanup, `LASTEXIT`, HTTP buffering/status handling, or other review findings. Claro remains a trusted-script interpreter, not a sandbox. ## Feature matrix diff --git a/src/claro.c b/src/claro.c index 702d6ac..8c05ece 100644 --- a/src/claro.c +++ b/src/claro.c @@ -43,6 +43,7 @@ int pclose(FILE *stream); #endif #define CLARO_VERSION "Claro v1.18.26" +#define CLARO_MAX_CALL_DEPTH 256 static void *xmalloc(size_t n){ void *p=malloc(n?n:1); if(!p){ fprintf(stderr,"Out of memory\n"); exit(1);} return p; } static void *xrealloc(void *p,size_t n){ void *q=realloc(p,n?n:1); if(!q){ fprintf(stderr,"Out of memory\n"); exit(1);} return q; } @@ -109,6 +110,7 @@ typedef struct Runtime { Str captured; int capture; int trace; int error; char *err_msg; char *err_file; int err_line; int returning; Value ret; + int call_depth; Program **programs; int pcount, pcap; char **import_stack; int import_depth; int script_argc; char **script_argv; @@ -360,7 +362,7 @@ static int call_object_method(Runtime *rt,const char *name,Value *args,int argc) if(rt->returning){ rt_set(rt,"RESULT",rt->ret); rt->returning=0; } rt->locals=old; rt_set(rt,objname,obj); if(c){ for(fd=c->fields;fd;fd=fd->next){ Value fv=map_get(obj.map,fd->name); snprintf(fieldname,sizeof(fieldname),"%s.%s",objname,fd->name); rt_set_checked(rt,"",0,fieldname,fd->type,fv); } } return 1; } -static void call_function(Runtime *rt,const char *name,Value *args,int argc){ int handled=0,i; Value br=builtin_call(rt,name,args,argc,&handled); if(handled){ rt_set(rt,"RESULT",br); return; } if(call_object_method(rt,name,args,argc)) return; Function *f=find_function(rt,name); if(!f){ rt_error(rt,"",0,"Unknown function: %s",name); return;} Var *old=rt->locals; rt->locals=NULL; for(i=0;ipcnt;i++) rt_set(rt,f->params[i], ireturning=0; exec_range(rt,f->prog,f->start,f->end); if(rt->returning){ rt_set(rt,"RESULT",rt->ret); rt->returning=0; } rt->locals=old; } +static void call_function(Runtime *rt,const char *name,Value *args,int argc){ int handled=0,i; Value br=builtin_call(rt,name,args,argc,&handled); if(handled){ rt_set(rt,"RESULT",br); return; } if(rt->call_depth>=CLARO_MAX_CALL_DEPTH){ rt_error(rt,"",0,"Claro function call depth exceeded the safe limit of %d. Simplify the recursion or add a stopping condition.",CLARO_MAX_CALL_DEPTH); return; } rt->call_depth++; if(call_object_method(rt,name,args,argc)){ rt->call_depth--; return; } { Function *f=find_function(rt,name); if(!f){ rt_error(rt,"",0,"Unknown function: %s",name); rt->call_depth--; return;} { Var *old=rt->locals; rt->locals=NULL; for(i=0;ipcnt;i++) rt_set(rt,f->params[i], ireturning=0; exec_range(rt,f->prog,f->start,f->end); if(rt->returning){ rt_set(rt,"RESULT",rt->ret); rt->returning=0; } rt->locals=old; } } rt->call_depth--; } static int stack_has(Runtime *rt,const char *path){ int i; for(i=0;iimport_depth;i++) if(strcmp(rt->import_stack[i],path)==0) return 1; return 0; } static int export_allows(char **exports,int ec,const char *name){ int i; if(ec==0) return 1; for(i=0;i