Skip to content

-mm=own gaps: fuzz-found borrowing literals, let-held getter reads; rc conditional record literal - #758

Merged
ASDAlexander77 merged 5 commits into
mainfrom
own-gaps-batch
Oct 31, 2026
Merged

ASDAlexander77 merged 5 commits into
mainfrom
own-gaps-batch

Conversation

@ASDAlexander77

@ASDAlexander77 ASDAlexander77 commented Oct 31, 2026 •

Copy link
Copy Markdown
Owner

A batch of -mm=own gap fixes (plus one rc fix), from a new fuzz campaign and the open leak list. There are five commits, each with its own tests.

1. rc: a conditional between record literals keeps the objects its branch made (d82cfbea)
c ? { start: this.pos, dir: new V(1) } : {...} caused heap corruption under rc. The hand-written fuzz seed review7__pM1 still failed on main. Each branch gave back its new V before the receiver after the ts.If retained the literal's fields. consumeConditionalResults now makes such a branch retain the literal it yields, as it already does for a field read out of a branch temporary.

  • Test: tests/00conditional_record_literal_temporary.ts (gc, plus rc and none through the corpus).

2. own: three borrowing literals that compiled and then crashed (edeb47a9)
A 2000-program mm_fuzz round (seeds 73000-74999) found 3 own crashes among the 1735 programs own accepted. Each is now a compile error:

  • f([HG.v]) where f does p[0] = v: an array literal that borrows its elements no longer borrows when its callee may overwrite an element or destroy anything.
  • f({ v: n.items[0] }) where f does p.v = ...: a borrowing record given to a callee whose overwrites include a field is treated as a write of that field.
  • walkRecord treated ts.Invoke (a call inside try) as a branch and skipped it. A borrowing record given to a call in a try was therefore never checked: f({ v: n.items[0] }, (n = mk(), n)) freed the element first.
  • Tests: own_err_literal_element_overwritten, own_err_record_field_overwritten, own_err_record_borrow_in_try. Each compiled without error before this change.

3. own: a getter-filled interface field read put in a let is freed at the returns (e5f304ba)
let x = i.item; read across a loop leaked on the getter's path, because #751 only frees reads that stay in their block. For 2M calls, peak working set went from 280.5 MB to 4.3 MB. The read is released at each return when all of these hold:

  • the let is only read, and nothing read from it leaves the function;
  • the read reaches every return;
  • the object it reads through is a parameter's.

A read that would still leave its block is now an error (commit 4).

  • Tests: own_interface_getter_let (own, no-counting, rc, gc, none) and test-own-interface-getter-let-released, which reads the IR (fails before the fix).

4. own: a getter-filled interface field read that would leave its block is an error (a588196d, your ruling)
return i.item is now a compile error when a class in the module fills item with a getter. Before, it leaked one value per call on the getter's path. A field that no class fills with accessors is a borrow on every path and may still be returned. A method called on the read (i.item.get()) is now followed, so that read is freed rather than rejected. The DefaultLib's src/lib.ts compiles under own and rc with no errors.

  • Test: own_err_getter_read_returned.

5. own: a callback may give a captured object a new value (9804fff0, item 4)
bag.forEach(b => { last = new Box(b.n) }) was rejected. Assigning a captured variable in a closure counted as "may destroy anything", so a method that walks a field's array and calls the callback could not borrow its elements. Spec 52 already handled numbers and strings precisely; this extends it to objects and arrays:

  • The assignment is now its own drop place, OWN_CAPTURED_CELL_POSITION. It drops only borrows read through a captured variable's cell.
  • To keep that sound, a call that may run such a closure and is also given something read through the assigned variable is an error at the call (bag.forEach(b => { bag = new Bag() })).
  • Tests: own_callback_assigns_capture (own, no-counting, rc, gc, none; rejected before), own_err_callback_assigns_receiver, own_err_callback_assigns_borrowed.

The same commit also fixes an older own crash found by fuzz seed 77055, which predates this batch: f(n, (n = mk(3), ...)), where n is given as an interface and reassigned in a later argument. checkArgumentsAssigned looked through casts but not through the interface view, so it never found n. It now looks through views.

  • Test: own_err_interface_arg_assigned_in_call.

Verification

  • Release ctest: 6518/6518.
  • Fuzz: seeds 72000-72399 had 0 failures before these fixes. Seeds 75000-75599 run after them: 0 failures in 1111 checked programs; seeds 76000-76399 after commit 4, and 77000-77599 and 78000-78599 after commit 5, also had 0 failures.
  • Seed replay: 2 of 30 still fail, review6r1__v1/v1b. Those are rc crashes from a function cast through as any to another interface's signature; they are not addressed here.

🤖 Generated with Claude Code

ASDAlexander77 and others added 5 commits October 30, 2026 23:45
…ranch made

`c ? { start: this.pos, dir: new V(1) } : { ... }`: each branch yields the literal it built, whose
field holds an object the branch made, and the branch gave that object back before it ended. The
receiver's retain after the `ts.If` - which retains the literal's fields - came too late, and the
field was freed under it (heap corruption, 0xC0000374; hand-written fuzz seed review7__pM1).
consumeConditionalResults now treats such a branch as it treats one yielding a field of a temporary
it made: the branch retains what it yields. Not under own, which settles a literal's fields itself
(and rejects this shape).

Test: tests/00conditional_record_literal_temporary.ts (gc, and rc/none through the corpus).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds 73095, 73544, 74808)

A 2000-program mm_fuzz round (seeds 73000-74999) found three own crashes among 1735 accepted programs;
each is now a compile error.
- An array literal argument that borrows its elements (`f([HG.v])`, classifyArgumentArrays) given to a
  callee that may overwrite an element (`p[0] = v`) or destroy anything: the overwrite freed the
  global's value and the new element was lost with the shallow release. Such an array no longer
  borrows, so the element read is the error it is anywhere else.
- A borrowing record literal (`{ v: n.items[0] }`) given to a callee whose known overwrites include a
  field (`p.v = ...`): walkRecord treats it as a write of the field here, which keeps the record.
- walkRecord treated a `ts.Invoke` (a call inside `try`) as a plain branch and skipped it, so a
  borrowing record given to a call in a `try` had no use there: `f({ v: n.items[0] }, (n = mk(), n))`
  freed the element before the call read it.

Tests: own_err_literal_element_overwritten, own_err_record_field_overwritten,
own_err_record_borrow_in_try (each compiled without error before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… at the returns

`let x = i.item; for (...) use(x); return x.n` - a read through an interface field a getter may fill,
held in a `let` read across blocks - was never freed: #751 releases such a read only when it and what
is borrowed from it stay in its block. Two million calls kept two million Boxes (280.5 MB peak; 4.3 MB
now). releaseCellReads now also frees a read that is the initializer of a `let` nobody assigns or
captures, whose reads stay in the function (staysInBlock over any block), when the read reaches every
return and the object it reads through is a parameter's, which outlives them: the same conditional
release, at each return. A read returned or stored still leaks on the getter's path.

Tests: own_interface_getter_let (own, no counting, rc, gc, none) and
test-own-interface-getter-let-released, which reads the IR (fails before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ock is an error

User ruling (2026-10-31): `return i.item`, where a class of the module fills `item` with a getter, is
a compile error rather than a leak. On the getter's path the read is a new value only its own release
frees; returned (a borrow of `i` to the caller), stored, or kept where neither the in-block release
nor the `let` release reaches, it was lost. releaseCellReads now reports such a read when a class in
the module fills a field of that name with accessors (its `<class>.<interface>..vtbl.<field>.accessor`
cell); a field no class fills that way is a borrow on every path and may still be returned.

A method called on the read (`i.item.get()`) is followed by staysInBlock now (the bound method, its
object and function, the call), so it is freed rather than rejected.

DefaultLib src/lib.ts compiles under own and rc with no errors.

Test: own_err_getter_read_returned. Release ctest 6506/6506; mm_fuzz seeds 76000-76399 clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`bag.forEach(b => { last = new Box(b.n) })` was rejected: the assignment through a captured
variable's cell in the closure body was a drop of anything (fieldDropsInBody), so a method that walks
the array a field holds and calls the callback had its element borrow "used after it may be released".
Spec 52 made this precise for numbers and strings; this does it for objects and arrays.

- The assignment is a place drop of its own, OWN_CAPTURED_CELL_POSITION: it frees what only the cell
  owns, so it drops a chain read through a captured variable's cell (dropsChain), nothing else.
- A call given anything read through a variable a closure assigns, when the call may run such a
  closure, is an error where it is made (checkArgumentsAssigned): no parameter of the callee can be
  under the value the closure frees. `bag.forEach(b => { bag = new Bag() })` is that error.

Tests: own_callback_assigns_capture (own, no counting, rc, gc, none; rejected before),
own_err_callback_assigns_receiver, own_err_callback_assigns_borrowed. DefaultLib src/lib.ts compiles
under own and rc with no errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 1cd17c3 into main Oct 31, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the own-gaps-batch branch October 31, 2026 00:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant