From a84ae6f83371988fa75ffc8fc427a8f04e3ac2fa Mon Sep 17 00:00:00 2001 From: ringabout <43030857+ringabout@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:51:13 +0800 Subject: [PATCH] fixes #25992; fix GC tracing of stale bytes in case objects during reset (#26003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes #25992 ```nim 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 Add nimZeroMem after specializeResetN for case objects to clear the entire union including unused branch bytes. (cherry picked from commit 16920b56d141680cc347020c536ee17158ad3e59) (cherry picked from commit 84a5613c3502be16c4271d65ba3d4d1d1f2d250e) --- compiler/ccgreset.nim | 16 ++++++++++ tests/gc/tmove_case_object.nim | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 tests/gc/tmove_case_object.nim diff --git a/compiler/ccgreset.nim b/compiler/ccgreset.nim index 2fa562ee53..1c045f9785 100644 --- a/compiler/ccgreset.nim +++ b/compiler/ccgreset.nim @@ -73,6 +73,22 @@ proc specializeResetT(p: BProc, accessor: Rope, typ: PType) = [accessor, 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 + lineCg(p, cpsStmts, "#nimZeroMem((void**)&$1, sizeof($2));$n", + [accessor, 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()