snapshot_point() runs with m_mutex held and, in the non-deferred path,
temporarily released it across m_point_file.write() to keep the slow I/O
off the lock. The release used a raw mutex::unlock() / mutex::lock()
pair, so a throw from write() unwound out of snapshot_point() with the
mutex left unlocked. save_point() then called m_mutex.unlock() on the
unlocked mutex, which is undefined behaviour.
Wrap the write in a try/catch that re-locks before rethrowing, so the
postcondition "lock is held on return" holds in both the success and
exception paths. This is the minimum fix; it does not change the
clean/dirty bookkeeping or the recovery semantics, just the mutex
state on the exception edge.
Assisted-by: Claude-Code:claude-opus-4-7
Signed-off-by: Zygo Blaxell <bees@furryterror.org>
if (!m_checkpoint_defer) {
// Immediate write (current behaviour): write while lock is temporarily released.
- // We must re-acquire the lock before updating m_crawl_clean.
- // Use a raw unlock/lock here since snapshot_point() is called with lock held.
+ // We must re-acquire the lock before updating m_crawl_clean, and before
+ // returning to the caller (who holds the lock as a postcondition). Use a
+ // try/catch so a throw from m_point_file.write() still re-locks the mutex
+ // before the exception propagates -- otherwise save_point() would call
+ // m_mutex.unlock() on an unlocked mutex.
m_mutex.unlock();
- m_point_file.write(ini_string);
+ try {
+ m_point_file.write(ini_string);
+ } catch (...) {
+ m_mutex.lock();
+ throw;
+ }
m_mutex.lock();
m_crawl_clean = crawl_saved;
} else {