Skip to content

Commit 2d052a6

Browse files
authored
Merge pull request #846 from godot-rust/bugfix/object-arg-values
Fix user-after-free in `AsObjectArg` pass-by-value (in default-param methods)
2 parents a50057e + 0315266 commit 2d052a6

File tree

13 files changed

+130
-41
lines changed

13 files changed

+130
-41
lines changed

.github/workflows/full-ci.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,8 @@ jobs:
402402
- name: "Build and test hot-reload"
403403
if: ${{ matrix.with-hot-reload }}
404404
working-directory: examples/hot-reload/godot/test
405-
run: ./run-test.sh
405+
# Repeat a few times, our hot reload integration test can sometimes be a bit flaky.
406+
run: $RETRY ./run-test.sh
406407
shell: bash
407408

408409

godot-codegen/src/generator/default_parameters.rs

+28-15
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ pub fn make_function_definition_with_defaults(
4949
let receiver_param = &code.receiver.param;
5050
let receiver_self = &code.receiver.self_prefix;
5151

52-
let [required_params_impl_asarg, _, required_args_asarg] =
52+
let [required_params_impl_asarg, _, _] =
5353
functions_common::make_params_exprs(required_fn_params.iter().cloned(), false, true, true);
5454

55-
let [required_params_plain, _, required_args_internal] =
55+
let [_, _, required_args_internal] =
5656
functions_common::make_params_exprs(required_fn_params.into_iter(), false, false, false);
5757

5858
let return_decl = &sig.return_value().decl;
@@ -78,7 +78,7 @@ pub fn make_function_definition_with_defaults(
7878
impl #builder_lifetime #builder_ty #builder_lifetime {
7979
fn new(
8080
#object_param
81-
#( #required_params_plain, )*
81+
#( #required_params_impl_asarg, )*
8282
) -> Self {
8383
Self {
8484
#( #builder_inits, )*
@@ -114,7 +114,7 @@ pub fn make_function_definition_with_defaults(
114114
) -> #builder_ty #builder_anon_lifetime {
115115
#builder_ty::new(
116116
#object_arg
117-
#( #required_args_asarg, )*
117+
#( #required_args_internal, )*
118118
)
119119
}
120120
};
@@ -247,26 +247,33 @@ fn make_extender(
247247
default_value,
248248
} = param;
249249

250-
let (field_type, needs_conversion) = type_.private_field_decl();
250+
let (field_type, needs_conversion) = type_.default_extender_field_decl();
251251

252252
// Initialize with default parameters where available, forward constructor args otherwise
253253
let init = if let Some(value) = default_value {
254-
make_field_init(name, value, needs_conversion)
254+
make_field_init(name, Some(value), needs_conversion)
255255
} else {
256-
quote! { #name }
256+
//quote! { #name }
257+
make_field_init(name, None, needs_conversion)
258+
};
259+
260+
let builder_arg = if needs_conversion {
261+
quote! { self.#name.cow_as_object_arg() }
262+
} else {
263+
quote! { self.#name }
257264
};
258265

259266
result.builder_fields.push(quote! { #name: #field_type });
260-
result.builder_args.push(quote! { self.#name });
267+
result.builder_args.push(builder_arg);
261268
result.builder_inits.push(init);
262269
}
263270

264271
for param in default_fn_params {
265272
let FnParam { name, type_, .. } = param;
266273
let param_type = type_.param_decl();
267-
let (_, field_needs_conversion) = type_.private_field_decl();
274+
let (_, field_needs_conversion) = type_.default_extender_field_decl();
268275

269-
let field_init = make_field_init(name, &quote! { value }, field_needs_conversion);
276+
let field_init = make_field_init(name, Some(&quote! { value }), field_needs_conversion);
270277

271278
let method = quote! {
272279
#[inline]
@@ -285,10 +292,16 @@ fn make_extender(
285292
result
286293
}
287294

288-
fn make_field_init(name: &Ident, expr: &TokenStream, needs_conversion: bool) -> TokenStream {
289-
if needs_conversion {
290-
quote! { #name: #expr.as_object_arg() }
291-
} else {
292-
quote! { #name: #expr }
295+
// Rewrite the above using match
296+
fn make_field_init(
297+
name: &Ident,
298+
expr: Option<&TokenStream>, // None if the parameter has the same name as the field, and can use shorthand syntax.
299+
needs_conversion: bool,
300+
) -> TokenStream {
301+
match (needs_conversion, expr) {
302+
(true, Some(expr)) => quote! { #name: #expr.consume_object() },
303+
(true, None) /*. */ => quote! { #name: #name.consume_object() },
304+
(false, Some(expr)) => quote! { #name: #expr },
305+
(false, None) /*. */ => quote! { #name },
293306
}
294307
}

godot-codegen/src/generator/functions_common.rs

+1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ pub fn make_vis(is_private: bool) -> TokenStream {
303303
// ----------------------------------------------------------------------------------------------------------------------------------------------
304304
// Implementation
305305

306+
// Method could possibly be split -- only one invocation uses all 3 return values, the rest uses only index [0] or [2].
306307
pub(crate) fn make_params_exprs<'a>(
307308
method_args: impl Iterator<Item = &'a FnParam>,
308309
is_virtual: bool,

godot-codegen/src/models/domain.rs

+8-4
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,8 @@ pub enum RustTy {
627627
impl_as_object_arg: TokenStream,
628628

629629
/// only inner `T`
630-
#[allow(dead_code)] // only read in minimal config
630+
#[allow(dead_code)]
631+
// only read in minimal config + RustTy::default_extender_field_decl()
631632
inner_class: Ident,
632633
},
633634

@@ -645,10 +646,13 @@ impl RustTy {
645646
}
646647
}
647648

648-
/// Returns `( <field tokens>, <needs .as_object_arg()> )`.
649-
pub fn private_field_decl(&self) -> (TokenStream, bool) {
649+
/// Returns `( <field tokens>, <needs .consume_object()> )`.
650+
pub fn default_extender_field_decl(&self) -> (TokenStream, bool) {
650651
match self {
651-
RustTy::EngineClass { object_arg, .. } => (object_arg.clone(), true),
652+
RustTy::EngineClass { inner_class, .. } => {
653+
let cow_tokens = quote! { ObjectCow<crate::classes::#inner_class> };
654+
(cow_tokens, true)
655+
}
652656
other => (other.to_token_stream(), false),
653657
}
654658
}

godot-codegen/src/util.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub fn make_imports() -> TokenStream {
2828
use crate::meta::{ClassName, PtrcallSignatureTuple, VarcallSignatureTuple};
2929
use crate::classes::native::*;
3030
use crate::classes::Object;
31-
use crate::obj::{Gd, ObjectArg, AsObjectArg};
31+
use crate::obj::{Gd, AsObjectArg};
32+
use crate::obj::object_arg::{ObjectArg, ObjectCow};
3233
use crate::sys::GodotFfi as _;
3334
}
3435
}

godot-core/src/meta/sealed.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl Sealed for Variant {}
6363
impl<T: ArrayElement> Sealed for Array<T> {}
6464
impl<T: GodotClass> Sealed for Gd<T> {}
6565
impl<T: GodotClass> Sealed for RawGd<T> {}
66-
impl<T: GodotClass> Sealed for ObjectArg<T> {}
66+
impl<T: GodotClass> Sealed for object_arg::ObjectArg<T> {}
6767
impl<T> Sealed for Option<T>
6868
where
6969
T: GodotType,

godot-core/src/obj/gd.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -646,8 +646,8 @@ where
646646
///
647647
/// let mut shape: Gd<Node> = some_node();
648648
/// shape.set_owner(Gd::null_arg());
649-
pub fn null_arg() -> crate::obj::ObjectNullArg<T> {
650-
crate::obj::ObjectNullArg(std::marker::PhantomData)
649+
pub fn null_arg() -> crate::obj::object_arg::ObjectNullArg<T> {
650+
crate::obj::object_arg::ObjectNullArg(std::marker::PhantomData)
651651
}
652652
}
653653

godot-core/src/obj/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,18 @@ mod base;
1515
mod gd;
1616
mod guards;
1717
mod instance_id;
18-
mod object_arg;
1918
mod onready;
2019
mod raw_gd;
2120
mod traits;
2221

22+
pub(crate) mod object_arg;
2323
pub(crate) mod rtti;
2424

2525
pub use base::*;
2626
pub use gd::*;
2727
pub use guards::{BaseMut, BaseRef, GdMut, GdRef};
2828
pub use instance_id::*;
29-
pub use object_arg::*;
29+
pub use object_arg::AsObjectArg;
3030
pub use onready::*;
3131
pub use raw_gd::*;
3232
pub use traits::*;

godot-core/src/obj/object_arg.rs

+49
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ where
3131
{
3232
#[doc(hidden)]
3333
fn as_object_arg(&self) -> ObjectArg<T>;
34+
35+
/// Returns
36+
#[doc(hidden)]
37+
fn consume_object(self) -> ObjectCow<T>;
3438
}
3539

3640
impl<T, U> AsObjectArg<T> for Gd<U>
@@ -41,6 +45,10 @@ where
4145
fn as_object_arg(&self) -> ObjectArg<T> {
4246
<&Gd<U>>::as_object_arg(&self)
4347
}
48+
49+
fn consume_object(self) -> ObjectCow<T> {
50+
ObjectCow::Owned(self.upcast())
51+
}
4452
}
4553

4654
impl<T, U> AsObjectArg<T> for &Gd<U>
@@ -53,6 +61,10 @@ where
5361
// This function is not part of the public API.
5462
unsafe { ObjectArg::from_raw_gd(&self.raw) }
5563
}
64+
65+
fn consume_object(self) -> ObjectCow<T> {
66+
ObjectCow::Borrowed(self.as_object_arg())
67+
}
5668
}
5769

5870
impl<T, U> AsObjectArg<T> for Option<U>
@@ -64,6 +76,13 @@ where
6476
self.as_ref()
6577
.map_or_else(ObjectArg::null, AsObjectArg::as_object_arg)
6678
}
79+
80+
fn consume_object(self) -> ObjectCow<T> {
81+
match self {
82+
Some(obj) => obj.consume_object(),
83+
None => Gd::null_arg().consume_object(),
84+
}
85+
}
6786
}
6887

6988
impl<T> AsObjectArg<T> for ObjectNullArg<T>
@@ -73,13 +92,43 @@ where
7392
fn as_object_arg(&self) -> ObjectArg<T> {
7493
ObjectArg::null()
7594
}
95+
96+
fn consume_object(self) -> ObjectCow<T> {
97+
// Null pointer is safe to borrow.
98+
ObjectCow::Borrowed(ObjectArg::null())
99+
}
76100
}
77101

78102
// ----------------------------------------------------------------------------------------------------------------------------------------------
79103

80104
#[doc(hidden)]
81105
pub struct ObjectNullArg<T>(pub(crate) std::marker::PhantomData<*mut T>);
82106

107+
/// Exists for cases where a value must be moved, and retaining `ObjectArg<T>` may not be possible if the source is owned.
108+
///
109+
/// Only used in default-param extender structs.
110+
#[doc(hidden)]
111+
pub enum ObjectCow<T: GodotClass> {
112+
Owned(Gd<T>),
113+
Borrowed(ObjectArg<T>),
114+
}
115+
116+
impl<T> ObjectCow<T>
117+
where
118+
T: GodotClass + Bounds<Declarer = bounds::DeclEngine>,
119+
{
120+
/// Returns the actual `ObjectArg` to be passed to function calls.
121+
///
122+
/// [`ObjectCow`] does not implement [`AsObjectArg<T>`] because a differently-named method is more explicit (less errors in codegen),
123+
/// and because [`AsObjectArg::consume_object()`] is not meaningful.
124+
pub fn cow_as_object_arg(&self) -> ObjectArg<T> {
125+
match self {
126+
ObjectCow::Owned(gd) => gd.as_object_arg(),
127+
ObjectCow::Borrowed(obj) => obj.clone(),
128+
}
129+
}
130+
}
131+
83132
// ----------------------------------------------------------------------------------------------------------------------------------------------
84133

85134
/// View for object arguments passed to the Godot engine. Never owning; must be null or backed by `Gd<T>`.

godot-core/src/tools/save_load.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ where
164164
{
165165
// TODO unclone GString
166166
let res = ResourceSaver::singleton()
167-
.save_ex(obj.upcast())
167+
.save_ex(obj)
168168
.path(path.clone())
169169
.done();
170170

itest/rust/src/builtin_tests/containers/array_test.rs

+12-10
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ fn untyped_array_return_from_godot_func() {
394394
let mut node = Node::new_alloc();
395395
let mut child = Node::new_alloc();
396396
child.set_name("child_node".into());
397-
node.add_child(child.clone());
397+
node.add_child(&child);
398398
node.queue_free(); // Do not leak even if the test fails.
399399
let result = node.get_node_and_resource("child_node".into());
400400

@@ -431,7 +431,7 @@ fn typed_array_return_from_godot_func() {
431431
let mut node = Node::new_alloc();
432432
let mut child = Node::new_alloc();
433433
child.set_name("child_node".into());
434-
node.add_child(child.clone());
434+
node.add_child(&child);
435435
node.queue_free(); // Do not leak even if the test fails.
436436
let children = node.get_children();
437437

@@ -477,10 +477,7 @@ fn array_should_format_with_display() {
477477
#[cfg(since_api = "4.2")]
478478
fn array_sort_custom() {
479479
let mut a = array![1, 2, 3, 4];
480-
let func = Callable::from_fn("sort backwards", |args: &[&Variant]| {
481-
let res = i32::from_variant(args[0]) > i32::from_variant(args[1]);
482-
Ok(Variant::from(res))
483-
});
480+
let func = backwards_sort_callable();
484481
a.sort_unstable_custom(func);
485482
assert_eq!(a, array![4, 3, 2, 1]);
486483
}
@@ -489,14 +486,19 @@ fn array_sort_custom() {
489486
#[cfg(since_api = "4.2")]
490487
fn array_binary_search_custom() {
491488
let a = array![5, 4, 2, 1];
492-
let func = Callable::from_fn("sort backwards", |args: &[&Variant]| {
493-
let res = i32::from_variant(args[0]) > i32::from_variant(args[1]);
494-
Ok(Variant::from(res))
495-
});
489+
let func = backwards_sort_callable();
496490
assert_eq!(a.bsearch_custom(&1, func.clone()), 3);
497491
assert_eq!(a.bsearch_custom(&3, func), 2);
498492
}
499493

494+
#[cfg(since_api = "4.2")]
495+
fn backwards_sort_callable() -> Callable {
496+
Callable::from_fn("sort backwards", |args: &[&Variant]| {
497+
let res = args[0].to::<i32>() > args[1].to::<i32>();
498+
Ok(res.to_variant())
499+
})
500+
}
501+
500502
#[itest]
501503
fn array_shrink() {
502504
let mut a = array![1, 5, 4, 3, 8];

itest/rust/src/engine_tests/node_test.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ fn node_scene_tree() {
6767

6868
let mut parent = Node::new_alloc();
6969
parent.set_name("parent".into());
70-
parent.add_child(child.clone());
70+
parent.add_child(&child);
7171

7272
let mut scene = PackedScene::new_gd();
73-
let err = scene.pack(parent.clone());
73+
let err = scene.pack(&parent);
7474
assert_eq!(err, global::Error::OK);
7575

7676
let mut tree = SceneTree::new_alloc();

itest/rust/src/object_tests/object_arg_test.rs

+20-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
*/
77

88
use godot::builtin::Variant;
9-
use godot::classes::{ClassDb, Node};
9+
use godot::classes::{ClassDb, Node, ResourceFormatLoader, ResourceLoader};
1010
use godot::global;
11-
use godot::obj::{Gd, NewAlloc};
11+
use godot::obj::{Gd, NewAlloc, NewGd};
1212

1313
use crate::framework::itest;
1414
use crate::object_tests::object_test::{user_refc_instance, RefcPayload};
@@ -78,6 +78,24 @@ fn object_arg_null_arg() {
7878
assert_eq!(error, global::Error::ERR_UNAVAILABLE);
7979
}
8080

81+
// Regression test for https://github.com/godot-rust/gdext/issues/835.
82+
#[itest]
83+
fn object_arg_owned_default_params() {
84+
// Calls the _ex() variant behind the scenes.
85+
let a = ResourceFormatLoader::new_gd();
86+
let b = ResourceFormatLoader::new_gd();
87+
88+
// Use direct and explicit _ex() call syntax.
89+
ResourceLoader::singleton().add_resource_format_loader(a.clone()); // by value
90+
ResourceLoader::singleton()
91+
.add_resource_format_loader_ex(b.clone()) // by value
92+
.done();
93+
94+
// Clean up (no leaks).
95+
ResourceLoader::singleton().remove_resource_format_loader(a);
96+
ResourceLoader::singleton().remove_resource_format_loader(b);
97+
}
98+
8199
// ----------------------------------------------------------------------------------------------------------------------------------------------
82100
// Helpers
83101

0 commit comments

Comments
 (0)