godot icon indicating copy to clipboard operation
godot copied to clipboard

Fix call with wrong arguments in Callable.callv() when passing a readonly (const) Array

Open Nolkaloid opened this issue 1 year ago • 13 comments

Use a duplicate of the passed array when it is read-only.

Prior to this fix, when the array was marked as read-only, the temporary address of p_arguments->read_only was used for all parameters. Consequently, this caused all parameters to reference the last element of the array.

Fixes #93600.

Nolkaloid avatar Jun 26 '24 14:06 Nolkaloid

This would really benefit from having a test added under modules/gdscript/tests/scripts/runtime/features to make sure it actually fixes the bug and makes sure it remains fixed

AThousandShips avatar Jun 26 '24 15:06 AThousandShips

As this is not GDScript specific, shouldn't it be a unit test in tests/core/variant/test_callable.h ?

Nolkaloid avatar Jun 26 '24 15:06 Nolkaloid

We do many of these tests there

~Also how did you confirm this was not script specific? I at least suspect it's specific to arguments in script methods being called, due to how the bindings and data passing works~

My bad missed some details, but I think the script side tests works best to fix this

AThousandShips avatar Jun 26 '24 15:06 AThousandShips

Given some of the details here I think I have a potential alternative solution that might be better, I'll look at it along with my other changes in:

  • #88905

But if you want to try it out I'll drop the alternative here, it probably would be faster and easier avoiding a full duplication with:

	Variant *args = nullptr;
	const Variant **argptrs = nullptr;

	if (argcount) {
		argptrs = (const Variant **)alloca(sizeof(Variant *) * argcount);

		// Copy the arguments if read-only, otherwise the address of p_arguments->read_only will be copied.
		if (p_arguments.is_read_only()) {
			args = (Variant *)alloca(sizeof(Variant) * argcount);
			for (int i = 0; i < argcount; i++) {
				memnew_placement(&args[i], Variant(p_arguments[i]));
				argptrs[i] = &args[i];
			}
		} else {
			for (int i = 0; i < argcount; i++) {
				argptrs[i] = &p_arguments[i];
			}
		}
	}

	CallError ce;
	Variant ret;
	callp(argptrs, argcount, ret, ce);

	if (args) {
		for (int i = 0; i < argcount; i++) {
			args[i].~Variant();
		}
	}

	return ret;

Edit: Tested and it works well, a bit more code but avoids unnecessary duplication

AThousandShips avatar Jun 26 '24 15:06 AThousandShips

Having a second buffer allocated for a copy of the elements is what I have done initially, but I chose to use duplicate instead in the end, because I found it more readable.

I'm not sure that the call to duplicate has that much overhead since the underlying Vector uses CoW if I recall correctly. But I might be wrong.

Nolkaloid avatar Jun 26 '24 16:06 Nolkaloid

I'm not sure that the call to duplicate has that much overhead since the underlying Vector uses CoW if I recall correctly. But I might be wrong.

I'm not sure, that's a good point and it might not matter, but i don't think the code is significantly less readable and it avoids potential weirdness

AThousandShips avatar Jun 26 '24 16:06 AThousandShips

I did a quick check, and indeed the duplication is done using CoW. If it were up to me I'd keep the duplicate as it's simpler IMO, but I don't have strong opinions about it. You tell me. I'll add the test in modules/gdscript/tests/scripts/runtime/features in the meantime.

Nolkaloid avatar Jun 26 '24 22:06 Nolkaloid

You decide, both are fine by me, and let's see what the core team says

AThousandShips avatar Jun 27 '24 08:06 AThousandShips

A few thoughts:

  • CoW involves dealing with the atomic refcount, with performance implications.
  • This looks like an ad hoc fix for an issue due to how read-only arrays are implemented. It may be better to assess solutions to the root issue.

RandomShaper avatar Jun 27 '24 14:06 RandomShaper

I think it might be difficult to handle the issue with read access on the array itself, given the nature of the reference, but I'd say those points reinforce the use of my suggestion above, it avoids the COW issues, and avoids any issues with the data access

I can't think of a reliable way to handle the read only status of array without breaking compatibility, or allocating more data somehow, but regardless I think this situation warrants at least a temporary local solution

AThousandShips avatar Jun 27 '24 14:06 AThousandShips

This also needs to be done for Object::callv

AThousandShips avatar Jun 27 '24 14:06 AThousandShips

Indeed this solution is not perfect, and we should consider modifying how read-only arrays are implemented. I will try to think of a nice solution when I have more time.

Nolkaloid avatar Jun 28 '24 14:06 Nolkaloid

Made an issue tracking the broader issues of the read-only state:

  • https://github.com/godotengine/godot/issues/93702

AThousandShips avatar Jun 28 '24 15:06 AThousandShips

Agreed, I removed the changes.

Nolkaloid avatar Apr 10 '25 18:04 Nolkaloid

Thanks!

Repiteo avatar Apr 10 '25 21:04 Repiteo