Fix a double-free: a heap-string field taken from a borrowed local was freed - #1927
Fix a double-free: a heap-string field taken from a borrowed local was freed#1927paul-hammant wants to merge 1 commit into
Conversation
…s freed
When a heap-string struct field is assigned from a bare heap-tracked local
(`a.value = v`), the field's `_heap_<field>` tracker was hard-coded to 1. But a
local classified as a heap-string var can hold a BORROWED value at runtime —
e.g. one returned by a function that passes a parameter or a literal straight
through, which leaves the local's own `_heap_<var>` at 0. The struct destructor
then freed a pointer the program never owned: a string literal (an invalid
free of read-only data) or a value still owned elsewhere (a double free).
It reached a downstream HTML sanitizer as a callback/FFI crash: an
`on_filter_url` hook returns a literal `string`, the engine threads it through
several `-> string` helpers and stores it into a heap-boxed attribute's string
field; the program printed correct output, then aborted with `invalid pointer`
/ SIGSEGV when the node tree was freed. The generated store read
`attr->_heap_value = 1` even though the source local's `_heap_...` was 0.
Fix: when the RHS is a bare heap-tracked-var identifier, the field store now
MOVES the source's runtime ownership into the field tracker rather than
asserting 1:
a->value = v; a->_heap_value = _heap_v; _heap_v = 0;
A borrowed value (`_heap_v == 0`) is then not freed by the destructor; a
genuinely-owned one (`_heap_v == 1`) is still freed exactly once, and the
source is disowned so it is not also freed at scope exit. This is the field-
store analogue of the existing `_heap_dest = _heap_src` alias move, and applies
to the direct-pointer, value-struct, nested-path, and untrusted-box store
paths. Regression follow-up to #1866/#1879.
Regression test (tests/integration/heap_field_borrowed_var_no_double_free)
asserts the generated store moves the runtime tracker (not `= 1`), the program
runs to completion (the bug aborted at teardown), and — via the `owned` half —
that a genuinely-heap value is still reclaimed. Confirmed to FAIL on the
unfixed compiler (emits `_heap_value = 1`) and pass here. Valgrind clean on
both the borrowed (no double free) and owned (freed once, no leak) cases.
make test 409/409; make test-ae 1081/1083 (the 2 are the pre-existing
windows_crt_symbols + h2 50-stream env flakes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Do not merge — converting to draft. This branch's commit holds the first (store-site) attempt at the fix, which is not safe: it trades the double-free for a leak in the escape case ( Full diagnosis, the two indistinguishable cases, both reproducers, and the sound fix (per-variable provenance dataflow rather than a store-site heuristic) are in #1928. The correct fix needs to mark a string var "borrowed" when assigned from a non-heap-owned RHS and consult that at the field store — that is dataflow, not a local guard, so it wants a fresh pass rather than another store-site patch. Leaving this open as draft for reference; the work should land from #1928. |
Fixes a memory-corruption regression a downstream HTML sanitizer hit on 0.640→0.645: a callback returns a literal
string, the engine stores it into a heap-boxed struct field, and teardown aborts withinvalid pointer/ SIGSEGV — after printing correct output.Root cause
When a heap-string struct field is assigned from a bare heap-tracked local
(
a.value = v), the field's_heap_<field>tracker was hard-coded to 1.But a local classified as a heap-string var can hold a borrowed value at
runtime — e.g. one returned by a function that passes a parameter or a literal
straight through, leaving the local's own
_heap_<var>at 0. The structdestructor then freed a pointer the program never owned: a string literal (an
invalid free of read-only data) or a value still owned elsewhere (a double
free).
The generated store read
attr->_heap_value = 1even though the source's_heap_...was0— the static "is a heap var" classification overrode thevalue's actual runtime ownership.
Fix
When the RHS is a bare heap-tracked-var identifier, the field store now moves
the source's runtime ownership into the field tracker instead of asserting 1:
A borrowed value (
_heap_v == 0) is then not freed by the destructor; agenuinely-owned one (
_heap_v == 1) is still freed exactly once, and the sourceis disowned so it is not also freed at scope exit. This is the field-store
analogue of the existing
_heap_dest = _heap_srcalias move, applied to thedirect-pointer, value-struct, nested-path, and untrusted-box store paths.
Regression follow-up to #1866/#1879.
Verification
tests/integration/heap_field_borrowed_var_no_double_free)asserts the generated store moves the runtime tracker (not
= 1), the programruns to completion (the bug aborted at teardown), and — via an
ownedhalf —that a genuinely-heap value is still reclaimed. Confirmed to FAIL on the
unfixed compiler (emits
_heap_value = 1) and pass here.leak) cases.
attr->value = …store sitesnow emit the runtime-tracker move.
make test409/409;make test-ae1081/1083 (the 2 are the pre-existingwindows_crt_symbols+ h2 50-stream env flakes).🤖 Generated with Claude Code