Skip to content

fix(lite): state-save hardening — locking, torn records, .bak rotation, name sanitising - #191

Merged
yeroo merged 1 commit into
mainfrom
fix/lite-state-hardening
Aug 18, 2026
Merged

fix(lite): state-save hardening — locking, torn records, .bak rotation, name sanitising#191
yeroo merged 1 commit into
mainfrom
fix/lite-state-hardening

Conversation

@yeroo

@yeroo yeroo commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Four fixes to the save/restore path, all in the class the restore matrix exists to guard: losing saved sessions quietly.

  • g_userEmptied is set and read under the lock, with the session list it was judged from. It's written from the control-pipe thread (closeSessionAt) and read on the UI thread, and it gates both the zero-session refusal and the .bak delete — the two decisions that can cost saved sessions.
  • stateFileSessionCount counts only records that would actually restore (≥4 tabs), not every line beginning S\t. A record cut mid-write parses to nothing, so counting it would let the save rotate that wreckage into the .bak — over the one generation still holding the sessions — and call a good file "not empty".
  • Rotation keeps a generation only when the primary is worth keeping, and drops the .bak only when the user emptied the window on purpose. Otherwise the next launch reads the fallback and brings back exactly the sessions they just closed. A primary that exists but can't be read right now (AV scan, transient lock) still counts as worth keeping.
  • Instance names from the HKCU registry are sanitised before becoming filenames. An older build or a hand edit could hold ..\..\x, which would delete state outside the state directory.

Also: saveSessionState now logs when it returns on an empty path. That was the last remaining way to get "restore doesn't work" with nothing in the log — on exactly the redirected or policy-locked profile the field reports come from.

New coverage

inplace-fallback covers the half of the atomic write nothing else reached: the temp file can't be created at all (policy-locked profile, DLP/AV agent blocking new files), so the save falls back to writing in place. publish-blocked locks the primary, which forces a failed publish rather than a failed create — so this path shipped untested. It must still keep both promises: a generation in the .bak, and that .bak dropped on a deliberate empty.

Verification

Full lite suite green — 30 matrix cells plus the log and diagnose suites:

PASS  inplace-fallback  (in-place save keeps a generation, and drops it on a deliberate empty)
restore-matrix: all cells passed
all lite checks passed

Authored by Boris; landed as-is.

…n, name sanitising

Four fixes to the save/restore path, all in the class the restore matrix exists to guard:
losing saved sessions quietly.

  - g_userEmptied is set and read UNDER THE LOCK, with the session list it was judged from.
    It is written from the control-pipe thread (closeSessionAt) and read on the UI thread,
    and it gates both the zero-session refusal and the .bak delete — the two decisions that
    can cost saved sessions.
  - stateFileSessionCount counts only records that would actually RESTORE (>=4 tabs), not
    every line beginning "S\t". A record cut mid-write parses to nothing, so counting it
    would let the save rotate that wreckage into the .bak, over the one generation still
    holding the sessions, and call a good file "not empty".
  - Rotation keeps a generation only when the primary is worth keeping, and drops the .bak
    only when the user emptied the window on purpose — otherwise the next launch reads the
    fallback and brings back exactly the sessions they just closed. A primary that exists but
    cannot be read right now (AV scan, transient lock) still counts as worth keeping.
  - Instance names from the HKCU registry are sanitised before becoming filenames: an older
    build or a hand edit could hold "..\..\x", which would delete state outside the state
    directory.

Also: saveSessionState logs when it returns on an empty path. That was the last way to get
"restore doesn't work" with nothing in the log, on exactly the redirected or policy-locked
profile the field reports come from.

New matrix cell `inplace-fallback` covers the half of the atomic write nothing else reached:
the temp file cannot be CREATED at all (policy-locked profile, DLP/AV agent blocking new
files), so the save falls back to writing in place. publish-blocked locks the PRIMARY, which
forces a failed publish rather than a failed create, so this path shipped untested. It must
still keep both promises: a generation in the .bak, and that .bak dropped on a deliberate
empty.

lite suite green (30 matrix cells + log/diagnose suites).
@yeroo
yeroo merged commit 3ac2b00 into main Aug 18, 2026
1 check passed
@yeroo
yeroo deleted the fix/lite-state-hardening branch August 18, 2026 11:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant