Revamp solver memory management
Needs ReviewPublic

Authored by fabiank on Aug 24 2019, 4:29 PM.

Details

Reviewers
aacid
shlomif
Group Reviewers
KDE Games
Summary

Why:
As is evident from BUG 395624, we do have an issue with memory safety
in KPatience. However, debugging it is really hard, and it is hard to
find out the root cause.
To ease further debugging, simplifying and modernizing the memory
management seemed to be of utmost importance.

State before this patch:

  • The TREE datastructure was used to store the state after a specific move. However, you would not know that by looking at it, as it only had a depth, left and right member. The actual data was always stored directly after it (as an array of PileBytes bytes). TREE actual data ------------------=-------------------- |depth|left|right|=Pile1|Pile2|Pile3... ------------------=-------------------- ^ ^ | |----was accesed via p+(sizeof(TREE)) | pointer p to here gets returned
  • The memory manager used a bump allocator which was used for both the POSITION and TREE structs. The bumb allocator used a simple linked list of blocks, which contained an array of data. The contents of that array looked like --------------------------------------------------------------------- |TREE1|Tree1Data|Tree2|Tree2Data |POSITION|TREE3|TREE3DATA|... ---------------------------------------------------------------------
  • To decide how much memory needs to be allocated, the size of the required memory was passed to the MemoryManager at runtime.
  • (De)allocations were made with C functions, so neither ctors nor dtors were called.
  • The MemoryManager stored and accessed state in static variables. Those were partially set by the concrete Solver instance on initialization.

State after this patch:

  • The MemoryManager, the TREE and datastructures involving TREE take PileBytes as a template argument. TREE has been renamed to TTree (templated tree), mostly to make finding usages easier.
  • As the TTree now knows how much data it has to store, it has an actual data member. Furthermore, in debug builds there is a canary value afterwards, which can be used to check for out-of-bound writes. ------------------------------------------------ |depth|left|right|data [PileBytes large]|CANARY| ------------------------------------------------ {--------------always there------------}={in debug bulids}
  • As the information is passed via template parameters, the static variables used for communicating sizes could be removed.
  • The bump allocator has been splitted, in one for TTrees and one for POSITIONs. They still share the total allocation limit. Splitting the allocator also allowdes for a now even simple bump allocator. It uses a simple array, and just returns a pointer to array[fill] upon request. If the array runs out of space, a new block gets allocated, just as before.
  • The blocks of the bump allocator now use unique_ptr, instead of having to manually release the memory at the end.
  • No code outside of the core memory allocation has been altered.

Downsides:

  • There is a somewhat larger initial memory footprint, as we are allocating a block for both POSITIONs and TTreees. However, in actual usage that should not matter, as the solver normally needs quite a lot of both of them, and would allocate a new block anyway.
  • The new bump allocator contains an array, which means that for each member the constructor gets called (previously the data was just memzeroed). However, except for debug builds, the constructors of TTree and POSITION are trivial, so there shouldn't be any work done.
  • (De)Allocations are now done with new and delete to ensure that ctors and dtors run.
  • All those templates aren't great for readability.

    Testing:
    • Only manual testing to check whether the solver
    • The existing unit tests partially fail, but did so already before this change.
    • Application was run under sanitizers to check for new memory issues; none were found so far.

Diff Detail

Repository
R410 KPatience
Branch
fixmemory
Lint
No Linters Available
Unit
No Unit Test Coverage
Build Status
Buildable 15772
Build 15790: arc lint + arc unit
fabiank created this revision.Aug 24 2019, 4:29 PM
Restricted Application added a reviewer: KDE Games. · View Herald TranscriptAug 24 2019, 4:29 PM
Restricted Application added a subscriber: kde-games-devel. · View Herald Transcript
fabiank requested review of this revision.Aug 24 2019, 4:29 PM

The reflow in Phabricator's UI breks my ASCII diagrams, the git commit message should hopefully still be readable.

wbauer added a subscriber: wbauer.Aug 28 2019, 11:44 AM

I gave it a try and it does seem to fix the crashes from BUG 395624 here.

I did get another crash while playing Grandfather's Clock now though, no idea if it is related to this change or not.
Thread 1 (Thread 0x7f655a167900 (LWP 11734)):
[KCrash Handler]
#6 MemoryManager<19>::BLOCK<TTree<19> >::getItem (this=<optimized out>) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/memory.h:90
#7 MemoryManager<19>::new_tree (this=<optimized out>) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/memory.h:183
#8 Solver<9ul>::pack_position (this=this@entry=0x55dd50a0bf10) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/patsolve.cpp:209
#9 0x000055dd4dfb4874 in Solver<9ul>::insert (this=this@entry=0x55dd50a0bf10, cluster=cluster@entry=0x7ffc8ac1b8cc, d=0, node=node@entry=0x7ffc8ac1b8d0) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/patsolve.cpp:854
#10 0x000055dd4dfb4a2e in Solver<9ul>::new_position (this=this@entry=0x55dd50a0bf10, parent=parent@entry=0x0, m=m@entry=0x7ffc8ac1b920) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/patsolve.cpp:881
#11 0x000055dd4dfc25b6 in Solver<9ul>::doit (this=0x55dd50a0bf10) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/patsolve.cpp:472
#12 0x000055dd4dfc2647 in Solver<9ul>::patsolve (this=0x55dd50a0bf10, _max_positions=<optimized out>) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/patsolve/patsolve.cpp:767
#13 0x000055dd4df87a51 in DealerScene::isGameLost (this=0x55dd5051eb50) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/dealer.cpp:1722
#14 0x000055dd4df8f0a6 in DealerScene::takeState (this=0x55dd5051eb50) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/dealer.cpp:1313
#15 0x000055dd4df932fc in DealerScene::animationDone (this=0x55dd5051eb50) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/dealer.cpp:1529
#16 0x00007f655635944f in QtPrivate::QSlotObjectBase::call (a=0x7ffc8ac1bba0, r=0x55dd5051eb50, this=<optimized out>) at ../../include/QtCore/../../src/corelib/kernel/qobjectdefs_impl.h:394
#17 QMetaObject::activate (sender=0x55dd5051eb50, signalOffset=<optimized out>, local_signal_index=<optimized out>, argv=<optimized out>) at kernel/qobject.cpp:3787
#18 0x00007f655635944f in QtPrivate::QSlotObjectBase::call (a=0x7ffc8ac1bca0, r=0x55dd5051eb50, this=<optimized out>) at ../../include/QtCore/../../src/corelib/kernel/qobjectdefs_impl.h:394
#19 QMetaObject::activate (sender=0x55dd504af590, signalOffset=<optimized out>, local_signal_index=<optimized out>, argv=<optimized out>) at kernel/qobject.cpp:3787
#20 0x00007f655635944f in QtPrivate::QSlotObjectBase::call (a=0x7ffc8ac1bdf0, r=0x55dd5048a970, this=<optimized out>) at ../../include/QtCore/../../src/corelib/kernel/qobjectdefs_impl.h:394
#21 QMetaObject::activate (sender=sender@entry=0x55dd504b13d0, signalOffset=<optimized out>, local_signal_index=local_signal_index@entry=0, argv=<optimized out>, argv@entry=0x7ffc8ac1bdf0) at kernel/qobject.cpp:3787
#22 0x00007f65563599f7 in QMetaObject::activate (sender=sender@entry=0x55dd504b13d0, m=m@entry=0x7f65567dfb60 <QTimer::staticMetaObject>, local_signal_index=local_signal_index@entry=0, argv=argv@entry=0x7ffc8ac1bdf0) at kernel/qobject.cpp:3658
#23 0x00007f6556365ff7 in QTimer::timeout (this=this@entry=0x55dd504b13d0, _t1=...) at .moc/moc_qtimer.cpp:205
#24 0x00007f6556366358 in QTimer::timerEvent (this=0x55dd504b13d0, e=<optimized out>) at kernel/qtimer.cpp:255
#25 0x00007f6556359e4b in QObject::event (this=0x55dd504b13d0, e=<optimized out>) at kernel/qobject.cpp:1282
#26 0x00007f655781dfac in QApplicationPrivate::notify_helper (this=this@entry=0x55dd502837d0, receiver=receiver@entry=0x55dd504b13d0, e=e@entry=0x7ffc8ac1c160) at kernel/qapplication.cpp:3740
#27 0x00007f6557825520 in QApplication::notify (this=0x7ffc8ac1c500, receiver=0x55dd504b13d0, e=0x7ffc8ac1c160) at kernel/qapplication.cpp:3486
#28 0x00007f6556328958 in QCoreApplication::notifyInternal2 (receiver=0x55dd504b13d0, event=0x7ffc8ac1c160) at kernel/qcoreapplication.cpp:1065
#29 0x00007f65563859d9 in QTimerInfoList::activateTimers (this=0x55dd502eed80) at kernel/qtimerinfo_unix.cpp:643
#30 0x00007f65563861d9 in timerSourceDispatch (source=<optimized out>) at kernel/qeventdispatcher_glib.cpp:183
#31 idleTimerSourceDispatch (source=<optimized out>) at kernel/qeventdispatcher_glib.cpp:230
#32 0x00007f654f395e87 in g_main_context_dispatch () from /usr/lib64/libglib-2.0.so.0
#33 0x00007f654f396230 in ?? () from /usr/lib64/libglib-2.0.so.0
#34 0x00007f654f3962bc in g_main_context_iteration () from /usr/lib64/libglib-2.0.so.0
#35 0x00007f655638654f in QEventDispatcherGlib::processEvents (this=0x55dd50306160, flags=...) at kernel/qeventdispatcher_glib.cpp:423
#36 0x00007f6556326b6a in QEventLoop::exec (this=this@entry=0x7ffc8ac1c3b0, flags=..., flags@entry=...) at kernel/qeventloop.cpp:225
#37 0x00007f655632fe30 in QCoreApplication::exec () at kernel/qcoreapplication.cpp:1373
#38 0x000055dd4df82670 in main (argc=<optimized out>, argv=<optimized out>) at /usr/src/debug/kpat5-19.08.0-lp151.105.1.x86_64/main.cpp:346
[Inferior 1 (process 11734) detached]

I did get another crash while playing Grandfather's Clock now though, no idea if it is related to this change or not.

I played 3 times and got the same crash every time (at some point during play), so it seems to be rather reproducible.

Therefore I tried again without this patch, and successfully finished the game 3 times in a row without crash.

So it certainly looks like a regression introduced by this patch...

Thank you very much for testing the patch. Could you maybe compile kpat with
cmake -DCMAKE_BUILD_TYPE=Debug -DECM_ENABLE_SANITIZERS='address,leak,undefined' ..
and report back any backtrace you get from playing Grandfather's Clock? I again cannot reproduce the crash :-(

Thank you very much for testing the patch. Could you maybe compile kpat with
cmake -DCMAKE_BUILD_TYPE=Debug -DECM_ENABLE_SANITIZERS='address,leak,undefined' ..
and report back any backtrace you get from playing Grandfather's Clock? I again cannot reproduce the crash :-(

I'll try.
Btw, it always happens quite to the end of the game, when it's nearly solved.

wbauer added a comment.EditedAug 28 2019, 3:52 PM

Here's a valgrind log for now, maybe it contains a clue as well...

==17308== Memcheck, a memory error detector
==17308== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==17308== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==17308== Command: /opt/kf5/bin/kpat
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x4C34D80: __memcmp_sse2 (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==17308==    by 0x1227A08A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12279235: FIPS_selftest (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1227409C: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12287FE0: FIPS_mode_set (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1218D16A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x400FBF9: call_init.part.0 (dl-init.c:72)
==17308==    by 0x400FD05: call_init (dl-init.c:119)
==17308==    by 0x400FD05: _dl_init (dl-init.c:120)
==17308==    by 0x4000ED9: ??? (in /lib64/ld-2.26.so)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x4C34DA6: __memcmp_sse2 (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==17308==    by 0x1227A08A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12279235: FIPS_selftest (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1227409C: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12287FE0: FIPS_mode_set (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1218D16A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x400FBF9: call_init.part.0 (dl-init.c:72)
==17308==    by 0x400FD05: call_init (dl-init.c:119)
==17308==    by 0x400FD05: _dl_init (dl-init.c:120)
==17308==    by 0x4000ED9: ??? (in /lib64/ld-2.26.so)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x4C34DCF: __memcmp_sse2 (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==17308==    by 0x1227A08A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12279235: FIPS_selftest (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1227409C: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12287FE0: FIPS_mode_set (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1218D16A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x400FBF9: call_init.part.0 (dl-init.c:72)
==17308==    by 0x400FD05: call_init (dl-init.c:119)
==17308==    by 0x400FD05: _dl_init (dl-init.c:120)
==17308==    by 0x4000ED9: ??? (in /lib64/ld-2.26.so)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x4C34DF1: __memcmp_sse2 (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==17308==    by 0x1227A08A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12279235: FIPS_selftest (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1227409C: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12287FE0: FIPS_mode_set (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1218D16A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x400FBF9: call_init.part.0 (dl-init.c:72)
==17308==    by 0x400FD05: call_init (dl-init.c:119)
==17308==    by 0x400FD05: _dl_init (dl-init.c:120)
==17308==    by 0x4000ED9: ??? (in /lib64/ld-2.26.so)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x122740A1: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x12287FE0: FIPS_mode_set (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x1218D16A: ??? (in /usr/lib64/libcrypto.so.1.1)
==17308==    by 0x400FBF9: call_init.part.0 (dl-init.c:72)
==17308==    by 0x400FD05: call_init (dl-init.c:119)
==17308==    by 0x400FD05: _dl_init (dl-init.c:120)
==17308==    by 0x4000ED9: ??? (in /lib64/ld-2.26.so)
==17308== 
==17308== Thread 4 SolverThread:
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x4C34DF1: __memcmp_sse2 (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==17308==    by 0x1608E8: insert_node (memory.h:287)
==17308==    by 0x1608E8: Solver<9ul>::insert(unsigned int*, int, TTree<19>**) (patsolve.cpp:861)
==17308==    by 0x160A2D: Solver<9ul>::new_position(POSITION<19>*, MOVE*) (patsolve.cpp:881)
==17308==    by 0x16E35E: Solver<9ul>::solve(POSITION<19>*) (patsolve.cpp:557)
==17308==    by 0x16E5DA: Solver<9ul>::doit() (patsolve.cpp:483)
==17308==    by 0x16E646: Solver<9ul>::patsolve(int) (patsolve.cpp:767)
==17308==    by 0x140736: SolverThread::run() (dealer.cpp:159)
==17308==    by 0x86A5511: QThreadPrivate::start(void*) (qthread_unix.cpp:360)
==17308==    by 0xD821568: start_thread (pthread_create.c:465)
==17308==    by 0x95039EE: clone (clone.S:95)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x1608EB: insert_node (memory.h:288)
==17308==    by 0x1608EB: Solver<9ul>::insert(unsigned int*, int, TTree<19>**) (patsolve.cpp:861)
==17308==    by 0x160A2D: Solver<9ul>::new_position(POSITION<19>*, MOVE*) (patsolve.cpp:881)
==17308==    by 0x16E35E: Solver<9ul>::solve(POSITION<19>*) (patsolve.cpp:557)
==17308==    by 0x16E5DA: Solver<9ul>::doit() (patsolve.cpp:483)
==17308==    by 0x16E646: Solver<9ul>::patsolve(int) (patsolve.cpp:767)
==17308==    by 0x140736: SolverThread::run() (dealer.cpp:159)
==17308==    by 0x86A5511: QThreadPrivate::start(void*) (qthread_unix.cpp:360)
==17308==    by 0xD821568: start_thread (pthread_create.c:465)
==17308==    by 0x95039EE: clone (clone.S:95)
==17308== 
==17308== Conditional jump or move depends on uninitialised value(s)
==17308==    at 0x1608ED: insert_node (memory.h:291)
==17308==    by 0x1608ED: Solver<9ul>::insert(unsigned int*, int, TTree<19>**) (patsolve.cpp:861)
==17308==    by 0x160A2D: Solver<9ul>::new_position(POSITION<19>*, MOVE*) (patsolve.cpp:881)
==17308==    by 0x16E35E: Solver<9ul>::solve(POSITION<19>*) (patsolve.cpp:557)
==17308==    by 0x16E5DA: Solver<9ul>::doit() (patsolve.cpp:483)
==17308==    by 0x16E646: Solver<9ul>::patsolve(int) (patsolve.cpp:767)
==17308==    by 0x140736: SolverThread::run() (dealer.cpp:159)
==17308==    by 0x86A5511: QThreadPrivate::start(void*) (qthread_unix.cpp:360)
==17308==    by 0xD821568: start_thread (pthread_create.c:465)
==17308==    by 0x95039EE: clone (clone.S:95)
==17308== 
==17308== Invalid read of size 8
==17308==    at 0x160AFB: getItem (memory.h:90)
==17308==    by 0x160AFB: new_position (memory.h:198)
==17308==    by 0x160AFB: Solver<9ul>::new_position(POSITION<19>*, MOVE*) (patsolve.cpp:898)
==17308==    by 0x16E5B5: Solver<9ul>::doit() (patsolve.cpp:472)
==17308==    by 0x16E646: Solver<9ul>::patsolve(int) (patsolve.cpp:767)
==17308==    by 0x140736: SolverThread::run() (dealer.cpp:159)
==17308==    by 0x86A5511: QThreadPrivate::start(void*) (qthread_unix.cpp:360)
==17308==    by 0xD821568: start_thread (pthread_create.c:465)
==17308==    by 0x95039EE: clone (clone.S:95)
==17308==  Address 0x20000 is not stack'd, malloc'd or (recently) free'd
==17308== 
KCrash: Application 'kpat' crashing...
KCrash: Attempting to start /usr/lib64/libexec/drkonqi from kdeinit
The X11 connection broke (error 1). Did the X11 server die?
sock_file=/run/user/1000/kdeinit5__0

[1]+  Angehalten              valgrind /opt/kf5/bin/kpat
wolfi@linux-lf90:~/Desktop> QSocketNotifier: Invalid socket 8 and type 'Read', disabling...
==17308== 
==17308== HEAP SUMMARY:
==17308==     in use at exit: 9,172,071 bytes in 25,912 blocks
==17308==   total heap usage: 688,988 allocs, 663,076 frees, 245,849,097 bytes allocated
==17308== 
==17308== LEAK SUMMARY:
==17308==    definitely lost: 6,732 bytes in 84 blocks
==17308==    indirectly lost: 40,466 bytes in 159 blocks
==17308==      possibly lost: 2,232 bytes in 2 blocks
==17308==    still reachable: 9,122,641 bytes in 25,667 blocks
==17308==                       of which reachable via heuristic:
==17308==                         newarray           : 96 bytes in 3 blocks
==17308==                         multipleinheritance: 21,448 bytes in 25 blocks
==17308==         suppressed: 0 bytes in 0 blocks
==17308== Rerun with --leak-check=full to see details of leaked memory
==17308== 
==17308== Use --track-origins=yes to see where uninitialised values come from
==17308== For lists of detected and suppressed errors, rerun with: -s
==17308== ERROR SUMMARY: 137373 errors from 9 contexts (suppressed: 0 from 0)

Ah, I managed to reproduce the crash, and the interesting line of my backtrace is
#7 0x0000555a64cb306d in MemoryManager<19>::BLOCK<POSITION<19> >::getItem (this=0x0) at /home/fabian/projects/kpat/patsolve/memory.h:90

I'm going to debug this further, I think I'll now have a chance of finding the bug. Thanks again for saying that the bug occurs at the end of the game, that helped when trying to reproduce the issue.

fabiank updated this revision to Diff 64877.Aug 28 2019, 6:12 PM
  • fix memory accounting

The issue with the previous version was that the memory accounting code was wrong, and the allocation would fail at some point. The most recent version fixes this, and I could successfully complete a game of Grandfather's Clock again.

fabiank updated this revision to Diff 64887.Aug 28 2019, 7:50 PM
  • add missing call to free in FcSolveSolver::patsolve

And this might have been the source of all problems (and adding it to the original code is probably sufficient, though I still think that the revamped memory management is a good idea): The FcSolveSolver did not call free, which left the MemoryManager in an unideal state, and would probably lead to (non-graciously handled) out-of-memory situations. This too should be fixed now.

The issue with the previous version was that the memory accounting code was wrong, and the allocation would fail at some point. The most recent version fixes this, and I could successfully complete a game of Grandfather's Clock again.

Yes, I can confirm that the crash is gone now.
I'll probably test a bit more (especially other games), but it does look good now from my POV (although, I haven't tried the latest version with the additional free() yet).

And this might have been the source of all problems (and adding it to the original code is probably sufficient, though I still think that the revamped memory management is a good idea)

This patch is rather intrusive though. Maybe it would be good to just add this free() to the stable branch? (and only do the full revamp in master)
I'll try if it helps anyway.

There is a problem though: QScopeGuard was introduced in Qt 5.12, while the minimum required version is 5.9.

wbauer added inline comments.Aug 29 2019, 8:11 AM
patsolve/abstract_fc_solve_solver.cpp
58

This requires Qt 5.12, the minimum version is 5.9 though.

aacid added inline comments.Sep 1 2019, 5:54 PM
patsolve/abstract_fc_solve_solver.cpp
58

This is easily fixed by just increasing the minimum Qt (but if this is the only thing that requires 5.12 and it's not too hard to do requiring 5.9 maybe makes sense to do it the 5.9 way)