From e927887b7ef90a843de9a2cb5f6db0e0e166ea35 Mon Sep 17 00:00:00 2001 From: Andreas Rumpf Date: Wed, 2 Sep 2026 16:16:57 +0200 Subject: [PATCH] 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 --- compiler/ccgexprs.nim | 5 +-- tests/assign/tshallow_asgn_from_static.nim | 32 +++++++++++++++++ tests/async/tawait_in_array_literal_loop.nim | 37 ++++++++++++++++++++ 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tests/assign/tshallow_asgn_from_static.nim create mode 100644 tests/async/tawait_in_array_literal_loop.nim diff --git a/compiler/ccgexprs.nim b/compiler/ccgexprs.nim index c38ea3aa60..eb3ee4bfb7 100644 --- a/compiler/ccgexprs.nim +++ b/compiler/ccgexprs.nim @@ -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) diff --git a/tests/assign/tshallow_asgn_from_static.nim b/tests/assign/tshallow_asgn_from_static.nim new file mode 100644 index 0000000000..1dd62e103f --- /dev/null +++ b/tests/assign/tshallow_asgn_from_static.nim @@ -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() diff --git a/tests/async/tawait_in_array_literal_loop.nim b/tests/async/tawait_in_array_literal_loop.nim new file mode 100644 index 0000000000..77048c8920 --- /dev/null +++ b/tests/async/tawait_in_array_literal_loop.nim @@ -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"