Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GDScript: Set has_type false if it is BUILTIN but Variant::NIL #88020

Merged

Conversation

emre0altan
Copy link
Contributor

This change fixes #87932 by preventing edge case for null value when used with a match. Even though edge case is happening with third check in IS_BUILTIN_TYPE(), m_var.type.builtin_type == m_type, there is no other default type other than Variant::NIL, so this check wouldn't change. But returning false with has_type in the first check for Variant::NIL makes more sense since it is also the default value in GDScriptDataType.

#define IS_BUILTIN_TYPE(m_var, m_type) \
(m_var.type.has_type && m_var.type.kind == GDScriptDataType::BUILTIN && m_var.type.builtin_type == m_type)
void GDScriptByteCodeGenerator::write_type_adjust(const Address &p_target, Variant::Type p_new_type) {

class GDScriptDataType {
public:
Vector<GDScriptDataType> container_element_types;
enum Kind {
UNINITIALIZED,
BUILTIN,
NATIVE,
SCRIPT,
GDSCRIPT,
};
Kind kind = UNINITIALIZED;
bool has_type = false;
Variant::Type builtin_type = Variant::NIL;
StringName native_type;
Script *script_type = nullptr;
Ref<Script> script_type_ref;

@emre0altan emre0altan requested a review from a team as a code owner February 6, 2024 13:40
@AThousandShips AThousandShips added bug topic:gdscript crash cherrypick:4.2 Considered for cherry-picking into a future 4.2.x release labels Feb 6, 2024
@AThousandShips AThousandShips added this to the 4.3 milestone Feb 6, 2024
@dalexeev
Copy link
Member

dalexeev commented Feb 6, 2024

This makes sense since BUILTIN types cannot accept null value and there is no Nil type in GDScript. But in my opinion, this should be fixed first in the analyzer (GDScriptAnalyzer::type_from_variant()). In the compiler we can add a check in case of a bug in the analyzer, like here:

https://github.com/godotengine/godot/blob/2e684ef4f7e706ac3990e7431db84e30a4d81f3e/modules/gdscript/gdscript_compiler.cpp#L121-L126

Plus I think it's worth adding a test.

@emre0altan
Copy link
Contributor Author

I have looked what is happening in GDScriptAnalyzer::type_from_variant() and can't really say anything that needs to be changed there. Btw, also I am fairly new to the codebase. So, It returns BUILTIN data type which is ANNOTATED_EXPLICIT and doesn't have has_type property yet, it will be added when converted to GDScriptDataType. There is no other type other than Variant::NIL that can represent null and it also stated as a built in type in here: https://docs.godotengine.org/en/stable/tutorials/scripting/gdscript/gdscript_basics.html#null

I think the main problem is with IS_BUILTIN_TYPE() macro returning false except if the argument is Variant::NIL.

@dalexeev
Copy link
Member

dalexeev commented Feb 6, 2024

There is no other type other than Variant::NIL that can represent null

Sorry, I was thinking that we could use Variant::OBJECT, but I forgot that it is also not suitable for BUILTIN, and NATIVE/SCRIPT/CLASS is ambiguous for null, it can cause unexpected consequences. So I guess we can leave it as is, just add a comment and test.

Or, if look at it differently, GDScript has an implicit type Nil, it's just that the user can't specify it. Perhaps the bug can be fixed differently, in GDScriptByteCodeGenerator.

@emre0altan
Copy link
Contributor Author

I think the one you referring is this one https://docs.godotengine.org/en/3.1/classes/class_nil.html and probably it's the default version of Variant with default NIL type. My gut feeling says that only difference from the other types is just null is more commonly used than nil and being used as null. But problem arises when NIL is treated the same way with the other types and also being used as the default type like in Variant::get_utility_function_argument_type(). Later if you used it to check something, then there will be an edge case for NIL.

Variant::Type Variant::get_utility_function_argument_type(const StringName &p_name, int p_arg) {
const VariantUtilityFunctionInfo *bfi = utility_function_table.lookup_ptr(p_name);
if (!bfi) {
return Variant::NIL;
}
return bfi->get_arg_type(p_arg);
}

@emre0altan emre0altan force-pushed the match-null-crashes-GDScript-compiler branch from 2e684ef to b0f8c06 Compare February 6, 2024 18:31
@emre0altan emre0altan force-pushed the match-null-crashes-GDScript-compiler branch 2 times, most recently from 027d39a to 47d0b50 Compare February 6, 2024 19:08
Copy link
Member

@dalexeev dalexeev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer to solve it here:

if (!IS_BUILTIN_TYPE(p_arguments[i], Variant::get_utility_function_argument_type(p_function, i))) {

-			if (!IS_BUILTIN_TYPE(p_arguments[i], Variant::get_utility_function_argument_type(p_function, i))) {
+			const Variant::Type arg_type = Variant::get_utility_function_argument_type(p_function, i);
+			// `Variant::get_utility_function_argument_type()` returns `Variant::NIL` for `Variant` arguments.
+			if (arg_type == Variant::NIL || !IS_BUILTIN_TYPE(p_arguments[i], arg_type)) {

We have some problem distinguishing between Nil and Variant (there is PROPERTY_USAGE_NIL_IS_VARIANT). In some cases, Variant::NIL is used as the default value, in others Variant::VARIANT_MAX.

@dalexeev dalexeev requested a review from a team February 7, 2024 08:31
@emre0altan emre0altan force-pushed the match-null-crashes-GDScript-compiler branch from 47d0b50 to 8858b41 Compare February 7, 2024 10:20
@akien-mga akien-mga changed the title GDScript: set has_type false if it is BUILTIN but Variant::NIL GDScript: Set has_type false if it is BUILTIN but Variant::NIL Feb 7, 2024
Copy link
Member

@dalexeev dalexeev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me, but I'd appreciate a double check. We fixed the crash for now, but I'm not sure there aren't similar cases elsewhere. Perhaps we should change the IS_BUILTIN_TYPE macro or use Variant::VARIANT_MAX as the default value instead of Variant::NIL.

@dalexeev dalexeev requested a review from vnen February 8, 2024 13:24
@emre0altan
Copy link
Contributor Author

Thanks, I'll check it again. I agree, it could be better to add it to IS_BUILTIN_TYPE and I was mistaken about where the default value comes from. It returns NIL but it didn't change when I changed the return value to VARIANT_MAX here:

Variant::Type Variant::get_utility_function_argument_type(const StringName &p_name, int p_arg) {
const VariantUtilityFunctionInfo *bfi = utility_function_table.lookup_ptr(p_name);
if (!bfi) {
return Variant::NIL;
}
return bfi->get_arg_type(p_arg);
}

I tried to trace the registered function but it got complicated quickly. I'll have a second look there and try to find out if it's better to return VARIANT_MAX.

@emre0altan emre0altan force-pushed the match-null-crashes-GDScript-compiler branch from 8858b41 to 081fa32 Compare February 11, 2024 13:47
@emre0altan
Copy link
Contributor Author

emre0altan commented Feb 11, 2024

I think that if a method has a Variant parameter, this parameter returns Variant::NIL from get_arg_type() and that is where it is used as a default value. But it is not only for typeof method, it is like this for any method with a Variant parameter. So I don't think we can change the default value but I think it is better to fix this issue by changing IS_BUILTIN_TYPE macro.

https://github.com/godotengine/godot/blob/master/doc/classes/%40GlobalScope.xml#L1409-L1425

@emre0altan emre0altan closed this Feb 13, 2024
@emre0altan emre0altan reopened this Feb 13, 2024
@vnen
Copy link
Member

vnen commented Feb 23, 2024

There's an extra problem here that we cannot properly validate the call if the argument is of type Variant, because the function itself might restrict which types are actually acceptable and there's no API to know that. In the case of typeof, which is what is being triggered here, it actually accepts any type, so it would be fine to do a validated call.

The issue is that the crash is not even because it's trying to do a validated call. It crashes only because of this:

CallTarget ct = get_call_target(p_target, result_type);
Variant::Type temp_type = temporaries[ct.target.address].type;

The target might not be always a temporary, so trying to access the temporaries' table might be bogus, which is exactly what happens with this crash. The code should check if it's actually a temporary or not before trying to get its type.

I'm approving this because it "solves" the first point (as it won't emit validated calls when the argument is Variant), but the other point should be checked as well.

@akien-mga akien-mga merged commit 7766628 into godotengine:master Feb 23, 2024
32 checks passed
@akien-mga
Copy link
Member

Thanks! And congrats for your first merged Godot contribution 🎉

@emre0altan
Copy link
Contributor Author

Thank you🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug cherrypick:4.2 Considered for cherry-picking into a future 4.2.x release crash topic:gdscript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

match null crashes GDScript compiler
5 participants