It looks like loading/unloading happen in different tasks. Something like
kbd_task in fselect triggers textread load
spytask module_tick_unloader unloads fselect
_module_load has a static 'lock' variable, but aside from probably not being strictly safe, it isn't used in unload at all.
I tried protecting _module_load and module_unload_idx with a semaphore (quick and dirty patch attached), and wasn't able to trigger the crash in >20 tries, where it usually happens within ~4. However, I haven't traced this carefully enough be sure this could cause the problem. It's also possible that there could be deadlocks: Modules trying to load/unload other modules in from their load/unload callbacks.
edit:
I tried camera logging the start/end of _module_load and module_unload idx. Crash case:
00016300: SS:_module_load start txtread.flt
00016300: SS:module_unload_idx start 1
00016300: SS:MOD 16300,UN, ,fselect.flt
00016300: SS:module_unload_idx end 1
00016300: SS:MOD 16300,LD,003a9fdc,txtread.flt
00016300: SS:_module_load txtread.flt 2
If the order in the camera log is trusted, the fselect unload does happen between the start and end txtread load. However, they don't end up using the same index, which was my initial guess at how they could stomp on each other. (edit: numbers at the end are the indexes of the modules)
However libtxtread->read_file(selected_file) in gui_fselect presumably doesn't return until the filereader is completely loaded... after fselect is freed. If that memory were re-allocated in between, it would be Very Bad
It seems like, at a minimum, the running=0 currently done by exit_fselect should only be done at the very end of gui_fselect_kbd_process, and certainly after any additional modules are loaded. The rest of exit_fselect probably needs to happen before the new module start.