Skip to content

fix(registry): stop deleting whole registry branches on a failed key parse - #261

Merged
dbfx merged 2 commits into
mainfrom
fix/registry-container-key-deletion
Jul 27, 2026
Merged

fix(registry): stop deleting whole registry branches on a failed key parse#261
dbfx merged 2 commits into
mainfrom
fix/registry-container-key-deletion

Conversation

@dbfx

@dbfx dbfx commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes a Registry Cleaner bug that could delete an entire registry branch, found while investigating #244.

The bug

reg.exe echoes results using long-form hive names (HKEY_LOCAL_MACHINE\...), but nine scans in registry-cleaner.ipc.ts matched key headers against the short form (HKLM\...). Those matches never succeeded — verified against real reg query output, where the App Paths header pattern failed on 4/4 blocks.

Eight of the nine scans guarded on the match (if (keyMatch && ...)) and were silently dead. The App Paths scan did not. It fell back to the container key:

keyPath: keyMatch?.[1] || 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths',
fix: { op: 'delete-key' }

So a single App Paths entry pointing at an uninstalled program produced:

reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths" /f

…removing every App Paths registration on the machine instead of the one dead entry. The finding was pre-ticked and labelled risk: 'low'. Stale App Paths entries are common on any machine that has uninstalled software, so this was reachable in ordinary use.

The bug predates #68, so it has been present for a long time.

Changes

  • Normalize hive names in execReg output (normalizeHiveNames) so key headers parse. Only line-leading hive names are rewritten — value lines are indented four spaces, so registry data containing a hive name is untouched. This mirrors normalizeKeyPath already used in context-menu-cleaner.ipc.ts.
  • Remove the container-key fallbacks in the App Paths and shellex ContextMenuHandlers scans. If a scan cannot identify the specific subkey it now reports nothing rather than guessing upward.
  • Add isProtectedDeleteKeydelete-key hard-refuses hive roots and the container keys the scans enumerate, at execution time, regardless of how the entry was produced. Defence in depth so a future parsing regression cannot wipe a branch again. Refusals surface as a normal per-entry failure.
  • Ship the eight revived scans deselected (UNVALIDATED_SCAN_SELECTED). They have never executed in production and several delete whole COM registration keys (HKCR\CLSID\{…}, HKCR\Interface\{…}), so they are opt-in per finding until validated on real machines.

On that last point

Fixing the root cause activates eight scans that have never run. On this machine they now produce matches at scale — e.g. the COM Interface scan matches 28,630 blocks (capped at 30 findings). Landing those pre-ticked in a bugfix for a registry-corruption report would risk causing the next one, hence the deselect. Turning them on is a separate change that deserves its own validation.

Testing

New registry-cleaner.scan.test.ts drives the real scanRegistry / fixRegistryEntries against verbatim reg.exe output:

  • App Paths targets the stale subkey, never the container
  • nothing is emitted when the key header cannot be parsed
  • revived findings are reported but arrive deselected
  • isProtectedDeleteKey blocks hive roots and containers in both hive forms, and allows genuine leaf keys
  • fixRegistryEntries refuses a container-key delete and issues no reg delete, but still deletes a real leaf key

npm test: 2229 passing. No new type errors (tsc reports only pre-existing failures in unrelated files).

Why this wasn't caught

registry-cleaner.ipc.test.ts replicates the helpers under test rather than importing them, so a regex that never matched real reg.exe output could not fail a test. The new file imports the real module instead. Other replica-based suites in the repo likely share this blind spot — worth a follow-up.

Note on #244

This does not explain that reporter's PresentationCore error — that is a .NET Framework/GAC problem, and Kudu touches neither the GAC nor C:\Windows\Microsoft.NET. Said so plainly in the issue and pointed them at .NET repair plus their Documents\Kudu Backups .reg files. Fixing this bug is worthwhile on its own merits.

Refs #244

…parse

reg.exe echoes long-form hive names (HKEY_LOCAL_MACHINE\...), but nine scans
matched key headers against short forms (HKLM\...), so those matches never
succeeded.

The App Paths scan fell back to its container key when the match failed, so a
single stale entry produced a delete of HKLM\...\CurrentVersion\App Paths
itself — removing every App Paths registration on the machine rather than the
one dead entry. The other eight scans guarded on the match and were silently
dead, so they have never run in production.

- normalize hive names in execReg output so key headers parse
- drop the container-key fallbacks in the App Paths and shellex scans
- refuse delete-key on hive roots and scan container keys at execution time
- ship the eight revived scans deselected pending validation

Refs #244

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8795b1f4fe

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +53 to +54
const result = await execNativeUtf8('reg', args, opts)
return { ...result, stdout: normalizeHiveNames(result.stdout) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep every newly revived delete scan unticked

Normalizing every reg response also makes the generic header matches in the ContextMenuHandlers, services, uninstall, and registered-client scans succeed for the first time, but those findings still use selected: true at lines 566/581, 802, 909, and 1041. On normal reg.exe output, a scan followed by the default fix action can therefore issue reg delete for heuristics that, according to the new safety comment, have never run against a real machine; apply UNVALIDATED_SCAN_SELECTED to these newly reachable delete-key findings as well.

Useful? React with 👍 / 👎.

Comment thread src/main/ipc/registry-cleaner.ipc.ts Outdated
Comment on lines 419 to 423
valueName: appName,
issue: `File association references unregistered app: ${appName}`,
risk: 'low',
selected: true,
selected: UNVALIDATED_SCAN_SELECTED,
fix: { op: 'delete-value' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Capture the OpenWithList value name before deleting

When an OpenWithList block contains a normal entry such as a REG_SZ firefox.exe, appMatch captures firefox.exe, and the revived scan stores that data as valueName. If the user opts into this finding, the fix consequently runs reg delete ... /v firefox.exe instead of deleting value a, so the operation fails and leaves the stale association intact; parse the value name and executable data separately.

Useful? React with 👍 / 👎.

Comment thread src/main/ipc/registry-cleaner.ipc.ts Outdated
Comment on lines 681 to 682
selected: UNVALIDATED_SCAN_SELECTED,
fix: { op: 'delete-key', key: parentTypeLibKey }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid TypeLib siblings when cleaning one path

When a TypeLib GUID has multiple registered versions or both win32 and win64 entries, one missing win32 file causes this newly enabled scan to offer a fix whose explicit key is the entire HKCR\TypeLib\{GUID} parent. Opting into that finding deletes every valid sibling registration under the GUID, potentially breaking applications that use another version or architecture; target the affected version/leaf or verify that every registration below the GUID is stale before deleting the parent.

Useful? React with 👍 / 👎.

…scans

Two defects in scans this branch revives, both found in review.

TypeLib offered a fix whose key was the whole HKCR\TypeLib\{GUID} parent. A
GUID node holds every registered version and architecture — {00000300-...}
carries both 2.8 and 6.0 on a stock install — so one missing win32 file would
have deleted the surviving registrations. It now deletes the matched platform
key alone, and ignores a GUID node with no version segment below it.

OpenWithList stores the app in the value data under an ordinal value name
(`a REG_SZ firefox.exe`), but the scan matched REG_SZ alone and stored the
data as valueName, producing `reg delete /v firefox.exe` — a delete that can
only fail. It now parses name and data separately, checks every value in the
block rather than the first, and skips the MRUList ordering index.
@dbfx

dbfx commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — both P2s were real and are fixed in 746af3b. Pushing back on the P1.

P2 — TypeLib parent delete ✅ fixed

Confirmed on a stock Windows install: {00000300-0000-0010-8000-00AA006D2EA4} carries both 2.8 and 6.0 under the same GUID. Deleting the GUID parent because one win32 file was missing would have taken the surviving version with it — which is precisely the "delete a parent because a child is stale" defect this PR exists to fix, so thanks for catching that it survived in another scan.

The fix targets the matched platform key alone (…\{GUID}\2.8\0\win32) and drops the key override. The regex now requires a version segment below the GUID, so a bare GUID node can't be flagged either.

P2 — OpenWithList value name ✅ fixed

Also confirmed against the real key:

    a          REG_SZ    Microsoft.WindowsNotepad_8wekyb3d8bbwe!App
    c          REG_SZ    sublime_text.exe
    d          REG_SZ    firefox.exe
    MRUList    REG_SZ    cdabe

The app is the value data; the value name is an ordinal. REG_SZ\s+(.+\.exe) captured the data, so the fix would have run reg delete /v firefox.exe against a value actually named d — a delete that can only fail.

Now parses name and data separately. Two related bugs fixed alongside it: the old code matched only the block's first .exe, so additional stale entries were invisible; and MRUList is now skipped explicitly, since it's the ordering index rather than an app reference.

P1 — newly reachable delete scans ❌ premise is incorrect

The claim is that normalization makes the ContextMenuHandlers, services, uninstall, and registered-client scans "succeed for the first time". They were already succeeding. Those four don't use short-form patterns — they match /^(HK[^\r\n]+)/m and /^(HK\w+\\SOFTWARE\\…)/m, both of which match HKEY_LOCAL_MACHINE\… fine (\w covers the underscores).

Measured against real reg query output without normalization:

ok (9/9)               shellex ContextMenuHandlers
ok (820/821)           Services ImagePath
ok (150/150)           Uninstall
ok (1/2)               Clients

So they were live and selected: true before this branch. Deselecting them would be an unrelated behaviour change to scans this PR doesn't make reachable, and it would weaken a shipping feature on a false premise. UNVALIDATED_SCAN_SELECTED is deliberately scoped to the nine scans that genuinely never executed — the ones whose patterns were short-form only.

One thing the review missed that's worth noting: normalization does change the uninstall scan, just in the safe direction. Its if (subKey === uninstallKey) continue guard compared a long-form subKey against a short-form constant and therefore never fired. It now works as intended.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 746af3b15b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

fix: { op: 'delete-value' }
})
}
entries.push({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check per-user App Paths before marking an app stale

When an executable is registered only under HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths—a supported per-user App Paths location—the HKLM query fails and this catch reports its OpenWithList entry as obsolete. The user is therefore offered a fix that deletes a working association; query the HKCU location as well before creating the finding.

Useful? React with 👍 / 👎.

@dbfx
dbfx merged commit 106bb56 into main Jul 27, 2026
8 of 9 checks passed
@dbfx
dbfx deleted the fix/registry-container-key-deletion branch July 27, 2026 03:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant