Skip to content

Commit b7e0eb6

Browse files
committed
Bugfix: prevent Gd destruction while user object is bound
Previously, free() and engine-induced destruction of manually managed objects did not check whether a bind() or bind_mut() call was ongoing, which allowed to pull the rug under the guard, leaving user references dangling.
1 parent c9c2793 commit b7e0eb6

File tree

4 files changed

+145
-17
lines changed

4 files changed

+145
-17
lines changed

godot-core/src/obj/gd.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,10 +206,12 @@ impl<T: GodotClass> Gd<T> {
206206
})
207207
}
208208

209-
/// ⚠️ Returns the last known, possibly invalid instance ID of this object.
209+
/// Returns the last known, possibly invalid instance ID of this object.
210210
///
211211
/// This function does not check that the returned instance ID points to a valid instance!
212212
/// Unless performance is a problem, use [`instance_id()`][Self::instance_id] instead.
213+
///
214+
/// This method is safe and never panics.
213215
pub fn instance_id_unchecked(&self) -> InstanceId {
214216
// SAFETY:
215217
// A `Gd` can only be created from a non-null `RawGd`. Meaning `raw.instance_id_unchecked()` will
@@ -390,10 +392,15 @@ where
390392
/// example to the node tree in case of nodes.
391393
///
392394
/// # Panics
393-
/// * When the referred-to object has already been destroyed.
394-
/// * When this is invoked on an upcast `Gd<Object>` that dynamically points to a reference-counted type (i.e. operation not supported).
395+
/// - When the referred-to object has already been destroyed.
396+
/// - When this is invoked on an upcast `Gd<Object>` that dynamically points to a reference-counted type (i.e. operation not supported).
397+
/// - When the object is bound by an ongoing `bind()` or `bind_mut()` call (through a separate `Gd` pointer).
395398
pub fn free(self) {
399+
// Note: this method is NOT invoked when the free() call happens dynamically (e.g. through GDScript or reflection).
400+
// As such, do not use it for operations and validations to perform upon destruction.
401+
396402
// TODO disallow for singletons, either only at runtime or both at compile time (new memory policy) and runtime
403+
use dom::Domain;
397404

398405
// Runtime check in case of T=Object, no-op otherwise
399406
let ref_counted = T::Mem::is_ref_counted(&self.raw);
@@ -408,7 +415,17 @@ where
408415
"called free() on already destroyed object"
409416
);
410417

411-
// This destroys the Storage instance, no need to run destructor again
418+
// SAFETY: object must be alive, which was just checked above. No multithreading here.
419+
// Also checked in the C free_instance_func callback, however error message can be more precise here and we don't need to instruct
420+
// the engine about object destruction. Both paths are tested.
421+
let bound = unsafe { T::Declarer::is_currently_bound(&self.raw) };
422+
assert!(
423+
!bound,
424+
"called free() while a bind() or bind_mut() call is active"
425+
);
426+
427+
// SAFETY: object alive as checked.
428+
// This destroys the Storage instance, no need to run destructor again.
412429
unsafe {
413430
sys::interface_fn!(object_destroy)(self.raw.obj_sys());
414431
}

godot-core/src/obj/traits.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,15 @@ pub mod dom {
275275
where
276276
T: GodotClass<Declarer = Self>,
277277
F: FnOnce(&mut T) -> R;
278+
279+
/// Check if the object is a user object *and* currently locked by a `bind()` or `bind_mut()` guard.
280+
///
281+
/// # Safety
282+
/// Object must be alive.
283+
#[doc(hidden)]
284+
unsafe fn is_currently_bound<T>(obj: &RawGd<T>) -> bool
285+
where
286+
T: GodotClass<Declarer = Self>;
278287
}
279288

280289
/// Expresses that a class is declared by the Godot engine.
@@ -293,6 +302,13 @@ pub mod dom {
293302
.expect("scoped mut should not be called on a null object"),
294303
)
295304
}
305+
306+
unsafe fn is_currently_bound<T>(_obj: &RawGd<T>) -> bool
307+
where
308+
T: GodotClass<Declarer = Self>,
309+
{
310+
false
311+
}
296312
}
297313

298314
/// Expresses that a class is declared by the user.
@@ -309,6 +325,13 @@ pub mod dom {
309325
let mut guard = obj.bind_mut();
310326
closure(&mut *guard)
311327
}
328+
329+
unsafe fn is_currently_bound<T>(obj: &RawGd<T>) -> bool
330+
where
331+
T: GodotClass<Declarer = Self>,
332+
{
333+
obj.storage().unwrap_unchecked().is_bound()
334+
}
312335
}
313336
}
314337

godot-core/src/storage.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ mod single_threaded {
4040
base: Base<T::Base>,
4141

4242
// Declared after `user_instance`, is dropped last
43-
pub lifecycle: cell::Cell<Lifecycle>,
43+
pub(super) lifecycle: cell::Cell<Lifecycle>,
4444
godot_ref_count: cell::Cell<u32>,
4545
}
4646

@@ -81,6 +81,11 @@ mod single_threaded {
8181
);
8282
}
8383

84+
pub fn is_bound(&self) -> bool {
85+
// Needs to borrow mutably, otherwise it succeeds if shared borrows are alive.
86+
self.user_instance.try_borrow_mut().is_err()
87+
}
88+
8489
pub fn get(&self) -> cell::Ref<T> {
8590
self.user_instance.try_borrow().unwrap_or_else(|_e| {
8691
panic!(
@@ -122,7 +127,7 @@ mod multi_threaded {
122127
use std::sync;
123128
use std::sync::atomic::{AtomicU32, Ordering};
124129

125-
use crate::obj::{Base, Gd, GodotClass, Inherits, Share};
130+
use crate::obj::{Base, Gd, GodotClass, Inherits};
126131
use crate::out;
127132

128133
use super::Lifecycle;
@@ -158,7 +163,7 @@ mod multi_threaded {
158163
base: Base<T::Base>,
159164

160165
// Declared after `user_instance`, is dropped last
161-
pub lifecycle: AtomicLifecycle,
166+
pub(super) lifecycle: AtomicLifecycle,
162167
godot_ref_count: AtomicU32,
163168
}
164169

@@ -195,6 +200,11 @@ mod multi_threaded {
195200
);
196201
}
197202

203+
pub fn is_bound(&self) -> bool {
204+
// Needs to borrow mutably, otherwise it succeeds if shared borrows are alive.
205+
self.user_instance.try_write().is_err()
206+
}
207+
198208
pub fn get(&self) -> sync::RwLockReadGuard<T> {
199209
self.user_instance.read().unwrap_or_else(|_e| {
200210
panic!(
@@ -311,7 +321,14 @@ pub unsafe fn as_storage<'u, T: GodotClass>(
311321
/// # Safety
312322
/// `instance_ptr` is assumed to point to a valid instance. This function must only be invoked once for a pointer.
313323
pub unsafe fn destroy_storage<T: GodotClass>(instance_ptr: sys::GDExtensionClassInstancePtr) {
314-
let _drop = Box::from_raw(instance_ptr as *mut InstanceStorage<T>);
324+
let raw = instance_ptr as *mut InstanceStorage<T>;
325+
326+
assert!(
327+
!(*raw).is_bound(),
328+
"tried to destroy object while a bind() or bind_mut() call is active"
329+
);
330+
331+
let _drop = Box::from_raw(raw);
315332
}
316333

317334
// ----------------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/object_tests/object_test.rs

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,20 @@ fn object_new_has_instance_id() {
224224
obj.free();
225225
}
226226

227+
#[itest]
228+
fn object_dynamic_free() {
229+
let mut obj = Gd::<SignalEmitter>::new_default();
230+
let id = obj.instance_id();
231+
232+
obj.call("free".into(), &[]);
233+
234+
assert_eq!(
235+
Gd::<SignalEmitter>::try_from_instance_id(id),
236+
None,
237+
"dynamic free() call must destroy object"
238+
);
239+
}
240+
227241
#[itest]
228242
fn object_user_bind_after_free() {
229243
let obj = Gd::new(SignalEmitter {}); // type doesn't matter, just Object-derived
@@ -235,6 +249,53 @@ fn object_user_bind_after_free() {
235249
});
236250
}
237251

252+
#[itest]
253+
fn object_user_free_during_bind() {
254+
let obj = Gd::new(SignalEmitter {}); // type doesn't matter, just Object-derived
255+
let guard = obj.bind();
256+
257+
let copy = obj.clone(); // TODO clone allowed while bound?
258+
259+
expect_panic("direct free() on user while it's bound", move || {
260+
copy.free();
261+
});
262+
263+
drop(guard);
264+
assert!(
265+
obj.is_instance_valid(),
266+
"object lives on after failed free()"
267+
);
268+
obj.free(); // now succeeds
269+
}
270+
271+
#[itest]
272+
fn object_user_dynamic_free_during_bind() {
273+
// Note: we could also test if GDScript can access free() when an object is bound, to check whether the panic is handled or crashes
274+
// the engine. However, that is only possible under the following scenarios:
275+
// 1. Multithreading -- needs to be outlawed on Gd<T> in general, anyway. If we allow a thread-safe Gd<T>, we however need to handle that.
276+
// 2. Re-entrant calls -- Rust binds a Gd<T>, calls GDScript, which frees the same Gd. This is the same as the test here.
277+
// 3. Holding a guard (GdRef/GdMut) across function calls -- not possible, guard's lifetime is coupled to a Gd and cannot be stored in
278+
// fields or global variables due to that.
279+
280+
let obj = Gd::new(SignalEmitter {}); // type doesn't matter, just Object-derived
281+
let guard = obj.bind();
282+
283+
let mut copy = obj.clone(); // TODO clone allowed while bound?
284+
285+
expect_panic("engine-routed free() on user while it's bound", move || {
286+
copy.call("free".into(), &[]);
287+
});
288+
289+
drop(guard);
290+
assert!(
291+
obj.is_instance_valid(),
292+
"object lives on after failed dynamic free()"
293+
);
294+
obj.free(); // now succeeds
295+
}
296+
297+
// TODO test if engine destroys it, eg. call()
298+
238299
#[itest]
239300
fn object_user_call_after_free() {
240301
let obj = Gd::new(SignalEmitter {}); // type doesn't matter, just Object-derived
@@ -617,7 +678,6 @@ fn object_user_bad_downcast() {
617678
#[itest]
618679
fn object_engine_manual_free() {
619680
// Tests if no panic or memory leak
620-
621681
{
622682
let node = Node3D::new_alloc();
623683
let node2 = node.clone();
@@ -637,10 +697,11 @@ fn object_engine_shared_free() {
637697

638698
#[itest]
639699
fn object_engine_manual_double_free() {
640-
expect_panic("double free()", || {
641-
let node = Node3D::new_alloc();
642-
let node2 = node.clone();
643-
node.free();
700+
let node = Node3D::new_alloc();
701+
let node2 = node.clone();
702+
node.free();
703+
704+
expect_panic("double free()", move || {
644705
node2.free();
645706
});
646707
}
@@ -653,6 +714,17 @@ fn object_engine_refcounted_free() {
653714
expect_panic("calling free() on RefCounted object", || node2.free())
654715
}
655716

717+
#[itest]
718+
fn object_user_double_free() {
719+
let mut obj = Gd::<SignalEmitter>::new_default();
720+
let obj2 = obj.clone();
721+
obj.call("free".into(), &[]);
722+
723+
expect_panic("double free()", move || {
724+
obj2.free();
725+
});
726+
}
727+
656728
#[itest]
657729
fn object_user_share_drop() {
658730
let drop_count = Rc::new(RefCell::new(0));
@@ -865,11 +937,10 @@ impl SignalEmitter {
865937
fn do_use();
866938
}
867939

868-
/// Test that godot can call a method that takes `&self`, while there already exists an immutable reference
940+
/// Test that Godot can call a method that takes `&self`, while there already exists an immutable reference
869941
/// to that type acquired through `bind`.
870942
///
871-
/// This test is not signal-specific, the original bug would happen whenever godot would call a method that
872-
/// takes `&self`. However this was the easiest way to test the bug i could find.
943+
/// This test is not signal-specific, the original bug would happen whenever Godot would call a method that takes `&self`.
873944
#[itest]
874945
fn double_use_reference() {
875946
let double_use: Gd<DoubleUse> = Gd::new_default();
@@ -891,7 +962,7 @@ fn double_use_reference() {
891962

892963
assert!(guard.used.get(), "use_1 was not called");
893964

894-
std::mem::drop(guard);
965+
drop(guard);
895966

896967
double_use.free();
897968
emitter.free();

0 commit comments

Comments
 (0)