Skip to content

Commit 4809391

Browse files
authored
fix(Lua): Use-after-free when UnregisterHook is called in a RegisterHook callback (#1010)
The fix was to delay the removal of the hook until after the callback data is done being used. For script hooks, we no longer delete the registry index from the callback data vector, instead we just mark it unregistered and skip it when iterating.
1 parent 49eda85 commit 4809391

File tree

2 files changed

+60
-38
lines changed

2 files changed

+60
-38
lines changed

UE4SS/include/Mod/LuaMod.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ namespace RC
8787
const LuaMadeSimple::Lua* lua;
8888
Unreal::UClass* instance_of_class;
8989
std::vector<std::pair<const LuaMadeSimple::Lua*, RegistryIndex>> registry_indexes;
90+
bool scheduled_for_removal{};
9091
};
9192
struct LuaCancellableCallbackData
9293
{

UE4SS/src/Mod/LuaMod.cpp

Lines changed: 59 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,19 @@ namespace RC
9696
return lua.get_bool(-1);
9797
}
9898

99+
static auto get_function_name_without_prefix(const StringType& function_full_name) -> StringType
100+
{
101+
static constexpr StringViewType function_prefix{STR("Function ")};
102+
if (auto prefix_pos = function_full_name.find(function_prefix); prefix_pos != function_full_name.npos)
103+
{
104+
return function_full_name.substr(prefix_pos + function_prefix.size());
105+
}
106+
else
107+
{
108+
return function_full_name;
109+
}
110+
}
111+
99112
struct LuaUnrealScriptFunctionData
100113
{
101114
Unreal::CallbackId pre_callback_id;
@@ -110,6 +123,7 @@ namespace RC
110123
bool has_return_value{};
111124
// Will be non-nullptr if the UFunction has a return value
112125
Unreal::FProperty* return_property{};
126+
bool scheduled_for_removal{};
113127
};
114128
static std::vector<std::unique_ptr<LuaUnrealScriptFunctionData>> g_hooked_script_function_data{};
115129

@@ -215,6 +229,14 @@ namespace RC
215229
// The API will automatically call the original function
216230
// This function continues in 'lua_unreal_script_function_hook_post' which executes immediately after the original function gets called
217231

232+
if (lua_data.scheduled_for_removal)
233+
{
234+
auto native_hook_pre_id_it = LuaMod::m_generic_hook_id_to_native_hook_id.find(lua_data.pre_callback_id);
235+
auto function_name_no_prefix = get_function_name_without_prefix(lua_data.unreal_function->GetFullName());
236+
Output::send<LogLevel::Verbose>(STR("Unregistering native pre-hook ({}) for {}\n"), native_hook_pre_id_it->first, function_name_no_prefix);
237+
lua_data.unreal_function->UnregisterHook(native_hook_pre_id_it->second);
238+
}
239+
218240
// No longer promising to be in the game thread
219241
set_is_in_game_thread(lua_data.lua, false);
220242
}
@@ -371,6 +393,25 @@ namespace RC
371393
process_return_value();
372394
process_return_value();
373395

396+
if (lua_data.scheduled_for_removal)
397+
{
398+
auto native_hook_post_id_it = LuaMod::m_generic_hook_id_to_native_hook_id.find(lua_data.post_callback_id);
399+
auto function_name_no_prefix = get_function_name_without_prefix(lua_data.unreal_function->GetFullName());
400+
Output::send<LogLevel::Verbose>(STR("Unregistering native post-hook ({}) for {}\n"), native_hook_post_id_it->first, function_name_no_prefix);
401+
lua_data.unreal_function->UnregisterHook(native_hook_post_id_it->second);
402+
403+
auto mod = get_mod_ref(lua_data.lua);
404+
luaL_unref(lua_data.lua.get_lua_state(), LUA_REGISTRYINDEX, lua_data.lua_callback_ref);
405+
if (lua_data.lua_post_callback_ref != -1)
406+
{
407+
luaL_unref(lua_data.lua.get_lua_state(), LUA_REGISTRYINDEX, lua_data.lua_post_callback_ref);
408+
}
409+
luaL_unref(mod->lua().get_lua_state(), LUA_REGISTRYINDEX, lua_data.lua_thread_ref);
410+
std::erase_if(g_hooked_script_function_data, [&](const std::unique_ptr<LuaUnrealScriptFunctionData>& elem) {
411+
return elem.get() == &lua_data;
412+
});
413+
}
414+
374415
// No longer promising to be in the game thread
375416
set_is_in_game_thread(lua_data.lua, false);
376417
}
@@ -1137,19 +1178,6 @@ namespace RC
11371178
}
11381179
}
11391180

1140-
static auto get_function_name_without_prefix(const StringType& function_full_name) -> StringType
1141-
{
1142-
static constexpr StringViewType function_prefix{STR("Function ")};
1143-
if (auto prefix_pos = function_full_name.find(function_prefix); prefix_pos != function_full_name.npos)
1144-
{
1145-
return function_full_name.substr(prefix_pos + function_prefix.size());
1146-
}
1147-
else
1148-
{
1149-
return function_full_name;
1150-
}
1151-
}
1152-
11531181
auto static setup_lua_global_functions_internal(const LuaMadeSimple::Lua& lua, Mod::IsTrueMod is_true_mod) -> void
11541182
{
11551183
lua.register_function("print", LuaLibrary::global_print);
@@ -1717,38 +1745,25 @@ No overload found for function 'UnregisterHook'.
17171745
if (native_hook_pre_id_it != LuaMod::m_generic_hook_id_to_native_hook_id.end() &&
17181746
native_hook_post_id_it != LuaMod::m_generic_hook_id_to_native_hook_id.end())
17191747
{
1720-
Output::send<LogLevel::Verbose>(STR("Unregistering native pre-hook ({}) for {}\n"), native_hook_pre_id_it->first, function_name_no_prefix);
1721-
unreal_function->UnregisterHook(static_cast<int32_t>(native_hook_pre_id_it->second));
1722-
Output::send<LogLevel::Verbose>(STR("Unregistering native post-hook ({}) for {}\n"), native_hook_post_id_it->first, function_name_no_prefix);
1723-
unreal_function->UnregisterHook(static_cast<int32_t>(native_hook_post_id_it->second));
1724-
1725-
// LuaUnrealScriptFunctionData contains the hook's lua registry references, captured in RegisterHook in two different lua states.
1726-
for (auto hook_iter = g_hooked_script_function_data.begin(); hook_iter != g_hooked_script_function_data.end(); hook_iter++)
1727-
{
1728-
RC::LuaUnrealScriptFunctionData* hook_data = (*hook_iter).get();
1729-
if (hook_data->post_callback_id == post_id && hook_data->pre_callback_id == pre_id)
1730-
{
1731-
auto mod = get_mod_ref(lua);
1732-
luaL_unref(hook_data->lua.get_lua_state(), LUA_REGISTRYINDEX, hook_data->lua_callback_ref);
1733-
if (hook_data->lua_post_callback_ref != -1)
1734-
{
1735-
luaL_unref(hook_data->lua.get_lua_state(), LUA_REGISTRYINDEX, hook_data->lua_post_callback_ref);
1736-
}
1737-
luaL_unref(mod->lua().get_lua_state(), LUA_REGISTRYINDEX, hook_data->lua_thread_ref);
1738-
g_hooked_script_function_data.erase(hook_iter);
1739-
break;
1740-
}
1741-
}
1748+
const auto hook_data = std::ranges::find_if(g_hooked_script_function_data, [&](const std::unique_ptr<LuaUnrealScriptFunctionData>& elem) {
1749+
return elem->post_callback_id == post_id && elem->pre_callback_id == pre_id;
1750+
});
1751+
hook_data->get()->scheduled_for_removal = true;
17421752
}
17431753
else
17441754
{
17451755
if (auto data_ptr = LuaMod::find_function_hook_data(LuaMod::m_script_hook_callbacks, unreal_function); data_ptr)
17461756
{
17471757
Output::send<LogLevel::Verbose>(STR("Unregistering script hook with id: {}, FunctionName: {}\n"), post_id, function_name_no_prefix);
17481758
auto& registry_indexes = data_ptr->callback_data.registry_indexes;
1749-
std::erase_if(registry_indexes, [&](const auto& pair) -> bool {
1750-
return post_id == pair.second.identifier;
1751-
});
1759+
for (auto& registry_index : registry_indexes)
1760+
{
1761+
if (post_id == registry_index.second.identifier)
1762+
{
1763+
registry_index.second.lua_index = -1;
1764+
break;
1765+
}
1766+
}
17521767
}
17531768
}
17541769

@@ -4231,6 +4246,12 @@ No overload found for function 'FPackageName:IsValidLongPackageName'.
42314246
{
42324247
const auto& lua = *lua_ptr;
42334248

4249+
// -1 is a special value that signifies that the hook has been unregistered.
4250+
if (registry_index.lua_index == -1)
4251+
{
4252+
continue;
4253+
}
4254+
42344255
set_is_in_game_thread(lua, true);
42354256

42364257
lua.registry().get_function_ref(registry_index.lua_index);

0 commit comments

Comments
 (0)