Skip to content

Harden install records and downloads (review 2026-10, D1-D7) - #5

Merged
ApolloF merged 3 commits into
mainfrom
fix/review-2026-10
Oct 4, 2026
Merged

ApolloF merged 3 commits into
mainfrom
fix/review-2026-10

Conversation

@ApolloF

@ApolloF ApolloF commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Fixes the DLSS Updater findings from the 2026-10-04 correctness/security review (Notes/claude/reviews/syncer-dlssupdater.md, findings D1 to D7). It doesn't overlap #4.

Finding → fix → test

Finding Fix Regression test
D1 (medium) Manifest paths not contained, so Uninstall or a Seaglass launch could delete or move files outside the game InstallManifest.Contain(target, gameRoot) runs before every install, uninstall and restore:
• Files and Backups.Original must resolve inside the game root.
• Dirs must be strictly inside the install folder, so a recursive delete never leaves it.
• Backups.Backup must be inside .dlssupdater\backup.
• Rooted paths, :, and .. in entries are rejected; RootRel may only be ./.. parts.
• No junction or symlink between the game root and any entry, and none on the .dlssupdater\backup path.
• Proxy (from the manifest or the options) must be a known proxy name.
• Any violation throws UnsafeManifestException and nothing changes. Ctx.Full also refuses paths outside the root, as defence in depth.
CraftedManifest_UninstallRefuses_AndTouchesNothingOutside (7 cases: dir .., absolute dir, dir outside the install folder, RootRel up, file .., backup original .., backup escaping the backup folder)
CraftedManifest_LaunchStyleReinstall_RefusesProxyOutsideTheGame
ManifestPathThroughJunction_IsRefused
D2 (medium) Same install folder reached from two game roots loses the originals Entries are rebased onto the current game root and RootRel is updated. If the root is narrower and can't reach existing entries, the install is refused instead of misplacing them. SameFolderFromNarrowerRoot_IsRefused_OriginalsStayRestorable
SameFolderFromWiderRoot_RebasesRecord_RestoreReturnsOriginals
D3 (medium) An unreadable manifest is treated as "never installed" and backups get overwritten InstallManifest.Read throws InvalidDataException when the file exists but can't be read; the installer uses it. Load keeps returning null for display code. New backups get a free name and never overwrite a file in the backup folder. AtomicWriteText flushes to disk before the rename. UnreadableManifest_InstallRefuses_BackupsSurvive
LostManifest_NewInstallNeverOverwritesSavedOriginals
(the flush has no test)
D4 (medium) Weak integrity checks; Seaglass upgrades silently • MFG Unlock accepts only the exact renodx-mfgunlock.addon64 asset (no "any .addon64" fallback).
• GitHub assets with no published digest are refused. All current OptiScaler, MFG and Streamline releases have digests, checked via the API.
• beforeLaunch uses UpdateOptions(m).KeepingInstalledVersions(m): it reinstalls the cached OptiTag/MfgTag, and leaves a component alone if that version is no longer cached. Moving to a new release takes a click in the GUI.
Mfg_AnyOtherAddonInTheRelease_IsNotAccepted
Mfg_AssetWithoutDigest_IsRefused
LaunchReinstall_KeepsInstalledOptiAndMfgVersions
D5 (low) Restore downgrades a DLL the game has since updated Restore skips a file whose hash isn't the recorded InstalledSha (the game owns it now) and drops the stale backup. RestoreDlss_KeepsAFileTheGameUpdatedSince
D6 (low) A transient HEAD failure leaves a pinned DLSS version incomplete or mixes versions GitHubClient.ExistsAsync returns false only on a 404 and throws on anything else. A rate limit no longer skips files or falls back to main. The main fallback is kept for a genuine 404 on the latest tag. Dlss_PinnedVersion_RateLimitedCheckFails_InsteadOfSkippingFiles
Dlss_Latest_RateLimitedCheck_NeverFallsBackToMain
D7 (low) GitHub token stored in plain text The token is stored DPAPI-encrypted (CurrentUser) as GitHubTokenProtected. A legacy plain GitHubToken is read once and rewritten encrypted. GitHubToken_IsSavedEncrypted_AndPlainTextIsMigrated

Test seam: GitHubClient has an internal constructor that takes an HttpMessageHandler, so the store tests use a fake GitHub and nothing leaves the machine.

Fails before, passes after

  • I ran the 15 behavioural tests (D1, D2, D3, D5, D7) against origin/main, with only the new exception types loosened. All 15 fail there.
  • The 5 tests for D4 and D6 use the new API or test seam, so they don't compile on main.

Not done (needs the owner)

  • D4: Authenticode checks (NVIDIA for nvngx_*, crosire for the ReShade setup). I couldn't check real signed files offline, so a wrong check could block every DLSS install.
  • D7: loading nvapi64.dll from System32 only; I didn't touch NvAPI code.
  • D7: asking before auto-importing DLLs found next to the exe. This needs a UI prompt.
  • Unchanged on purpose: licensing, naming, and the DLSS 5 / MFG Unlock feature decisions.

Follow-up fixes (0ed78f7)

  • Regression fixed: install folders outside the game. D1 had stopped installs into a folder picked outside the game root. Existing installs of that kind also couldn't be uninstalled or restored. Entries may now resolve inside the game root or the chosen install folder. .. in entries and any relative RootRel are accepted once the resolved path lands in one of those two folders. Ctx.Full follows the same rule. The junction, backup-folder, owned-folder and proxy checks are unchanged. Tests: InstallFolderOutsideGame_InstallUpdateUninstall_RoundTrip and InstallFolderOutsideGame_RecordWrittenByEarlierVersion_StillUninstalls.
  • Pre-existing bug on main: a retried uninstall could delete an original. Uninstall deleted every file first and then restored the backups, but it never saved the record along the way. If one restore failed (a locked dll), the retry deleted originals that had already been restored. Uninstall now saves the record after each file deleted, folder removed and original restored, so a retry continues from where it stopped. Test: Uninstall_FailsPartWay_RetryKeepsOriginalsAlreadyRestored.
  • All 3 new tests fail without these changes. dotnet build -warnaserror: 0 warnings, 0 errors. dotnet test: 79 passed.

Gate

  • dotnet build -warnaserror: 0 warnings, 0 errors. Main was already at 0.
  • dotnet test with integration tests off: 76 passed (56 existing + 20 new).
  • The app wasn't run against real games, and NvAPI wasn't called.

- D1: every manifest path is canonicalised and contained: Files and backup
  originals must stay inside the game root, owned folders inside the install
  folder, backups inside the backup folder, no junctions or symlinks on the way,
  proxy must be a known proxy name. Anything else refuses the whole operation.
- D2: entries are rebased onto the current game root; a narrower root that
  can't reach them refuses instead of misplacing the originals.
- D3: an unreadable manifest stops installs/uninstalls instead of starting a
  fresh record; new backups never overwrite an existing file; manifest and ini
  writes are flushed to disk before the rename.
- D4: MFG Unlock only accepts its exact asset name; GitHub assets without a
  published SHA-256 digest are refused; a Seaglass launch reinstalls the cached
  OptiScaler/MFG versions the game already has instead of upgrading them.
- D5: restore leaves a DLSS/Streamline file the game updated since in place.
- D6: only a 404 means "this DLSS version has no such file"; rate limits and
  server errors fail instead of skipping files or falling back to main.
- D7: the GitHub token is stored DPAPI-encrypted; plain tokens are migrated.
- Install records may point into the game root OR the chosen install folder,
  so installs into a folder picked outside the game work again and records
  written by 1.5.1 for such folders still uninstall and restore. Any relative
  RootRel and ".." entries are accepted once the resolved path lands in one of
  those two folders; junction, backup-folder and proxy checks are unchanged.
- Uninstall saves the record after every file deleted, folder removed and
  original restored, so a retry after a failure part-way (a locked dll) carries
  on where it stopped instead of deleting originals already put back.
- Install records now store each entry relative to the folder it belongs to:
  the game root as before, or the install folder behind an explicit
  "|install|\" tag. Both round-trip across drive letters, subst drives and
  UNC shares, where a path relative to the game root can't be expressed.
- Backups of install-folder files go under backup\_install instead of
  being joined onto a full path, which left the displaced original beside
  itself as "<name>.N" (and on a share could land in the wrong place).
- Records written by 0ed78f7 for such folders (full-path entries, full
  RootRel, "<original>.N" backups) are read, contained and rewritten, so
  their displaced originals restore through Uninstall and Restore DLSS.
- Containment is unchanged: every entry must resolve inside the game or
  install folder, owned folders inside the install folder, backups inside
  the backup folder (or the legacy "<original>.N" beside the original), no
  links on the way, no streams or device paths.
- The add-on's reverted-DLSS check resolves entries the same way.
@ApolloF
ApolloF marked this pull request as ready for review October 4, 2026 17:07
@ApolloF
ApolloF merged commit bc134d0 into main Oct 4, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant