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

Fixes crash in ClassDB::deinitialize due to usage of invalid iterator. #1260

Merged
merged 1 commit into from
Oct 5, 2023

Conversation

Klaim
Copy link
Contributor

@Klaim Klaim commented Oct 4, 2023

I took a stab at trying the hot-reloading system (in master) but when using my experimental/prototyping GDExtension (which exposes 2 Node types) but hit a bug:
When opening Godot (4.2-dev6) which loaded my extension (built with cd61a9b) my extension appears to run perfectly well. Closing Godot at any time will trigger an error due to invalid access to std::vector internals. Exact same issue when attempting a hot-reloading, at the moment we get the focus after changing the extension binary.
Note that this does not happen in the example provided in this project, but removing any active code or even usage in the project of my GDExtension , except the node classes registrations (2 types) systematically reproduces the issue for now (it's UB so it's random, see below).

As my extension and godot-cpp were built in debug, I quickly spotted that the issue was in the ClassDB::deinitialize function, after the first iteration.

Reading the code I spotted that line:

class_register_order.erase((i + 1).base());

This call to std::vector::erase invalidates any iterator (including i) and the result can be random because it's Undefiend Behavior. We dont know if the vector will or not re-allocate it's internal array. So the whole loop is incorrect as long as the iterator is used after that erase.

There is usually two ways to solve this issue:
A) do like I did in this PR, that is, collect a way to identify the elements to remove, then remove them after the pass detecting what to remove;
B) std::vector::erase returns an iterator to the next valid iterator, use that.

Solution A):

This change works for me/my project when I use it in my copy of godot-cpp , but it needs eyes to be sure (because UB will not be easilly checked).
Also more expensive than B because we have to have a second container to grow. I chose a set for convenience but it's definitely not the best performance wise for this kind of scenario - if you have some custom container that would fit better feel free to suggest them to me. I'm thinking something like a flat_set or stack-first-set/vector or something like that.

Also note that I dont know why it was necessary to go in reverse, nor do I know why i + 1 was used here. Feel free to inform me on this, I failed to find how/where to chat with the authors.

Solution B):

I attempted to use the iterator from erase but it's a normal iterator and originally we were going with reverse iterators. I need to check how to obtain a reverse iterator from an iterator. I suspect this solution to be less expensive in both time and speed, just didnt manage to make it work yet.

@Klaim Klaim requested a review from a team as a code owner October 4, 2023 22:49
src/core/class_db.cpp Outdated Show resolved Hide resolved
src/core/class_db.cpp Outdated Show resolved Hide resolved
src/core/class_db.cpp Outdated Show resolved Hide resolved
Copy link
Collaborator

@dsnopek dsnopek left a comment

Choose a reason for hiding this comment

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

Thanks!

src/core/class_db.cpp Outdated Show resolved Hide resolved
src/core/class_db.cpp Outdated Show resolved Hide resolved
@dsnopek
Copy link
Collaborator

dsnopek commented Oct 5, 2023

The code is looking good to me now! However, this'll need to be squashed into a single commit before it can be merged, and we need to wait to see what the CI says.

@Klaim
Copy link
Contributor Author

Klaim commented Oct 5, 2023

No problem, I was not sure if you want to do it automatically or I should do it. Incoming.

After the removed call to `std::vector::erase` all iterators,
`i` included, are invalidated and therefore this code has undefined
behavior (which can or not lead to a crash).
This change delays the removal of class names from
`class_register_order` to after having gone through it's content,
removing the undefined behavior.
@dsnopek dsnopek merged commit ef2f63a into godotengine:master Oct 5, 2023
12 checks passed
@dsnopek
Copy link
Collaborator

dsnopek commented Oct 5, 2023

Thanks! And congrats on your first merged PR! 🎉 🚀

@dsnopek dsnopek added the bug This has been identified as a bug label Oct 5, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This has been identified as a bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants