From 8e1be64d8d670beb640361a9df10e03315bc7797 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 9 Aug 2026 17:50:59 +0200 Subject: [PATCH] fix(windows): rewrite the preference store in place when a reader vetoes the rename dart:io opens files without FILE_SHARE_DELETE, so MoveFileExW with MOVEFILE_REPLACE_EXISTING - and any other replacement strategy - fails while such a reader holds the document open. Fall back to the upstream in-place rewrite so a hostile reader costs at most crash-atomicity for that one write instead of silently dropping it, and sweep the staging copy the rename did not consume. --- .../lib/shared_preferences_windows.dart | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences_windows/lib/shared_preferences_windows.dart index c8f53cfe..8257c53f 100644 --- a/packages/shared_preferences_windows/lib/shared_preferences_windows.dart +++ b/packages/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -427,7 +427,29 @@ Future _writePreferences( staged.parent.createSync(recursive: true); } staged.writeAsStringSync(stringMap, flush: true); - staged.renameSync(localDataFile.path); + try { + staged.renameSync(localDataFile.path); + } on FileSystemException { + // PLEZY DELTA: a reader that denies delete sharing vetoes every rename + // over the document. `MoveFileExW` with MOVEFILE_REPLACE_EXISTING — and + // even a POSIX-semantics rename — fails while an open handle on the + // destination lacks FILE_SHARE_DELETE, and dart:io's own `File.open` + // produces exactly such a handle (`_wopen` shares read/write only), as + // do some antivirus and indexing services. The atomic path is not slow + // there; it is unavailable. + // + // Fall back to the upstream in-place rewrite: the sharing mode that + // blocks the rename still admits opening the document for write. This + // reintroduces the truncate window only while such a reader holds the + // store — a brief loss of crash-atomicity beats turning every + // preference write into a silent no-op on exactly the machines whose + // scanners are most likely to have damaged the store in the first + // place. + localDataFile.writeAsStringSync(stringMap, flush: true); + // The staging copy holds the whole document, credentials included, and + // was not consumed by a rename, so it must not outlive the write. + _removeStagingFile(localDataFile); + } } catch (e) { debugPrint('Error saving preferences to disk: $e'); return false;