Skip to content

Commit f9db5d6

Browse files
bors[bot]Bromeon
andauthored
Merge #133
133: Fix use-after-free and 3 memory leaks; enforce AddressSanitizer in CI r=Bromeon a=Bromeon Changes implementation of the `Gd` smart pointer to cache the instance ID in each object. To my knowledge, instance IDs are the only reliable way to check for object validity in Godot, as raw object pointers may become dangling. In addition, this PR fixes 3 memory leaks around arrays and dictionaries. Those occurred due to `from_sys_init()` using `T::default()` in too many places. This essentially lead to allocating a default-constructed object, which is then immediately overwritten by the `init` function, which _also_ allocates a new object. I refactored the `GodotFfi::from_sys_init()` to **never** call `default()`, and add an explicit `from_sys_init_default()` for cases where this is desired. This also cuts down on some of the boilerplate. --- Both use-after-free and memory leaks were discovered using AddressSanitizer/LeakSanitizer. Great tooling from the C++ world, which also proves useful for us, as miri can't be used in FFI contexts. From now on, UB or leaks detected by ASan/LSan will cause a hard error in CI. The tools are not 100% bullet-proof; they didn't detect the following UAF case in a short test, but they are still of great value as a more systematic counter to memory errors. <details><summary>use-after-free, false negative</summary> ```rs let mut boks = Box::new(44); let ptr = std::ptr::addr_of_mut!(*boks); println!("deref={}", unsafe { *ptr }); // output: deref=44 drop(boks); println!("deref={}", unsafe { *ptr }); // output: deref=1719666059 ``` </details> On a side note, getting these working correctly in CI was a bit of a marathon because ASan/LSan don't have stacktraces for dynamically loaded libraries (a known wontfix problem, see google/sanitizers#89). Additionally, false positives for memory leaks were reported: a simple `println!` would cause 1024 bytes of non-reclaimable memory. Therefore, I had to compile a special version of nightly Godot that disables dynamic library unloading via `dlclose`, to keep the stacktrace around, and this seemed to fix the false-positive issue as well. Although likely unrelated, what I also found during research was rust-lang/rust#19776. Fixes #89. Co-authored-by: Jan Haller <[email protected]>
2 parents b3b9e0e + cbd22fa commit f9db5d6

File tree

18 files changed

+223
-239
lines changed

18 files changed

+223
-239
lines changed

.github/workflows/full-ci.yml

+5-2
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,7 @@ jobs:
138138
godot-itest:
139139
name: godot-itest (${{ matrix.name }})
140140
runs-on: ${{ matrix.os }}
141-
# TODO: continue-on-error: false, as soon as memory errors are fixed
142-
continue-on-error: ${{ contains(matrix.name, 'memcheck') }}
141+
continue-on-error: false
143142
timeout-minutes: 24
144143
strategy:
145144
fail-fast: false # cancel all jobs as soon as one fails?
@@ -165,6 +164,10 @@ jobs:
165164
rust-toolchain: stable
166165
godot-binary: godot.linuxbsd.editor.dev.x86_64
167166

167+
# Special Godot binaries compiled with AddressSanitizer/LeakSanitizer to detect UB/leaks.
168+
# Additionally, the Godot source is patched to make dlclose() a no-op, as unloading dynamic libraries loses stacktrace and
169+
# cause false positives like println!. See https://github.com/google/sanitizers/issues/89.
170+
# The gcc version can possibly be removed later, as it is slower and needs a larger artifact than the clang one.
168171
- name: linux-memcheck-gcc
169172
os: ubuntu-20.04
170173
rust-toolchain: stable

godot-codegen/src/class_generator.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -686,7 +686,7 @@ impl VariantFfi {
686686
fn type_ptr() -> Self {
687687
Self {
688688
sys_method: ident("sys_const"),
689-
from_sys_init_method: ident("from_sys_init"),
689+
from_sys_init_method: ident("from_sys_init_default"),
690690
}
691691
}
692692
}
@@ -876,7 +876,7 @@ fn make_return(
876876
}
877877
(None, Some(return_ty)) => {
878878
quote! {
879-
<#return_ty as sys::GodotFfi>::from_sys_init(|return_ptr| {
879+
<#return_ty as sys::GodotFfi>::from_sys_init_default(|return_ptr| {
880880
#ptrcall_invocation
881881
})
882882
}

godot-core/src/builtin/array.rs

+18-29
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,11 @@ impl_builtin_froms!(Array;
7070

7171
impl<T: VariantMetadata> TypedArray<T> {
7272
fn from_opaque(opaque: sys::types::OpaqueArray) -> Self {
73-
let array = Self {
73+
// Note: type is not yet checked at this point, because array has not yet been initialized!
74+
Self {
7475
opaque,
7576
_phantom: PhantomData,
76-
};
77-
array.check_type();
78-
array
77+
}
7978
}
8079

8180
/// Returns the number of elements in the array. Equivalent of `size()` in Godot.
@@ -320,8 +319,9 @@ impl<T: VariantMetadata> TypedArray<T> {
320319
/// # Panics
321320
///
322321
/// If the inner type doesn't match `T` or no type is set at all.
323-
fn check_type(&self) {
322+
fn with_checked_type(self) -> Self {
324323
assert_eq!(self.type_info(), TypeInfo::new::<T>());
324+
self
325325
}
326326

327327
/// Sets the type of the inner array. Can only be called once, directly after creation.
@@ -567,17 +567,9 @@ impl<T: VariantMetadata + ToVariant> TypedArray<T> {
567567
// }
568568

569569
impl<T: VariantMetadata> GodotFfi for TypedArray<T> {
570-
ffi_methods! {
571-
type sys::GDExtensionTypePtr = *mut Opaque;
572-
fn from_sys;
573-
fn sys;
574-
fn write_sys;
575-
}
576-
577-
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
578-
// Can't use uninitialized pointer -- Array CoW implementation in C++ expects that on
579-
// assignment, the target CoW pointer is either initialized or nullptr
570+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
580571

572+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
581573
let mut result = Self::default();
582574
init_fn(result.sys_mut());
583575
result
@@ -598,28 +590,25 @@ impl<T: VariantMetadata> fmt::Debug for TypedArray<T> {
598590
/// [`Array::duplicate_deep()`].
599591
impl<T: VariantMetadata> Share for TypedArray<T> {
600592
fn share(&self) -> Self {
601-
unsafe {
593+
let array = unsafe {
602594
Self::from_sys_init(|self_ptr| {
603595
let ctor = ::godot_ffi::builtin_fn!(array_construct_copy);
604596
let args = [self.sys_const()];
605597
ctor(self_ptr, args.as_ptr());
606598
})
607-
}
599+
};
600+
array.with_checked_type()
608601
}
609602
}
610603

611604
impl<T: VariantMetadata> Default for TypedArray<T> {
612605
#[inline]
613606
fn default() -> Self {
614-
// Note: can't use from_sys_init(), as that calls the default constructor
615-
// (because most assignments expect initialized target type)
616-
let mut uninit = std::mem::MaybeUninit::<TypedArray<T>>::uninit();
617607
let mut array = unsafe {
618-
let self_ptr = (*uninit.as_mut_ptr()).sys_mut();
619-
sys::builtin_call! {
620-
array_construct_default(self_ptr, std::ptr::null_mut())
621-
};
622-
uninit.assume_init()
608+
Self::from_sys_init(|self_ptr| {
609+
let ctor = sys::builtin_fn!(array_construct_default);
610+
ctor(self_ptr, std::ptr::null_mut())
611+
})
623612
};
624613
array.init_inner_type();
625614
array
@@ -661,14 +650,14 @@ impl<T: VariantMetadata> FromVariant for TypedArray<T> {
661650
if variant.get_type() != Self::variant_type() {
662651
return Err(VariantConversionError);
663652
}
664-
let result = unsafe {
665-
Self::from_sys_init(|self_ptr| {
653+
let array = unsafe {
654+
Self::from_sys_init_default(|self_ptr| {
666655
let array_from_variant = sys::builtin_fn!(array_from_variant);
667656
array_from_variant(self_ptr, variant.var_sys());
668657
})
669658
};
670659

671-
Ok(result)
660+
Ok(array.with_checked_type())
672661
}
673662
}
674663

@@ -773,7 +762,7 @@ impl<T: VariantMetadata> PartialEq for TypedArray<T> {
773762
let mut result = false;
774763
sys::builtin_call! {
775764
array_operator_equal(self.sys(), other.sys(), result.sys_mut())
776-
};
765+
}
777766
result
778767
}
779768
}

godot-core/src/builtin/dictionary.rs

+2-10
Original file line numberDiff line numberDiff line change
@@ -240,17 +240,9 @@ impl Dictionary {
240240
// Traits
241241

242242
impl GodotFfi for Dictionary {
243-
ffi_methods! {
244-
type sys::GDExtensionTypePtr = *mut Opaque;
245-
fn from_sys;
246-
fn sys;
247-
fn write_sys;
248-
}
249-
250-
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
251-
// Can't use uninitialized pointer -- Dictionary CoW implementation in C++ expects that on
252-
// assignment, the target CoW pointer is either initialized or nullptr
243+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
253244

245+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
254246
let mut result = Self::default();
255247
init_fn(result.sys_mut());
256248
result

godot-core/src/builtin/macros.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ macro_rules! impl_builtin_traits_inner {
3333
#[inline]
3434
fn clone(&self) -> Self {
3535
unsafe {
36-
Self::from_sys_init(|self_ptr| {
36+
Self::from_sys_init_default(|self_ptr| {
3737
let ctor = ::godot_ffi::builtin_fn!($gd_method);
3838
let args = [self.sys_const()];
3939
ctor(self_ptr, args.as_ptr());
@@ -116,7 +116,7 @@ macro_rules! impl_builtin_traits_inner {
116116
return Err($crate::builtin::variant::VariantConversionError)
117117
}
118118
let result = unsafe {
119-
Self::from_sys_init(|self_ptr| {
119+
Self::from_sys_init_default(|self_ptr| {
120120
let converter = sys::builtin_fn!($gd_method);
121121
converter(self_ptr, variant.var_sys());
122122
})

godot-core/src/builtin/node_path.rs

+9-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
use crate::builtin::GodotString;
88
use godot_ffi as sys;
9-
use godot_ffi::{ffi_methods, GodotFfi};
9+
use godot_ffi::{ffi_methods, GDExtensionTypePtr, GodotFfi};
1010
use std::fmt::{Display, Formatter, Result as FmtResult};
1111

1212
pub struct NodePath {
@@ -21,12 +21,18 @@ impl NodePath {
2121

2222
impl GodotFfi for NodePath {
2323
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
24+
25+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(GDExtensionTypePtr)) -> Self {
26+
let mut result = Self::default();
27+
init_fn(result.sys_mut());
28+
result
29+
}
2430
}
2531

2632
impl From<&GodotString> for NodePath {
2733
fn from(path: &GodotString) -> Self {
2834
unsafe {
29-
Self::from_sys_init(|self_ptr| {
35+
Self::from_sys_init_default(|self_ptr| {
3036
let ctor = sys::builtin_fn!(node_path_from_string);
3137
let args = [path.sys_const()];
3238
ctor(self_ptr, args.as_ptr());
@@ -38,7 +44,7 @@ impl From<&GodotString> for NodePath {
3844
impl From<&NodePath> for GodotString {
3945
fn from(path: &NodePath) -> Self {
4046
unsafe {
41-
Self::from_sys_init(|self_ptr| {
47+
Self::from_sys_init_default(|self_ptr| {
4248
let ctor = sys::builtin_fn!(string_from_node_path);
4349
let args = [path.sys_const()];
4450
ctor(self_ptr, args.as_ptr());

godot-core/src/builtin/others.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ impl Callable {
5050
// upcast not needed
5151
let method = method.into();
5252
unsafe {
53-
Self::from_sys_init(|self_ptr| {
53+
Self::from_sys_init_default(|self_ptr| {
5454
let ctor = sys::builtin_fn!(callable_from_object_method);
5555
let args = [object.sys_const(), method.sys_const()];
5656
ctor(self_ptr, args.as_ptr());
@@ -66,6 +66,7 @@ impl Callable {
6666

6767
impl_builtin_traits! {
6868
for Callable {
69+
// Default => callable_construct_default;
6970
FromVariant => callable_from_variant;
7071
}
7172
}

godot-core/src/builtin/packed_array.rs

+2-10
Original file line numberDiff line numberDiff line change
@@ -386,17 +386,9 @@ macro_rules! impl_packed_array {
386386
}
387387

388388
impl GodotFfi for $PackedArray {
389-
ffi_methods! {
390-
type sys::GDExtensionTypePtr = *mut Opaque;
391-
fn from_sys;
392-
fn sys;
393-
fn write_sys;
394-
}
395-
396-
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
397-
// Can't use uninitialized pointer -- Vector CoW implementation in C++ expects that
398-
// on assignment, the target CoW pointer is either initialized or nullptr
389+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
399390

391+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
400392
let mut result = Self::default();
401393
init_fn(result.sys_mut());
402394
result

godot-core/src/builtin/string.rs

+3-28
Original file line numberDiff line numberDiff line change
@@ -37,43 +37,18 @@ impl GodotString {
3737
}
3838

3939
impl GodotFfi for GodotString {
40-
ffi_methods! {
41-
type sys::GDExtensionTypePtr = *mut Opaque;
42-
fn from_sys;
43-
fn sys;
44-
fn write_sys;
45-
}
46-
47-
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
48-
// Can't use uninitialized pointer -- String CoW implementation in C++ expects that on assignment,
49-
// the target CoW pointer is either initialized or nullptr
40+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
5041

42+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
5143
let mut result = Self::default();
5244
init_fn(result.sys_mut());
5345
result
5446
}
5547
}
5648

57-
impl Default for GodotString {
58-
fn default() -> Self {
59-
// Note: can't use from_sys_init(), as that calls the default constructor
60-
// (because most assignments expect initialized target type)
61-
62-
let mut uninit = std::mem::MaybeUninit::<GodotString>::uninit();
63-
64-
unsafe {
65-
let self_ptr = (*uninit.as_mut_ptr()).sys_mut();
66-
sys::builtin_call! {
67-
string_construct_default(self_ptr, std::ptr::null_mut())
68-
};
69-
70-
uninit.assume_init()
71-
}
72-
}
73-
}
74-
7549
impl_builtin_traits! {
7650
for GodotString {
51+
Default => string_construct_default;
7752
Clone => string_construct_copy;
7853
Drop => string_destroy;
7954
Eq => string_operator_equal;

godot-core/src/builtin/string_name.rs

+4-12
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,9 @@ impl StringName {
3333
}
3434

3535
impl GodotFfi for StringName {
36-
ffi_methods! {
37-
type sys::GDExtensionTypePtr = *mut Opaque;
38-
fn from_sys;
39-
fn sys;
40-
fn write_sys;
41-
}
42-
43-
unsafe fn from_sys_init(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
44-
// Can't use uninitialized pointer -- StringName implementation in C++ expects that on assignment,
45-
// the target type is a valid string (possibly empty)
36+
ffi_methods! { type sys::GDExtensionTypePtr = *mut Opaque; .. }
4637

38+
unsafe fn from_sys_init_default(init_fn: impl FnOnce(sys::GDExtensionTypePtr)) -> Self {
4739
let mut result = Self::default();
4840
init_fn(result.sys_mut());
4941
result
@@ -106,7 +98,7 @@ impl Hash for StringName {
10698
impl From<&GodotString> for StringName {
10799
fn from(s: &GodotString) -> Self {
108100
unsafe {
109-
Self::from_sys_init(|self_ptr| {
101+
Self::from_sys_init_default(|self_ptr| {
110102
let ctor = sys::builtin_fn!(string_name_from_string);
111103
let args = [s.sys_const()];
112104
ctor(self_ptr, args.as_ptr());
@@ -128,7 +120,7 @@ where
128120
impl From<&StringName> for GodotString {
129121
fn from(s: &StringName) -> Self {
130122
unsafe {
131-
Self::from_sys_init(|self_ptr| {
123+
Self::from_sys_init_default(|self_ptr| {
132124
let ctor = sys::builtin_fn!(string_from_string_name);
133125
let args = [s.sys_const()];
134126
ctor(self_ptr, args.as_ptr());

godot-core/src/builtin/variant/impls.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,21 @@ macro_rules! impl_variant_traits {
5555

5656
impl FromVariant for $T {
5757
fn try_from_variant(variant: &Variant) -> Result<Self, VariantConversionError> {
58+
// Type check -- at the moment, a strict match is required.
59+
if variant.get_type() != Self::variant_type() {
60+
return Err(VariantConversionError)
61+
}
62+
5863
// In contrast to T -> Variant, the conversion Variant -> T assumes
5964
// that the destination is initialized (at least for some T). For example:
6065
// void String::operator=(const String &p_str) { _cowdata._ref(p_str._cowdata); }
6166
// does a copy-on-write and explodes if this->_cowdata is not initialized.
6267
// We can thus NOT use Self::from_sys_init().
63-
if variant.get_type() != Self::variant_type() {
64-
return Err(VariantConversionError)
65-
}
66-
let mut value = <$T>::default();
6768
let result = unsafe {
68-
let converter = sys::builtin_fn!($to_fn);
69-
converter(value.sys_mut(), variant.var_sys());
70-
value
69+
Self::from_sys_init_default(|self_ptr| {
70+
let converter = sys::builtin_fn!($to_fn);
71+
converter(self_ptr, variant.var_sys());
72+
})
7173
};
7274

7375
Ok(result)

0 commit comments

Comments
 (0)