diff --git a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp index e6702772..b212c884 100644 --- a/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp +++ b/lib/tdeck_ui/Hardware/TDeck/MapTileStore.cpp @@ -597,6 +597,15 @@ TileStoreResult MapTileStore::finishPut() { return result; } const bool transactional_eviction = victim_count != 0U; + if (duplicate) { + // A stale backup must not become the rollback generation recorded by a + // new transaction. Verify its removal before publishing the manifest. + result = storage_.remove(put_backup_); + if ((result != TileStoreResult::OK) && (result != TileStoreResult::MISS)) { + storage_.remove(put_temp_); + return result; + } + } if (transactional_eviction) { result = writeEvictionTransaction(put_key_, duplicate, victims, victim_count); if (result != TileStoreResult::OK) { @@ -605,7 +614,6 @@ TileStoreResult MapTileStore::finishPut() { } } if (duplicate) { - storage_.remove(put_backup_); result = storage_.rename(put_live_, put_backup_); if (result != TileStoreResult::OK) { if (transactional_eviction) recoverEvictionTransaction(); diff --git a/tests/native/test_map_tile_store.cpp b/tests/native/test_map_tile_store.cpp index da98a09a..2bc17013 100644 --- a/tests/native/test_map_tile_store.cpp +++ b/tests/native/test_map_tile_store.cpp @@ -231,6 +231,12 @@ void testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim() { beginTest(); drain(recovered,a,40U); drain(recovered,b,40U); } } +void testStaleDuplicateBackupMustClearBeforeManifest() { beginTest(); FakeStorage fs; MapTileStore s(fs,config(2U,80U,80U)); CHECK(s.initialize()==TileStoreResult::OK); + const TileKey a={1U,0U,0U}, b={1U,1U,0U}; CHECK(put(s,a,png())==TileStoreResult::OK); CHECK(put(s,b,png())==TileStoreResult::OK); + fs.add("/pyxis-map/tiles/1/0/0.png.bak",png(60U)); fs.fail_remove_path="/pyxis-map/tiles/1/0/0.png.bak"; + CHECK(put(s,a,png(80U))==TileStoreResult::IO_ERROR); fs.fail_remove_path.clear(); + CHECK(s.entryCount()==2U); CHECK(s.totalBytes()==80U); drain(s,a,40U); drain(s,b,40U); +} void testMalformedEvictionManifestFailsClosed() { beginTest(); FakeStorage fs; fs.add("/pyxis-map/tiles/.evict.txn",std::vector(21U,0U)); MapTileStore s(fs,config()); CHECK(s.initialize()==TileStoreResult::INDEX_MISMATCH); @@ -249,4 +255,4 @@ void testDeterministicStress() { beginTest(); FakeStorage fs; MapTileStore s(fs, std::uint32_t size=0U; for(std::uint32_t i=0U;i<100000U;++i) { const TileKey k={2U,i&3U,(i>>2)&3U}; TileStoreResult r=s.beginGet(k,size); CHECK(r==TileStoreResult::OK||r==TileStoreResult::MISS); if(r==TileStoreResult::OK)s.endGet(); } } } -int main() { testKeyAndCanonicalPath(); testMissHitAndRemoval(); testMalformedPngs(); testShortWriteAbortsTemp(); testExactQuotaAndLruEviction(); testDuplicateAtomicReplacement(); testInterruptedFilesRecover(); testLiveWinsRecovery(); testCorruptLiveRecoversValidBackup(); testCorruptLiveWithoutBackupIsRemoved(); testStaleTempRemovalFailureAbortsPut(); testRecoveryRejectsMalformedAndExhaustion(); testRecoveryQuotaFailsClosed(); testRenameFailureRestoresDuplicate(); testPromotionFailureDoesNotEvictVictims(); testEvictionPreflightFailurePreservesAllVictims(); testEvictionStageFailureRollsBackAllVictims(); testEvictionPowerCutsRestoreWholeOldGeneration(); testDuplicateEvictionPowerCutsRestoreOldCandidateAndVictim(); testMalformedEvictionManifestFailsClosed(); testPostCommitCleanupResidueKeepsWholeNewGeneration(); testDeterministicStress(); std::cout<<"map tile store: "< None: env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1" ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env) assert ran.returncode == 0, ran.stdout + ran.stderr - assert ran.stdout == "map tile store: 22 tests passed\n" + assert ran.stdout == "map tile store: 23 tests passed\n"