Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ All notable changes to EigenScript are documented here.

### Fixed

- **Memory corruption: values escaping an `arena_mark`…`arena_reset`
scope (#873).** `promote_if_arena` copied only numbers and strings to
the heap on store; a LIST escaping the scope became a dangling
reference into memory the next `arena_mark` handed back out — silent
wrong values, type confusion, and (via `append` into a heap list) a
`free(): invalid pointer` abort, all reachable from pure EigenScript
and invisible to ASan. Fixed at every store seam: `promote_if_arena`
now deep-promotes lists (recursively; lists are the only
arena-capable container), and the append / indexed-store /
`OP_SET_LOCAL` / `set_at` / `list_insert_at` / `copy_into` paths
promote arena values landing in heap containers (num fast paths
heap-force under an open window). The JIT is gated off while an arena
window is open — entry, OSR, and a deep-bail when a builtin opens one
mid-thunk — because emitted stores don't promote; arena scopes run
interpreted. Escaping a stored value is now *documented, safe
behavior*: the arena reclaims only unstored intermediates.
`tests/test_arena_escape.eigs` pins all seams under a stomp loop that
overwrites the reclaimed region, plus an OSR-threshold hot variant.

- **[62] audio capture no longer flakes on an opened-but-silent device
(#876).** A real capture device that opens but delivers no samples in
the bounded poll (suspended/held source) failed two checks — and the
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ arena_mark of null # save allocation point
arena_reset of null # reclaim all transient allocations
```

Bounded computation for constrained environments.
Bounded computation for constrained environments. Values that escape the
scope are safe: anything stored into a binding or a container that
outlives the window is promoted to the heap at the store (#873) — the
arena reclaims only the unstored intermediates.

## Standard Library

Expand Down
41 changes: 39 additions & 2 deletions src/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -3089,9 +3089,19 @@ Value* builtin_copy_into(Value *arg) {
if (!dest || dest->type != VAL_LIST || !src || src->type != VAL_LIST) return make_null();
if (offset < 0) return make_null();
for (int i = 0; i < src->data.list.count && offset + i < dest->data.list.count; i++) {
val_incref(src->data.list.items[i]);
Value *item = src->data.list.items[i];
/* #873: promote arena items landing in a heap destination. */
if (item && item->arena && !dest->arena) {
Value *promoted = promote_if_arena(item);
if (promoted != item) {
val_decref(dest->data.list.items[offset + i]);
dest->data.list.items[offset + i] = promoted;
continue;
}
}
val_incref(item);
val_decref(dest->data.list.items[offset + i]);
dest->data.list.items[offset + i] = src->data.list.items[i];
dest->data.list.items[offset + i] = item;
}
return dest;
}
Expand Down Expand Up @@ -3303,6 +3313,15 @@ Value* builtin_set_at(Value *arg) {
int idx;
if (!at_index(arg->data.list.items[1], list->data.list.count, "set_at", &idx))
return make_null();
/* #873: promote an arena value stored into a heap list. */
if (val && val->arena && !list->arena) {
Value *promoted = promote_if_arena(val);
if (promoted != val) {
val_decref(list->data.list.items[idx]);
list->data.list.items[idx] = promoted;
return list;
}
}
val_incref(val);
val_decref(list->data.list.items[idx]);
list->data.list.items[idx] = val;
Expand All @@ -3327,6 +3346,15 @@ Value* builtin_set_at(Value *arg) {
int col;
if (!at_index(arg->data.list.items[2], rowv->data.list.count, "set_at col", &col))
return make_null();
/* #873: promote an arena value stored into a heap row. */
if (val && val->arena && !rowv->arena) {
Value *promoted = promote_if_arena(val);
if (promoted != val) {
val_decref(rowv->data.list.items[col]);
rowv->data.list.items[col] = promoted;
return list;
}
}
val_incref(val);
val_decref(rowv->data.list.items[col]);
rowv->data.list.items[col] = val;
Expand Down Expand Up @@ -5452,6 +5480,15 @@ Value* builtin_list_insert_at(Value *arg) {
list_append(list, old_last);
memmove(&list->data.list.items[idx + 1], &list->data.list.items[idx],
(count - idx) * sizeof(Value *));
/* #873: promote an arena value inserted into a heap list. */
if (val && val->arena && !list->arena) {
Value *promoted = promote_if_arena(val);
if (promoted != val) {
list->data.list.items[idx] = promoted;
val_decref(old_last);
return list;
}
}
val_incref(val);
list->data.list.items[idx] = val;
val_decref(old_last);
Expand Down
42 changes: 40 additions & 2 deletions src/eigenscript.c
Original file line number Diff line number Diff line change
Expand Up @@ -1104,8 +1104,33 @@ Value* promote_if_arena(Value *v) {
* and a heap VAL_NULL leaks via slot_bridge_wrap's pointer-drop. */
return v;
}
/* Lists, dicts, functions: leave as-is (complex deep copy).
* Callers should avoid storing arena-allocated complex types. */
if (v->type == VAL_LIST) {
/* #873: an arena list stored into a binding or heap container
* outlives arena_reset as a dangling reference — silent wrong
* values, type confusion, even free() aborts when a decref
* walks the stale pointer. Deep-promote instead: a fresh heap
* list; arena children promote recursively (fresh rc=1,
* adopted), heap children are shared (incref'd). Arena lists
* are acyclic at promotion time — building a cycle requires
* mutating through a binding, and binding stores promote — so
* the recursion terminates. Aliasing between two references to
* the same UNBOUND arena temporary is not preserved (each
* promotes to its own copy); observing that would require a
* binding, which promotes. Lists are the only arena-capable
* container: make_dict/make_fn/buffers/text builders are
* heap-only constructors and never carry v->arena. */
Value *h = make_list_heap(v->data.list.count);
for (int i = 0; i < v->data.list.count; i++) {
Value *c = v->data.list.items[i];
Value *pc = promote_if_arena(c);
if (pc == c) val_incref(pc);
h->data.list.items[i] = pc;
}
h->data.list.count = v->data.list.count;
return h;
}
/* Remaining types (dict/fn/builtin/buffer/text builder) are
* heap-only at construction; an arena flag on one is unreachable. */
return v;
}

Expand Down Expand Up @@ -1420,6 +1445,19 @@ void list_append(Value *list, Value *item) {
}
list->data.list.capacity = new_cap;
}
/* #873: an arena item appended into a HEAP list dangles after
* arena_reset (the abort repro: decref of the stale pointer corrupts
* the allocator). Promote on the way in — same contract as the env
* and dict store paths. Arena-into-arena stays raw (both die at
* reset), heap-into-arena keeps the documented leak-side sharp edge
* (test_arena_ownership). */
if (__builtin_expect(item && item->arena && !list->arena, 0)) {
Value *promoted = promote_if_arena(item);
if (promoted != item) {
list->data.list.items[list->data.list.count++] = promoted;
return;
}
}
list->data.list.items[list->data.list.count++] = item;
val_incref(item);
}
Expand Down
1 change: 1 addition & 0 deletions src/eigenscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,7 @@ void free_weight_val(Value *v);

Value* make_num(double n);
Value* promote_if_arena(Value *v);
Value* make_num_permanent(double n); /* heap-only make_num (#873 store paths) */
void recycle_intermediate(Value *v);
Value* make_str(const char *s);
Value* make_str_owned(char *s);
Expand Down
Loading
Loading