Files
qelectrotech-source-mirror/tests/ipc-regression/README.md
T
ispyisail 9363e9bc2e Add a regression test for the forwarded-file use-after-free
Covers the crash fixed in #868: a file forwarded from a second instance was
opened inside SingleApplication's socket handler, so the backup prompt's
nested event loop ran while that handler was still on the stack.

Run by hand; no build system or CI changes.

    tests/ipc-regression/run.sh --binary build/qelectrotech

Validated in both directions on Qt 6.10.2: ceda1e082 (before the fix) crashes
3 times in 3 with exit 139, 199444b6 (after) survives 3 times in 3.

Three requirements are not obvious and are documented in the script:

- Qt 6 only. An unfixed Qt 5 build survives every attempt, so the script
  refuses to run on a Qt 5 binary rather than report a pass that cannot fail.
- A Debug build. The same unfixed commit survives every attempt built
  -O3 -DNDEBUG; whether a use-after-free faults depends on what the allocator
  does with the freed block.
- Dismissing the backup prompt is the step that triggers it. Left open, the
  stack never unwinds and nothing fails, which is why the bug was twice
  reported as not reproducible.

The test runs in its own sandbox on its own X display, works on a copy of the
project so backup files do not land in examples/, and cleans up after itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 07:09:16 +12:00

74 lines
3.1 KiB
Markdown

# IPC open-forwarding regression test
Guards the use-after-free fixed in PR #868.
With QElectroTech already running, opening a `.qet` from a file manager forwards
the path to the running instance over SingleApplication's local socket. Before
#868, `QETApp::receiveMessage()` called `openFiles()` directly — inside the
socket's `readyRead` handler. Loading the project raises a modal backup prompt
whose `exec()` runs a nested event loop while that handler is still on the
stack. The second instance then exits, its socket is deleted, and when the
prompt is dismissed and the stack unwinds, `QMetaObject::activate()` carries on
emitting on freed memory.
## Running it
```bash
tests/ipc-regression/run.sh --binary build/qelectrotech
```
Needs `Xvfb`, `openbox` and `xdotool`. It allocates its own display, runs
entirely in a sandbox, and cleans up after itself.
Exit codes: `0` survived, `1` crashed, `2` inconclusive or unusable input.
## Requirements that are not obvious
**Qt 6.** An unfixed Qt 5 build survives this scenario every time — measured,
not assumed. The script refuses to run on a Qt 5 binary rather than report a
pass that cannot fail. Reproduced on Qt 6.10.2.
**A Debug build.** Whether a use-after-free faults depends on what the allocator
does with the freed block. The same unfixed commit crashes 3 times in 3 built
Debug, and survives every attempt built `-O3 -DNDEBUG`.
**A large project.** The load has to take long enough that the prompt is up
while the socket handler is still on the stack. `examples/industrial.qet`
(2.6 MB, ~4 s) is used by default.
## Why it repeats
The crash is a race: the second instance's disconnect has to be processed while
the nested event loop is on the stack. Single attempts reproduced it 2 times in
3, so one attempt would let a reintroduced bug through about a third of the
time. Three attempts is roughly 96%; `QET_IPC_ATTEMPTS` changes it. Any single
crash fails the run, and survival must be unanimous.
## Validation
| build | result |
|---|---|
| `ceda1e082` (before #868) | 3/3 crashed, exit 139 |
| `199444b6` (after #868) | 3/3 survived |
If this test is changed, re-validate it against a build without the fix. A gate
nobody has watched go red is not evidence.
## Things that made earlier versions of this test pass a crashing build
Recorded because each one looked exactly like success.
**Answering the wrong dialog.** A progress dialog appears several seconds before
the backup prompt and both stay up. Dismissing the progress dialog satisfies a
naive "a modal appeared, dismiss it" check while never touching the crash path.
**Filtering that dialog out by title.** That is a locale filter: it worked in
French and silently stopped working in English. The script now dismisses every
dialog and identifies none, and treats a run that saw fewer than two distinct
dialogs as inconclusive rather than passing.
**Sharing a home directory between attempts.** A crashed attempt leaves a backup
file, so the next one opens with the restore prompt instead of the backup
prompt — a path that never unwinds the socket stack. Each attempt now gets a
fresh `HOME`.