Skip to content

Textures module can handle textures in unsafe ways #6045

Open

Description

@ab9rf

This came out of analyzing a crash report provided by a user using the "Detailed Weather" mod published on the Steam workshop. This mod dynamically creates a large number of surfaces which it cycles through DFHack's mechanics to push textures into DF's texture system. This runs at very high velocity and exposes a couple of bugs in DFHack's texture handlers:

  • The g_handle_to_surface cache map can, under some circumstances, hold dangled SDL_Surface* - specifically, the cache map can still hold references to surfaces that will be deleted DF calls delete_all_post_init_textures and so a handle that remains in this map across one of DF's texture clears (which occur during world load) will be stale.
  • while (surface->recount) DFSDL_FreeSurface(surface) has an (obvious) UAF: FreeSurface deletes surface if its refcount goes to 0, and so when the refcount goes to user surface no longer points to a live object, and so the next (and final) iteration of the loop reads via surface which is now a dangled pointer. Unlikely to actually malfunction, but still incorrect.

The first bug is the one that caused the crash reported in Discord: the user loaded a game while Detailed Weather was active, unloaded that game, then loaded another one. Detailed Weather uses amortized unloading of disposed dynamic textures to avoid frame lags, but doesn't dump its queue when the world it's running in is unloaded, so if a second world is loaded in the same session, it'll resume trying to remove old dynamic textures from the previously loaded world, which DF has already dumped, resulting in a UAF. The fact that Detailed Weather doesn't handle its deferred deletion queue correctly is a bug in Detailed Weather, but their defect shouldn't cause DFHack to crash.

@myk002 you were the architect of this solution, so any insights you might have as to how to best fix it would be welcome

Activity

  1. ab9rf commented on Nov 14, 2026

    @ab9rf
    MemberAuthor

    dfhack lua crashlog for this issue:

    ===== DFHack crash report =====
    DFHack 53.17-r1, DF 53.17
    Exception 0xc0000005 at 00007FFFC65D3609 (module: ?), thread 31152
    access violation reading address 00000000000000E9
    lua53.dll on native stack: yes
    thread-local active lua_State: 000001E754C9C3F8
    lua_State 000001E754C9C3F8 traceback:
    crash while Lua was executing
    stack traceback:
        [C]: in field 'deleteHandle'
        ...)/scripts_modactive/internal/detailed_weather/colour.lua:660: in upvalue 'empty_trash'
        ...)/scripts_modactive/internal/detailed_weather/colour.lua:1417: in local 'fn'
        ...iled_weather (14)/scripts_modactive/detailed_weather.lua:539: in method 'timed'
        ...iled_weather (14)/scripts_modactive/detailed_weather.lua:555: in method 'draw_weather'
        ...iled_weather (14)/scripts_modactive/detailed_weather.lua:526: in method 'onRenderFrame'
        F:\SteamLibrary\steamapps\common\DFHack\hack\lua\gui.lua:813: in method 'render'
        ...ary\steamapps\common\DFHack\hack\lua\plugins\overlay.lua:571: in local 'fn'
        ...ary\steamapps\common\DFHack\hack\lua\plugins\overlay.lua:435: in upvalue 'detect_frame_change'
        ...ary\steamapps\common\DFHack\hack\lua\plugins\overlay.lua:570: in upvalue '_render_viewscreen_widgets'
        ...ary\steamapps\common\DFHack\hack\lua\plugins\overlay.lua:582: in function 'plugins.overlay.render_viewscreen_widgets'
    ===== end DFHack crash report =====
    

    (Yes, the crashlogger added in #6000 works, at least it did here)

  2. ab9rf commented on Nov 14, 2026

    @ab9rf
    MemberAuthor

    df crashlog for this issue:

    Fortress mode Version 53.17
    0> Dwarf_Fortress+0xAEFC36
    1> Dwarf_Fortress+0xAEFDD9
    2> KERNELBASE!UnhandledExceptionFilter+0x1F3
    3> ntdll!strncpy+0x1F20
    4> ntdll!_C_specific_handler+0x93
    5> ntdll!_chkstk+0x9F
    6> ntdll!RtlWow64GetCurrentMachine+0x655
    7> ntdll!KiUserExceptionDispatcher+0x2E
    8> SDL2+0x3609
    9> SDL2!SDL_DYNAPI_entry+0xBD181
    10> SDL2!SDL_DYNAPI_entry+0xC5C59
    11> dfhack!DFHack::Textures::deleteHandle+0x31F
    12> dfhack!df::enabler::zoom_display+0xDF92
    13> lua53!lua_setlocal+0xE9A
    14> lua53!luaopen_utf8+0x2810
    15> lua53!lua_setlocal+0x72B
    16> lua53!lua_setlocal+0x11B6
    17> lua53!lua_setlocal+0x9B0
    18> lua53!lua_pcallk+0x10C
    19> dfhack!DFHack::Lua::SafeCall+0xF8
    20> dfhack!DFHack::Lua::CallLuaModuleFunction+0xD1
    21> overlay_plug+0x1D77B
    22> overlay_plug+0x1C475
    23> Dwarf_Fortress+0xAF4F47
    24> Dwarf_Fortress+0x8FF535
    25> Dwarf_Fortress+0x9009E9
    26> SDL2!SDL_DYNAPI_entry+0x799D7
    27> SDL2!SDL_DYNAPI_entry+0x12EEDE
    28> ucrtbase!wcsrchr+0x150
    29> KERNEL32!BaseThreadInitThunk+0x17
    30> ntdll!RtlUserThreadStart+0x2C
    

    the crash is fairly clearly caused by trying to delete a SDL surface that has already been deleted

  3. myk002 commented on Nov 15, 2026

    @myk002
    Member

    I'll take a look

  4. myk002 commented on Nov 15, 2026

    @myk002
    Member

    Just notes for myself from my memory: I thought we increased the surface refcount so df's call to free wouldn't actually deallocate the surface

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions