Anti-Bootloop Protection, Automatic Rollback & Acknowledgement
A critical threat in any dynamic code delivery system is shipping a patch that crashes on startup, throwing users into an unrecoverable crash loop (bootloop). TkushPatch defends against this hazard using a dual-layer security design:
- At the native Rust engine layer (
updater/library/src/updater.rs): automatic boot failure detection, immediate local patch revocation, and delivery of a telemetryPatchInstallFailureevent. - At the Flutter SDK layer (
OtaRollbackTrackerinlib/src/security/ota_security_shield.dart): detection of patch version regression (current_patch < last_executed_patch) and execution of theonRollbackDetectedcallback to purge local caches.
1. Native Anti-Bootloop Mechanism (Rust Engine)
- Monitored Boot: When the application launches with a new patch, boot attempts are tracked.
- Stability Confirmation: Once the UI renders successfully, the patch is marked as healthy.
- Automatic Fallback: If an uncaught crash occurs before confirmation, the engine revokes the patch and falls back to the safe base release.
Startup Crash Detection (handle_prior_boot_failure_if_necessary)
When a patch is booted, the Rust engine records its patch number in state.json as currently_booting_patch with timestamp boot_started_at.
If the mobile app crashes before the Flutter runtime signals completion, the subsequent app launch executes handle_prior_boot_failure_if_necessary() (updater/library/src/updater.rs):
- The engine checks that
currently_booting_patchis still set. - It executes
state.record_boot_failure_for_patch(patch.number): the patch is marked permanently as bad (PatchKnownBad) on this client device so it is never loaded again (UpdateStatus::UpdateIsBadPatch). - It constructs a detailed diagnostic message:
crash_recovery: patch <N> failed to boot (detected_at=<ts>,boot_started_at=<ts>,file_ok=<bool>,file_size=<bytes>) - It queues an
EventType::PatchInstallFailure(__patch_install_failure__) event which is transmitted to the backend atPOST /api/v1/patches/events.
Server-Driven Rollback (rolled_back_patch_numbers)
In addition to local crash recovery, the server can order fleet revocations through the rolled_back_patch_numbers array returned in POST /api/v1/patches/check. Upon receiving this array, roll_back_patches_if_needed(rolled_back_patches) uninstalls the revoked patches locally.
2. Dart-Side Resilience & Cache Invalidation (OtaRollbackTracker)
When an app rolls back from Patch #2 to Patch #1 (or baseline Store binary #0), Patch #2 might have written entries to SharedPreferences, Hive, or SQLite using a schema unknown to Patch #1.
The tk_ota_push SDK solves this through OtaRollbackTracker:
final ota = TkOtaPush.init(
appReleaseVersion: '1.0.0+3',
// Optional: use FileOtaPatchStateStore to persist last_executed_patch to disk
rollbackTracker: OtaRollbackTracker(
store: FileOtaPatchStateStore(File('/path/to/sandbox/ota_patch_state.json')),
),
onRollbackDetected: (OtaRollbackEvent event) async {
debugPrint(
'Rollback detected on release ${event.releaseVersion} '
'at ${event.detectedAt.toIso8601String()}: '
'Patch #${event.previousPatchNumber} -> Patch #${event.restoredPatchNumber}',
);
// Purge potentially incompatible local caches before runApp()
},
);
Operation of verifyStartupAndHandleRollback()
- On each
syncWithRemoteConfigcall (Step 0a) or explicit call toverifyStartupAndHandleRollback(), the SDK inspects the running patch (currentPatchNumber() ?? 0). OtaRollbackTracker.evaluateAndRecordchecks this number againstlast_executed_patchrecorded for thisreleaseVersioninOtaPatchStateStore(InMemoryOtaPatchStateStoreby default, orFileOtaPatchStateStoreon disk).- It persists
last_executed_patchwith the current value. - If
effectiveCurrent < previous, it returns anOtaRollbackEventholding:releaseVersion(String)previousPatchNumber(int)restoredPatchNumber(int,0if reverted to original Store binary)detectedAt(DateTimein UTC)
- The
onRollbackDetectedcallback is awaited before further initialization proceeds.
3. Telemetry Dispatched to Server (POST /api/v1/patches/events)
The Rust engine reports the following events (declared in updater/library/src/events.rs) to ShorebirdUpdaterController.reportPatchEvent:
Rust Enum (EventType) | JSON ID (type) | Trigger |
|---|---|---|
PatchDownload | __patch_download__ | Patch binary download has finished. |
PatchInstallSuccess | __patch_install__ | Patch booted successfully on subsequent launch. |
PatchInstallFailure | __patch_install_failure__ | Patch failed during boot (crash_recovery) or installation. |
PatchUpdateFailure | __patch_update_failure__ | Error during update check or staging phase. |
Every PatchEvent carries a normalized failure_category (FailureCategory in updater.rs):
"network"(retryable)"server"(retryable)"out_of_space"(non-retryable)"permission_denied"(non-retryable)"filesystem"(non-retryable)"corrupt_patch"(non-retryable)"invalid_signature"(non-retryable)"unknown"(retryable cautiously)
4. Rollback Alert Acknowledgement in Support Console (/support)
When a device submits __patch_install_failure__, its health flag switches to an active Rollback Alert in the Device Fleet & Support dashboard (/support) of the TkushPatch Web Console.
- A support operator can search for the device (using its UUID
client_idor short code#6959347F) and click Acknowledge Rollback Alert. - The server records
rollback_acknowledged_aton the device entity without mutating or deleting historical audit log events. - Device status reverts to healthy as long as no subsequent failure (
latestFailureAt > rollbackAcknowledgedAt) occurs.