diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 6ca5715..fa34f0b 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -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 diff --git a/src/claro.c b/src/claro.c index e168a15..86ae90d 100644 --- a/src/claro.c +++ b/src/claro.c @@ -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(xhi)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); } } diff --git a/tests/38_builtin_arity.claro b/tests/38_builtin_arity.claro new file mode 100644 index 0000000..8905776 --- /dev/null +++ b/tests/38_builtin_arity.claro @@ -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 diff --git a/tests/38_builtin_arity.out b/tests/38_builtin_arity.out new file mode 100644 index 0000000..b21a277 --- /dev/null +++ b/tests/38_builtin_arity.out @@ -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.