From 03acd33cda96db560276c5983caada09104857e9 Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:00:56 +0800 Subject: [PATCH] fixes #25992; fix GC tracing of stale bytes in case objects during reset --- compiler/ccgreset.nim | 17 +++++++++++ tests/gc/tmove_case_object.nim | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/gc/tmove_case_object.nim diff --git a/compiler/ccgreset.nim b/compiler/ccgreset.nim index 84478dd07e..57ea5fc793 100644 --- a/compiler/ccgreset.nim +++ b/compiler/ccgreset.nim @@ -75,6 +75,23 @@ proc specializeResetT(p: BProc, accessor: Rope, typ: PType) = cSizeof(getTypeDesc(p.module, typ))) else: specializeResetN(p, accessor, typ.n, typ) + if isCaseObj(typ.n): + # The active branch was released above. Clear the complete object so + # stale bytes from overlapping branches cannot be traced by the GC. + # type + # Foo = object + # case kind: bool + # of true: + # a: ref Bar # 8 bytes (pointer) + # of false: + # b: int # 4 bytes + # specializeResetT for b emits accessor.b = 0 — writes 4 bytes + # But the union is 8 bytes wide (sized by the largest branch) + # The remaining 4 bytes where a used to live are untouched + # Those stale bytes could contain a heap pointer the GC traces → crash + p.s(cpsStmts).addCallStmt(cgsymValue(p.module, "nimZeroMem"), + cCast(CPointer, cAddr(accessor)), + cSizeof(getTypeDesc(p.module, typ))) of tyTuple: let typ = getUniqueType(typ) for i, a in typ.ikids: diff --git a/tests/gc/tmove_case_object.nim b/tests/gc/tmove_case_object.nim new file mode 100644 index 0000000000..8f4d9ba450 --- /dev/null +++ b/tests/gc/tmove_case_object.nim @@ -0,0 +1,56 @@ +discard """ + matrix: "--mm:refc; --mm:orc" +""" + +type + A = object of RootObj + + V = object + case g: bool + of true: + v: A + of false: + e: string + +var r = V(g: true, v: A()) +discard move r +GC_fullCollect() + +type + Kind = enum nested, other + Nested = object + case kind: Kind + of nested: + case enabled: bool + of true: payload: A + of false: message: string + of other: + discard + +var n = Nested(kind: nested, enabled: true, payload: A()) +discard move n +GC_fullCollect() + +# Moving from the other branch must keep its value alive and leave the source +# in the default state. +var s = V(g: false, e: "hello") +let moved = move s +doAssert moved.e == "hello" +doAssert not s.g +doAssert s.e.len == 0 + +# Reinitializing the zeroed value must also restore embedded object type +# headers. +type W = object + a: A + value: V + text: string + +var w = W(a: A(), value: V(g: true, v: A()), text: "content") +let movedW = move w +doAssert movedW.text == "content" +doAssert cast[ptr pointer](addr w.a)[] != nil +doAssert not w.value.g +doAssert w.value.e.len == 0 +doAssert w.text.len == 0 +GC_fullCollect()