[2.1.14] Crash: signal handler aborts if interrupted mid-malloc
Posted: Sat Aug 22, 2026 4:40 am
What happened
Sending SIGTERM to a running Factorio process sometimes aborts it instead of shutting it down cleanly. macOS reports:
What I expected
The process logs "Received SIGTERM, shutting down" and exits cleanly, which is what happens most of the time.
Steps to reproduce
This is a race, so it takes a few attempts -- roughly one run in five for me. The mod below just keeps the Lua allocator busy so the window is wide.
Install the attached factorio-sigterm-repro mod (base game only; its entire content is an on_tick handler that allocates a few thousand short-lived tables).
Create a save and run a benchmark long enough to signal it mid-run:
Note the abort is NOT visible on stdout and the shell sees exit status 1, so watch the crash reporter rather than the process's output.
How I hit it by accident
Piping a running Factorio into a command that exits early:
factorio --benchmark save.zip --benchmark-ticks 2000 | head -1
head exits, the pipe closes, and the process is signalled while running.
Sending SIGTERM to a running Factorio process sometimes aborts it instead of shutting it down cleanly. macOS reports:
Code: Select all
Application Specific Information:
BUG IN CLIENT OF LIBPLATFORM: Trying to recursively lock an os_unfair_lock
Abort Cause 259
Thread 0 Crashed:: Dispatch queue: com.apple.main-thread
0 libsystem_platform.dylib 0x188ee3644 _os_unfair_lock_recursive_abort + 36
1 libsystem_platform.dylib 0x188edd43c _os_unfair_lock_lock_slow + 296
2 libsystem_malloc.dylib 0x188ccad48 tiny_malloc_should_clear + 128
3 libsystem_malloc.dylib 0x188cc9c2c szone_malloc_should_clear + 84
4 factorio 0x102d15064 operator new(unsigned long) + 52
5 factorio 0x102c6601c void* std::__1::__libcpp_operator_new[abi:v160006]<unsigned long>(unsigned long) + 24
6 factorio 0x102c65fa4 std::__1::__libcpp_allocate[abi:v160006](unsigned long, unsigned long) + 72
7 factorio 0x102c65f1c std::__1::allocator<char>::allocate[abi:v160006](unsigned long) + 72
8 factorio 0x102c5bf6c std::__1::__allocation_result<std::__1::allocator_traits<std::__1::allocator<char>>::pointer> std::__1::__allocate_at_least[abi:v160006]<std::__1::allocator<char>>(std::__1::allocator<char>&, unsigned long) + 32
9 factorio 0x102c5b9ac std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__grow_by_and_replace(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, char const*) + 252
10 factorio 0x102c5d6a8 std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>>::__assign_external(char const*, unsigned long) + 176
11 factorio 0x10559ac2c assign + 24 [inlined]
12 factorio 0x10559ac2c operator= + 24 (string:1032) [inlined]
13 factorio 0x10559ac2c vssprintf(char const*, char*) + 248 (StringUtil.cpp:79)
14 factorio 0x102d1c4a8 Logging::log(char const*, unsigned int, LogLevel, char const*, ...) + 56 (Logging.cpp:23)
15 factorio 0x102f6faac MainLoop::handleTerminationSignal(int) + 200 (MainLoop.cpp:132)
16 libsystem_platform.dylib 0x188edf744 _sigtramp + 56
...The process logs "Received SIGTERM, shutting down" and exits cleanly, which is what happens most of the time.
Steps to reproduce
This is a race, so it takes a few attempts -- roughly one run in five for me. The mod below just keeps the Lua allocator busy so the window is wide.
Install the attached factorio-sigterm-repro mod (base game only; its entire content is an on_tick handler that allocates a few thousand short-lived tables).
Create a save and run a benchmark long enough to signal it mid-run:
Code: Select all
factorio --create /tmp/repro.zip
factorio --benchmark /tmp/repro.zip --benchmark-ticks 100000 &
sleep 3
for i in $(seq 200); do kill -TERM $!; doneNote the abort is NOT visible on stdout and the shell sees exit status 1, so watch the crash reporter rather than the process's output.
How I hit it by accident
Piping a running Factorio into a command that exits early:
factorio --benchmark save.zip --benchmark-ticks 2000 | head -1
head exits, the pipe closes, and the process is signalled while running.