Close the three holes the #1486 review found (#1488)

* Fix the three follow-ups from the #1486 review

Three defects shipped with the censorship-resilience merge, all
confirmed against main:

1. Source-manifest verification silently accepted added files.
   shasum -c checks only the files the manifest lists, and the Xcode
   project compiles every source file present in the tree — so a
   hostile mirror could pass verification by adding a file rather than
   modifying one. The manifest header and VERIFYING-A-BUILD.md now
   require the completeness check (git status --porcelain, or a path
   diff for tarballs) alongside the hash check.

2. A relay removed while Tor was bootstrapping reconnected anyway.
   dropRelays never subtracted from pendingTorConnectionURLs, and a
   custom relay passes the allow-list filter, so draining the pending
   queue resurrected a relay someone had explicitly deleted.

3. Turning Tor off mid-bootstrap read as 'network may be blocking tor'.
   shutdownCompletely left the detached 75s poll loop running, which
   then stamped bootstrapDidStall over the clean shutdown state; and
   the stall handler guarded on torEnforced, which is compile-time true
   in release, instead of the runtime preference. The poll loop is now
   generation-fenced (shutdown, dormancy, and restart each invalidate
   it) and the handler consults persistedTorPreference().

Both app-side fixes carry regression tests proven to fail pre-fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address Codex review: ignored files and manifest placement

git status --porcelain omits ignored paths, and .gitignore covers
build/ — a planted bitchat/build/Evil.swift would compile via the
synchronized group while the documented check stayed silent. The
checkout check now uses --ignored.

The downloaded manifest also has to live outside the tree, or it trips
the completeness checks itself; the doc now says so and references it
at /tmp throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-26 21:01:38 +02:00
committed by GitHub
co-authored by jack Claude Fable 5
parent c1ce9029d8
commit 132120a88e
7 changed files with 135 additions and 23 deletions
+26 -18
View File
@@ -84,6 +84,10 @@ public final class TorManager: ObservableObject {
private var shutdownsInFlight = 0
private var startPendingAfterShutdown = false
private var bootstrapMonitorStarted = false
// Fences the detached poll loop: shutdown, dormancy, and restart each bump
// this, so a loop from a previous attempt cannot run out its deadline and
// report a stall over state that a newer lifecycle event already owns.
private var bootstrapGeneration = 0
private var pathMonitor: NWPathMonitor?
private var isAppForeground: Bool = true
private var lastRestartAt: Date? = nil
@@ -268,24 +272,25 @@ public final class TorManager: ObservableObject {
private func startBootstrapMonitor() {
guard !bootstrapMonitorStarted else { return }
bootstrapMonitorStarted = true
bootstrapGeneration += 1
let generation = bootstrapGeneration
Task.detached(priority: .utility) { [weak self] in
await self?.bootstrapPollLoop()
await self?.bootstrapPollLoop(generation: generation)
}
}
private func bootstrapPollLoop() async {
private func bootstrapPollLoop(generation: Int) async {
let deadline = Date().addingTimeInterval(75)
var didComplete = false
while Date() < deadline {
guard generation == bootstrapGeneration else { return }
let progress = Int(arti_bootstrap_progress())
let summary = getBootstrapSummary()
await MainActor.run {
self.bootstrapProgress = progress
self.bootstrapSummary = summary
if progress >= 100 { self.isStarting = false }
self.recomputeReady()
}
self.bootstrapProgress = progress
self.bootstrapSummary = summary
if progress >= 100 { self.isStarting = false }
self.recomputeReady()
if progress >= 100 {
didComplete = true
@@ -296,17 +301,17 @@ public final class TorManager: ObservableObject {
// Running out the deadline is a reportable outcome, not silence. The
// loop previously just ended, leaving `isStarting` true forever, so a
// blocked network was indistinguishable from a slow one.
// blocked network was indistinguishable from a slow one. A deliberate
// shutdown mid-bootstrap is not a stall, hence the generation check.
if !didComplete {
await MainActor.run {
self.isStarting = false
self.bootstrapDidStall = true
SecureLogger.warning(
"TorManager: bootstrap did not complete within its deadline (progress=\(self.bootstrapProgress)); network may be blocking Tor",
category: .session
)
NotificationCenter.default.post(name: .TorBootstrapDidStall, object: nil)
}
guard generation == bootstrapGeneration else { return }
self.isStarting = false
self.bootstrapDidStall = true
SecureLogger.warning(
"TorManager: bootstrap did not complete within its deadline (progress=\(self.bootstrapProgress)); network may be blocking Tor",
category: .session
)
NotificationCenter.default.post(name: .TorBootstrapDidStall, object: nil)
}
}
@@ -352,6 +357,7 @@ public final class TorManager: ObservableObject {
// Clear isStarting so foreground recovery can proceed if bootstrap was interrupted.
SecureLogger.debug("TorManager: goDormantOnBackground() called", category: .session)
Task { @MainActor in
self.bootstrapGeneration += 1
self.isReady = false
self.socksReady = false
self.isStarting = false
@@ -361,6 +367,7 @@ public final class TorManager: ObservableObject {
public func shutdownCompletely() {
SecureLogger.debug("TorManager: shutdownCompletely() called", category: .session)
startPendingAfterShutdown = false
bootstrapGeneration += 1
shutdownsInFlight += 1
Task.detached { [weak self] in
guard let self = self else { return }
@@ -398,6 +405,7 @@ public final class TorManager: ObservableObject {
SecureLogger.debug("TorManager: restartArti() starting", category: .session)
await MainActor.run {
NotificationCenter.default.post(name: .TorWillRestart, object: nil)
self.bootstrapGeneration += 1
self.isReady = false
self.socksReady = false
self.bootstrapProgress = 0