fix: release READ FILE expression values

This commit is contained in:
Hermes Agent
2026-10-07 23:34:49 +00:00
parent e6a1e02790
commit 7275f457ad
5 changed files with 62 additions and 1 deletions
+2
View File
@@ -55,6 +55,8 @@ The `DELETE FILE ...` and `DELETE FOLDER ...` commands now release their evaluat
`claro check` now releases expression-token arrays and strings after variable-name analysis, and frees its temporary variable environment at the end of each check. The untracked focused regression validator `tools/validate_check_token_cleanup.py` first reproduced 310,447 leaked bytes across 2,000 repeated-string expressions; after the cleanup, `python3 tools/validate_check_token_cleanup.py` passes under ASan/UBSan/LSan. Fresh verification: `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This is a narrow, runtime-verified cleanup for check mode; it does not establish that all allocations are leak-free.
`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.
## Feature matrix
### Beginner scripting core
+8
View File
@@ -56,6 +56,14 @@ python3 tools/validate_http_hardening.py
This validator uses a local HTTP server to check marker-like response text, status `200`, and the oversized-response diagnostic.
## READ FILE expression cleanup
`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.
## 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.
## External command trust boundary
`RUN COMMAND` intentionally executes a shell command with the user's permissions. It is a trusted-code capability, not a sandbox or an untrusted-script safety feature. Claro does not attempt a fragile blacklist sanitizer; users must review scripts before running them.
+1 -1
View File
@@ -441,7 +441,7 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf
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,"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; str_init(&b); while((c=fgetc(f))!=EOF) str_ch(&b,(char)c); fclose(f); rt_set(rt,var,v_str(b.s?b.s:"")); free(b.s);} free(path); free(pe); free(var); } 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; }
if(strcmp(up,"COPY")==0){ const char *to=find_word_ci(t,"TO"); if(starts_ci(t+4," FILE")&&to){ char *se=substr(t+9,to); char *de=xstrdup(to+2); Value sv=eval_expr(rt,se), dv=eval_expr(rt,de); char *src=v_to_string(sv), *dst=v_to_string(dv); if(!copy_file_bytes(src,dst)) rt_error(rt,p->path,pc+1,"Could not copy file"); free(src); free(dst); free(se); free(de); value_free(sv); value_free(dv); } return; }
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Check that READ FILE releases its evaluated path expression."""
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="read-expression-cleanup-", dir=os.environ.get("TMPDIR")) as tmp:
tmp_path = Path(tmp)
source = tmp_path / "input.txt"
source.write_text("hello\n", encoding="utf-8")
script = tmp_path / "repeated_read.claro"
quoted = str(source).replace('\\', '\\\\').replace('"', '\\"')
script.write_text((f'READ FILE "{quoted}" AS contents\n' * 2000) + 'SAY contents\n', 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 READ FILE expressions retain allocations")
print(run.stdout, end="")
print(run.stderr, end="", file=sys.stderr)
return 1
if run.stdout != "hello\n\n":
print(f"FAIL: unexpected READ FILE output: {run.stdout!r}")
return 1
print("PASS: repeated READ FILE expressions release temporary values")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1
View File
@@ -50,6 +50,7 @@ def main():
run([sys.executable, "tools/validate_runtime_error_cleanup.py"])
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_version_convention.py"])
run([str(EXE), "examples/networking.claro"])
print("v1.18.26 validation complete")