fix: release ASK temporary values
This commit is contained in:
@@ -28,7 +28,7 @@ Verified in this checkout on 2026-09-23:
|
|||||||
- `./claro doctor`: all checks `OK`.
|
- `./claro doctor`: all checks `OK`.
|
||||||
- `./claro validate`: validation passed.
|
- `./claro validate`: validation passed.
|
||||||
|
|
||||||
The memory cleanup slices release the previous deep value when a runtime variable or map entry is overwritten, release temporary split argument arrays and strings from `DO`, `CALL`, `TEXT ... CONTAINS`, and `RANDOM`, release loaded program paths, lines, and pointer arrays at the end of each script run, and now release the remaining runtime-owned variables, functions, modules, classes, import paths, captured output, return value, and error strings before the interpreter exits. The expression evaluator now releases discarded intermediate `Value` operands and command boundaries release evaluated `SET`/`SAY` values after copying or printing them. `FOR EACH` now releases its copied collection after iteration, preventing discarded list/map copies from accumulating in long scripts. `IF` and `DO ... TIMES` now release their temporary control-expression values after branch/loop selection. Focused coverage is `tools/validate_memory_cleanup.py`, `tools/validate_expression_cleanup.py`, `tools/validate_control_flow_cleanup.py`, and `tools/validate_control_expression_cleanup.py`; the latter reports `PASS: discarded control-flow expression values are released` under an ASan/UBSan build with LeakSanitizer checking. Remaining memory-growth areas include other expression temporaries and command boundaries. The `RUN COMMAND` path remains trusted shell execution, not a sandbox.
|
The memory cleanup slices release the previous deep value when a runtime variable or map entry is overwritten, release temporary split argument arrays and strings from `DO`, `CALL`, `TEXT ... CONTAINS`, and `RANDOM`, release loaded program paths, lines, and pointer arrays at the end of each script run, and now release the remaining runtime-owned variables, functions, modules, classes, import paths, captured output, return value, and error strings before the interpreter exits. The expression evaluator now releases discarded intermediate `Value` operands and command boundaries release evaluated `SET`/`SAY` values after copying or printing them. `FOR EACH` now releases its copied collection after iteration, preventing discarded list/map copies from accumulating in long scripts. `IF` and `DO ... TIMES` now release their temporary control-expression values after branch/loop selection. `ASK` now releases evaluated prompt values and temporary input values after converting/copying them into runtime storage. Focused coverage is `tools/validate_memory_cleanup.py`, `tools/validate_expression_cleanup.py`, `tools/validate_control_flow_cleanup.py`, `tools/validate_control_expression_cleanup.py`, and `tools/validate_ask_prompt_cleanup.py`; each focused cleanup validator runs an ASan/UBSan build with LeakSanitizer checking. Verified on 2026-09-24: `python3 tools/validate_ask_prompt_cleanup.py` passes (2,000 prompt/input operations under ASan/UBSan/LSan); `make -s all`, `./claro test`, `./claro doctor`, and `./claro validate` all pass. A malformed-input ASan/UBSan/LSan smoke run did not pass: it reports two leaked `rt_error` message strings (140 bytes total) at `src/claro.c:142`. This unrelated existing diagnostic-path leak remains unaddressed and blocks claiming sanitizer-clean malformed-input handling. Remaining memory-growth areas include other expression temporaries and command boundaries. The `RUN COMMAND` path remains trusted shell execution, not a sandbox.
|
||||||
|
|
||||||
The `RUN COMMAND` path now decodes POSIX `pclose()` wait status before storing `LASTEXIT`, so a child that exits with code 3 exposes `3` rather than the encoded status `768`. Focused coverage is `tests/42_last_exit_code.claro`. HTTP responses now have a 1,048,576-byte cap and marker-like response bodies are preserved while extracting the final HTTP status marker. Focused coverage is `tools/validate_http_hardening.py`. Remaining memory-growth areas include other expression temporaries. Claro remains a trusted-script interpreter, not a sandbox.
|
The `RUN COMMAND` path now decodes POSIX `pclose()` wait status before storing `LASTEXIT`, so a child that exits with code 3 exposes `3` rather than the encoded status `768`. Focused coverage is `tests/42_last_exit_code.claro`. HTTP responses now have a 1,048,576-byte cap and marker-like response bodies are preserved while extracting the final HTTP status marker. Focused coverage is `tools/validate_http_hardening.py`. Remaining memory-growth areas include other expression temporaries. Claro remains a trusted-script interpreter, not a sandbox.
|
||||||
|
|
||||||
|
|||||||
@@ -69,3 +69,18 @@ python3 tools/validate_control_expression_cleanup.py
|
|||||||
The validator builds with AddressSanitizer/UndefinedBehaviorSanitizer,
|
The validator builds with AddressSanitizer/UndefinedBehaviorSanitizer,
|
||||||
executes 2,000 temporary `IF` conditions under LeakSanitizer, and checks the
|
executes 2,000 temporary `IF` conditions under LeakSanitizer, and checks the
|
||||||
expected output.
|
expected output.
|
||||||
|
|
||||||
|
## ASK temporary-value cleanup
|
||||||
|
|
||||||
|
`ASK` releases the evaluated prompt value after converting it to display text,
|
||||||
|
and releases the temporary input value after `rt_set_checked` copies it into
|
||||||
|
runtime storage. This keeps prompt and input strings from accumulating during
|
||||||
|
repeated input loops without changing prompt or input behavior.
|
||||||
|
|
||||||
|
Focused verification:
|
||||||
|
|
||||||
|
```text
|
||||||
|
python3 tools/validate_ask_prompt_cleanup.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The validator runs 2,000 prompt/input operations with AddressSanitizer, UndefinedBehaviorSanitizer, and LeakSanitizer enabled. The dedicated validator passes. A separate malformed-input sanitizer smoke run (`gcc -std=c99 -O0 -g -fsanitize=address,undefined src/claro.c -o ... -lm` followed by the generated malformed script) exits 1 due to two existing leaked error-message strings (140 bytes total) allocated in `rt_error` at `src/claro.c:142`; this is outside the ASK cleanup slice and remains a blocker to sanitizer-clean malformed-input validation.
|
||||||
|
|||||||
+1
-1
@@ -427,7 +427,7 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf
|
|||||||
if(strcmp(up,"SET")==0){ char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); if(as&&to&&as<to){ char *name=substr(rest,as); char *type=substr(as+2,to); char *n=trim_inplace(name); char *ty=trim_inplace(type); Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,ty,v); value_free(v); free(name); free(type); }
|
if(strcmp(up,"SET")==0){ char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); if(as&&to&&as<to){ char *name=substr(rest,as); char *type=substr(as+2,to); char *n=trim_inplace(name); char *ty=trim_inplace(type); Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,ty,v); value_free(v); free(name); free(type); }
|
||||||
else if(to){ char *name=substr(rest,to); char *n=trim_inplace(name); char *space=strchr(n,' '); const char *type=NULL; if(space){ *space=0; if(claro_is_type_word(trim_inplace(space+1))) type=trim_inplace(space+1); else { *space=' '; } } { Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,type,v); value_free(v); } free(name); }
|
else if(to){ char *name=substr(rest,to); char *n=trim_inplace(name); char *space=strchr(n,' '); const char *type=NULL; if(space){ *space=0; if(claro_is_type_word(trim_inplace(space+1))) type=trim_inplace(space+1); else { *space=' '; } } { Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,type,v); value_free(v); } free(name); }
|
||||||
else { const char *pcur=rest; char *name=unquote_token(&pcur); char *n=trim_inplace(name); const char *save=pcur; char *maybe=unquote_token(&pcur); char *ty=trim_inplace(maybe); if(claro_is_type_word(ty)){ char *after=trim_inplace((char*)pcur); Value v=*after?eval_expr(rt,after):claro_default_for_type(ty); if(*n) rt_set_checked(rt,p->path,pc+1,n,ty,v); value_free(v); } else { Value v=eval_expr(rt,save); if(*n) rt_set_checked(rt,p->path,pc+1,n,NULL,v); value_free(v); } free(maybe); free(name); } return; }
|
else { const char *pcur=rest; char *name=unquote_token(&pcur); char *n=trim_inplace(name); const char *save=pcur; char *maybe=unquote_token(&pcur); char *ty=trim_inplace(maybe); if(claro_is_type_word(ty)){ char *after=trim_inplace((char*)pcur); Value v=*after?eval_expr(rt,after):claro_default_for_type(ty); if(*n) rt_set_checked(rt,p->path,pc+1,n,ty,v); value_free(v); } else { Value v=eval_expr(rt,save); if(*n) rt_set_checked(rt,p->path,pc+1,n,NULL,v); value_free(v); } free(maybe); free(name); } return; }
|
||||||
if(strcmp(up,"ASK")==0){ const char *as=find_word_ci(t,"AS"); char input[1024]; char *prompt_text=NULL; char *var=NULL; char *type=NULL; if(as){ char *prompt_expr=substr(t+3,as); const char *pcur=as+2; var=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } { Value prompt=eval_expr(rt,prompt_expr); prompt_text=v_to_string(prompt); } free(prompt_expr); } else { const char *pcur=t+3; prompt_text=unquote_token(&pcur); var=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } if(prompt_text&&prompt_text[0]){ if(rt->capture){ str_add(&rt->captured,prompt_text); str_ch(&rt->captured,'\n'); } else { printf("%s\n",prompt_text); fflush(stdout); } } if(var&&*trim_inplace(var)){ Value val=v_str(""); if(fgets(input,sizeof(input),stdin)){ size_t n=strlen(input); while(n&&(input[n-1]=='\n'||input[n-1]=='\r')) input[--n]=0; } else input[0]=0; if(type&&*type){ if(!claro_value_from_text(type,input,&val)){ rt_error(rt,p->path,pc+1,"%s needs %s input. Try a value like %s.",trim_inplace(var),type,ci_eq(type,"NUMBER")?"5":(ci_eq(type,"YESNO")?"YES":"text")); } else rt_set_checked(rt,p->path,pc+1,trim_inplace(var),type,val); } else rt_set_checked(rt,p->path,pc+1,trim_inplace(var),NULL,v_str(input)); } else rt_error(rt,p->path,pc+1,"ASK needs a variable name. Try: ASK \"What is your name?\" name"); free(prompt_text); free(var); free(type); return; }
|
if(strcmp(up,"ASK")==0){ const char *as=find_word_ci(t,"AS"); char input[1024]; char *prompt_text=NULL; char *var=NULL; char *type=NULL; if(as){ char *prompt_expr=substr(t+3,as); const char *pcur=as+2; var=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } { Value prompt=eval_expr(rt,prompt_expr); prompt_text=v_to_string(prompt); value_free(prompt); } free(prompt_expr); } else { const char *pcur=t+3; prompt_text=unquote_token(&pcur); var=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } if(prompt_text&&prompt_text[0]){ if(rt->capture){ str_add(&rt->captured,prompt_text); str_ch(&rt->captured,'\n'); } else { printf("%s\n",prompt_text); fflush(stdout); } } if(var&&*trim_inplace(var)){ Value val=v_none(); if(fgets(input,sizeof(input),stdin)){ size_t n=strlen(input); while(n&&(input[n-1]=='\n'||input[n-1]=='\r')) input[--n]=0; } else input[0]=0; if(type&&*type){ if(!claro_value_from_text(type,input,&val)){ rt_error(rt,p->path,pc+1,"%s needs %s input. Try a value like %s.",trim_inplace(var),type,ci_eq(type,"NUMBER")?"5":(ci_eq(type,"YESNO")?"YES":"text")); } else rt_set_checked(rt,p->path,pc+1,trim_inplace(var),type,val); } else { val=v_str(input); rt_set_checked(rt,p->path,pc+1,trim_inplace(var),NULL,val); } value_free(val); } else rt_error(rt,p->path,pc+1,"ASK needs a variable name. Try: ASK \"What is your name?\" name"); free(prompt_text); free(var); free(type); return; }
|
||||||
if(strcmp(up,"IF")==0){ int elsepos=-1; int end=match_block(p,pc,"IF","ENDIF","ELSE",&elsepos); char *e=expr_after_word(t,"IF"); Value cond=eval_expr(rt,e); free(e); if(v_truth(cond)) exec_range(rt,p,pc+1,elsepos>=0?elsepos:end); else if(elsepos>=0) exec_range(rt,p,elsepos+1,end); value_free(cond); *pcp=end<0?pc:end; return; }
|
if(strcmp(up,"IF")==0){ int elsepos=-1; int end=match_block(p,pc,"IF","ENDIF","ELSE",&elsepos); char *e=expr_after_word(t,"IF"); Value cond=eval_expr(rt,e); free(e); if(v_truth(cond)) exec_range(rt,p,pc+1,elsepos>=0?elsepos:end); else if(elsepos>=0) exec_range(rt,p,elsepos+1,end); value_free(cond); *pcp=end<0?pc:end; return; }
|
||||||
if(strcmp(up,"DO")==0){ int end=find_done(p,pc); const char *times=find_word_ci(t,"TIMES"); if(!times){ const char *pcur=t+2; char *name=unquote_token(&pcur); char **parts=NULL; int ac=0,i; Value *args=NULL; char *n=trim_inplace(name); if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); args=(Value*)xmalloc(sizeof(Value)*(ac?ac:1)); for(i=0;i<ac;i++) args[i]=eval_expr(rt,parts[i]); call_function(rt,n,args,ac); split_args_free(parts,ac); call_args_free(args,ac); free(name); return; } else { char *e=substr(t+2,times); Value times_value=eval_expr(rt,e); int n=(int)v_number(times_value); int i; value_free(times_value); free(e); for(i=0;i<n && !rt->error && !rt->returning;i++) exec_range(rt,p,pc+1,end); *pcp=end<0?pc:end; return; } }
|
if(strcmp(up,"DO")==0){ int end=find_done(p,pc); const char *times=find_word_ci(t,"TIMES"); if(!times){ const char *pcur=t+2; char *name=unquote_token(&pcur); char **parts=NULL; int ac=0,i; Value *args=NULL; char *n=trim_inplace(name); if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); args=(Value*)xmalloc(sizeof(Value)*(ac?ac:1)); for(i=0;i<ac;i++) args[i]=eval_expr(rt,parts[i]); call_function(rt,n,args,ac); split_args_free(parts,ac); call_args_free(args,ac); free(name); return; } else { char *e=substr(t+2,times); Value times_value=eval_expr(rt,e); int n=(int)v_number(times_value); int i; value_free(times_value); free(e); for(i=0;i<n && !rt->error && !rt->returning;i++) exec_range(rt,p,pc+1,end); *pcp=end<0?pc:end; return; } }
|
||||||
if(strcmp(up,"FOR")==0){ int end=find_done(p,pc); if(starts_ci(t,"FOR EACH")){ char *r=t+8; const char *in=find_word_ci(r,"IN"); char *vars=substr(r,in?in:r); char *expr=xstrdup(in?in+2:""); Value col=eval_expr(rt,expr); char *comma=strchr(vars,','); if(comma&&col.type==V_MAP){ *comma=0; char *k=trim_inplace(vars), *v=trim_inplace(comma+1); int i; for(i=0;i<col.map->count&&!rt->error&&!rt->returning;i++){ rt_set(rt,k,v_str(col.map->keys[i])); rt_set(rt,v,col.map->vals[i]); exec_range(rt,p,pc+1,end); } } else if(col.type==V_LIST){ char *v=trim_inplace(vars); int i; for(i=0;i<col.list->count&&!rt->error&&!rt->returning;i++){ rt_set(rt,v,col.list->items[i]); exec_range(rt,p,pc+1,end); } } value_free(col); free(vars); free(expr); } else { char var[128]; const char *from=find_word_ci(t,"FROM"), *to=find_word_ci(t,"TO"), *step=find_word_ci(t,"STEP"); double a,b,s=1; if(from&&to){ snprintf(var,sizeof(var),"%.*s",(int)(from-(t+3)),t+3); { char *vt=trim_inplace(var); memmove(var,vt,strlen(vt)+1); } { char *ea=substr(from+4,to); char *eb=step?substr(to+2,step):xstrdup(to+2); a=v_number(eval_expr(rt,ea)); b=v_number(eval_expr(rt,eb)); if(step) s=v_number(eval_expr(rt,step+4)); free(ea); free(eb); } if(s==0) s=1; if(s>0){ for(;a<=b&&!rt->error&&!rt->returning;a+=s){ rt_set(rt,var,v_num(a)); exec_range(rt,p,pc+1,end);} } else { for(;a>=b&&!rt->error&&!rt->returning;a+=s){ rt_set(rt,var,v_num(a)); exec_range(rt,p,pc+1,end);} } } } *pcp=end<0?pc:end; return; }
|
if(strcmp(up,"FOR")==0){ int end=find_done(p,pc); if(starts_ci(t,"FOR EACH")){ char *r=t+8; const char *in=find_word_ci(r,"IN"); char *vars=substr(r,in?in:r); char *expr=xstrdup(in?in+2:""); Value col=eval_expr(rt,expr); char *comma=strchr(vars,','); if(comma&&col.type==V_MAP){ *comma=0; char *k=trim_inplace(vars), *v=trim_inplace(comma+1); int i; for(i=0;i<col.map->count&&!rt->error&&!rt->returning;i++){ rt_set(rt,k,v_str(col.map->keys[i])); rt_set(rt,v,col.map->vals[i]); exec_range(rt,p,pc+1,end); } } else if(col.type==V_LIST){ char *v=trim_inplace(vars); int i; for(i=0;i<col.list->count&&!rt->error&&!rt->returning;i++){ rt_set(rt,v,col.list->items[i]); exec_range(rt,p,pc+1,end); } } value_free(col); free(vars); free(expr); } else { char var[128]; const char *from=find_word_ci(t,"FROM"), *to=find_word_ci(t,"TO"), *step=find_word_ci(t,"STEP"); double a,b,s=1; if(from&&to){ snprintf(var,sizeof(var),"%.*s",(int)(from-(t+3)),t+3); { char *vt=trim_inplace(var); memmove(var,vt,strlen(vt)+1); } { char *ea=substr(from+4,to); char *eb=step?substr(to+2,step):xstrdup(to+2); a=v_number(eval_expr(rt,ea)); b=v_number(eval_expr(rt,eb)); if(step) s=v_number(eval_expr(rt,step+4)); free(ea); free(eb); } if(s==0) s=1; if(s>0){ for(;a<=b&&!rt->error&&!rt->returning;a+=s){ rt_set(rt,var,v_num(a)); exec_range(rt,p,pc+1,end);} } else { for(;a>=b&&!rt->error&&!rt->returning;a+=s){ rt_set(rt,var,v_num(a)); exec_range(rt,p,pc+1,end);} } } } *pcp=end<0?pc:end; return; }
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check ASK prompt expression values are released under LeakSanitizer."""
|
||||||
|
from pathlib import Path
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if os.name == "nt":
|
||||||
|
print("SKIP: LeakSanitizer check is POSIX-only")
|
||||||
|
return 0
|
||||||
|
with tempfile.TemporaryDirectory(prefix="claro-ask-prompt-cleanup-") as tmp:
|
||||||
|
tmp_path = Path(tmp)
|
||||||
|
binary = tmp_path / "claro-lsan"
|
||||||
|
script = tmp_path / "ask_prompts.claro"
|
||||||
|
script.write_text(
|
||||||
|
''.join('ASK "question" + " value" AS answer\n' for _ in range(2000)),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
build = subprocess.run(
|
||||||
|
["gcc", "-std=c99", "-O0", "-g", "-fsanitize=address,undefined",
|
||||||
|
"src/claro.c", "-o", str(binary), "-lm"],
|
||||||
|
cwd=ROOT, text=True, capture_output=True,
|
||||||
|
)
|
||||||
|
if build.returncode:
|
||||||
|
print(build.stdout, end="")
|
||||||
|
print(build.stderr, end="", file=sys.stderr)
|
||||||
|
return build.returncode
|
||||||
|
run = subprocess.run(
|
||||||
|
[str(binary), str(script)], cwd=ROOT, input="\n" * 2000,
|
||||||
|
text=True, capture_output=True,
|
||||||
|
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"},
|
||||||
|
)
|
||||||
|
if run.returncode:
|
||||||
|
print("FAIL: ASK prompt cleanup probe failed")
|
||||||
|
print(run.stdout, end="")
|
||||||
|
print(run.stderr, end="", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||||
|
print("FAIL: ASK prompt expression values still leak")
|
||||||
|
print(run.stderr, end="", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
if run.stdout.count("question value\n") != 2000:
|
||||||
|
print("FAIL: ASK prompt cleanup probe produced the wrong output")
|
||||||
|
return 1
|
||||||
|
print("PASS: ASK prompt expression values are released")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user