fixes #25992; fix GC tracing of stale bytes in case objects during reset (#26003)

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 16920b56d1)
(cherry picked from commit 84a5613c35)
This commit is contained in:
ringabout
2026-08-15 13:51:13 +08:00
committed by narimiran
parent 7fef8700e0
commit a84ae6f833
2 changed files with 72 additions and 0 deletions

View File

@@ -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:

View File

@@ -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()