From d9a226bd2d674522d31ae2f398d37e43622b2471 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 22 Sep 2026 04:14:07 +0000 Subject: [PATCH] fix: reject inverted random integer ranges --- docs/CURRENT_STATUS.md | 8 +++++--- src/claro.c | 2 +- tests/39_random_inverted_range.claro | 7 +++++++ tests/39_random_inverted_range.out | 1 + 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 tests/39_random_inverted_range.claro create mode 100644 tests/39_random_inverted_range.out diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index fa34f0b..6a85b44 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -13,16 +13,18 @@ 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`. +Focused regression coverage: `tests/38_builtin_arity.claro` and `tests/39_random_inverted_range.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. 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. +- `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. - `./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. +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. ## Feature matrix diff --git a/src/claro.c b/src/claro.c index 86ae90d..702d6ac 100644 --- a/src/claro.c +++ b/src/claro.c @@ -322,7 +322,7 @@ static int builtin_required_args(const char *kind,const char *fn,const char **sh 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); } } + 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; if(a>b){ rt_error(rt,"",0,"random.int needs the lower bound to be less than or equal to the upper bound."); return v_none(); } r=a + (rand() % (b-a+1)); return v_num(r); } } if(strcmp(kind,"csv")==0){ if(strcmp(fn,"read")==0){ char *txt=argc>0?v_to_string(args[0]):xstrdup(""); char *del=argc>1?v_to_string(args[1]):xstrdup(","); char d=del[0]?del[0]:','; Value rows=v_list(); const char *p=txt; while(*p){ Value row=v_list(); Str field; str_init(&field); int inq=0; while(*p&&*p!='\n'&&*p!='\r'){ if(*p=='"'){ if(inq&&p[1]=='"'){ str_ch(&field,'"'); p+=2; continue;} inq=!inq; p++; continue;} if(!inq&&*p==d){ list_add(row.list,v_str(field.s?field.s:"")); field.len=0; if(field.s) field.s[0]=0; p++; continue;} str_ch(&field,*p++); } list_add(row.list,v_str(field.s?field.s:"")); free(field.s); list_add(rows.list,row); while(*p=='\r'||*p=='\n') p++; } free(txt); free(del); return rows; } if(strcmp(fn,"write")==0){ char *del=argc>1?v_to_string(args[1]):xstrdup(","); char d=del[0]?del[0]:','; Str out; str_init(&out); int i,j; if(argc>0&&args[0].type==V_LIST){ for(i=0;icount;i++){ Value row=args[0].list->items[i]; if(row.type==V_LIST){ for(j=0;jcount;j++){ char *f=v_to_string(row.list->items[j]); int quote=strchr(f,d)||strchr(f,'\n')||strchr(f,'"'); if(j) str_ch(&out,d); if(quote){ const char *q=f; str_ch(&out,'"'); while(*q){ if(*q=='"') str_add(&out,"\"\""); else str_ch(&out,*q); q++; } str_ch(&out,'"'); } else str_add(&out,f); free(f); } } str_ch(&out,'\n'); } } free(del); return v_str(out.s?out.s:""); } } if(strcmp(kind,"path")==0){ diff --git a/tests/39_random_inverted_range.claro b/tests/39_random_inverted_range.claro new file mode 100644 index 0000000..3581346 --- /dev/null +++ b/tests/39_random_inverted_range.claro @@ -0,0 +1,7 @@ +IMPORT "lib/random.claro" AS random + +TRY + CALL random.int WITH 10, 1 +CATCH +ENDTRY +SAY LASTERROR diff --git a/tests/39_random_inverted_range.out b/tests/39_random_inverted_range.out new file mode 100644 index 0000000..2786a97 --- /dev/null +++ b/tests/39_random_inverted_range.out @@ -0,0 +1 @@ +random.int needs the lower bound to be less than or equal to the upper bound.