Commit graph

1 commit

Author SHA1 Message Date
Vadim Peretokin
9b8e53b6bf
fix: a queued quit no longer runs from inside a profile save (#9813)
#### Brief overview of PR changes/additions

- `Host::saveProfile()` no longer pumps the event loop between marking
the save as started and making the `QFutureWatcher` that retires that
mark. A quit is queued (`closeMudlet()` arms it on a zero timer), so it
could be delivered in that gap and run the entire application shutdown
from inside the save - and the rest of `saveProfile()` then carried on
using a `Host` that teardown had already destroyed.
- The nested `Host::waitForProfileSave()` was what let that shutdown
through: it was waiting for a finish notification whose watcher did not
exist yet, and its escape hatch counted a thousand event loop passes,
which on a fast machine are over in well under a millisecond. It is
bounded in wall-clock time now, waits out the background writes on every
pass, and the state it prints if it does give up says something that can
be acted on.
- `saveProfileAs()` had the same pump, and announced
`profileSaveStarted()` before finding out it was going to refuse - which
left the editor's Save Profile action disabled and captioned "Saving…"
with no `profileSaveFinished()` ever coming.

#### Motivation for adding to Mudlet

Uninstall a package and quit straight away and Mudlet can crash on the
way out. It showed up on CI as an intermittent SIGSEGV after a fully
green run, on macOS arm64 and windows64 but not on the slower legs,
preceded by `waitForProfileSave() WARNING - save did not complete after
1000 event loop iterations. State: mWritingHostAndModules=true, writers
pending=0` - which is exactly what a save looks like in the window
between the mark and the watcher.

#### Other info (issues closed, discussion etc)

Closes #9807. Adjacent to #9653/#9684 and #9690, and composes with them:
this is the re-entrancy that opened the window rather than another
dangling pointer.

Reproduced deterministically under AddressSanitizer as a
heap-use-after-free in `Host::pendingXmlSaveFutures()` called from
`Host::saveProfile()`, on a `Host` freed by `~Host()`. Draining a save
that a package change has only queued was considered and left out: the
close path never reaches it (it has always either just started a save or
found one running), and it would turn a multi-select module import from
one coalesced save into one full save per module.

**Test case:** uninstall a package, then quit Mudlet immediately - it
exits cleanly. New `ProfileSaveShutdownRaceTest`; both cases verified to
fail against the unfixed source, the shutdown one by aborting the run
under the sanitizer.

Assisted-by: Claude:claude-opus-5
2026-08-13 15:22:50 +02:00