diff --git a/compiler/varpartitions.nim b/compiler/varpartitions.nim index ddba3d4bcb..fea6cc540b 100644 --- a/compiler/varpartitions.nim +++ b/compiler/varpartitions.nim @@ -185,6 +185,9 @@ proc root(v: var Partitions; start: int): int = proc potentialMutation(v: var Partitions; s: PSym; level: int; info: TLineInfo) = let id = variableId(v, s) if id >= 0: + # mutated here => alive here: keep aliveEnd in sync so dangerousMutation catches + # mutations recorded after the var's last use (e.g. via a call arg). See #25595. + v.s[id].aliveEnd = max(v.s[id].aliveEnd, v.abstractTime) let r = root(v, id) let flags = if s.kind == skParam: if isConstParam(s): diff --git a/tests/arc/t25595.nim b/tests/arc/t25595.nim new file mode 100644 index 0000000000..6cfaa7673a --- /dev/null +++ b/tests/arc/t25595.nim @@ -0,0 +1,43 @@ +discard """ + matrix: "--mm:orc; --mm:arc; --mm:refc" +""" + +# bug #25595: cursor inference must not borrow a case object whose source can be +# mutated through the cursor's own ref across a call. `let c = h.w` was inferred as a +# non-owning cursor; `clear(c.r)` overwrites `h.w` via the cursor's back-reference, +# freeing the ref while the borrow still uses it -> use-after-free. Detected here +# deterministically: the element's destructor must not run during the call. + +var destroyed = false + +type + O = ref object + value: int + home: H + W = object + case k: bool + of true: r: O + of false: discard + H = ref object + w: W + +proc `=destroy`(o: var typeof(O()[])) = + destroyed = true + +proc clear(o: O): int = + o.home.w = W() + doAssert not destroyed, "use-after-free: element destroyed during the call" + result = o.value + +proc go(h: H): int = + let c = h.w + result = clear(c.r) + +proc main = + let h = H() + let o = O(value: 42) + o.home = h + h.w = W(k: true, r: o) + doAssert go(h) == 42 + +main()