Skip to content

Commit c92de58

Browse files
authored
Merge pull request #546 from godot-rust/bugfix/subtype-ub
Harden safety around dead and badly typed `Gd<T>` instances
2 parents 92dce9c + 38eb2b1 commit c92de58

File tree

16 files changed

+541
-112
lines changed

16 files changed

+541
-112
lines changed

godot-codegen/src/class_generator.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,15 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
600600
// notify() and notify_reversed() are added after other methods, to list others first in docs.
601601
let notify_methods = make_notify_methods(class_name, ctx);
602602

603+
let internal_methods = quote! {
604+
fn __checked_id(&self) -> Option<crate::obj::InstanceId> {
605+
// SAFETY: only Option due to layout-compatibility with RawGd<T>; it is always Some because stored in Gd<T> which is non-null.
606+
let rtti = unsafe { self.rtti.as_ref().unwrap_unchecked() };
607+
let instance_id = rtti.check_type::<Self>();
608+
Some(instance_id)
609+
}
610+
};
611+
603612
let memory = if class_name.rust_ty == "Object" {
604613
ident("DynamicRefCount")
605614
} else if class.is_refcounted {
@@ -608,7 +617,7 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
608617
ident("ManualMemory")
609618
};
610619

611-
// mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub
620+
// mod re_export needed, because class should not appear inside the file module, and we can't re-export private struct as pub.
612621
let imports = util::make_imports();
613622
let tokens = quote! {
614623
#![doc = #module_doc]
@@ -625,14 +634,18 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
625634
#[repr(C)]
626635
pub struct #class_name {
627636
object_ptr: sys::GDExtensionObjectPtr,
628-
instance_id: crate::obj::InstanceId,
637+
638+
// This field should never be None. Type Option<T> is chosen to be layout-compatible with Gd<T>, which uses RawGd<T> inside.
639+
// The RawGd<T>'s identity field can be None because of generality (it can represent null pointers, as opposed to Gd<T>).
640+
rtti: Option<crate::private::ObjectRtti>,
629641
}
630642
#virtual_trait
631643
#notification_enum
632644
impl #class_name {
633645
#constructor
634646
#methods
635647
#notify_methods
648+
#internal_methods
636649
#constants
637650
}
638651
unsafe impl crate::obj::GodotClass for #class_name {
@@ -646,12 +659,12 @@ fn make_class(class: &Class, class_name: &TyName, ctx: &mut Context) -> Generate
646659
}
647660
}
648661
impl crate::obj::EngineClass for #class_name {
649-
fn as_object_ptr(&self) -> sys::GDExtensionObjectPtr {
650-
self.object_ptr
651-
}
652-
fn as_type_ptr(&self) -> sys::GDExtensionTypePtr {
653-
std::ptr::addr_of!(self.object_ptr) as sys::GDExtensionTypePtr
654-
}
662+
fn as_object_ptr(&self) -> sys::GDExtensionObjectPtr {
663+
self.object_ptr
664+
}
665+
fn as_type_ptr(&self) -> sys::GDExtensionTypePtr {
666+
std::ptr::addr_of!(self.object_ptr) as sys::GDExtensionTypePtr
667+
}
655668
}
656669
#(
657670
impl crate::obj::Inherits<crate::engine::#all_bases> for #class_name {}
@@ -1180,7 +1193,7 @@ fn make_class_method_definition(
11801193
let maybe_instance_id = if method.is_static {
11811194
quote! { None }
11821195
} else {
1183-
quote! { Some(self.instance_id) }
1196+
quote! { self.__checked_id() }
11841197
};
11851198

11861199
let fptr_access = if cfg!(feature = "codegen-lazy-fptrs") {

godot-core/src/builtin/meta/class_name.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,18 +61,18 @@ impl ClassName {
6161
self.c_str.to_str().unwrap()
6262
}
6363

64-
/// Converts the class name to a GString.
65-
pub fn to_godot_string(&self) -> GString {
64+
/// Converts the class name to a `GString`.
65+
pub fn to_gstring(&self) -> GString {
6666
self.with_string_name(|s| s.into())
6767
}
6868

69-
/// Converts the class name to a StringName.
69+
/// Converts the class name to a `StringName`.
7070
pub fn to_string_name(&self) -> StringName {
7171
self.with_string_name(|s| s.clone())
7272
}
7373

7474
/// The returned pointer is valid indefinitely, as entries are never deleted from the cache.
75-
/// Since we use Box<StringName>, HashMap reallocations don't affect the validity of the StringName.
75+
/// Since we use `Box<StringName>`, `HashMap` reallocations don't affect the validity of the StringName.
7676
#[doc(hidden)]
7777
pub fn string_sys(&self) -> sys::GDExtensionStringNamePtr {
7878
self.with_string_name(|s| s.string_sys())

godot-core/src/builtin/meta/signature.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,8 @@ macro_rules! impl_varcall_signature_for_tuple {
167167
unsafe { varcall_arg::<$Pn, $n>(args_ptr, method_name) },
168168
)*) ;
169169

170-
varcall_return::<$R>(func(instance_ptr, args), ret, err)
170+
let rust_result = func(instance_ptr, args);
171+
varcall_return::<$R>(rust_result, ret, err)
171172
}
172173

173174
#[inline]
@@ -181,9 +182,9 @@ macro_rules! impl_varcall_signature_for_tuple {
181182
) -> Self::Ret {
182183
//$crate::out!("out_class_varcall: {method_name}");
183184

184-
// Note: varcalls are not safe from failing, if the happen through an object pointer -> validity check necessary.
185+
// Note: varcalls are not safe from failing, if they happen through an object pointer -> validity check necessary.
185186
if let Some(instance_id) = maybe_instance_id {
186-
crate::engine::ensure_object_alive(Some(instance_id), object_ptr, method_name);
187+
crate::engine::ensure_object_alive(instance_id, object_ptr, method_name);
187188
}
188189

189190
let class_fn = sys::interface_fn!(object_method_bind_call);
@@ -298,7 +299,7 @@ macro_rules! impl_ptrcall_signature_for_tuple {
298299
) -> Self::Ret {
299300
// $crate::out!("out_class_ptrcall: {method_name}");
300301
if let Some(instance_id) = maybe_instance_id {
301-
crate::engine::ensure_object_alive(Some(instance_id), object_ptr, method_name);
302+
crate::engine::ensure_object_alive(instance_id, object_ptr, method_name);
302303
}
303304

304305
let class_fn = sys::interface_fn!(object_method_bind_ptrcall);

godot-core/src/engine/mod.rs

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ mod script_instance;
2424
pub use gfile::{GFile, NotUniqueError};
2525
pub use script_instance::{create_script_instance, ScriptInstance};
2626

27+
#[cfg(debug_assertions)]
28+
use crate::builtin::meta::ClassName;
29+
2730
/// Support for Godot _native structures_.
2831
///
2932
/// Native structures are a niche API in Godot. These are low-level data types that are passed as pointers to/from the engine.
@@ -218,15 +221,14 @@ pub(crate) fn object_ptr_from_id(instance_id: InstanceId) -> sys::GDExtensionObj
218221
unsafe { sys::interface_fn!(object_get_instance_from_id)(instance_id.to_u64()) }
219222
}
220223

224+
// ----------------------------------------------------------------------------------------------------------------------------------------------
225+
// Implementation of this file
226+
221227
pub(crate) fn ensure_object_alive(
222-
instance_id: Option<InstanceId>,
228+
instance_id: InstanceId,
223229
old_object_ptr: sys::GDExtensionObjectPtr,
224230
method_name: &'static str,
225231
) {
226-
let Some(instance_id) = instance_id else {
227-
panic!("{method_name}: cannot call method on null object")
228-
};
229-
230232
let new_object_ptr = object_ptr_from_id(instance_id);
231233

232234
assert!(
@@ -242,8 +244,26 @@ pub(crate) fn ensure_object_alive(
242244
);
243245
}
244246

245-
// ----------------------------------------------------------------------------------------------------------------------------------------------
246-
// Implementation of this file
247+
#[cfg(debug_assertions)]
248+
pub(crate) fn ensure_object_inherits(
249+
derived: &ClassName,
250+
base: &ClassName,
251+
instance_id: InstanceId,
252+
) -> bool {
253+
// TODO static cache.
254+
255+
if derived == base
256+
|| base == &Object::class_name() // always true
257+
|| ClassDb::singleton().is_parent_class(derived.to_string_name(), base.to_string_name())
258+
{
259+
return true;
260+
}
261+
262+
panic!(
263+
"Instance of ID {instance_id} has type {derived} but is incorrectly stored in a Gd<{base}>.\n\
264+
This may happen if you change an object's identity through DerefMut."
265+
)
266+
}
247267

248268
// Separate function, to avoid constructing string twice
249269
// Note that more optimizations than that likely make no sense, as loading is quite expensive
@@ -253,7 +273,7 @@ where
253273
{
254274
ResourceLoader::singleton()
255275
.load_ex(path.clone())
256-
.type_hint(T::class_name().to_godot_string())
276+
.type_hint(T::class_name().to_gstring())
257277
.done() // TODO unclone
258278
.and_then(|res| res.try_cast::<T>().ok())
259279
}

godot-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ pub mod private {
6565
sys::plugin_foreach!(__GODOT_PLUGIN_REGISTRY; visitor);
6666
}
6767

68+
pub use crate::obj::rtti::ObjectRtti;
69+
6870
pub struct ClassConfig {
6971
pub is_tool: bool,
7072
}

godot-core/src/obj/gd.rs

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,7 @@ impl<T: GodotClass> Gd<T> {
263263
///
264264
/// This method is safe and never panics.
265265
pub fn instance_id_unchecked(&self) -> InstanceId {
266-
// SAFETY:
267-
// A `Gd` can only be created from a non-null `RawGd`. Meaning `raw.instance_id_unchecked()` will
266+
// SAFETY: a `Gd` can only be created from a non-null `RawGd`, meaning `raw.instance_id_unchecked()` will
268267
// always return `Some`.
269268
unsafe { self.raw.instance_id_unchecked().unwrap_unchecked() }
270269
}
@@ -278,8 +277,7 @@ impl<T: GodotClass> Gd<T> {
278277
/// and will panic in a defined manner. Encountering such panics is almost always a bug you should fix, and not a
279278
/// runtime condition to check against.
280279
pub fn is_instance_valid(&self) -> bool {
281-
// This call refreshes the instance ID, and recognizes dead objects.
282-
self.instance_id_or_none().is_some()
280+
self.raw.is_instance_valid()
283281
}
284282

285283
/// **Upcast:** convert into a smart pointer to a base class. Always succeeds.
@@ -443,7 +441,7 @@ impl<T: GodotClass> Gd<T> {
443441
impl<T, M> Gd<T>
444442
where
445443
T: GodotClass<Mem = M>,
446-
M: mem::PossiblyManual + mem::Memory,
444+
M: mem::Memory + mem::PossiblyManual,
447445
{
448446
/// Destroy the manually-managed Godot object.
449447
///
@@ -463,38 +461,61 @@ where
463461
// Note: this method is NOT invoked when the free() call happens dynamically (e.g. through GDScript or reflection).
464462
// As such, do not use it for operations and validations to perform upon destruction.
465463

464+
// free() is likely to be invoked in destructors during panic unwind. In this case, we cannot panic again.
465+
// Instead, we print an error and exit free() immediately. The closure is supposed to be used in a unit return statement.
466+
let is_panic_unwind = std::thread::panicking();
467+
let error_or_panic = |msg: String| {
468+
if is_panic_unwind {
469+
crate::godot_error!(
470+
"Encountered 2nd panic in free() during panic unwind; will skip destruction:\n{msg}"
471+
);
472+
} else {
473+
panic!("{}", msg);
474+
}
475+
};
476+
466477
// TODO disallow for singletons, either only at runtime or both at compile time (new memory policy) and runtime
467478
use dom::Domain;
468479

469480
// Runtime check in case of T=Object, no-op otherwise
470481
let ref_counted = T::Mem::is_ref_counted(&self.raw);
471-
assert_ne!(
472-
ref_counted, Some(true),
473-
"called free() on Gd<Object> which points to a RefCounted dynamic type; free() only supported for manually managed types\n\
474-
object: {self:?}"
475-
);
482+
if ref_counted == Some(true) {
483+
return error_or_panic(format!(
484+
"Called free() on Gd<Object> which points to a RefCounted dynamic type; free() only supported for manually managed types\n\
485+
Object: {self:?}"
486+
));
487+
}
476488

477489
// If ref_counted returned None, that means the instance was destroyed
478-
assert!(
479-
ref_counted == Some(false) && self.is_instance_valid(),
480-
"called free() on already destroyed object"
481-
);
490+
if ref_counted != Some(false) || !self.is_instance_valid() {
491+
return error_or_panic("called free() on already destroyed object".to_string());
492+
}
493+
494+
// If the object is still alive, make sure the dynamic type matches. Necessary because subsequent checks may rely on the
495+
// static type information to be correct. This is a no-op in Release mode.
496+
// Skip check during panic unwind; would need to rewrite whole thing to use Result instead. Having BOTH panic-in-panic and bad type is
497+
// a very unlikely corner case.
498+
if !is_panic_unwind {
499+
self.raw.check_dynamic_type("free");
500+
}
482501

483502
// SAFETY: object must be alive, which was just checked above. No multithreading here.
484503
// Also checked in the C free_instance_func callback, however error message can be more precise here and we don't need to instruct
485504
// the engine about object destruction. Both paths are tested.
486505
let bound = unsafe { T::Declarer::is_currently_bound(&self.raw) };
487-
assert!(
488-
!bound,
489-
"called free() while a bind() or bind_mut() call is active"
490-
);
506+
if bound {
507+
return error_or_panic(
508+
"called free() while a bind() or bind_mut() call is active".to_string(),
509+
);
510+
}
491511

492512
// SAFETY: object alive as checked.
493513
// This destroys the Storage instance, no need to run destructor again.
494514
unsafe {
495515
sys::interface_fn!(object_destroy)(self.raw.obj_sys());
496516
}
497517

518+
// TODO: this might leak associated data in Gd<T>, e.g. ClassName.
498519
std::mem::forget(self);
499520
}
500521
}
@@ -508,10 +529,12 @@ impl<T: GodotClass> GodotConvert for Gd<T> {
508529

509530
impl<T: GodotClass> ToGodot for Gd<T> {
510531
fn to_godot(&self) -> Self::Via {
532+
self.raw.check_rtti("Gd<T>::to_godot");
511533
self.clone()
512534
}
513535

514536
fn into_godot(self) -> Self::Via {
537+
self.raw.check_rtti("Gd<T>::into_godot");
515538
self
516539
}
517540
}
@@ -622,7 +645,7 @@ impl<T: GodotClass> Export for Gd<T> {
622645

623646
// Godot does this by default too; the hint is needed when the class is a resource/node,
624647
// but doesn't seem to make a difference otherwise.
625-
let hint_string = T::class_name().to_godot_string();
648+
let hint_string = T::class_name().to_gstring();
626649

627650
PropertyHintInfo { hint, hint_string }
628651
}

godot-core/src/obj/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod onready;
1919
mod raw;
2020
mod traits;
2121

22+
pub(crate) mod rtti;
23+
2224
pub use base::*;
2325
pub use gd::*;
2426
pub use guards::*;
@@ -27,4 +29,6 @@ pub use onready::*;
2729
pub use raw::*;
2830
pub use traits::*;
2931

32+
// Do not re-export rtti here.
33+
3034
type GdDerefTarget<T> = <<T as GodotClass>::Declarer as dom::Domain>::DerefTarget<T>;

0 commit comments

Comments
 (0)