A sync that processes records sequentially without transaction semantics can leave the local database in a partially-updated state if the process is interrupted mid-way. CWE-362 applies when an interrupted sync produces orphaned references — a local record pointing to a remote entity that was never fetched. ISO 25010:2011 reliability.recoverability requires that interrupted operations leave data in a valid, queryable state. The failure mode is subtle: the app appears to function normally after the crash, but relationship integrity is broken and queries silently return incomplete results.
Medium because partial sync failures corrupt relational integrity locally, but the exposure is confined to the affected device rather than the server or other users.
Batch sync updates inside a database transaction so the local state either advances fully or not at all. Validate that all remote records can be fetched before writing any of them.
async function syncAtomic(records: LocalRecord[]) {
// Phase 1: fetch everything before writing anything
const remoteRecords = await Promise.all(
records.map(r => fetchRemote(r.id))
);
// Phase 2: merge and write inside a transaction
await db.transaction(async (tx) => {
for (let i = 0; i < records.length; i++) {
const merged = merge(records[i], remoteRecords[i]);
await tx.update('records', merged.id, merged);
}
});
}
For storage layers without native transactions (e.g., AsyncStorage), implement a write-ahead staging key: write the full batch to a staging key first, then atomically swap it into the live key. Never partially update the live key.
ID: mobile-offline-storage.sync-conflicts.partial-sync-safety
Severity: medium
What to look for: Examine sync logic for error handling. Look for transaction semantics, rollback logic, or defensive merging that prevents partially synced data from corrupting the local state.
Pass criteria: Count all sync error handling paths. If sync fails mid-way (e.g., some records sync, others do not), the local state remains consistent. At least 1 transaction boundary, rollback mechanism, or validation pass must exist. Either all changes sync or none do (atomic), or partial syncs leave data in a valid state.
Fail criteria: If sync partially completes, local state could be left inconsistent (e.g., references to records that did not sync). No more than 0 sync operations should lack error handling.
Skip (N/A) when: Never — data consistency is essential.
Detail on fail: "Sync processes multiple records sequentially with no rollback. If sync fails mid-way, app could have orphaned references or inconsistent relations."
Remediation: Use transactions for critical sync operations:
async function syncRecordsAtomic(records: Record[]) {
try {
// Use database transaction
await db.transaction(async (tx) => {
for (const record of records) {
const remote = await fetchRemoteRecord(record.id);
const merged = mergeConflict(record, remote);
await tx.update('records', merged.id, merged);
}
});
console.log('Sync completed successfully');
} catch (error) {
console.error('Sync failed, all changes rolled back:', error);
// Local state unchanged
}
}
// For simpler cases without a transaction layer:
async function syncRecordsWithValidation(records: Record[]) {
const updated = [];
try {
// First pass: validate all records can be fetched
for (const record of records) {
const remote = await fetchRemoteRecord(record.id);
updated.push({ local: record, remote });
}
// Second pass: only update if all fetches succeeded
for (const { local, remote } of updated) {
const merged = mergeConflict(local, remote);
await saveLocalRecord(merged);
}
console.log('All records synced');
} catch (error) {
console.error('Sync validation failed:', error);
// Local state unchanged
}
}