Skip to content

syncthing-rust v3.0.5 — Data-Loss Hardening + E2E Race Elimination

Latest

Choose a tag to compare

@github-actions github-actions released this 18 Oct 04:15

2026-10-18(Data-Loss Hardening + E2E Race Elimination)

Fixed

  • Remote-deletion hardening (2026-07-20 data-loss incident): three layered defenses. (1) Per-pull deletion quota — refuses remote deletion batches > max(20, 25% of local files); the v3.0.4 50% full-index guard did not cover incremental IndexUpdate batches (114 files lost to a stale peer index). (2) Soft delete — puller quarantines remote-driven deletions to <folder>/.sttrash/<date>/ (7-day retention, cleaned per pull, scanner-ignored) instead of hard removal. (3) Deletion audit — every accepted remote deletion is logged at info level with source device and both version vectors. Also: stale-index health warning when a pull sees >10 NO_SUCH_FILE (error code 2) responses.
  • Version vector used hardcoded counter id 1: all local modifications bumped the vector under a constant device id, so concurrent offline edits on two nodes looked like linear history — a remote delete silently dominated and overwrote the peer's local changes (downstream Obsidian bridge report: deletions resurrecting / modifications lost). Local counter id is now derived from the real device ID (DeviceId::counter_id(), first 8 bytes big-endian, Go ShortID semantics) and plumbed through SyncService → FolderModel → Scanner. Concurrent delete-vs-modify is now detected and preserved as .sync-conflict-*. TestNode populates config.local_device_id; a missing id falls back to 0 with a loud warning.
  • BEP Ping storm: BepSession no longer replies to received Ping messages. BEP Ping is unidirectional keepalive (no Pong exists); two syncthing-rust nodes previously ping-ponged at wire speed (thousands of Pings/sec, ~16MB debug logs in 16 minutes). Ping is now only sent by the 30s heartbeat timer. Reported via Obsidian bridge Android deployment (battery/log impact).
  • TUI Add Folder silently dropped: submit_add_folder built the Folder but never pushed it into the config. Folder additions via TUI (a/Ins on Folders tab) now persist and hot-reload into the running daemon. Stale share selections from a previous Edit Folder no longer leak into new folders.
  • set_block_source after add_folder had no effect: FolderModel captured block_source only at creation, so pulls failed with "No block source configured". Puller now holds the source behind a lock and SyncService::set_block_source propagates to all existing folder models.
  • Config changes now trigger session renegotiation: update_config/add_device/remove_device/add_folder/remove_folder fire a renegotiation hook with the connected device list; the daemon reconnects those devices so ClusterConfig is re-exchanged (matching Go Syncthing behavior). syncthing-sync stays network-free via the injected hook.
  • Spurious remote deletion of in-flight files (found 2026-10-16, data-loss adjacent): a peer's watcher-triggered subtree scan could infer a deletion for a file whose blocks were still being pulled — mark_deleted_subtree() saw a DB record (written by the remote index) with no file on disk, marked it deleted with a fresh version vector, and broadcast it. The peer holding the higher version then moved its only copy into .sttrash, and the file never synced. FolderModel::scan_incremental() now disables subtree delete inference while a pull is in flight (pending_pulls non-empty or status Pulling); watcher-reported explicit delete events and idle-folder scans still infer deletions. Regression test: folder_model::tests::test_incremental_scan_skips_delete_inference_while_pulling (with an idle positive control).
  • .stignore directory rules did not apply to watcher incremental paths: IgnoreMatcher::matches only honoured dir/ rules when is_dir=true, so edits inside ignored directories were still indexed and synced (a privacy leak). Added is_ignored_with_ancestors() (walks ancestor directories) and used it for scan_changed_file and sub-scan roots; full scans keep directory pruning.
  • Failed pulls were not retried until the next index event: FolderModel::start_pull_loop now retries early with a bounded exponential backoff (PULL_RETRY_BACKOFF_INIT_MS 500 ms, doubling up to PULL_RETRY_BACKOFF_MAX_MS = the 10 s period, reset on success) when a pull round reports files_failed > 0; correctness stays with check_needed_files() re-deriving the needed set from DB/FS.
  • E2E ClusterConfig race root-caused (test infrastructure): the WARN Timeout waiting for ClusterConfig that fired on every run came from both peers dialing each other — race resolution closes one of the two connections, and the BepSession on that connection then sat on a link nobody read until its 10 s timeout, pushing every case 10–30 s over its budget and into flakiness. registry.rs race resolution was verified correct and left untouched. Fixes: (1) single-side dialing — e2e_crud::setup_two_node and e2e_sync now have A connect_to(B) while B only add_peer_device(&A) (new TestNode::add_peer_device(), with connect_to/reconnect_to sharing peer_device()); (2) bep_bridge now claims the session slot and the connection synchronously inside on_connected (so concurrent spawns can no longer overwrite each other or bind to a replaced connection) and disconnects only its own conn_id even when the session task is aborted (session slots hold an AbortHandle). Result: zero ClusterConfig-timeout warnings, e2e_crud 6/6 green at ~2.5 s per run (was 26–85 s), e2e_sync 3/3 at ~0.2 s.
  • Both #[ignore]s removed: test_two_node_single_file_sync and test_e2e_modify_file are back to normal execution; the old 30 s disconnect/re-dial workaround in e2e_sync is gone.

Added

  • MCP Bridge toolset 11 → 19: get_health, get_metrics, get_db_status, browse_folder, get_file, pause_folder, resume_folder, poll_events. Optional SYNCTHING_API_KEY is forwarded as X-API-Key; folder/file values are percent-encoded. The tool catalog moved to src/tools.rs to respect the 600-line soft cap, and tests/mcp_bridge.rs drives the real binary against an in-process stub HTTP server (no HTTP framework dependency).
  • TestNode readiness primitives: wait_for_cluster_config() / wait_for_folder_ready(), backed by a SessionReadiness event pump over BepSessionEvent::ClusterConfigComplete — replaces fixed sleeps in E2E setup (connection established ≠ BEP session ready).
  • scripts/agent-facts.ps1: read-only export of drift-prone facts (version, ports, MCP tool count, CI job list, production files over 600 lines) straight from their single sources of truth.
  • docs/design/AGENT_INTEGRATION.md: the Skill / MCP / DSH integration surfaces, including the DSH plugin feasibility conclusion.

Changed

  • Top-level docs restructured for production use: AGENTS.md and CLAUDE.md are now short entry pages holding only red lines and navigation; detailed rules live in docs/agent/. Baseline numbers (test counts, CI job counts) are no longer duplicated across files — docs/agent/testing.md is the single source of truth and the hardcoded test-count README badge was dropped.
  • E2E block-request retry timeout shortened in the test harness (TestBlockSource: 10 s → 5 s per attempt) so retries re-acquire a live connection faster under session churn.
  • Single source of truth for versions: root Cargo.toml [workspace.package] version = "3.0.4"; all 14 member crates inherit via version.workspace = true.
  • Removed stale acceptance-tests/ package: referenced long-removed APIs (e.g. IrohDiscovery); coverage superseded by cmd/syncthing/tests/e2e_* and syncthing-net discovery unit tests.
  • Removed stray git tags main and latest (broke git push origin main refspec resolution).

Docs

  • Archived 7 completed/superseded files from docs/plans/ to docs/archive/plans/; docs/plans/ now holds only 3 active plans + the audit report, with a rewritten INDEX.md.
  • Registered KNOWN_ISSUES.md §24 (the CI e2e-test job runs zero assertions because its only case is #[ignore]d) and refreshed §23 with the 2026-10-16 evidence and this round's mitigations.
  • Rewrote .agents/skills/bep-protocol/SKILL.md (it still described v0.2.8 Alpha): it now points at the authoritative docs instead of restating drifting numbers.
  • Consolidated the documentation tree: the Obsidian [[wikilink]] navigation layer (docs/_Home.md + 9 _MOC.md) moved to docs/archive/obsidian-nav/, leaving docs/README.md as the single OKF entry; docs/README.md rewritten with a role-based entry table (developer / user / agent) and an accurate tree; ENGINEERING_DISCIPLINE.md and GRAY_CLOUD_OPS_MANUAL.md relocated under docs/operations/; 16 broken internal links repaired and the 4 orphan docs indexed.

Stats

  • Tests: 454 passed / 5 ignored / 0 failed (+62 vs v3.0.4 baseline 392; 2026-10-16 实测, cargo test --workspace)
  • Clippy: 0 warnings
  • cargo deny / cargo audit: 通过(3 项上游 unmaintained 传递依赖按 .cargo/audit.toml 显式接受)
  • E2E: e2e_crud 6/6 全绿 ~2.5s/次、e2e_sync 3/3 ~0.2s/次、Timeout waiting for ClusterConfig 告警 0 条
  • Interop: syncthing-rust ↔ official Go Syncthing v2.1.1 verified on loopback (2026-07-21): handshake, bidirectional file sync, 1 MiB multi-block transfer with identical SHA-256