ignore the results of superseded download verifications
What changed, and why it matters
This commit fixes a timing bug in Sparrow Wallet's download verification dialog. Previously, if a user started a new file verification while an earlier one was still running, the old verification could finish later and overwrite the new results on screen. That could briefly show a wrong file as 'verified' or 'matched.' The fix adds a counter so results from an outdated verification are ignored, and it cancels any running checks before starting a new one.
Review the drongo submodule bump (7f0e5630a68c5b1487bb4b4ba40fdc72ea4307f7) to confirm it does not introduce unrelated security-relevant changes. Otherwise, the dialog fix should be accepted as a defensive hardening patch; no immediate incident response is indicated.
Security signals we found
TOCTOU-like UI race between asynchronous verification tasks
stale async result could overwrite current verification UI state
missing cancellation of background services before starting new verification
submodule update (drongo) without disclosed contents
Evidence from the diff
DownloadVerifierDialog runs PGP signature verification and SHA-256 manifest comparison as JavaFX Services. Before this patch, each call to verify() created fresh PGPVerifyService/FileSha256Service instances without cancelling prior ones. Because these services run asynchronously, a slow earlier task could complete after a newer verification and its setOnSucceeded/setOnFailed handlers would update the UI with stale data. The patch introduces a verificationCount that is incremented on the FX thread via cancelVerification(), stores the current count in a local verification variable, and each handler returns early if verification != verificationCount. It also cancels and nulls the running services. The drongo submodule bump is included but no details of its changes are provided.
Changed components
src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.javadrongo submoduleInspect captured patch +54 / −6
### drongo
@@ -1 +1 @@
-Subproject commit fcda9f5fc485473bfe30c59bb835f6a70f5bbc81
+Subproject commit 7f0e5630a68c5b1487bb4b4ba40fdc72ea4307f7
### src/main/java/com/sparrowwallet/sparrow/control/DownloadVerifierDialog.java
@@ -82,6 +82,10 @@ public class DownloadVerifierDialog extends Dialog<ButtonBar.ButtonData> {
private final Label releaseVerified;
private final Hyperlink releaseLink;
+ private PGPVerifyService pgpVerifyService;
+ private FileSha256Service hashService;
+ private long verificationCount;
+
private static File lastFileParent;
public DownloadVerifierDialog(File initialFile) {
@@ -152,6 +156,7 @@ public DownloadVerifierDialog(File initialFile) {
setOnCloseRequest(event -> {
if(ButtonBar.ButtonData.CANCEL_CLOSE.equals(getResult())) {
+ cancelVerification();
signature.set(null);
manifest.set(null);
publicKey.set(null);
@@ -274,6 +279,9 @@ private void setupDrag(DialogPane dialogPane) {
}
private void verify() {
+ cancelVerification();
+ long verification = verificationCount;
+
manifestDisabled.set(false);
publicKeyDisabled.set(false);
@@ -282,14 +290,22 @@ private void verify() {
return;
}
- PGPVerifyService pgpVerifyService = new PGPVerifyService(signature.get(), manifest.get(), publicKey.get());
+ pgpVerifyService = new PGPVerifyService(signature.get(), manifest.get(), publicKey.get());
pgpVerifyService.setOnRunning(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
signedBy.setText("Verifying...");
signedBy.setGraphic(GlyphUtils.getBusyGlyph());
signedBy.setTooltip(null);
clearReleaseFields();
});
pgpVerifyService.setOnSucceeded(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
PGPVerificationResult result = pgpVerifyService.getValue();
String message = result.userId() + " on " + signatureDateFormat.format(result.signatureTimestamp()) + (result.expired() ? " (key expired)" : "");
@@ -310,10 +326,14 @@ private void verify() {
releaseVerified.setGraphic(GlyphUtils.getSuccessGlyph());
releaseLink.setText(release.get().getName());
} else {
- verifyManifest();
+ verifyManifest(verification);
}
});
pgpVerifyService.setOnFailed(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
Throwable e = event.getSource().getException();
signedBy.setText(getDisplayMessage(e));
signedBy.setGraphic(GlyphUtils.getFailureGlyph());
@@ -324,6 +344,21 @@ private void verify() {
pgpVerifyService.start();
}
+ //Supersedes any verification in progress - the counter is only changed on the FX thread, so a task that completes as it is replaced cannot render its result
+ private void cancelVerification() {
+ verificationCount++;
+
+ if(pgpVerifyService != null) {
+ pgpVerifyService.cancel();
+ pgpVerifyService = null;
+ }
+
+ if(hashService != null) {
+ hashService.cancel();
+ hashService = null;
+ }
+ }
+
private void clearReleaseFields() {
releaseHash.setText("");
releaseHash.setGraphic(null);
@@ -333,11 +368,16 @@ private void clearReleaseFields() {
releaseLink.setText("");
}
- private void verifyManifest() {
+ private void verifyManifest(long verification) {
File releaseFile = release.get();
if(releaseFile != null && releaseFile.exists()) {
- FileSha256Service hashService = new FileSha256Service(releaseFile);
+ File manifestFile = manifest.get();
+ hashService = new FileSha256Service(releaseFile);
hashService.setOnRunning(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
releaseHash.setText("Calculating...");
releaseHash.setGraphic(GlyphUtils.getBusyGlyph());
releaseHash.setTooltip(null);
@@ -346,9 +386,13 @@ private void verifyManifest() {
releaseLink.setText("");
});
hashService.setOnSucceeded(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
String calculatedHash = hashService.getValue();
try {
- Map<File, String> manifestMap = getManifest(manifest.get());
+ Map<File, String> manifestMap = getManifest(manifestFile);
String manifestHash = getManifestHash(releaseFile.getName(), manifestMap);
if(calculatedHash.equalsIgnoreCase(manifestHash)) {
releaseHash.setText("Matched manifest hash");
@@ -382,6 +426,10 @@ private void verifyManifest() {
}
});
hashService.setOnFailed(event -> {
+ if(verification != verificationCount) {
+ return;
+ }
+
releaseHash.setText("Could not calculate manifest");
releaseHash.setGraphic(GlyphUtils.getFailureGlyph());
releaseHash.setTooltip(new Tooltip(event.getSource().getException().getMessage()));Why this scored 41/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.