Skip to content

Commit 97d96d3

Browse files
Fix GC use-after-free on operands across dunder dispatch
1 parent e74e06f commit 97d96d3

5 files changed

Lines changed: 27 additions & 3 deletions

File tree

compiler/src/modules/vm/dispatch.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ impl<'a> VM<'a> {
130130
#[inline]
131131
pub(crate) fn record_dunder_hit(&self, ip: usize, cache: &mut OpcodeCache, recv: Val, name: &str, arity: u8) {
132132
if !recv.is_heap() { return; }
133-
let HeapObj::Instance(cls, _) = self.heap.get(recv) else { return; };
133+
// try_get is a backstop; callers root operands across the dunder call.
134+
let Some(HeapObj::Instance(cls, _)) = self.heap.try_get(recv) else { return; };
134135
let cls = *cls;
135136
let Some((method, _)) = self.lookup_class_member(cls, name) else { return; };
136137
cache.record_inst(ip, cls.as_heap(), method, arity);

compiler/src/modules/vm/gc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ impl<'a> VM<'a> {
77
pub(crate) fn collect(&mut self, current_slots: &[Val]) {
88
for &v in &self.stack { self.heap.mark(v); }
99
for &v in &self.with_stack { self.heap.mark(v); }
10+
for &v in &self.temp_roots { self.heap.mark(v); }
1011
for &v in &self.yields { self.heap.mark(v); }
1112
for &v in &self.event_queue { self.heap.mark(v); }
1213
// Scheduler holds parked coroutines (and their `WaitingForChildren` task lists) across `top_loop` resumes; mark them so the saved state isn't swept under us.

compiler/src/modules/vm/handlers/arith.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,15 @@ impl<'a> VM<'a> {
3939

4040
let (a, b) = self.pop2()?;
4141

42+
// Root operands: the dunder runs user code that can GC, and we read a/b after it (record + fallback).
43+
let roots = self.temp_roots.len();
44+
self.temp_roots.push(a);
45+
self.temp_roots.push(b);
46+
let dunder = self.try_binary_dunder(op, a, b, chunk, slots);
47+
self.temp_roots.truncate(roots);
48+
4249
// instance dunder protocol, try user-defined operator before any builtin coercion.
43-
if let Some(r) = self.try_binary_dunder(op, a, b, chunk, slots)? {
50+
if let Some(r) = dunder? {
4451
// record the resolved class+method so the IC can fire on subsequent iterations of a hot loop.
4552
if let Some(name) = binary_dunder_name(op) {
4653
self.record_dunder_hit(rip, cache, a, name, 2);
@@ -179,8 +186,15 @@ impl<'a> VM<'a> {
179186
// Record type-key for every compare op; `cache::specialize` picks the FastOp variant.
180187
cached_binop!(self.heap, rip, &op, a, b, cache);
181188

189+
// Root operands: the dunder runs user code that can GC, and we read a/b after it (record + fallback).
190+
let roots = self.temp_roots.len();
191+
self.temp_roots.push(a);
192+
self.temp_roots.push(b);
193+
let dunder = self.try_compare_dunder(op, a, b, chunk, slots);
194+
self.temp_roots.truncate(roots);
195+
182196
// try the user-defined comparison dunder before falling back to numeric/string compare.
183-
if let Some(r) = self.try_compare_dunder(op, a, b, chunk, slots)? {
197+
if let Some(r) = dunder? {
184198
// monomorphic comparison sites cache the resolved method like arithmetic ones.
185199
if let Some(name) = compare_dunder_name(op) {
186200
self.record_dunder_hit(rip, cache, a, name, 2);

compiler/src/modules/vm/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ pub struct VM<'a> {
114114
/* Cached `ops == usize::MAX` so the hot path skips the budget decrement. */
115115
pub(crate) sandbox_off: bool,
116116
pub(crate) with_stack: Vec<Val>,
117+
/* GC roots for operands popped off the stack but still read after a dunder call that can collect. */
118+
pub(crate) temp_roots: Vec<Val>,
117119
pub(crate) pending: Pending,
118120
/* Monotonic correlation id handed to each deferred host call; matched by `set_host_result_by_id`. */
119121
pub(crate) next_host_call_id: u64,
@@ -167,6 +169,7 @@ impl<'a> VM<'a> {
167169
depth: 0,
168170
max_calls: limits.calls,
169171
with_stack: Vec::new(),
172+
temp_roots: Vec::new(),
170173
pending: Pending::new(),
171174
next_host_call_id: 0,
172175
pending_sync_frames: Vec::new(),

compiler/tests/cases/vm.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1712,6 +1712,11 @@
17121712
{"src": "class S:\n def __init__(self, data):\n self.data = data\n def __getitem__(self, i):\n return self.data[i]\ns = S([10, 20, 30, 40, 50])\ntotal = 0\nfor i in range(5):\n total = total + s[i]\nprint(total)", "output": ["150"]},
17131713
{"src": "class V:\n def __init__(self, n):\n self.n = n\n def __add__(self, o):\n if isinstance(o, V):\n return V(self.n + o.n)\n return NotImplemented\n def __radd__(self, o):\n return V(o + self.n)\ntotal = V(0)\nfor i in range(5):\n total = total + V(i)\nprint(total.n)\nprint((10 + V(5)).n)", "output": ["10", "15"]},
17141714
{"src": "class A:\n def __eq__(self, o):\n return isinstance(o, A)\nclass B:\n def __eq__(self, o):\n return False\nhits = 0\nfor i in range(10):\n if A() == A(): hits = hits + 1\n if B() == A(): hits = hits + 100\nprint(hits)", "output": ["10"]},
1715+
{"src": "class B:\n def __eq__(self, /):\n x = {i for i in range(9)}\n return True\nn = 0\nfor i in range(300):\n if str(i) == B():\n n = n + 1\nprint(n)", "output": ["300"]},
1716+
{"src": "class B:\n def __eq__(self, /):\n x = {i for i in range(9)}\n return NotImplemented\nn = 0\nfor i in range(300):\n if str(i) == B():\n n = n + 1\nprint(n)", "output": ["0"]},
1717+
{"src": "class B:\n def __eq__(self, /):\n x = {i for i in range(9)}\n return NotImplemented\nn = 0\nfor i in range(300):\n if (i,) == B():\n n = n + 1\nprint(n)", "output": ["0"]},
1718+
{"src": "class B:\n def __eq__(self, /):\n junk = [j for j in range(20)]\n return True\nhits = 0\nfor i in range(300):\n if [i, i + 1] == B():\n hits = hits + 1\nprint(hits)", "output": ["300"]},
1719+
{"src": "class B:\n def __lt__(self, /):\n x = {i for i in range(9)}\n return True\nn = 0\nfor i in range(300):\n if [i] > B():\n n = n + 1\nprint(n)", "output": ["300"]},
17151720
{"src": "class C:\n def __init__(self, n):\n self.n = n\n def __getitem__(self, i):\n return self.n + i\n def __setitem__(self, i, v):\n self.n = v\n def __neg__(self):\n return C(-self.n)\nc = C(10)\nresults = []\nfor i in range(3):\n results.append(c[i])\nc[0] = 99\nresults.append(c[0])\nresults.append((-c).n)\nprint(results)", "output": ["[10, 11, 12, 99, -99]"]},
17161721
{"src": "class S:\n def __init__(self):\n self.data = list(range(20))\n def __getitem__(self, i):\n garbage = [j * j for j in range(50)]\n return self.data[i] + garbage[0]\ns = S()\nacc = 0\nfor i in range(20):\n acc = acc + s[i]\nprint(acc)", "output": ["190"]},
17171722
{"src": "class Base:\n def __add__(self, o):\n return 'Base.add'\n def __radd__(self, o):\n return 'Base.radd'\nclass Sub(Base):\n def __radd__(self, o):\n return 'Sub.radd'\nprint(Base() + Sub())", "output": ["Sub.radd"]},

0 commit comments

Comments
 (0)