fix: release WRITE FILE expression values
This commit is contained in:
@@ -57,6 +57,8 @@ The `DELETE FILE ...` and `DELETE FOLDER ...` commands now release their evaluat
|
||||
|
||||
`READ FILE ... AS ...` now releases both its evaluated path and the temporary file-content value after copying contents into runtime storage. `python3 tools/validate_read_file_expression_cleanup.py` reproduced 204,000 leaked bytes across 2,000 reads before the fix, then passed under ASan/UBSan/LSan. The check is wired into `tools/validate_v1_17.py`; this is one verified cleanup boundary, not a claim that all file-command temporaries are leak-free.
|
||||
|
||||
`WRITE FILE` and `APPEND FILE` now release their evaluated path and content values after the operation. `python3 tools/validate_write_file_expression_cleanup.py` reproduced 206,000 leaked bytes across 2,000 writes before the fix; the focused ASan/UBSan/LSan validator is wired into `tools/validate_v1_17.py`. Other file-command temporary ownership remains separate.
|
||||
|
||||
## Feature matrix
|
||||
|
||||
### Beginner scripting core
|
||||
|
||||
@@ -60,6 +60,8 @@ This validator uses a local HTTP server to check marker-like response text, stat
|
||||
|
||||
`READ FILE ... AS ...` now releases its evaluated path value and the temporary file-content value after runtime storage copies the contents. `python3 tools/validate_read_file_expression_cleanup.py` first reproduced 204,000 leaked bytes across 2,000 reads under ASan/UBSan/LSan; after the cleanup it passes with the expected output and no reported leak. This is a narrow ownership fix; other file-command temporaries remain separate.
|
||||
|
||||
`WRITE FILE ... WITH ...` and `APPEND FILE ... WITH ...` now release their evaluated path and content values after writing. `python3 tools/validate_write_file_expression_cleanup.py` reproduced 206,000 leaked bytes across 2,000 writes before the cleanup; it verifies repeated writes under ASan/UBSan/LSan and checks the resulting file content.
|
||||
|
||||
## Verification
|
||||
|
||||
2026-10-07: `python3 tools/validate_read_file_expression_cleanup.py` passes under ASan/UBSan/LSan; `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `python3 tools/validate_v1_17.py` all pass. Runtime-verified narrow cleanup; full validation does not establish that every allocation path is leak-free.
|
||||
|
||||
+1
-1
@@ -440,7 +440,7 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf
|
||||
if(strcmp(up,"COUNT")==0){ const char *as=find_word_ci(t,"AS"); if(as){ if(starts_ci(t+5," ARGUMENTS")){ char *var=xstrdup(trim_inplace((char*)as+2)); rt_set(rt,var,v_num(rt->script_argc)); free(var); } else { char *ex=substr(t+5,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value v=eval_expr(rt,ex); rt_set(rt,var,v_num(v.type==V_LIST?v.list->count:(v.type==V_MAP?v.map->count:0))); value_free(v); free(ex); free(var); } } return; }
|
||||
if(strcmp(up,"GET")==0){ const char *at=find_word_ci(t,"AT"), *key=find_word_ci(t,"KEY"), *as=find_word_ci(t,"AS"); if(as&&starts_ci(t+3," ALL ARGUMENTS")){ char *var=xstrdup(trim_inplace((char*)as+2)); Value arr=v_list(); int i; for(i=0;i<rt->script_argc;i++) list_add(arr.list,v_str(rt->script_argv[i])); rt_set(rt,var,arr); free(var); } else if(as&&starts_ci(t+3," ARGUMENT")){ char *ie=substr(t+12,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value index_value=eval_expr(rt,ie); int idx=(int)v_number(index_value); Value argument_value; value_free(index_value); argument_value=v_str(idx>=1&&idx<=rt->script_argc?rt->script_argv[idx-1]:""); rt_set(rt,var,argument_value); value_free(argument_value); free(ie); free(var); } else if(as&&starts_ci(t+3," ENV")){ const char *pcur=t+7; char *name=substr(pcur,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value nv=eval_expr(rt,name); char *ns=v_to_string(nv); const char *ev=getenv(ns); Value result=v_str(ev?ev:""); rt_set(rt,var,result); value_free(result); free(ns); value_free(nv); free(name); free(var); } else if(as&&at){ char *ce=substr(t+3,at); char *ie=substr(at+2,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce); int idx=(int)v_number(eval_expr(rt,ie)); if(c.type==V_LIST && idx>=1 && idx<=c.list->count) rt_set(rt,var,c.list->items[idx-1]); else { Value empty=v_str(""); rt_set(rt,var,empty); value_free(empty); } value_free(c); free(ce); free(ie); free(var); } else if(as&&key){ char *ce=substr(t+3,key); char *ke=substr(key+3,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value c=eval_expr(rt,ce); Value kv=eval_expr(rt,ke); char *ks=v_to_string(kv); if(c.type==V_MAP){ Value got=map_get(c.map,ks); rt_set(rt,var,got); value_free(got); } else rt_set(rt,var,v_str("")); free(ks); value_free(c); value_free(kv); free(ce); free(ke); free(var); } return; }
|
||||
if(strcmp(up,"PUT")==0){ const char *key=find_word_ci(t,"KEY"), *val=find_word_ci(t,"VALUE"), *as=find_word_ci(t,"AS"), *into=find_word_ci(t,"INTO"); if(key&&val){ char *me=substr(t+3,key); char *ke=substr(key+3,val); char *ve=xstrdup(val+5); Value m=eval_expr(rt,me); Value k=eval_expr(rt,ke); Value v=eval_expr(rt,ve); char *ks=v_to_string(k); if(m.type==V_MAP){ char *mn=trim_inplace(me); map_put(m.map,ks,v); rt_set(rt,mn,m); } free(ks); free(me); free(ke); free(ve); value_free(m); value_free(k); value_free(v); } else if(as&&into){ char *ke=substr(t+3,as); char *ve=substr(as+2,into); char *mn=xstrdup(trim_inplace((char*)into+4)); Value m=rt_get(rt,mn); Value k=eval_expr(rt,ke); Value v=eval_expr(rt,ve); char *ks=v_to_string(k); if(m.type==V_MAP){ map_put(m.map,ks,v); rt_set(rt,mn,m); } free(ks); free(ke); free(ve); free(mn); } return; }
|
||||
if(strcmp(up,"WRITE")==0||strcmp(up,"APPEND")==0){ if(starts_ci(t+strlen(w)," FILE")){ const char *pcur=t+strlen(w)+5; const char *with=find_word_ci(pcur,"WITH"); if(with){ char *pe=substr(pcur,with); char *ve=xstrdup(with+4); Value pv=eval_expr(rt,pe); Value vv=eval_expr(rt,ve); char *path=v_to_string(pv), *text=v_to_string(vv); FILE *f=fopen(path,strcmp(up,"APPEND")==0?"ab":"wb"); if(!f) rt_error(rt,p->path,pc+1,"Could not write file: %s",strerror(errno)); else { fwrite(text,1,strlen(text),f); fclose(f);} free(path); free(text); free(pe); free(ve); } } return; }
|
||||
if(strcmp(up,"WRITE")==0||strcmp(up,"APPEND")==0){ if(starts_ci(t+strlen(w)," FILE")){ const char *pcur=t+strlen(w)+5; const char *with=find_word_ci(pcur,"WITH"); if(with){ char *pe=substr(pcur,with); char *ve=xstrdup(with+4); Value pv=eval_expr(rt,pe); Value vv=eval_expr(rt,ve); char *path=v_to_string(pv), *text=v_to_string(vv); FILE *f=fopen(path,strcmp(up,"APPEND")==0?"ab":"wb"); if(!f) rt_error(rt,p->path,pc+1,"Could not write file: %s",strerror(errno)); else { fwrite(text,1,strlen(text),f); fclose(f);} free(path); free(text); value_free(pv); value_free(vv); free(pe); free(ve); } } return; }
|
||||
if(strcmp(up,"READ")==0){ const char *pcur=t+4; const char *as=find_word_ci(pcur,"AS"); if(as&&starts_ci(pcur," FILE")){ char *pe=substr(pcur+5,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value pv=eval_expr(rt,pe); char *path=v_to_string(pv); FILE *f=fopen(path,"rb"); if(!f) rt_error(rt,p->path,pc+1,"Could not read file: %s",strerror(errno)); else { Str b; int c; Value contents; str_init(&b); while((c=fgetc(f))!=EOF) str_ch(&b,(char)c); fclose(f); contents=v_str(b.s?b.s:""); rt_set(rt,var,contents); value_free(contents); free(b.s);} free(path); value_free(pv); free(pe); free(var); } return; }
|
||||
if(strcmp(up,"EXISTS")==0){ const char *pcur=t+6; const char *as=find_word_ci(pcur,"AS"); if(as&&starts_ci(pcur," FILE")){ char *pe=substr(pcur+5,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value pv=eval_expr(rt,pe); char *path=v_to_string(pv); rt_set(rt,var,v_bool(access(path,F_OK)==0)); free(path); value_free(pv); free(pe); free(var);} return; }
|
||||
if(strcmp(up,"CREATE")==0){ const char *pcur=t+6; if(starts_ci(pcur," FOLDER")){ char *pe=xstrdup(pcur+7); Value pv=eval_expr(rt,pe); char *path=v_to_string(pv); if(!make_folder(path)) rt_error(rt,p->path,pc+1,"Could not create folder: %s",strerror(errno)); free(path); value_free(pv); free(pe); } return; }
|
||||
|
||||
@@ -51,6 +51,7 @@ def main():
|
||||
run([sys.executable, "tools/validate_get_argument_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_get_env_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_read_file_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_write_file_expression_cleanup.py"])
|
||||
run([sys.executable, "tools/validate_version_convention.py"])
|
||||
run([str(EXE), "examples/networking.claro"])
|
||||
print("v1.18.26 validation complete")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check WRITE FILE releases evaluated path and content values."""
|
||||
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="write-expression-cleanup-", dir=os.environ.get("TMPDIR")) as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
target = tmp_path / "output.txt"
|
||||
script = tmp_path / "repeated_write.claro"
|
||||
path = str(target).replace('\\', '\\\\').replace('"', '\\"')
|
||||
script.write_text((f'WRITE FILE "{path}" WITH "hello"\n' * 2000), encoding="utf-8")
|
||||
binary = tmp_path / "claro-asan"
|
||||
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, text=True, capture_output=True,
|
||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=1"})
|
||||
if run.returncode or "LeakSanitizer" in run.stderr or "SUMMARY:" in run.stderr:
|
||||
print("FAIL: repeated WRITE FILE expressions retain allocations")
|
||||
print(run.stdout, end="")
|
||||
print(run.stderr, end="", file=sys.stderr)
|
||||
return 1
|
||||
if target.read_text(encoding="utf-8") != "hello":
|
||||
print("FAIL: WRITE FILE output changed")
|
||||
return 1
|
||||
print("PASS: repeated WRITE FILE expressions release temporary values")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user