fix: release overwritten runtime values
This commit is contained in:
@@ -27,7 +27,7 @@ Verified in this checkout on 2026-09-22:
|
|||||||
- `./claro doctor`: all checks `OK`.
|
- `./claro doctor`: all checks `OK`.
|
||||||
- `./claro validate`: validation passed.
|
- `./claro validate`: validation passed.
|
||||||
|
|
||||||
This run's narrow memory slice releases expression token arrays and token strings after every `eval_expr()` call. Focused coverage is `tools/validate_memory_cleanup.py`; it reports `PASS: expression token allocations are released` under an ASan/UBSan build with LeakSanitizer checking. Remaining memory-growth areas include runtime-owned overwritten values, loaded program storage, and other expression temporaries. The `RUN COMMAND` path remains trusted shell execution, not a sandbox.
|
This run's narrow memory slice releases the previous deep value when a runtime variable or map entry is overwritten. 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 verifies the overwrite cleanup helper is present. Remaining memory-growth areas include final runtime teardown, loaded program storage, and 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`. This slice does not yet fix HTTP buffering/status handling or the remaining memory-growth boundaries. 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`. This slice does not yet fix HTTP buffering/status handling or the remaining memory-growth boundaries. Claro remains a trusted-script interpreter, not a sandbox.
|
||||||
|
|
||||||
|
|||||||
@@ -20,3 +20,9 @@ python3 tools/validate_memory_cleanup.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
The validator builds an AddressSanitizer/UndefinedBehaviorSanitizer binary, runs a repeated variable-overwrite probe, and confirms LeakSanitizer no longer reports allocations from `tokenize`/`toks_add`. The interpreter remains a trusted-script runtime, not a sandbox.
|
The validator builds an AddressSanitizer/UndefinedBehaviorSanitizer binary, runs a repeated variable-overwrite probe, and confirms LeakSanitizer no longer reports allocations from `tokenize`/`toks_add`. The interpreter remains a trusted-script runtime, not a sandbox.
|
||||||
|
|
||||||
|
## Overwritten-value cleanup
|
||||||
|
|
||||||
|
Runtime variables and map entries own deep copies of their values. Replacing an existing variable or map entry now releases the previous string, list, or map value before storing its replacement. This is intentionally limited to overwrite boundaries; final runtime teardown and discarded expression temporaries remain follow-up cleanup slices.
|
||||||
|
|
||||||
|
Focused verification also checks the cleanup helper in the ASan/UBSan build and exercises string, list, and map overwrites without sanitizer errors.
|
||||||
|
|||||||
+4
-3
@@ -82,9 +82,10 @@ static Map *map_new(void){ Map *m=(Map*)xmalloc(sizeof(Map)); m->keys=NULL; m->v
|
|||||||
static Value v_list(void){ Value v=v_none(); v.type=V_LIST; v.list=list_new(); return v; }
|
static Value v_list(void){ Value v=v_none(); v.type=V_LIST; v.list=list_new(); return v; }
|
||||||
static Value v_map(void){ Value v=v_none(); v.type=V_MAP; v.map=map_new(); return v; }
|
static Value v_map(void){ Value v=v_none(); v.type=V_MAP; v.map=map_new(); return v; }
|
||||||
static Value v_copy(Value v);
|
static Value v_copy(Value v);
|
||||||
|
static void value_free(Value v){ int i; if(v.type==V_STR){ free(v.str); } else if(v.type==V_LIST&&v.list){ for(i=0;i<v.list->count;i++) value_free(v.list->items[i]); free(v.list->items); free(v.list); } else if(v.type==V_MAP&&v.map){ for(i=0;i<v.map->count;i++){ free(v.map->keys[i]); value_free(v.map->vals[i]); } free(v.map->keys); free(v.map->vals); free(v.map); } }
|
||||||
static void list_add(List *l,Value v){ if(l->count>=l->cap){ l->cap=l->cap?l->cap*2:8; l->items=(Value*)xrealloc(l->items,sizeof(Value)*l->cap);} l->items[l->count++]=v_copy(v); }
|
static void list_add(List *l,Value v){ if(l->count>=l->cap){ l->cap=l->cap?l->cap*2:8; l->items=(Value*)xrealloc(l->items,sizeof(Value)*l->cap);} l->items[l->count++]=v_copy(v); }
|
||||||
static int map_index(Map *m,const char *key){ int i; for(i=0;i<m->count;i++) if(strcmp(m->keys[i],key)==0) return i; return -1; }
|
static int map_index(Map *m,const char *key){ int i; for(i=0;i<m->count;i++) if(strcmp(m->keys[i],key)==0) return i; return -1; }
|
||||||
static void map_put(Map *m,const char *key,Value v){ int i=map_index(m,key); if(i>=0){ m->vals[i]=v_copy(v); return;} if(m->count>=m->cap){ m->cap=m->cap?m->cap*2:8; m->keys=(char**)xrealloc(m->keys,sizeof(char*)*m->cap); m->vals=(Value*)xrealloc(m->vals,sizeof(Value)*m->cap);} m->keys[m->count]=xstrdup(key); m->vals[m->count++]=v_copy(v); }
|
static void map_put(Map *m,const char *key,Value v){ int i=map_index(m,key); if(i>=0){ value_free(m->vals[i]); m->vals[i]=v_copy(v); return;} if(m->count>=m->cap){ m->cap=m->cap?m->cap*2:8; m->keys=(char**)xrealloc(m->keys,sizeof(char*)*m->cap); m->vals=(Value*)xrealloc(m->vals,sizeof(Value)*m->cap);} m->keys[m->count]=xstrdup(key); m->vals[m->count++]=v_copy(v); }
|
||||||
static Value map_get(Map *m,const char *key){ int i=map_index(m,key); if(i>=0) return v_copy(m->vals[i]); return v_none(); }
|
static Value map_get(Map *m,const char *key){ int i=map_index(m,key); if(i>=0) return v_copy(m->vals[i]); return v_none(); }
|
||||||
static Value v_copy(Value v){ Value o=v_none(); int i; o.type=v.type; o.num=v.num; o.boolean=v.boolean; if(v.type==V_STR) o.str=xstrdup(v.str); else if(v.type==V_LIST){ o=v_list(); for(i=0;i<v.list->count;i++) list_add(o.list,v.list->items[i]); } else if(v.type==V_MAP){ o=v_map(); for(i=0;i<v.map->count;i++) map_put(o.map,v.map->keys[i],v.map->vals[i]); } return o; }
|
static Value v_copy(Value v){ Value o=v_none(); int i; o.type=v.type; o.num=v.num; o.boolean=v.boolean; if(v.type==V_STR) o.str=xstrdup(v.str); else if(v.type==V_LIST){ o=v_list(); for(i=0;i<v.list->count;i++) list_add(o.list,v.list->items[i]); } else if(v.type==V_MAP){ o=v_map(); for(i=0;i<v.map->count;i++) map_put(o.map,v.map->keys[i],v.map->vals[i]); } return o; }
|
||||||
static int v_truth(Value v){ if(v.type==V_BOOL) return v.boolean; if(v.type==V_NUM) return fabs(v.num)>0.0000001; if(v.type==V_STR) return v.str&&v.str[0]; if(v.type==V_LIST) return v.list&&v.list->count>0; if(v.type==V_MAP) return v.map&&v.map->count>0; return 0; }
|
static int v_truth(Value v){ if(v.type==V_BOOL) return v.boolean; if(v.type==V_NUM) return fabs(v.num)>0.0000001; if(v.type==V_STR) return v.str&&v.str[0]; if(v.type==V_LIST) return v.list&&v.list->count>0; if(v.type==V_MAP) return v.map&&v.map->count>0; return 0; }
|
||||||
@@ -120,8 +121,8 @@ static void rt_init(Runtime *rt){ memset(rt,0,sizeof(*rt)); str_init(&rt->captur
|
|||||||
static Var *env_find(Var *v,const char *name){ while(v){ if(strcmp(v->name,name)==0) return v; v=v->next;} return NULL; }
|
static Var *env_find(Var *v,const char *name){ while(v){ if(strcmp(v->name,name)==0) return v; v=v->next;} return NULL; }
|
||||||
static Value rt_get(Runtime *rt,const char *name){ Var *v=env_find(rt->locals,name); if(v) return v_copy(v->val); v=env_find(rt->globals,name); if(v) return v_copy(v->val); return v_none(); }
|
static Value rt_get(Runtime *rt,const char *name){ Var *v=env_find(rt->locals,name); if(v) return v_copy(v->val); v=env_find(rt->globals,name); if(v) return v_copy(v->val); return v_none(); }
|
||||||
static int rt_has(Runtime *rt,const char *name){ return env_find(rt->locals,name)||env_find(rt->globals,name); }
|
static int rt_has(Runtime *rt,const char *name){ return env_find(rt->locals,name)||env_find(rt->globals,name); }
|
||||||
static void rt_set(Runtime *rt,const char *name,Value val){ Var **head=rt->locals?&rt->locals:&rt->globals; Var *v=env_find(*head,name); if(!v && rt->locals) v=env_find(rt->globals,name); if(v){ v->val=v_copy(val); return;} v=(Var*)xmalloc(sizeof(Var)); v->name=xstrdup(name); v->val=v_copy(val); v->next=*head; *head=v; }
|
static void rt_set(Runtime *rt,const char *name,Value val){ Var **head=rt->locals?&rt->locals:&rt->globals; Var *v=env_find(*head,name); if(!v && rt->locals) v=env_find(rt->globals,name); if(v){ value_free(v->val); v->val=v_copy(val); return;} v=(Var*)xmalloc(sizeof(Var)); v->name=xstrdup(name); v->val=v_copy(val); v->next=*head; *head=v; }
|
||||||
static void rt_set_global(Runtime *rt,const char *name,Value val){ Var *v=env_find(rt->globals,name); if(v){ v->val=v_copy(val); return;} v=(Var*)xmalloc(sizeof(Var)); v->name=xstrdup(name); v->val=v_copy(val); v->next=rt->globals; rt->globals=v; }
|
static void rt_set_global(Runtime *rt,const char *name,Value val){ Var *v=env_find(rt->globals,name); if(v){ value_free(v->val); v->val=v_copy(val); return;} v=(Var*)xmalloc(sizeof(Var)); v->name=xstrdup(name); v->val=v_copy(val); v->next=rt->globals; rt->globals=v; }
|
||||||
|
|
||||||
static void rt_error(Runtime *rt,const char *file,int line,const char *fmt,...);
|
static void rt_error(Runtime *rt,const char *file,int line,const char *fmt,...);
|
||||||
static char *normalize_path_copy(const char *path){ char *out=xstrdup(path?path:""); char *p; for(p=out;*p;p++) if(*p=='\\') *p='/'; return out; }
|
static char *normalize_path_copy(const char *path){ char *out=xstrdup(path?path:""); char *p; for(p=out;*p;p++) if(*p=='\\') *p='/'; return out; }
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ def main() -> int:
|
|||||||
script.write_text(
|
script.write_text(
|
||||||
'SET value TO "first"\n'
|
'SET value TO "first"\n'
|
||||||
'SET value TO "second"\n'
|
'SET value TO "second"\n'
|
||||||
'SAY value\n',
|
'SET items TO LIST\n'
|
||||||
|
'ADD "one" TO items\n'
|
||||||
|
'SET items TO LIST\n'
|
||||||
|
'SET profile TO MAP\n'
|
||||||
|
'PUT profile KEY "name" VALUE "Ada"\n'
|
||||||
|
'PUT profile KEY "name" VALUE "Grace"\n'
|
||||||
|
'SAY profile\n',
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
build = subprocess.run(
|
build = subprocess.run(
|
||||||
@@ -43,12 +49,16 @@ def main() -> int:
|
|||||||
print("FAIL: expression token allocations still leak")
|
print("FAIL: expression token allocations still leak")
|
||||||
print(run.stderr, end="", file=sys.stderr)
|
print(run.stderr, end="", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
source = (ROOT / "src" / "claro.c").read_text(encoding="utf-8")
|
||||||
|
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
|
||||||
functional = subprocess.run(
|
functional = subprocess.run(
|
||||||
[str(binary), str(script)], cwd=ROOT, text=True,
|
[str(binary), str(script)], cwd=ROOT, text=True,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=0"},
|
env={**os.environ, "ASAN_OPTIONS": "detect_leaks=0"},
|
||||||
)
|
)
|
||||||
if functional.returncode or "second\n" not in functional.stdout:
|
if functional.returncode or "[map]\n" not in functional.stdout:
|
||||||
print("FAIL: overwrite cleanup probe produced the wrong output")
|
print("FAIL: overwrite cleanup probe produced the wrong output")
|
||||||
print(functional.stdout, end="", file=sys.stderr)
|
print(functional.stdout, end="", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
Reference in New Issue
Block a user