X Tutup
Skip to content

Android: Fix memory issues in _variant_to_jvalue()#113159

Merged
akien-mga merged 1 commit intogodotengine:masterfrom
dsnopek:android-variant-to-jvalue-memory-issues
Nov 27, 2025
Merged

Android: Fix memory issues in _variant_to_jvalue()#113159
akien-mga merged 1 commit intogodotengine:masterfrom
dsnopek:android-variant-to-jvalue-memory-issues

Conversation

@dsnopek
Copy link
Contributor

@dsnopek dsnopek commented Nov 25, 2025

There's some issues in _variant_to_jvalue() which mostly stem from jvalue being a union (so, all the fields are stored on an overlapping chunk of memory) and the code seems to be treating it sort of like it's a struct (or at least like it's 32-bits rather than 64-bits?).

For example, the jvalue value isn't initialized to anything, so it starts out with junk memory, but the default case is to set value.i = 0, which will only set the lower 32-bits and leave the upper 32-bits with junk in them:

default: {
value.i = 0;
} break;

Also, the function may set other fields like value.z, value.j or value.f (when force_object is false), but at the end, it will overwrite anything that's there by setting value.l:

value.l = env->PopLocalFrame(value.l);

This PR fixes all that by:

  • Initializing the jvalue value to zero (which also means we don't need a default case or some of the else branches, we can just leave it as-is)
  • Keeping track of if the value is an object or not, and only if it is will it set value.l at the end

Some aspects of the bugs addressed here are a regression caused by #107075 (which I discovered after rebasing #111732), but some are pre-existing problems.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should those be initialized as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In these cases, not initializing won't cause any problems, because NewObjectA() should only access the part of the jvalue that is actually needed for that particular class.

We could initialize it just in case we change it later or copy-paste this code to different context where that wouldn't be true? But, personally, I guess I'd lean towards leaving it be

Copy link
Contributor

@m4gr3d m4gr3d 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! Thanks for the fix!

Can you think of ways we could catch this with the instrumented tests?

@dsnopek dsnopek force-pushed the android-variant-to-jvalue-memory-issues branch from b04aad5 to ff070ae Compare November 26, 2025 14:56
@dsnopek
Copy link
Contributor Author

dsnopek commented Nov 26, 2025

Can you think of ways we could catch this with the instrumented tests?

For the case where force_object is true, we could create a Callable on the Godot side which returns null, pass that to an Android plugin, and then call it from there. Before my PR, that should crash 99% of time (or, in like 1% of cases lead to weird behavior or even work fine - that's the fun of memory corruption bugs ;-))

For the force_object is false case, we could create a Callable on the Godot side which returns Dictionary including values that are bool, int or float, pass that to an Android plugin which will call it and compare against the expected values. In this case, I think it won't crash before my PR, but the Dictionary values would be random and not necessarily match what was returned from the Callable

UPDATE: Actually, no, it looks like dictionaries will take the force_object is true path as well. I'm actually not seeing anything that end up creating plain bools, ints or floats, rather than the object versions? Should we just remove the force_object parameter and always create the object versions?

I haven't had a chance to try the instrumented tests yet! I'll take a look

@dsnopek dsnopek force-pushed the android-variant-to-jvalue-memory-issues branch from ff070ae to c2f8d1a Compare November 26, 2025 17:18
@dsnopek
Copy link
Contributor Author

dsnopek commented Nov 26, 2025

I've just push a new version of this which makes much more drastic changes:

  • It replaces _variant_to_jvalue() with _variant_to_jobject() since I couldn't find anywhere the force_object is false path was ever even used. So, now it always makes jobjects, and we don't need the fix for the path where it doesn't
  • It adds a test to the instrumented tests that can theoretically trigger the memory corruption issue with callables that return null, but in my testing I couldn't get it to happen without really messing with the C++ side. For whatever reason, the top 32-bits of the jvalue (from before the PR when it was a jvalue) would always be all zeros me, even though they aren't guaranteed to be - that's just what happened to be on the stack when running the instrumented tests. But I was able to confirm that it would catch it if it happened by temporarily engineering the right conditions :-)

However, I still have the more minimal version of this PR saved in a branch, if we want to go back to that

Copy link
Contributor

@m4gr3d m4gr3d left a comment

Choose a reason for hiding this comment

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

The fix looks good!

@m4gr3d
Copy link
Contributor

m4gr3d commented Nov 27, 2025

I've just push a new version of this which makes much more drastic changes:

  • It replaces _variant_to_jvalue() with _variant_to_jobject() since I couldn't find anywhere the force_object is false path was ever even used. So, now it always makes jobjects, and we don't need the fix for the path where it doesn't
  • It adds a test to the instrumented tests that can theoretically trigger the memory corruption issue with callables that return null, but in my testing I couldn't get it to happen without really messing with the C++ side. For whatever reason, the top 32-bits of the jvalue (from before the PR when it was a jvalue) would always be all zeros me, even though they aren't gaurnteed to be - that's just what happened to be on the stack when running the instrumented tests. But I was able to confirm that it would catch it if it happened by temporarily engineering the right conditions :-)

The changes look good and make sense. I tried to think of a scenario where the current version with force_jobject==false would be useful but can't think of any:

  • either we know we're returning a primitive value and can just retrieve that value directly from a Variant and return the jni version of it
  • or we don't know the type of the Variant we're handling, and thus can only return a jobject

However, I still have the more minimal version of this PR saved in a branch, if we want to go back to that

We will still need the minimal version to cherry-pick against 4.5

Edit: nevermind, my changes were merged in 4.6 so there's nothing to fix in 4.5.

@akien-mga akien-mga merged commit b22d153 into godotengine:master Nov 27, 2025
20 checks passed
@akien-mga
Copy link
Member

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Unassessed

Development

Successfully merging this pull request may close these issues.

3 participants

X Tutup