fix SIGSEGV on refc: genGenericAsgn must deep-copy from static data (#26160)

Supersedes #26120 (https://github.com/nim-lang/Nim/pull/26120), which
fixed the reported case but left the `tfShallow` half of the same
condition open.

Since #25860 array literals are materialized into temporaries so that
`lent` results keep valid backing storage. In an async proc such a
temporary is lifted into the closure environment, and the environment is
filled with an `nkFastAsgn`, i.e. without `needToCopy`. `genGenericAsgn`
then emitted `genericShallowAssign` from the static const array, and for
the `string` elements that ends in `unsureAsgnRef` ->
`incRef(usrToCell(literal))` on memory that has no GC header:

  proc f() {.async.} =
    for ip in ["::1", "2001:db8::", "::"]:
      await sleepAsync(1)

`OnStatic` sources must therefore always take the `genericAssign` path.
Note that the guard has to dominate the `tfShallow` test as well, not
just the `needToCopy` one: a `{.shallow.}` destination assigned from a
`const` crashes in exactly the same way. This is the precedence
`genOptAsgnTuple` and `genOptAsgnObject` already use, so
`genGenericAsgn` now agrees with its two siblings instead of
contradicting them.

refc only; the other GCs do not reference count in `unsureAsgnRef`.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Rumpf
2026-09-02 16:16:57 +02:00
committed by GitHub
parent 48bfe01a83
commit e927887b7e
3 changed files with 72 additions and 2 deletions

View File

@@ -251,8 +251,9 @@ proc genGenericAsgn(p: BProc, dest, src: TLoc, flags: TAssignmentFlags) =
# (for objects, etc.):
if optSeqDestructors in p.config.globalOptions:
simpleAsgn(p.s(cpsStmts), dest, src)
elif needToCopy notin flags or
tfShallow in skipTypes(dest.t, abstractVarRange).flags:
elif src.storage != OnStatic and
(needToCopy notin flags or
tfShallow in skipTypes(dest.t, abstractVarRange).flags):
if (dest.storage == OnStack and p.config.selectedGC != gcGo) or not usesWriteBarrier(p.config):
let rad = addrLoc(p.config, dest)
let ras = addrLoc(p.config, src)

View File

@@ -0,0 +1,32 @@
discard """
matrix: "--mm:refc; --mm:orc"
output: '''aaaeee
["p", "q", "r"]'''
"""
# A `{.shallow.}` destination type must not shortcut the deep copy when the
# source is static data: `genericShallowAssign` would `incRef` the string
# literals, which have no GC header. `genOptAsgnTuple`/`genOptAsgnObject`
# already give `OnStatic` priority over `tfShallow`; `genGenericAsgn` has to
# agree with them.
type
Obj {.shallow.} = object
a, b, c, d, e: string # asgnComplexity > 4, so genGenericAsgn is used
ShallowArray {.shallow.} = array[3, string]
Holder = ref object
o: Obj
a: ShallowArray
const
c1 = Obj(a: "aaa", b: "bbb", c: "ccc", d: "ddd", e: "eee")
ca: ShallowArray = ["p", "q", "r"]
proc main =
var h = Holder()
h.o = c1
h.a = ca
echo h.o.a, h.o.e
echo h.a
main()

View File

@@ -0,0 +1,37 @@
discard """
matrix: "--mm:refc; --mm:orc"
output: "ok"
"""
# the loop temp lifted into the env must be deep-copied: a shallow assign from
# the static const array made refc incRef string literals in static memory
import std/asyncdispatch
const ips = ["::1", "2001:db8::", "::"]
proc overLiteral(): Future[int] {.async.} =
var n = 0
for ip in ["::1", "2001:db8::", "::"]:
await sleepAsync(1)
n += ip.len
return n
proc overConst(): Future[int] {.async.} =
var n = 0
for ip in ips:
await sleepAsync(1)
n += ip.len
return n
proc overNested(): Future[int] {.async.} =
var n = 0
for x in [(1, "::1"), (2, "2001:db8::"), (3, "::")]:
await sleepAsync(1)
n += x[1].len
return n
doAssert waitFor(overLiteral()) == 15
doAssert waitFor(overConst()) == 15
doAssert waitFor(overNested()) == 15
echo "ok"