fix(compiler): Text interp of a place crossed let/assign uncopied
- copy_place_text matched only bare Ident/Field/Index, so a Text-typed
single-segment interpolation ("${r.method}", r a loop borrow) passed
the place's own register through a binding/assignment boundary — the
local aliased the row's field and its overwrite freed it
- release-build crash; invisible to ASan (in-arena free, no redzones)
- now asks is_borrowed_value_t && not is_container_read — exactly
drop_fresh_text's place test; Int segments (fresh int_to_text) and
container reads (already copies) stay uncopied as before
- pinned by tests/corpus/run/interp-borrowed-field (crashed both
runtimes before the fix, 50 iterations now exact)
- gates: woc-test 540/0, oop-e2e 89/0 (ASan stage included)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b264c06d5a
commit
81a9f882b5
4 changed files with 50 additions and 4 deletions
|
|
@ -133,6 +133,16 @@ REMOVES the element — the caller owns what it then ignores). The finding tool
|
|||
was an arena size-class census plus a pointer trace, not ASan: an in-arena
|
||||
leak is invisible to LeakSanitizer, because the arena is one allocation.
|
||||
|
||||
The framework-v1 slice (2026-08-20) found the copy-side mirror of the
|
||||
Int-segment lesson: `copy_place_text` matched only bare `Ident/Field/Index`,
|
||||
so a Text-typed SINGLE-SEGMENT interpolation of a place
|
||||
(`allow = "${r.method}"` with `r` a loop borrow) passed the place's own
|
||||
register through a `let`/assignment boundary uncopied — the binding aliased
|
||||
the row's field and its overwrite freed it (release-build crash the arena
|
||||
hid from ASan). It now asks `is_borrowed_value_t && not is_container_read`,
|
||||
exactly `drop_fresh_text`'s place test. Pinned by
|
||||
`tests/corpus/run/interp-borrowed-field`.
|
||||
|
||||
Two rules the measurements imposed, both easy to get backwards:
|
||||
|
||||
- **Never drop an argument register after a `CALL`.** The callee's frame
|
||||
|
|
|
|||
|
|
@ -1487,10 +1487,12 @@ let rec is_container_read (e : Ast.expr) : bool =
|
|||
own value and the new owner gets its own. A freshly built Text is already
|
||||
nobody else's and passes through untouched. *)
|
||||
let copy_place_text (p : pctx) (f : fstate) (reg : int) (e : Ast.expr) : unit =
|
||||
let is_place =
|
||||
(match e.Ast.kind with Ast.Ident _ | Ast.Field _ | Ast.Index _ -> true | _ -> false)
|
||||
&& not (is_container_read e)
|
||||
in
|
||||
(* seen through an interpolation exactly as drop_fresh_text sees it: a
|
||||
single Text segment passes the place's own register through untouched
|
||||
(`out = "${r.method}"` aliased the row's field and its overwrite freed
|
||||
it — the 405 Allow-header crash), while an Int segment is already a
|
||||
fresh int_to_text that must not be re-copied *)
|
||||
let is_place = is_borrowed_value_t p f e && not (is_container_read e) in
|
||||
let is_text =
|
||||
match ty_of_expr p f e with Some t -> field_kind p t = 3 (* WO_K_TEXT *) | None -> false
|
||||
in
|
||||
|
|
|
|||
1
tests/corpus/run/interp-borrowed-field/fixture.out
Normal file
1
tests/corpus/run/interp-borrowed-field/fixture.out
Normal file
|
|
@ -0,0 +1 @@
|
|||
ok
|
||||
33
tests/corpus/run/interp-borrowed-field/fixture.wo
Normal file
33
tests/corpus/run/interp-borrowed-field/fixture.wo
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
-- probe: Text interpolation of a borrowed field inside a loop.
|
||||
-- If the interpolation segment drops the field it only borrows, repeated
|
||||
-- calls corrupt the arena and the third-ish call crashes in release.
|
||||
|
||||
class Row {
|
||||
method: Text
|
||||
}
|
||||
|
||||
class Tab {
|
||||
rows: multi Row
|
||||
|
||||
fn scan() -> Text {
|
||||
let out = "";
|
||||
for r in self.rows {
|
||||
if out == "" { out = "${r.method}"; } else { out = "${out}, ${r.method}"; }
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Int {
|
||||
let t = Tab { rows: [] };
|
||||
push(t.rows, Row { method: "GET" });
|
||||
push(t.rows, Row { method: "POST" });
|
||||
let i = 0;
|
||||
while i < 50 {
|
||||
let s = t.scan();
|
||||
if s != "GET, POST" { print("bad: ${s} at ${i}"); return 1; }
|
||||
i = i + 1;
|
||||
}
|
||||
print("ok");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue