From 3fdc5a90f0ee57f88830af1137339ad89d0584c9 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Wed, 23 Sep 2026 07:01:45 +0000 Subject: [PATCH] fix: release remaining runtime-owned values --- docs/CURRENT_STATUS.md | 4 ++-- src/claro.c | 4 +++- tools/validate_memory_cleanup.py | 3 +++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 005e6db..6bb19cd 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -28,9 +28,9 @@ Verified in this checkout on 2026-09-23: - `./claro doctor`: all checks `OK`. - `./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`, and now release loaded program paths, lines, and pointer arrays at the end of each script run. Focused coverage is `tools/validate_memory_cleanup.py`; it reports `PASS: expression token allocations are released` under an ASan/UBSan build with LeakSanitizer checking and rejects any remaining `load_program` leak report while exercising repeated four-argument calls plus string/list/map overwrites. Remaining memory-growth areas include final runtime teardown for variables/functions/classes/modules and other expression temporaries. 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. Focused coverage is `tools/validate_memory_cleanup.py`; it reports `PASS: expression token allocations are released` under an ASan/UBSan build with LeakSanitizer checking and rejects any remaining `load_program` report while exercising repeated four-argument calls plus string/list/map overwrites. Remaining memory-growth areas include other expression temporaries. 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 final runtime teardown for variables/functions/classes/modules and 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. `RUN COMMAND` is documented and validated as a trusted-code capability: it executes shell commands with the user's permissions and is not a sandbox. Claro does not claim untrusted-script safety or use a fragile blacklist sanitizer. Focused documentation coverage is `tools/validate_trusted_command_docs.py`. diff --git a/src/claro.c b/src/claro.c index 13ac286..f7e4836 100644 --- a/src/claro.c +++ b/src/claro.c @@ -287,6 +287,8 @@ static void value_list_reverse(Value v){ int i; if(v.type!=V_LIST||!v.list) retu static Program *load_program(Runtime *rt,const char *path){ FILE *f; char buf[4096]; Program *p; int i; for(i=0;ipcount;i++) if(strcmp(rt->programs[i]->path,path)==0) return rt->programs[i]; f=fopen(path,"rb"); if(!f) return NULL; p=(Program*)xmalloc(sizeof(Program)); p->path=xstrdup(path); p->lines=NULL; p->count=p->cap=0; while(fgets(buf,sizeof(buf),f)){ size_t n=strlen(buf); while(n&& (buf[n-1]=='\n'||buf[n-1]=='\r')) buf[--n]=0; if(p->count>=p->cap){ p->cap=p->cap?p->cap*2:32; p->lines=(char**)xrealloc(p->lines,sizeof(char*)*p->cap);} p->lines[p->count++]=xstrdup(buf); } fclose(f); if(rt->pcount>=rt->pcap){ rt->pcap=rt->pcap?rt->pcap*2:16; rt->programs=(Program**)xrealloc(rt->programs,sizeof(Program*)*rt->pcap);} rt->programs[rt->pcount++]=p; return p; } static void program_free(Program *p){ int i; if(!p) return; for(i=0;icount;i++) free(p->lines[i]); free(p->lines); free(p->path); free(p); } static void runtime_programs_free(Runtime *rt){ int i; if(!rt) return; for(i=0;ipcount;i++) program_free(rt->programs[i]); free(rt->programs); rt->programs=NULL; rt->pcount=rt->pcap=0; } +static void runtime_vars_free(Var **head){ Var *v,*next; if(!head) return; for(v=*head;v;v=next){ next=v->next; free(v->name); value_free(v->val); free(v); } *head=NULL; } +static void runtime_free(Runtime *rt){ Function *fn,*fn_next; Module *m,*m_next; ClassDef *c,*c_next; FieldDef *field,*field_next; int i; if(!rt) return; runtime_programs_free(rt); runtime_vars_free(&rt->locals); runtime_vars_free(&rt->globals); for(fn=rt->funcs;fn;fn=fn_next){ fn_next=fn->next; free(fn->name); for(i=0;ipcnt;i++) free(fn->params[i]); free(fn->params); free(fn); } rt->funcs=NULL; for(m=rt->modules;m;m=m_next){ m_next=m->next; free(m->ns); free(m->kind); free(m); } rt->modules=NULL; for(c=rt->classes;c;c=c_next){ c_next=c->next; for(field=c->fields;field;field=field_next){ field_next=field->next; free(field->name); free(field->type); free(field); } free(c->name); free(c); } rt->classes=NULL; for(i=0;iimport_depth;i++) free(rt->import_stack[i]); free(rt->import_stack); rt->import_stack=NULL; rt->import_depth=0; free(rt->captured.s); rt->captured.s=NULL; value_free(rt->ret); rt->ret=v_none(); free(rt->err_msg); free(rt->err_file); rt->err_msg=NULL; rt->err_file=NULL; } static void scan_functions(Runtime *rt,Program *p,const char *prefix); static void line_word(Program *p,int i,char *up,size_t n){ char tmp[512],w[128]; strncpy(tmp,p->lines[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; first_word(trim_inplace(tmp),w,sizeof(w)); upper_copy(up,w,n); } static int line_is_block_opener(Program *p,int i,const char *up){ char *t; char tmp[512]; if(ci_eq(up,"IF")||ci_eq(up,"FOR")||ci_eq(up,"REPEAT")||ci_eq(up,"TEACH")||ci_eq(up,"TRY")||ci_eq(up,"COMMENT")||ci_eq(up,"CLASS")) return 1; if(ci_eq(up,"DO")){ strncpy(tmp,p->lines[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); return find_word_ci(t,"TIMES")!=NULL; } if(ci_eq(up,"START")){ strncpy(tmp,p->lines[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); return starts_ci(t+5," TASK"); } return 0; } @@ -833,4 +835,4 @@ static int file_exists_simple(const char *path){ FILE *f=fopen(path,"rb"); if(f) static int run_doctor(void){ int ok=1; const char *files[]={"src/claro.c","README.md","assets/Claro_Logo.jpg","lessons/01_hello.claro","examples/hello.claro","tests/01_hello.claro",NULL}; int i; printf("%s\n",CLARO_VERSION); printf("Doctor check:\n"); for(i=0;files[i];i++){ int has=file_exists_simple(files[i]); printf(" %s %s\n",has?"OK":"MISSING",files[i]); if(!has) ok=0; } printf("%s\n",ok?"Claro folder looks ready.":"Some Claro files are missing."); return ok?0:1; } static int run_validate(void){ int fails=0, i; const char *checks[]={"lessons/01_hello.claro","lessons/02_ask_name.claro","lessons/03_variables.claro","lessons/04_math.claro","lessons/05_if_else.claro","lessons/06_loops.claro","lessons/07_lists.claro","lessons/08_functions.claro","lessons/09_files.claro","lessons/10_final_quiz.claro","examples/hello.claro","examples/name_input.claro","examples/quiz.claro","examples/calculator.claro","examples/guessing_game.claro","examples/shopping_list.claro","examples/save_and_load_file.claro","examples/simple_functions.claro","examples/text_and_lists.claro","examples/text_polish.claro","examples/practical_scripting.claro","examples/type_hardening.claro","examples/objects_classes.claro","examples/networking.claro",NULL}; printf("%s\n",CLARO_VERSION); printf("Stable package validation:\n"); fails+=run_doctor(); fails+=run_tests(); printf("Checking lessons and main examples:\n"); for(i=0;checks[i];i++){ printf(" %s\n",checks[i]); fails+=check_file(checks[i]); } { const char *typecheck_good[] = {"tests/typecheck_function_branch_complete_good.claro","tests/typecheck_function_nested_branch_complete_good.claro","tests/typecheck_function_compat_branch_complete_good.claro","tests/typecheck_function_return_expression_good.claro","tests/typecheck_function_return_multiplication_good.claro","tests/typecheck_method_call_good.claro","tests/typecheck_method_field_assignment_good.claro","tests/typecheck_method_text_concat_good.claro","tests/typecheck_method_compat_text_concat_good.claro","tests/typecheck_method_text_field_assignment_good.claro","tests/typecheck_method_compat_text_field_assignment_good.claro","tests/typecheck_method_compat_yesno_field_assignment_good.claro","tests/typecheck_method_inline_field_annotation_as_to_good.claro","tests/typecheck_method_compat_inline_field_annotation_as_to_good.claro","tests/typecheck_method_field_check_type_good.claro","tests/typecheck_method_compat_field_check_type_good.claro","tests/typecheck_method_compat_return_good.claro","tests/typecheck_method_compat_lowercase_return_good.claro","tests/typecheck_method_compat_return_multiplication_good.claro","tests/typecheck_method_compat_return_addition_good.claro","tests/typecheck_method_nested_branch_complete_good.claro","tests/typecheck_method_return_expression_good.claro","tests/typecheck_method_return_addition_good.claro","tests/typecheck_method_return_good.claro","tests/typecheck_nested_container_good.claro","tests/typecheck_object_alias_method_call_good.claro","tests/typecheck_object_alias_method_do_good.claro","tests/typecheck_object_field_alias_good.claro","tests/typecheck_object_field_check_type_chained_alias_text_good.claro","tests/typecheck_object_field_check_type_yesno_good.claro","tests/typecheck_object_field_division_good.claro","tests/typecheck_object_field_multiplication_good.claro","tests/typecheck_object_field_text_concat_good.claro","tests/typecheck_object_field_text_concat_reverse_good.claro","tests/typecheck_object_field_subtraction_good.claro",NULL}; const char *typecheck_bad[] = {"tests/typecheck_function_branch_missing_return_bad.claro","tests/typecheck_function_call_missing_unchecked_arg_bad.claro","tests/typecheck_function_call_unknown_bad.claro","tests/typecheck_function_extra_arg_bad.claro","tests/typecheck_function_missing_arg_bad.claro","tests/typecheck_function_missing_return_bad.claro","tests/typecheck_function_compat_missing_return_bad.claro","tests/typecheck_function_return_expression_bad.claro","tests/typecheck_function_return_arithmetic_bad.claro","tests/typecheck_invalid_expected_type_bad.claro","tests/typecheck_missing_expression_bad.claro","tests/typecheck_missing_expression_with_is_bad.claro","tests/typecheck_invalid_return_type_bad.claro","tests/typecheck_method_call_bad.claro","tests/typecheck_method_call_unknown_method_bad.claro","tests/typecheck_method_call_unknown_object_bad.claro","tests/typecheck_method_call_unknown_object_unknown_method_bad.claro","tests/typecheck_method_compat_return_bad.claro","tests/typecheck_method_compat_return_multiplication_bad.claro","tests/typecheck_method_duplicate_name_bad.claro","tests/typecheck_duplicate_field_bad.claro","tests/typecheck_duplicate_class_bad.claro","tests/typecheck_method_extra_arg_bad.claro","tests/typecheck_method_invalid_return_type_bad.claro","tests/typecheck_method_missing_arg_bad.claro","tests/typecheck_method_missing_return_bad.claro","tests/typecheck_method_missing_unchecked_arg_bad.claro","tests/typecheck_method_return_bad.claro","tests/typecheck_method_return_addition_bad.claro","tests/typecheck_method_return_expression_bad.claro","tests/typecheck_method_second_method_bad.claro","tests/typecheck_method_second_method_extra_arg_bad.claro","tests/typecheck_method_unknown_method_bad.claro","tests/typecheck_method_field_assignment_bad.claro","tests/typecheck_method_short_field_missing_value_bad.claro","tests/typecheck_method_compat_short_field_missing_value_bad.claro","tests/typecheck_method_compat_unknown_field_bad.claro","tests/typecheck_method_unknown_field_typed_bad.claro","tests/typecheck_method_compat_unknown_field_typed_bad.claro","tests/typecheck_method_unknown_field_expression_bad.claro","tests/typecheck_method_text_field_assignment_bad.claro","tests/typecheck_method_compat_text_field_assignment_bad.claro","tests/typecheck_method_yesno_field_assignment_bad.claro","tests/typecheck_method_compat_yesno_field_assignment_bad.claro","tests/typecheck_method_field_check_type_bad.claro","tests/typecheck_method_compat_field_check_type_bad.claro","tests/typecheck_method_unknown_object_bad.claro","tests/typecheck_object_alias_method_bad.claro","tests/typecheck_object_alias_method_call_bad.claro","tests/typecheck_object_field_addition_bad.claro","tests/typecheck_object_field_after_method_bad.claro","tests/typecheck_object_field_alias_bad.claro","tests/typecheck_object_field_chained_alias_bad.claro","tests/typecheck_object_field_division_bad.claro","tests/typecheck_object_field_multiplication_bad.claro","tests/typecheck_object_field_subtraction_bad.claro","tests/typecheck_object_field_typed_bad.claro","tests/typecheck_object_field_missing_value_bad.claro","tests/typecheck_object_field_unknown_object_bad.claro","tests/typecheck_object_field_unknown_typed_bad.claro","tests/typecheck_missing_field_type_bad.claro","tests/typecheck_extra_class_tokens_bad.claro","tests/typecheck_extra_new_tokens_bad.claro","tests/typecheck_extra_field_tokens_bad.claro",NULL}; for(i=0;typecheck_good[i];i++) fails+=typecheck_file(typecheck_good[i]); for(i=0;typecheck_bad[i];i++) if(typecheck_file(typecheck_bad[i])==0) fails++; } fails+=typecheck_file("tests/typecheck_good.claro"); fails+=typecheck_file("tests/typecheck_container_good.claro"); fails+=typecheck_file("tests/typecheck_function_good.claro"); fails+=typecheck_file("tests/typecheck_function_return_good.claro"); fails+=typecheck_file("tests/typecheck_function_compat_return_good.claro"); fails+=typecheck_file("tests/typecheck_function_multi_good.claro"); fails+=typecheck_file("tests/typecheck_method_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_expression_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_compound_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_subtraction_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_text_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_text_concat_fields_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_yesno_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_check_type_number_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_check_type_text_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_check_type_text_yesno_good.claro"); fails+=typecheck_file("tests/typecheck_object_field_check_type_chained_alias_good.claro"); fails+=typecheck_file("tests/37_object_field_types.claro"); if(typecheck_file("tests/typecheck_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_container_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_duplicate_param_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_return_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_return_extra_tokens_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_compat_return_extra_tokens_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_compat_empty_return_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_compat_return_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_empty_return_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_multi_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_missing_unchecked_arg_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_function_unknown_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_method_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_method_branch_missing_return_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_text_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_yesno_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_unknown_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_unknown_text_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_unknown_yesno_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_unknown_expression_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_expression_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_compound_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_text_expression_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_text_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_yesno_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_number_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_unknown_number_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_unknown_text_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_unknown_yesno_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_unknown_object_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_alias_bad.claro")==0) fails++; if(typecheck_file("tests/typecheck_object_field_check_type_chained_alias_bad.claro")==0) fails++; printf("%s\n",fails?"Validation found problems.":"Validation passed. Claro v1.18.26 foundation checks are ready for use."); return fails?1:0; } -int main(int argc,char **argv){ int arg=1, trace=0; if(argc<2){ print_help(); return 0;} if(strcmp(argv[arg],"--trace")==0||strcmp(argv[arg],"trace")==0){ trace=1; arg++; } if(arg>=argc) return 0; if(strcmp(argv[arg],"help")==0||strcmp(argv[arg],"--help")==0){ print_help(); return 0; } if(strcmp(argv[arg],"test")==0) return run_tests(); if(strcmp(argv[arg],"repl")==0) return run_repl(); if(strcmp(argv[arg],"examples")==0) return show_examples(); if(strcmp(argv[arg],"doctor")==0) return run_doctor(); if(strcmp(argv[arg],"validate")==0) return run_validate(); if(strcmp(argv[arg],"package")==0) return run_package_cmd(argc,argv,arg+1); if(strcmp(argv[arg],"ide")==0) return print_ide_info(); if(strcmp(argv[arg],"new")==0&&arg+1=argc){ Runtime rt; int rc; char *mainfile=project_value("main"); if(!mainfile||!*mainfile){ if(mainfile) free(mainfile); mainfile=xstrdup("main.claro"); } rt_init(&rt); rt.trace=trace; rt.script_argc=0; rt.script_argv=NULL; rc=run_file(&rt,mainfile); free(mainfile); return rc; } } { Runtime rt; int rc; rt_init(&rt); rt.trace=trace; rt.script_argc=argc-arg-1; rt.script_argv=argv+arg+1; rc=run_file(&rt,argv[arg]); return rc; } } +int main(int argc,char **argv){ int arg=1, trace=0; if(argc<2){ print_help(); return 0;} if(strcmp(argv[arg],"--trace")==0||strcmp(argv[arg],"trace")==0){ trace=1; arg++; } if(arg>=argc) return 0; if(strcmp(argv[arg],"help")==0||strcmp(argv[arg],"--help")==0){ print_help(); return 0; } if(strcmp(argv[arg],"test")==0) return run_tests(); if(strcmp(argv[arg],"repl")==0) return run_repl(); if(strcmp(argv[arg],"examples")==0) return show_examples(); if(strcmp(argv[arg],"doctor")==0) return run_doctor(); if(strcmp(argv[arg],"validate")==0) return run_validate(); if(strcmp(argv[arg],"package")==0) return run_package_cmd(argc,argv,arg+1); if(strcmp(argv[arg],"ide")==0) return print_ide_info(); if(strcmp(argv[arg],"new")==0&&arg+1=argc){ Runtime rt; int rc; char *mainfile=project_value("main"); if(!mainfile||!*mainfile){ if(mainfile) free(mainfile); mainfile=xstrdup("main.claro"); } rt_init(&rt); rt.trace=trace; rt.script_argc=0; rt.script_argv=NULL; rc=run_file(&rt,mainfile); free(mainfile); runtime_free(&rt); return rc; } } { Runtime rt; int rc; rt_init(&rt); rt.trace=trace; rt.script_argc=argc-arg-1; rt.script_argv=argv+arg+1; rc=run_file(&rt,argv[arg]); runtime_free(&rt); return rc; } } diff --git a/tools/validate_memory_cleanup.py b/tools/validate_memory_cleanup.py index 562ec22..57d7e88 100644 --- a/tools/validate_memory_cleanup.py +++ b/tools/validate_memory_cleanup.py @@ -64,6 +64,9 @@ def main() -> int: if "static void value_free(Value v)" not in source or "value_free(v->val);" not in source: print("FAIL: overwritten runtime values are not released") return 1 + if "static void runtime_free(Runtime *rt)" not in source: + print("FAIL: runtime-owned values have no final cleanup boundary") + return 1 functional = subprocess.run( [str(binary), str(script)], cwd=ROOT, text=True, capture_output=True,