fix: validate builtin arity before argument access

This commit is contained in:
Hermes Agent
2026-09-22 02:11:21 +00:00
parent b77dd4dacc
commit 52a7ca5ff4
4 changed files with 75 additions and 1 deletions
+15
View File
@@ -9,6 +9,21 @@ This file is the beginner-safe status map for the current package. It separates
- **Experimental/planned**: do not rely on it in beginner lessons yet.
- **Historical**: kept for release history, not current instructions.
## Security and correctness review progress
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`.
Verified in this checkout on 2026-09-22:
- `gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o /tmp/claro-asan -lm` plus `ASAN_OPTIONS=detect_leaks=0 /tmp/claro-asan tests/38_builtin_arity.claro`: no AddressSanitizer or UndefinedBehaviorSanitizer report; LeakSanitizer still reports the pre-existing interpreter-wide cleanup backlog when leak detection is enabled.
- `./claro test`: `PASS: 0 failure(s)`.
- `./claro doctor`: all checks `OK`.
- `./claro validate`: validation passed.
This slice does not yet fix inverted `random.int` ranges, recursion depth, memory cleanup, `LASTEXIT`, HTTP buffering/status handling, or other review findings. Claro remains a trusted-script interpreter, not a sandbox.
## Feature matrix
### Beginner scripting core
+30 -1
View File
@@ -290,7 +290,36 @@ static void json_write(Str *b,Value v,int pretty,int indent){ int i; char *s; if
static char *make_json(Value v,int pretty){ Str b; str_init(&b); json_write(&b,v,pretty,0); return str_take(&b); }
/* ---------- stdlib built-ins ---------- */
static Value builtin_call(Runtime *rt,const char *name,Value *args,int argc,int *handled){ char ns[128], fn[128]; const char *dot=strrchr(name,'.'); const char *kind=NULL; *handled=0; if(!dot) return v_none(); snprintf(ns,sizeof(ns),"%.*s",(int)(dot-name),name); snprintf(fn,sizeof(fn),"%s",dot+1); kind=module_kind(rt,ns); if(!kind) return v_none(); *handled=1;
static int builtin_required_args(const char *kind,const char *fn,const char **shape){
if(strcmp(kind,"text")==0){
if(strcmp(fn,"upper")==0||strcmp(fn,"lower")==0){ if(shape) *shape="value"; return 1; }
if(strcmp(fn,"contains")==0||strcmp(fn,"endswith")==0||strcmp(fn,"split2")==0){ if(shape) *shape="text, part"; return 2; }
}
if(strcmp(kind,"math")==0){
if(strcmp(fn,"abs")==0){ if(shape) *shape="value"; return 1; }
if(strcmp(fn,"clamp")==0){ if(shape) *shape="value, lower, upper"; return 3; }
}
if(strcmp(kind,"random")==0){
if(strcmp(fn,"seed")==0){ if(shape) *shape="value"; return 1; }
if(strcmp(fn,"int")==0){ if(shape) *shape="lower, upper"; return 2; }
}
if(strcmp(kind,"csv")==0){
if(strcmp(fn,"read")==0){ if(shape) *shape="text"; return 1; }
if(strcmp(fn,"write")==0){ if(shape) *shape="rows"; return 1; }
}
if(strcmp(kind,"path")==0){
if(strcmp(fn,"join")==0){ if(shape) *shape="folder, name"; return 2; }
if(strcmp(fn,"basename")==0||strcmp(fn,"dirname")==0||strcmp(fn,"ext")==0||strcmp(fn,"stem")==0||strcmp(fn,"absolute")==0){ if(shape) *shape="path"; return 1; }
}
if(strcmp(kind,"collections")==0){
if(strcmp(fn,"length")==0||strcmp(fn,"reversed")==0||strcmp(fn,"keys")==0||strcmp(fn,"values")==0){ if(shape) *shape="collection"; return 1; }
if(strcmp(fn,"contains")==0||strcmp(fn,"index")==0||strcmp(fn,"has_key")==0){ if(shape) *shape="collection, value"; return 2; }
if(strcmp(fn,"join")==0){ if(shape) *shape="collection, separator"; return 2; }
if(strcmp(fn,"get")==0){ if(shape) *shape="map, key"; return 2; }
}
return 0;
}
static Value builtin_call(Runtime *rt,const char *name,Value *args,int argc,int *handled){ char ns[128], fn[128]; const char *dot=strrchr(name,'.'); const char *kind=NULL; const char *shape=NULL; int required; *handled=0; if(!dot) return v_none(); snprintf(ns,sizeof(ns),"%.*s",(int)(dot-name),name); snprintf(fn,sizeof(fn),"%s",dot+1); kind=module_kind(rt,ns); if(!kind) return v_none(); *handled=1; required=builtin_required_args(kind,fn,&shape); if(required>argc){ rt_error(rt,"",0,"%s.%s needs %d argument%s, but got %d. Try: CALL %s.%s WITH %s.",ns,fn,required,required==1?"":"s",argc,ns,fn,shape?shape:"the required values"); return v_none(); }
if(strcmp(kind,"text")==0){ char *a=argc>0?v_to_string(args[0]):xstrdup(""); char *b=argc>1?v_to_string(args[1]):xstrdup(""); int i; if(strcmp(fn,"upper")==0){ for(i=0;a[i];i++) a[i]=(char)toupper((unsigned char)a[i]); Value r=v_str(a); free(a); free(b); return r; } if(strcmp(fn,"lower")==0){ for(i=0;a[i];i++) a[i]=(char)tolower((unsigned char)a[i]); Value r=v_str(a); free(a); free(b); return r; } if(strcmp(fn,"contains")==0){ Value r=v_bool(strstr(a,b)!=NULL); free(a); free(b); return r; } if(strcmp(fn,"endswith")==0){ size_t la=strlen(a), lb=strlen(b); Value r=v_bool(lb<=la && strcmp(a+la-lb,b)==0); free(a); free(b); return r; } if(strcmp(fn,"split2")==0){ char *pos=strstr(a,b); Value arr=v_list(); if(pos){ char *left=substr(a,pos); list_add(arr.list,v_str(left)); list_add(arr.list,v_str(pos+strlen(b))); free(left); } else list_add(arr.list,v_str(a)); free(a); free(b); return arr; } free(a); free(b); }
if(strcmp(kind,"math")==0){ if(strcmp(fn,"abs")==0) return v_num(fabs(v_number(args[0]))); if(strcmp(fn,"clamp")==0){ double x=v_number(args[0]), lo=v_number(args[1]), hi=v_number(args[2]); if(x<lo)x=lo; if(x>hi)x=hi; return v_num(x); } }
if(strcmp(kind,"random")==0){ if(strcmp(fn,"seed")==0){ srand((unsigned)v_number(args[0])); return v_none(); } if(strcmp(fn,"int")==0){ int a=(int)v_number(args[0]), b=(int)v_number(args[1]); int r=a + (rand() % (b-a+1)); return v_num(r); } }
+26
View File
@@ -0,0 +1,26 @@
IMPORT "lib/math.claro" AS math
IMPORT "lib/random.claro" AS random
TRY
CALL math.abs
CATCH
ENDTRY
SAY LASTERROR
TRY
CALL math.clamp WITH 1, 0
CATCH
ENDTRY
SAY LASTERROR
TRY
CALL random.seed
CATCH
ENDTRY
SAY LASTERROR
TRY
CALL random.int WITH 1
CATCH
ENDTRY
SAY LASTERROR
+4
View File
@@ -0,0 +1,4 @@
math.abs needs 1 argument, but got 0. Try: CALL math.abs WITH value.
math.clamp needs 3 arguments, but got 2. Try: CALL math.clamp WITH value, lower, upper.
random.seed needs 1 argument, but got 0. Try: CALL random.seed WITH value.
random.int needs 2 arguments, but got 1. Try: CALL random.int WITH lower, upper.