tsudev # The safety model tsudev · tsudev.com
--- A debloater's failure mode is a machine that will not boot. This document is the design rationale for everything that stands between a user and that outcome, so that changes to the safety layer are made deliberately rather than by accident. ## The three layers They are independent on purpose. Any one of them failing still leaves two. ``` user selects items │ ┌────────────▼─────────────┐ │ 1. SafetyDatabase │ classify: Safe / Caution / Critical / Unknown │ safety.rs │ fails closed - unmatched is Unknown, not Safe └────────────┬─────────────┘ │ ┌────────────▼─────────────┐ │ 2. RemovalPlan::build │ Critical → refused, always │ plan.rs │ Caution/Unknown → needs per-item confirmation └────────────┬─────────────┘ │ ┌────────────▼─────────────┐ │ 3. guard::validate_* │ every path and key, immediately before deletion │ guard.rs │ drive roots, system dirs, user data, hive roots └────────────┬─────────────┘ │ restore point .reg backups transaction log │ execute ``` ## Layer 1 - classification `data/safety-db.json` holds 58 rules. Each carries a class, matching criteria, a bilingual reason, and optional knowledge about the product's processes, services, tasks and residue. ### The four classes | Class | Test | User experience | |---|---|---| | `safe` | Windows boots, logs in and works identically without it | Selectable in bulk | | `caution` | Windows still works, but a feature the user might rely on is gone | Needs a per-item confirmation dialog that names what is lost | | `critical` | Windows fails to boot, fails to log in, loses security, or loses the shell | Not selectable. No checkbox is rendered. | | `unknown` | No rule matched | Treated as `caution` for gating, but labelled distinctly | ### Two invariants **Fail closed.** An item that matches nothing is `Unknown`. It is tempting to default unmatched third-party software to `Safe` - most of it is - but the one time it is a line-of-business application, the user finds out afterwards. **Severity beats specificity.** If an item matches a broad `Safe` rule and a narrow `Critical` rule, the verdict is `Critical`. The classifier collects every match, takes the highest severity, and only then uses specificity to pick which rule's *explanation* to show. A more specific rule can never downgrade a protection. ### Choosing a class The question is not "does this break anything" but **"what does the user lose, and would they have predicted it?"** * Microsoft Edge is `caution`, not `safe`: Windows renders PDF previews, help pages and some Settings panes with it. It is also not `critical` - the machine boots and works fine, and people remove it deliberately every day. * Windows Camera is `caution`: other apps still reach the webcam, but Windows Hello enrolment stops working. That is a surprise worth one dialog. * The Xbox Game Bar is `safe`, but the Xbox **Identity Provider** is `caution` - Game Pass titles fail to sign in without it. Splitting a product family across two classes is normal and correct. * Shared runtimes (VC++, .NET, WebView2) are `critical` even though removing one does not stop the machine booting. They break *other* software, silently and later, which is worse than an obvious failure. When in doubt, classify one step stricter. A wrong `caution` costs the user a click. A wrong `safe` costs them something they never agreed to lose. ## Layer 2 - the planning gate `RemovalPlan::build` is the only way to produce a plan, and it is where selections become work. Three things happen there: 1. **`Critical` items are dropped.** Not warned about - dropped, with a `protected_component` rejection the UI displays. There is no flag, no CLI switch and no confirmation that overrides this. `assert_no_protected_items` re-checks the invariant immediately before execution, so a future refactor that reintroduced the hole would abort the run rather than proceed. 2. **`Caution` and `Unknown` need `confirmed: true`**, which only the per-item dialog sets. "Select all safe" cannot set it, by construction. 3. **Steps are ordered.** Services and scheduled tasks are quiesced before the programs that own them; AppX removal precedes deprovisioning; deep clean is last. ### Services are disabled, never deleted A service's registry key *is* the service. `Uninstall` on a service means stop and set `StartType = Disabled` - one call to undo. Deep clean deliberately does not apply to services, scheduled tasks or autostart entries; the guard also refuses `HKLM\SYSTEM\CurrentControlSet\Services` at every depth, so the two layers cover each other. ### A failed step stops that item If the vendor's uninstaller fails, the deep-clean steps for that item do not run. Sweeping a product's folders after a failed uninstall is how a machine ends up with a half-removed application that can neither run nor be reinstalled. ## Layer 3 - the deletion guard Residue paths come from the registry, which means they come from vendors, which means some of them are wrong. `InstallLocation = C:\` ships in real products. `guard::validate_delete_path` runs on every path immediately before deletion and rejects: * drive roots and the system directories - `C:\Windows`, `System32`, `SysWOW64`, `WinSxS`, `Program Files`, `ProgramData`, `WindowsApps` * **any path that is a parent of a protected directory**, which catches intermediates nobody thought to list * user profile roots, and everything under `Documents`, `Desktop`, `Downloads`, `Pictures`, `OneDrive` * shared containers - `AppData`, `AppData\Local`, `Packages`, `Temp`, `Start Menu\Programs` * anything with an unexpanded `%VARIABLE%`, a `..` traversal, a wildcard, or a UNC prefix The unexpanded-variable rule is load-bearing: if `%LOCALAPPDATA%` failed to expand, `%LOCALAPPDATA%\Vendor\App` must not quietly become `\Vendor\App`. Environment expansion deliberately leaves unknown variables intact so the guard trips on them. ### The distinction that matters ``` C:\Users\me\OneDrive <- the user's synced files: never C:\Users\me\AppData\Local\Microsoft\OneDrive <- the client's own state: yes ``` A folder *named* OneDrive is not the thing to protect; a known folder directly under a profile is. Getting this wrong in either direction is a real bug - too strict and deep clean does nothing useful, too loose and it deletes someone's documents. ## Rollback Before the first destructive step: 1. **System Restore Point** - `SRSetRestorePointW` with `APPLICATION_UNINSTALL`, bracketed by `BEGIN_`/`END_SYSTEM_CHANGE`. If it fails and `require_restore_point` is set (the default), the run is **cancelled**. Windows also throttles restore points to one per 24 hours by default; a throttled call returns success with sequence number 0, which the code detects and reports rather than treating as protection. 2. **`.reg` export** of every key the run will touch, in both registry views, via `reg.exe export` - a text file the user can read and re-import from Explorer, rather than a binary hive only this tool understands. A generated `restore-registry.cmd` re-imports them all. 3. **Transaction log** - JSON, one file per run, recording every step, its status, its duration and the artefacts it touched. ## Processes that are never terminated Step one of removal terminates the software's processes. Two groups are exempt regardless of what any rule says: * **Shared hosts** - `svchost.exe`, `dllhost.exe`, `RuntimeBroker.exe`, `taskhostw.exe`. Killing `svchost.exe` stops a dozen unrelated services. * **Boot and session critical** - `lsass.exe`, `csrss.exe`, `wininit.exe`, `winlogon.exe`, `services.exe`, `smss.exe`, `dwm.exe`, `explorer.exe`. * **Security** - `MsMpEng.exe`, `SecurityHealthService.exe`. Terminating these disables protection mid-run. A telemetry service running inside `svchost.exe` is stopped through the service control manager instead, which is the correct mechanism anyway. ## Testing the safety layer The tests that matter are the adversarial ones: | Test | What it proves | |---|---| | `selecting_everything_still_cannot_remove_a_critical_component` | Ticking every row, confirming every prompt and pressing go leaves Defender and RPC untouched | | `critical_beats_safe_when_both_match` | A specific rule cannot downgrade a protection | | `unmatched_software_is_unknown_not_safe` | The database fails closed | | `a_required_restore_point_that_fails_aborts_before_anything_is_touched` | No rollback means no run | | `a_failing_uninstaller_stops_that_item_but_not_the_run` | Deep clean never follows a failed uninstall | | `deep_clean_never_deletes_a_service_definition` | Services survive deep clean | | `a_folder_named_like_a_known_folder_is_still_residue_when_deep_in_appdata` | Both directions of the OneDrive distinction | | `shared_host_processes_are_never_terminated` | `svchost.exe` survives | Run them with: ```bash cargo test ``` ---
tsudev
tsudev