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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 28 additions & 15 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 8 additions & 4 deletions
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

Lines changed: 2 additions & 1 deletion
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 49 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

0 commit comments

Comments
 (0)