fix: release caught runtime diagnostic values

This commit is contained in:
Hermes Agent
2026-10-06 07:08:28 +00:00
parent 3ce494b56c
commit e64bfcd30e
6 changed files with 64 additions and 4 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog # Changelog
### Release caught runtime diagnostic values
- Runtime errors caught by `TRY` now release their prior owned message and file strings when the error state is cleared.
- Added `tools/validate_runtime_error_cleanup.py` to exercise builtin-arity and inverted-range diagnostics under ASan/UBSan/LSan.
### Diagnose missing values in untyped object-field assignments ### Diagnose missing values in untyped object-field assignments
- `claro typecheck` now explains how to repair `SET player.score` by adding a value expression after the field name. - `claro typecheck` now explains how to repair `SET player.score` by adding a value expression after the field name.
+1 -1
View File
@@ -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. `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. The `COUNT ... AS` command now releases its evaluated list/map value after extracting the item count. `python3 tools/validate_count_expression_cleanup.py` first reproduced a 28,000-byte leak across 2,000 list expressions, then passed with LeakSanitizer enabled after the fix. `GET ... AT ... AS ...` now releases its copied collection and empty-result temporary after storing the selected item; `python3 tools/validate_get_expression_cleanup.py` first reproduced 34,000 bytes of leaks across 2,000 empty-list lookups, then passed with LeakSanitizer enabled after the fix. 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. Caught `TRY` errors now release their previous message and source-path values when cleared; `python3 tools/validate_runtime_error_cleanup.py` confirms caught builtin-arity and inverted-range errors pass ASan/UBSan/LSan. Additional malformed-input leak sites may remain, so this does not claim all runtime allocations are leak-free. The `COUNT ... AS` command now releases its evaluated list/map value after extracting the item count. `python3 tools/validate_count_expression_cleanup.py` first reproduced a 28,000-... [truncated]
The `FIND ... IN ... AS ...` command now releases its evaluated search value and copied list/map after storing the result. Focused coverage is `tools/validate_find_expression_cleanup.py`; it runs 2,000 searches under ASan/UBSan with LeakSanitizer enabled. Verified on 2026-09-24: `python3 tools/validate_find_expression_cleanup.py` passes with no reported leaks; `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This slice is runtime-verified under sanitizers; broader malformed-input sanitizer coverage remains blocked by the previously documented `rt_error` message leak. The `FIND ... IN ... AS ...` command now releases its evaluated search value and copied list/map after storing the result. Focused coverage is `tools/validate_find_expression_cleanup.py`; it runs 2,000 searches under ASan/UBSan with LeakSanitizer enabled. Verified on 2026-09-24: `python3 tools/validate_find_expression_cleanup.py` passes with no reported leaks; `make -s all`, `./claro test` (0 failures), `./claro doctor`, `./claro validate`, and `git diff --check` pass. This slice is runtime-verified under sanitizers; broader malformed-input sanitizer coverage remains blocked by the previously documented `rt_error` message leak.
+5 -1
View File
@@ -35,7 +35,11 @@ The `REMOVE` command now releases its evaluated needle and copied list/map value
When `REMOVE` finds an item in its copied list, it now frees that removed item before shifting the remaining owned values left. `python3 tools/validate_remove_list_item_cleanup.py` repeatedly adds and removes strings under ASan/UBSan/LSan to check that the item ownership is released without changing the list update behavior. When `REMOVE` finds an item in its copied list, it now frees that removed item before shifting the remaining owned values left. `python3 tools/validate_remove_list_item_cleanup.py` repeatedly adds and removes strings under ASan/UBSan/LSan to check that the item ownership is released without changing the list update behavior.
The `PUT ... KEY ... VALUE ...` command now releases its evaluated map, key, and value copies after `map_put`/`rt_set` have copied the data they own. Focused verification: `python3 tools/validate_put_expression_cleanup.py` exercises 2,000 string-valued writes under ASan/UBSan/LSan and passes with no reported leaks. This covers expression-temporary ownership only; broader malformed-input sanitizer runs still report the previously documented `rt_error` diagnostic-string leaks. The `PUT ... KEY ... VALUE ...` command now releases its evaluated map, key, and value copies after `map_put`/`rt_set` have copied the data they own. Focused verification: `python3 tools/validate_put_expression_cleanup.py` exercises 2,000 string-valued writes under ASan/UBSan/LSan and passes with no reported leaks. This covers expression-temporary ownership only; caught runtime diagnostic-value leaks are covered by the separate cleanup slice below.
Caught runtime errors release the previous owned error-message and source-path strings when `TRY` clears the error state. `python3 tools/validate_runtime_error_cleanup.py` covers repeated caught builtin-arity errors and an inverted `random.int` range under ASan/UBSan/LSan.
Verified 2026-10-06: `python3 tools/validate_runtime_error_cleanup.py` passes with LeakSanitizer enabled; `make -s all`, `./claro test` (0 failures), `./claro doctor`, and `./claro validate` pass. The focused validator is also part of `tools/validate_v1_17.py`. This addresses caught diagnostic-value ownership only; it does not claim every malformed-input path is leak-free.
## HTTP response handling ## HTTP response handling
+2 -2
View File
@@ -139,8 +139,8 @@ static int rt_set_checked(Runtime *rt,const char *file,int line,const char *name
rt_set(rt,name,val); return 1; } rt_set(rt,name,val); return 1; }
static void graphics_note(Runtime *rt,const char *what){ (void)what; rt_set(rt,"GRAPHICS_READY",v_bool(1)); } static void graphics_note(Runtime *rt,const char *what){ (void)what; rt_set(rt,"GRAPHICS_READY",v_bool(1)); }
static void rt_error(Runtime *rt,const char *file,int line,const char *fmt,...){ va_list ap; char buf[1024]; char *nfile; if(rt->error) return; va_start(ap,fmt); vsnprintf(buf,sizeof(buf),fmt,ap); va_end(ap); nfile=normalize_path_copy(file?file:""); rt->error=1; rt->err_msg=xstrdup(buf); rt->err_file=xstrdup(nfile); rt->err_line=line; rt_set(rt,"LASTERROR",v_str(buf)); rt_set(rt,"LASTERRORFILE",v_str(nfile)); rt_set(rt,"LASTERRORLINE",v_num(line)); free(nfile); } static void rt_error(Runtime *rt,const char *file,int line,const char *fmt,...){ va_list ap; char buf[1024]; char *nfile; Value message,file_value; if(rt->error) return; va_start(ap,fmt); vsnprintf(buf,sizeof(buf),fmt,ap); va_end(ap); nfile=normalize_path_copy(file?file:""); rt->error=1; rt->err_msg=xstrdup(buf); rt->err_file=xstrdup(nfile); rt->err_line=line; message=v_str(buf); rt_set(rt,"LASTERROR",message); value_free(message); file_value=v_str(nfile); rt_set(rt,"LASTERRORFILE",file_value); value_free(file_value); rt_set(rt,"LASTERRORLINE",v_num(line)); free(nfile); }
static void rt_clear_error(Runtime *rt){ rt->error=0; } static void rt_clear_error(Runtime *rt){ rt->error=0; free(rt->err_msg); free(rt->err_file); rt->err_msg=NULL; rt->err_file=NULL; }
static void out_line(Runtime *rt,Value v){ char *s=v_to_string(v); if(rt->capture){ str_add(&rt->captured,s); str_ch(&rt->captured,'\n'); } else { printf("%s\n",s); } free(s); } static void out_line(Runtime *rt,Value v){ char *s=v_to_string(v); if(rt->capture){ str_add(&rt->captured,s); str_ch(&rt->captured,'\n'); } else { printf("%s\n",s); } free(s); }
static void add_function(Runtime *rt,const char *name,char **params,int pcnt,Program *p,int start,int end){ Function *f=(Function*)xmalloc(sizeof(Function)); int i; f->name=xstrdup(name); f->pcnt=pcnt; f->params=(char**)xmalloc(sizeof(char*)*(pcnt?pcnt:1)); for(i=0;i<pcnt;i++) f->params[i]=xstrdup(params[i]); f->prog=p; f->start=start; f->end=end; f->next=rt->funcs; rt->funcs=f; } static void add_function(Runtime *rt,const char *name,char **params,int pcnt,Program *p,int start,int end){ Function *f=(Function*)xmalloc(sizeof(Function)); int i; f->name=xstrdup(name); f->pcnt=pcnt; f->params=(char**)xmalloc(sizeof(char*)*(pcnt?pcnt:1)); for(i=0;i<pcnt;i++) f->params[i]=xstrdup(params[i]); f->prog=p; f->start=start; f->end=end; f->next=rt->funcs; rt->funcs=f; }
static Function *find_function(Runtime *rt,const char *name){ Function *f=rt->funcs; while(f){ if(strcmp(f->name,name)==0) return f; f=f->next;} return NULL; } static Function *find_function(Runtime *rt,const char *name){ Function *f=rt->funcs; while(f){ if(strcmp(f->name,name)==0) return f; f=f->next;} return NULL; }
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
"""Check that caught runtime diagnostics release their owned message values."""
from pathlib import Path
import os
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parent.parent
CASES = {
"tests/38_builtin_arity.claro": "math.abs needs 1 argument",
"tests/39_random_inverted_range.claro": "random.int needs the lower bound",
}
def main() -> int:
if os.name == "nt":
print("SKIP: LeakSanitizer check is POSIX-only")
return 0
with tempfile.TemporaryDirectory(prefix="runtime-error-cleanup-") as tmp:
binary = Path(tmp) / "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
for script, expected in CASES.items():
run = subprocess.run(
[str(binary), 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(f"FAIL: caught diagnostics in {script} retain allocations")
print(run.stdout, end="")
print(run.stderr, end="", file=sys.stderr)
return 1
if expected not in run.stdout:
print(f"FAIL: expected diagnostic was not printed for {script}")
print(run.stdout, end="")
return 1
print("PASS: caught builtin-arity and inverted-range diagnostics are leak-free")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+1
View File
@@ -47,6 +47,7 @@ def main():
run([sys.executable, "tools/validate_concurrency.py"]) run([sys.executable, "tools/validate_concurrency.py"])
run([sys.executable, "tools/validate_package_security.py"]) run([sys.executable, "tools/validate_package_security.py"])
run([sys.executable, "tools/validate_typecheck_diagnostics.py"]) run([sys.executable, "tools/validate_typecheck_diagnostics.py"])
run([sys.executable, "tools/validate_runtime_error_cleanup.py"])
run([sys.executable, "tools/validate_version_convention.py"]) run([sys.executable, "tools/validate_version_convention.py"])
run([str(EXE), "examples/networking.claro"]) run([str(EXE), "examples/networking.claro"])
print("v1.18.26 validation complete") print("v1.18.26 validation complete")