fix(ios): rollback keeps in-memory module state true to the VM — and sticks - #20
Conversation
…sticks A loaded module cannot be unloaded from the running VM. The Dart-side rollback (the OTA kill switch's path, and app-called rollback()) cleaned the disk but also cleared _moduleLoaded and the loaded identity — claiming the VM was back on baseline when it wasn't. Consequence: a same-session install after a rollback took the fresh-load path, the second loadModule into the still-occupied VM threw, and the failure handler quarantined the NEW patch — a healthy update lost until the server shipped a different one. The flags now keep telling the truth (rollback()'s doc has always said 'takes effect on next cold restart'): disk is cleaned, the module stays marked resident, and a later offer routes through the persist-don't-load branch. Adversarial review of the draft surfaced two consequences, both handled: - A public rollback() must STICK: without a quarantine marker the next check would silently re-persist the still-offered patch via the running-module match, undoing the API call minutes later. rollback() now quarantines the removed patch (cleared automatically when the server offers a different one). The OTA kill switch uses the non-quarantining internal path — after a re-enable the SAME patch must be able to resume service. - A kill-switch bounce (disable → re-enable, same patch) re-persists silently, but moduleResult was reverted and cannot be restored in-session — the 'restart changes nothing' premise is false there. A _contentRevertedThisSession latch routes that case to the 'Restart to apply' banner + onUpdateReady instead. moduleResult is still cleared as the app-facing kill-switch signal; status reports 'Patch removed — active until restart' honestly. Suite 137/137 (3 new rollback-quarantine tests; the resident-module behavior itself is Platform-gated and needs a device cycle).
🔴 CriticalNone. The change preserves the public 🟠 Medium
🟡 Low
🟢 Positives
|
- debugPersistOutcomeShowsRestart extracts the silent-vs-banner rule (persistSilently + nothing reverted → silent; anything else → banner) — 3 tests. - A debugModuleLoadedForTesting window makes the resident-flag invariant assertable: a new test proves rollback() keeps the flag, sets the honest status, and still clears moduleResult. - _contentRevertedThisSession also resets on the hard-reset path so 'latch implies resident revert' holds locally. - rollback() doc qualified: quarantine-on-rollback is the iOS Dart-side path; Android's engine rollback has its own bookkeeping. Suite 141/141.
Triage round 1
|
|
Reviewed the current head. This is a focused change to the iOS Dart-side rollback path: public 🔴 CriticalNone. The public 🟠 Medium
🟡 Low
🟢 Positives
|
ota_disabled + engine rollback returning false drives the Dart-side _rollbackInternal(quarantine: false): the patch file must go, and rolled_back_patch must NOT be written — the contract that lets a server re-enable resume with the same patch. Suite 142/142.
|
Round 2 triage: 🟠 the |
…dent-module # Conflicts: # lib/src/code_push.dart
🔴 CriticalNone. The public
The central insight (a loaded VM module cannot be unloaded, so 🟠 MediumNone. The prior rounds' test gaps are closed: 🟡 Low
🟢 Positives
|
…he identity-file reliance _rollbackInternal now deletes both patch.bytecode and the legacy patch.vmcode, mirroring the auto-rollback paths, so a device upgraded from an old SDK can't keep a stale artifact through a deliberate rollback. The survival of installed_patch_identity.json is documented at the delete site (it relies on the patch-file-exists gate in the already-installed skip). Suite 145/145.
|
Final round triage: both 🟡 Lows FIXED in |
🔴 CriticalNone. The public
Every new disk/marker operation is inside 🟠 MediumNone. The test gaps flagged in earlier rounds are closed: 🟡 Low
🟢 Positives
|
Follow-up tabled on #17/#19 (the
rollback()_moduleLoadedlie), approved for this batch.The bug
A loaded module cannot be unloaded from the running VM, but the Dart-side rollback (the OTA kill switch's path, and app-called
rollback()) cleared_moduleLoadedand the loaded identity while cleaning the disk — claiming the VM was back on baseline when it wasn't. A same-session install after a rollback then took the fresh-load path; the secondloadModuleinto the still-occupied VM threw, and the failure handler quarantined the NEW patch — a healthy update lost until the server shipped a different one.The fix
The flags now keep telling the truth (
rollback()'s doc has always promised 'takes effect on next cold restart'): disk cleaned, module stays marked resident, later offers route through the persist-don't-load branch.moduleResultstill clears as the app-facing kill-switch signal; status reportsPatch removed — active until restart.Hardening from adversarial review of the draft
rollback()must STICK. Without a marker, the next check silently re-persisted the still-offered patch via the running-module match — undoing the API call minutes later.rollback()now quarantines the removed patch (auto-cleared when the server offers a different one). The kill switch uses the non-quarantining internal path: there the server stopped offering, and a re-enable must resume service with the SAME patch.moduleResultwas reverted and can't be restored in-session — 'restart changes nothing' is false there. A_contentRevertedThisSessionlatch routes that case to theRestart to applybanner +onUpdateReady.Known-remaining (pre-existing, tabled): the in-flight double-load race (a load awaited in one path while another path passes its
_moduleLoadedcheck) exists onmainwith identical windows; this branch strictly shrinks load opportunities. Proper fix is a load-serialization gate — its own change.Testing
137/137 — 3 new tests for the Dart-side rollback (quarantine written from
patch_info, no marker without metadata, clean-device throw). The resident-module behavior itself is platform-gated and needs a device cycle.