Skip to content

rc: a string made over an array's memory is a view, copied where it is kept (#481) - #488

Merged
ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-string-over-array-data
Oct 4, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
fix-rc-string-over-array-data

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Fixes #481 with the design chosen there: under rc, <string><Opaque>Ref(buffer[i]) is a view of the array's memory, copied where it is kept.

The bug

Under rc a receiver retained such a string like any other, counting the word in front of the pointer:

  • Ref(buffer[0]): that word is the data block's count, so the block outlived the array. Once the array grew, the string's release decremented the count of the freed old block, and the grown block leaked. On main, a function that keeps such a string in a global and then grows the array, called three times, ends in heap corruption (0xC0000374).
  • Ref(buffer[8]): the "count" is elements 0-7. Keeping the string turns abcdefghijklmnop into bbcdefghijklmnop.

The fix (OwnedReturnConsumptionPass, rc only)

As under own, a view keeps no count. Each retain of a view becomes a copy for the receiver it was made for - a return, a store into a field, an element or a global, a push, a capture - made right there, after the bytes were written through the view. const s = <string><Opaque>Ref(buffer[0]); sprintf_s(s, ...); return s; (the default library's convertNumber and date formatters) returns a copy, and the buffer is freed with the array. A let that only ever holds a view borrows it (no count, no copy); one that is assigned again starts from a copy, since the assignment releases what it held.

gc, none and own are unchanged.

Limits, documented in the pass:

  • A view still points into the array: used after the array grows or dies, it reads freed memory in every model, as a C pointer would.
  • A view kept inside a tuple or record literal, or by a function it is passed to, is not copied (as on main).

The arrays spec's rc section notes the change.

Tests

00string_view_copy.ts (compile, JIT, rc and none corpora): return through a const and a let, a view at element 1, a view kept in a field, a global and an array, the bytes in front of a view at element 8, and a kept view whose array then grows. Main fails it under rc ("the bytes in front of a view are not a count"); it uses strcpy, so it runs on Linux too.

Gates

Gate Result
Windows Release suite (ctest -C Release -j 12 --timeout 300, staged default library) 3755 / 3755 passed
Linux (WSL, on main d91b36b, ninja release) 3734 / 3734 of the rest passed; the new test's 6 runs failed there at first (it used sprintf_s, which glibc lacks) and pass 6 / 6 after switching to strcpy
Default library rebuilt (release and debug) with this compiler, its suite under rc 158 / 159 in each of release/compile, release/jit, debug/compile, debug/jit - the one failure is weakref_basic, gc-only by design (#420)
Memory (measure.ps1, AOT) own_* programs unchanged; the growth case: rc 4.4 MB

own is not touched (the step runs under rc only), so the own corpus is unaffected.

Closes #481

🤖 Generated with Claude Code

…s kept (#481)

`<string><Opaque>Ref(buffer[i])` makes a string that points into an
array's data block. Under rc a receiver retained it like any string,
counting the word in front of the pointer: for `Ref(buffer[0])` that is
the data block's count, so the block outlived the array, and once the
array grew the string's release decremented the count of the freed old
block (heap corruption) while the grown block leaked; for
`Ref(buffer[8])` the "count" was elements 0-7, which the retain
incremented.

Now, as under own, such a string is a view under rc: it keeps no
count. OwnedReturnConsumptionPass (rc only) turns each retain of a view
into a copy for the receiver it was made for - a return, a store into a
field, an element or a global, a push, a capture - made right there,
after the bytes were written through the view (`sprintf_s(s, ...)`,
then `return s`, as the default library's convertNumber does). A `let`
that only ever holds a view borrows it; one assigned again starts from
a copy, since the assignment releases what it held.

A view still points into the array: used after the array grows or
dies, it reads freed memory in every model, as a C pointer would. A
view kept inside a tuple or record literal, or by a function it is
passed to, is not copied.

Closes #481

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 2e91a62 into main Oct 4, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-rc-string-over-array-data branch October 4, 2026 13:47
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.

A string cast over an array's data block dangles after the array grows; under rc it counts a block it does not own

1 participant