|
| 1 | +# Tweak System Remediation — Roadmap |
| 2 | + |
| 3 | +The authoritative stage-by-stage tracker for the tweak-system remediation. This is the map; the detail |
| 4 | +lives elsewhere and is linked per stage: |
| 5 | + |
| 6 | +- [TWEAK_SYSTEM_REVIEW.md](./TWEAK_SYSTEM_REVIEW.md) — the 47-finding audit |
| 7 | +- [TWEAK_SYSTEM_PLAN.md](./TWEAK_SYSTEM_PLAN.md) — reasoning, crate research, contradiction resolutions, stage-2 outcome |
| 8 | +- [adr/](./adr/) — the four design decisions taken deliberately |
| 9 | +- [spec/profile-v1.md](./spec/profile-v1.md) — the deleted profile format, preserved for the rebuild |
| 10 | + |
| 11 | +**Guiding priorities, in order:** correctness first, then less code, then optimized. Where these conflict, |
| 12 | +the conflict is named rather than silently resolved. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## Status at a glance |
| 17 | + |
| 18 | +| Stage | What | Status | |
| 19 | +| --- | --- | --- | |
| 20 | +| 0 | Critical rollback fix + `serde_yml` swap + build determinism | ✅ done (PR #31) | |
| 21 | +| 1 | Delete: profile system + dead code (~2,830 LOC) | ✅ done (PR #31) | |
| 22 | +| 2 | Test seams + CI (57 → 75 tests) | ✅ done (this branch) | |
| 23 | +| 3 | Effect layer: one "did-it-work" contract + API swaps | ⏳ next | |
| 24 | +| 3a | Elevation module (SYSTEM / TrustedInstaller) | 🔬 researching | |
| 25 | +| 4 | Collapse duplication (detection+inspection, build schema, loader) | ▫ planned | |
| 26 | +| 5 | Design fixes: the ADRs (Needs Attention, atomic rollback, hosts, snapshot identity) | ▫ planned | |
| 27 | +| 6 | UI: System Default selectable, Needs Attention surfaced | ▫ planned | |
| 28 | +| 7 | Docs rewritten against what exists | ▫ planned | |
| 29 | +| — | YAML corpus fixes (independent) | ▫ unblocked, can start anytime | |
| 30 | +| L | Later: profile rebuild, comprehensive logging | ▫ deferred | |
| 31 | + |
| 32 | +**Ordering rule that is easy to get wrong:** stage 4 (collapse detection+inspection) must precede stage 5. |
| 33 | +"Needs Attention" is a new status state; adding it while detection and inspection are still two divergent |
| 34 | +implementations means implementing it twice — and they already disagree. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## ✅ Stage 0 — Critical fixes (done, PR #31) |
| 39 | + |
| 40 | +- `apply.rs` no longer discards the rollback result and deletes the snapshot regardless. A snapshot is |
| 41 | + released only on a verified-complete rollback; otherwise it is kept and every unrestored resource |
| 42 | + reported. The decision is a pure `classify_rollback` with 6 tests. Implements ADR-0001, ADR-0002. |
| 43 | +- `serde_yml` → `serde_yaml_bw` (RUSTSEC-2025-0068: unsound, archived). Verified byte-identical output. |
| 44 | +- `build.rs` emits `tweaks.json` deterministically (`HashMap` → `BTreeMap`). |
| 45 | +- CRLF enforced as a repo property. |
| 46 | + |
| 47 | +## ✅ Stage 1 — Delete (done, PR #31) |
| 48 | + |
| 49 | +- Profile system removed entirely (~2,169 LOC, 7 commands, `zip`/`sha2`/`hex`/`hostname`). UI kept but |
| 50 | + disabled. Format recorded in [spec/profile-v1.md](./spec/profile-v1.md) first. |
| 51 | +- Dead code removed (~660 LOC): `models/backup.rs`, the second YAML pipeline in `models/tweak.rs`, |
| 52 | + `aliases`, 8 never-invoked Tauri commands, 4 unused `Error` variants. |
| 53 | +- A drift guard added: embedded tweak data must deserialize into the runtime types. |
| 54 | + |
| 55 | +## ✅ Stage 2 — Test seams + CI (done, this branch) |
| 56 | + |
| 57 | +Tests 57 → 75, driving the **real** registry under an HKCU scratch subtree rather than a mock. `AppHandle` |
| 58 | +removed from the apply chain (a `OnceLock` in `debug.rs`). Round-trips, hosts parsing, firewall arg |
| 59 | +building, and the detection↔inspection divergence all pinned. Accepted gaps and two audit corrections |
| 60 | +recorded in [TWEAK_SYSTEM_PLAN.md § Stage 2 outcome](./TWEAK_SYSTEM_PLAN.md). |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## ⏳ Stage 3 — The effect layer's success contract |
| 65 | + |
| 66 | +**The root-cause stage.** Six confirmed findings are one bug wearing different clothes: an error converted |
| 67 | +into a benign-looking value, so a failed privileged operation reports success and rollback never fires. |
| 68 | + |
| 69 | +| Site | Error becomes | |
| 70 | +| --- | --- | |
| 71 | +| `capture.rs:588` | access-denied → `existed: false` → **revert deletes a value that existed** | |
| 72 | +| `detection.rs:234` | read failure → "missing" → `*_missing_is_match` → inferred MATCH | |
| 73 | +| `ti_elevation.rs:468` | exit code returned, caller discards it | |
| 74 | +| `firewall_service.rs:25` | netsh exit status ignored → create silently no-ops | |
| 75 | +| `restore.rs:388` | provably-failed restore returns `Ok(())` → snapshot deleted | |
| 76 | +| `service_ops.rs:89` | `net.exe` exit 2 treated as success | |
| 77 | + |
| 78 | +**Goal:** one shared definition of "did it work" across the five effect services, replacing five different |
| 79 | +ones. Until this lands, ADR-0001 and ADR-0002 are decorative. |
| 80 | + |
| 81 | +**Tasks** |
| 82 | +- `service_control.rs` → raw `windows-sys` (already a dependency; kills locale-dependent `sc.exe`/`net.exe` |
| 83 | + string parsing). Keep the 4 public signatures so callers do not change. Handle the gotchas already |
| 84 | + catalogued in the plan (`SERVICE_NO_CHANGE`, `QueryServiceConfigW` sizing, `STOP_PENDING` polling, |
| 85 | + `EnumDependentServicesW`). |
| 86 | +- Task Scheduler → `windows` crate COM (`TASK_STATE` is a numeric enum — the actual fix for the locale class). |
| 87 | +- `capture.rs:588` → stop mapping access-denied to `existed: false`. **Blocker for ADR-0003.** |
| 88 | +- `winreg` stays: it already distinguishes `NotFound` from `PermissionDenied`; the 8 `.unwrap_or(false)` |
| 89 | + call sites coerce that away. Our bug, not the crate's. |
| 90 | +- Firewall: locale-dependent parsing is the only remaining driver (injection is disproven for that path — |
| 91 | + stage 2). Lower priority; hand-roll COM or fix the exit-status check, decide when we get here. |
| 92 | +- Collapse the elevated duplicates and the `if use_ti / else if use_system / else` triad behind one |
| 93 | + `Elevation` enum. |
| 94 | + |
| 95 | +**Verify:** the HKCU round-trip harness from stage 2 covers registry; extend the parser-fixture and |
| 96 | +nonexistent-name patterns for services/scheduler. A failed elevated op must now surface as an `Err`. |
| 97 | + |
| 98 | +**Unblocks:** stage 5 (the ADRs need truthful failure signals), stage 4's confidence. |
| 99 | + |
| 100 | +## ⏳ Stage 3a — Elevation module (researched; scoped) |
| 101 | + |
| 102 | +The owner was not confident the hand-written SYSTEM/TrustedInstaller elevation (~1,294 lines of `unsafe` |
| 103 | +FFI) is correct. A three-probe research pass (crate landscape, safety audit, design alternatives) settled |
| 104 | +it. Detail in [TWEAK_SYSTEM_PLAN.md § Elevation](./TWEAK_SYSTEM_PLAN.md); the verdict: |
| 105 | + |
| 106 | +**Do not replace it with an elevation crate — none worth having exists.** |
| 107 | +- crates.io has **zero** TrustedInstaller crates (verified). The whole landscape is a few C++/Go/twinBASIC |
| 108 | + PoCs; the current Rust code faithfully ports the de-facto reference (`nfedera/run-as-trustedinstaller`). |
| 109 | +- Every "SYSTEM elevation" crate solves a *different* problem — interactive UAC-to-admin, or privilege |
| 110 | + *dropping* — none silently duplicate winlogon's token. `runas-rs` is also **GPL-3.0** (would infect a |
| 111 | + shipped product). Adopting any wrapper of this technique means trusting an unaudited single author with |
| 112 | + the ability to run code as TrustedInstaller — an unacceptable supply-chain trade. |
| 113 | + |
| 114 | +**The hard parts are correct — this partly refutes the audit.** Token duplication, the parent-process |
| 115 | +spoof (attribute list sized-then-initialised, parent handle outliving `CreateProcessW`, |
| 116 | +`DeleteProcThreadAttributeList` on all paths), and handle/`SC_HANDLE` lifetimes are all sound. No active |
| 117 | +leak was found; the audit's "unclosed handles on error paths" is largely **not** borne out. The real |
| 118 | +weakness there is *fragility*: ~15 manual `CloseHandle` sites where one future early-return leaks. |
| 119 | + |
| 120 | +**The genuine defects are all one systemic mistake: shell command strings.** Every elevated op is funnelled |
| 121 | +through `cmd.exe /c <string>` with values escaped by the broken `escape_shell_arg` (`common.rs:72`, which |
| 122 | +mixes caret-escaping and quote-doubling — two incompatible models, both wrong inside `cmd` quotes). That |
| 123 | +one design yields: |
| 124 | + |
| 125 | +| Sev | Site | Effect | Review # | |
| 126 | +| --- | --- | --- | --- | |
| 127 | +| **critical** | `ti_elevation.rs:92` | `\"`-escape + `cmd /c` → a `"`+`&` in a PowerShell script runs a separate command as SYSTEM/TI. Author-controlled today, so a *correctness* landmine with SYSTEM blast radius; a real vuln if user input ever reaches it (profile rebuild). | #27 | |
| 128 | +| high | `system_elevation.rs:194` | REG_SZ with a space or `%VAR%` is double-mangled and silently corrupted | #26 | |
| 129 | +| high | `ti_elevation.rs:468` + callers `helpers.rs:772,870` | schtasks exit code discarded (`.map(\|_\| ())`) → failed elevated task = success | #8 | |
| 130 | +| medium | `system_elevation.rs:110`, `ti_elevation.rs:398` | `GetExitCodeProcess` return ignored, `WAIT_FAILED` not distinguished from `WAIT_TIMEOUT` → possible silent `Ok(0)` | new | |
| 131 | +| medium | `common.rs:107` | `SeDebugPrivilege` enabled process-wide and never dropped | new | |
| 132 | + |
| 133 | +**Scope (fits inside stage 3's contract — this IS how ops apply under SYSTEM/TI):** |
| 134 | +1. **Delete the `cmd.exe /c` design and `escape_shell_arg`.** Registry and service ops (the bulk) call |
| 135 | + Win32 APIs directly with **typed** args (`RegSetValueExW`/`RegDeleteValueW`, |
| 136 | + `ChangeServiceConfigW`/`StartServiceW`/`ControlService`) in the elevated broker — dissolving the |
| 137 | + injection *and* corruption classes and turning exit codes into return values. This is literally stage |
| 138 | + 3's "did-it-work contract" at the elevated layer. |
| 139 | +2. PowerShell (the only irreducible interpreter case) → spawn `powershell.exe` directly as argv with |
| 140 | + `-EncodedCommand` (base64 UTF-16LE); no shell ever parses script bytes. |
| 141 | +3. schtasks helpers map non-zero exit → `Err` at the source, so no caller can drop it. |
| 142 | +4. Adopt the official **`windows`** crate (0.62, Microsoft, 272M downloads) for *this module only* — not as |
| 143 | + an elevation crate but for `Owned<HANDLE>` (CloseHandle-on-drop) + typed `Result`s, making the correct |
| 144 | + cleanup structural. Coexists with `windows-sys` elsewhere. Keep `to_wide_string` (an 8-line helper |
| 145 | + doesn't justify `widestring`); reject `sysinfo` (heavy) for the ToolHelp lookup. |
| 146 | +5. Do **not** move to in-process `ImpersonateLoggedOnUser`: under `panic = "abort"` a panic before |
| 147 | + `RevertToSelf` leaks SYSTEM onto the thread. Keep the spawn-a-broker design. |
| 148 | + |
| 149 | +Verified caveat on the `windows` crate: `Owned`'s `Drop` still won't run on panic (same as today), but it |
| 150 | +covers the normal + `?`-early-return paths, which is strictly more than the manual pattern. |
| 151 | + |
| 152 | +## ▫ Stage 4 — Collapse duplication |
| 153 | + |
| 154 | +- **detection.rs + inspection.rs → one comparison core** (−331 LOC; the highest-value item). They disagree |
| 155 | + in four verified ways (REG_BINARY hex, `*_missing_is_match`, vacuous-empty-option, service errors). The |
| 156 | + stage-2 test `values_match_disagrees_with_registry_values_match_on_binary_hex_strings` is the red marker: |
| 157 | + when the collapse works, that test starts failing and should be deleted, not inverted. |
| 158 | +- **Share the build schema** (−304 LOC): move pure types to `models/tweak_schema.rs`, `build.rs` includes |
| 159 | + only that. Drift becomes a compile error instead of a runtime panic. |
| 160 | +- **Kill the loader clones**: `get_tweaks_for_version` deep-clones up to 189 tweaks per call; return |
| 161 | + `&'static` references. |
| 162 | + |
| 163 | +**Must precede stage 5.** |
| 164 | + |
| 165 | +## ▫ Stage 5 — Design fixes (the ADRs) |
| 166 | + |
| 167 | +- Rollback always attempts all five phases, never aborts early (ADR-0001). |
| 168 | +- **Needs Attention** state: retains its snapshot, names unrestorable resources, offers Retry and explicit |
| 169 | + "Keep current state". Enforces the ADR-0002 invariant (snapshot deleted only by verified restore or |
| 170 | + explicit consent). |
| 171 | +- `tempfile` for atomic snapshot writes (verified `MoveFileExW` + `MOVEFILE_REPLACE_EXISTING`). Drop `fs4` |
| 172 | + for `std::fs::File::lock`. |
| 173 | +- Hosts file: rewrite ~120 LOC by hand (no crate survives the requirements). Fixes the BOM, line-ending |
| 174 | + (`join("\n")` at `hosts_service.rs:210`), and multi-hostname bugs pinned in stage 2. |
| 175 | +- **Snapshot identity** (ADR-0003 consequence): stamp snapshots with `MachineGuid`, warn on mismatch. |
| 176 | + **Prerequisite:** add `#[serde(default)]` + a schema version to `TweakSnapshot` FIRST (finding #18), or |
| 177 | + the added field breaks every existing snapshot. |
| 178 | + |
| 179 | +## ▫ Stage 6 — UI |
| 180 | + |
| 181 | +- System Default made selectable whenever `has_backup` (ADR-0003). `TweakCard.svelte:176` currently only |
| 182 | + unstages; the dropdown entry is `disabled` and vanishes once any option is applied. |
| 183 | +- Needs Attention surfaced with its Retry / Keep-current-state actions. |
| 184 | + |
| 185 | +## ▫ Stage 7 — Docs |
| 186 | + |
| 187 | +`TWEAK_SYSTEM.md`, `TWEAK_AUTHORING.md`, `APP_CONTEXT.md` updated together against final behaviour: |
| 188 | +atomicity wording per ADR-0001 (*attempted atomically, with failure surfaced* — not a guarantee), System |
| 189 | +Default, Needs Attention, snapshot location, the full five-phase detection algorithm. |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## ▫ Independent track — YAML corpus |
| 194 | + |
| 195 | +Unblocked by everything above; can start anytime. From the review: |
| 196 | + |
| 197 | +- **`legacy_network_protocols` silently re-enables SMBv1** in two options while its name/description/info |
| 198 | + mention only LLMNR/WPAD/NetBIOS. **Treat as urgent** — a live security-relevant data bug needing no |
| 199 | + refactor. |
| 200 | +- 98 "(Default)" options write explicit Group Policy values that stock Windows ships *absent*. |
| 201 | +- Asymmetric toggles where option[1] does not undo everything option[0] does. |
| 202 | +- `mouse_input_mode` options 1 and 3 are byte-identical (option 3 unreachable). |
| 203 | +- Two gaming tweaks overwrite the same `DirectXUserGlobalSettings` string, erasing each other. |
| 204 | + |
| 205 | +## ▫ Deferred — Later |
| 206 | + |
| 207 | +- **Profile system rebuild** from scratch, sharing one machine-identity mechanism with the snapshot install |
| 208 | + ID. Read [spec/profile-v1.md](./spec/profile-v1.md) first — `.mgx` archives from 3.0.0 are recoverable |
| 209 | + only if the option content hash is reproduced exactly. |
| 210 | +- **Comprehensive logging** users can attach to bug reports. `tauri-plugin-log` stays (it does file logging |
| 211 | + with rotation); fix the dead `TargetKind::Webview` target and add `LogDir`. |
| 212 | + |
| 213 | +--- |
| 214 | + |
| 215 | +## Dependency ledger (net so far and planned) |
| 216 | + |
| 217 | +| Removed | Added | |
| 218 | +| --- | --- | |
| 219 | +| `serde_yml` (RUSTSEC) → `serde_yaml_bw` | — | |
| 220 | +| `zip`, `sha2`, `hex`, `hostname` (with profiles) | — | |
| 221 | +| planned: `fs4` → `std::fs::File::lock` | planned: `windows` (COM), `tempfile` | |
| 222 | +| planned: `reqwest` → `ureq` (−28 crates) | planned: `schemars` (build/dev) | |
| 223 | + |
| 224 | +Elevation crate decisions are pending stage-3a research. |
0 commit comments