Windows port, part 4b: compress as administrator (ogc-elevated), remove old LZNT1 compression, known issues (W4 part 2) #37

Manually merged
SkyfaR merged 22 commits from windows-w4b-main into main 2026-10-06 08:49:37 +02:00
Owner

Windows port, phase W4, part 2 (plan §1.3, §3.1, §3.3): compressing one game as administrator, removing old NTFS (LZNT1) compression first, and a list of games with known problems. It is based on main and independent of #36.

Compressing as administrator (ogc-elevated.exe, src/elevated/)

  • One game per run, asked for each time: "Als Administrator komprimieren" on a card whose game needs administrator rights, or ogc compress --elevated GAME.
    • Either starts ogc-elevated.exe with ShellExecuteExW("runas"), so Windows asks through UAC every time.
    • Its manifest (requireAdministrator, written by the new build.rs) is linked into that program alone.
    • There is no elevated scheduled task; ogc auto still skips such games.
  • It never trusts its caller:
    • The command line carries only a key (steam:, epic:, gog:, ubisoft:, ea:, heroic:, own:<index>) and checked options; nothing on it is a path.
    • The program reads the libraries itself, as the user in the caller's process token. It resolves that user's profile, shell folders and registry by SID (HKEY_USERS\<SID>), so over-the-shoulder elevation reads the right account. A caller in another session is refused.
    • Refused: anything not inside a known library or own folder, links, .., profiles, Windows' folder (from GetSystemWindowsDirectoryW, not the environment), drive tops, Program Files itself, Xbox, OneDrive, network drives.
  • No path TOCTOU:
    • The game folder is opened once without following links and held without FILE_SHARE_DELETE.
    • The walk follows no link.
    • Every file, and the handle used to remove LZNT1, is checked by its own handle's final path, strictly inside the held folder, before anything is read or changed.
    • Residual risk, documented: a junction swapped in mid-walk can make outside files be opened briefly for reading. They are refused, never changed.
  • It never writes the user's state as administrator:
    • Results go back as JSON lines over two named pipes that only the caller's SID may open. Both sides check the other end's process.
    • It compresses only after the caller confirms the reported folder (go).
    • It uses no skip cache.
    • The unelevated app or CLI records the history and games.tsv.
  • The per-user install folder is writable by the user: this is accepted per plan §1.3 and decision D2. Every run asks; the unelevated callers there are just as replaceable; signing (D4) will make a replaced file show as an unknown publisher. The shipped program imports only Windows' own DLLs (dist.sh checks the release build).

Old NTFS compression (LZNT1), owner decision D9

  • ogc compress --remove-lznt1, and on the card "Alte NTFS-Komprimierung zuerst entfernen" (as administrator where needed).
  • Each plain LZNT1 file is handled on its own:
    • room is checked first;
    • FSCTL_SET_COMPRESSION sets it to none and the result is read back;
    • the file is then WOF-compressed as usual.
  • WOF-backed files (also WOF over LZNT1) and other reparse points are never touched. Without --force, a file keeps LZNT1 where WOF would not store it smaller.
  • Failures after removal are counted with the real sizes. Background runs never remove LZNT1.

Known issues (§3.3)

  • Guild Wars 2, Secret World Legends, Ghost Recon Wildlands and The Lord of the Rings Online, the games listed in CompactGUI issue #101.
  • They are recognised by Steam app id, or elsewhere by their program file.
  • analyze and compress warn about them and still compress when asked. auto and --include-new leave them alone, as they do DirectStorage games, and the card says why.
  • The docs note the database's future known_issue field.

Tests

  • Linux: fmt, both clippy runs, and cargo test --all-features with Broadway and OGC_TEST_REQUIRE_BTRFS=1 (1637 passed, 0 failed).
  • Windows, in the container: check.sh, wine-test.sh (including a real pipe round trip, a held folder, the caller's SID and HKEY_USERS), check-gui.sh (manifest checks), build-gui.sh, gui-test.sh, gui-smoke.sh, the installer tests, and dist.sh with the app (it ships ogc-elevated.exe).
  • Negative tests:
    • forged or ambiguous keys;
    • junctions and links out of a library, ..;
    • profile, Documents, TEMP, OneDrive and Windows folders, also with the environment redirected;
    • forged pipes and ends, another folder reported, a declined UAC prompt;
    • LZNT1 edge cases.
  • Reviews: a security review with adversarial verification, then an integration and fix round; the final security review found nothing real.

Not verified yet (needs a real Windows PC)

  • The UAC prompt and over-the-shoulder elevation with a second account.
  • The medium- to high-integrity pipes.
  • A real admin-only folder.
  • LZNT1 removal on real NTFS.
Windows port, phase W4, part 2 (plan §1.3, §3.1, §3.3): compressing one game as administrator, removing old NTFS (LZNT1) compression first, and a list of games with known problems. It is based on main and independent of #36. ## Compressing as administrator (`ogc-elevated.exe`, `src/elevated/`) - **One game per run, asked for each time:** "Als Administrator komprimieren" on a card whose game needs administrator rights, or `ogc compress --elevated GAME`. - Either starts `ogc-elevated.exe` with `ShellExecuteExW("runas")`, so Windows asks through UAC every time. - Its manifest (`requireAdministrator`, written by the new `build.rs`) is linked into that program alone. - There is no elevated scheduled task; `ogc auto` still skips such games. - **It never trusts its caller:** - The command line carries only a key (`steam:`, `epic:`, `gog:`, `ubisoft:`, `ea:`, `heroic:`, `own:<index>`) and checked options; nothing on it is a path. - The program reads the libraries itself, as the user in the caller's process token. It resolves that user's profile, shell folders and registry by SID (`HKEY_USERS\<SID>`), so over-the-shoulder elevation reads the right account. A caller in another session is refused. - Refused: anything not inside a known library or own folder, links, `..`, profiles, Windows' folder (from `GetSystemWindowsDirectoryW`, not the environment), drive tops, Program Files itself, Xbox, OneDrive, network drives. - **No path TOCTOU:** - The game folder is opened once without following links and held without `FILE_SHARE_DELETE`. - The walk follows no link. - Every file, and the handle used to remove LZNT1, is checked by its own handle's final path, strictly inside the held folder, before anything is read or changed. - Residual risk, documented: a junction swapped in mid-walk can make outside files be opened briefly for reading. They are refused, never changed. - **It never writes the user's state as administrator:** - Results go back as JSON lines over two named pipes that only the caller's SID may open. Both sides check the other end's process. - It compresses only after the caller confirms the reported folder (`go`). - It uses no skip cache. - The unelevated app or CLI records the history and `games.tsv`. - **The per-user install folder is writable by the user:** this is accepted per plan §1.3 and decision D2. Every run asks; the unelevated callers there are just as replaceable; signing (D4) will make a replaced file show as an unknown publisher. The shipped program imports only Windows' own DLLs (`dist.sh` checks the release build). ## Old NTFS compression (LZNT1), owner decision D9 - `ogc compress --remove-lznt1`, and on the card "Alte NTFS-Komprimierung zuerst entfernen" (as administrator where needed). - Each plain LZNT1 file is handled on its own: - room is checked first; - `FSCTL_SET_COMPRESSION` sets it to none and the result is read back; - the file is then WOF-compressed as usual. - WOF-backed files (also WOF over LZNT1) and other reparse points are never touched. Without `--force`, a file keeps LZNT1 where WOF would not store it smaller. - Failures after removal are counted with the real sizes. Background runs never remove LZNT1. ## Known issues (§3.3) - Guild Wars 2, Secret World Legends, Ghost Recon Wildlands and The Lord of the Rings Online, the games listed in CompactGUI issue #101. - They are recognised by Steam app id, or elsewhere by their program file. - `analyze` and `compress` warn about them and still compress when asked. `auto` and `--include-new` leave them alone, as they do DirectStorage games, and the card says why. - The docs note the database's future `known_issue` field. ## Tests - **Linux:** fmt, both clippy runs, and `cargo test --all-features` with Broadway and `OGC_TEST_REQUIRE_BTRFS=1` (1637 passed, 0 failed). - **Windows, in the container:** `check.sh`, `wine-test.sh` (including a real pipe round trip, a held folder, the caller's SID and `HKEY_USERS`), `check-gui.sh` (manifest checks), `build-gui.sh`, `gui-test.sh`, `gui-smoke.sh`, the installer tests, and `dist.sh` with the app (it ships `ogc-elevated.exe`). - **Negative tests:** - forged or ambiguous keys; - junctions and links out of a library, `..`; - profile, Documents, TEMP, OneDrive and Windows folders, also with the environment redirected; - forged pipes and ends, another folder reported, a declined UAC prompt; - LZNT1 edge cases. - **Reviews:** a security review with adversarial verification, then an integration and fix round; the final security review found nothing real. ## Not verified yet (needs a real Windows PC) - The UAC prompt and over-the-shoulder elevation with a second account. - The medium- to high-integrity pipes. - A real admin-only folder. - LZNT1 removal on real NTFS.
Owner decision D9 (plan 3.1): files stored with NTFS' LZNT1 compression are
still reported and left alone, but with WofOptions::remove_lznt1 the engine
returns a plain LZNT1 file to plain storage first (FSCTL_SET_COMPRESSION with
COMPRESSION_FORMAT_NONE on a handle opened for writing, shared with readers
only) and then compresses it with WOF. A WOF file, also one over LZNT1 (H14),
and every other reparse point are never touched; a file another rule leaves
alone (sparse, linked, read-only) and one whose sample says WOF would not store
it smaller stay LZNT1, unless forced. Room for the plain file and for WOF's
write is kept first, as decompressing keeps it. The summary counts the files
and tells stored sizes before and after exactly.

ogc compress --remove-lznt1 asks for it, and the line of files left with LZNT1
names the option. Background runs never ask. Tested against the stand-in WOF,
and on real NTFS where OGC_TEST_REQUIRE_NTFS asks for it.
The elevated program has to read the launcher data of the user who
started it, who may be another account than the one it runs as. Add a
registry view with HKEY_USERS\<SID> as the current user, the SID of a
token, launchers::load_from for given sources, and the Windows folder
refusals by a given user's variables and registry.
A game whose files NTFS stores with LZNT1 (the card says "Alte
NTFS-Komprimierung (LZNT1)") offers "Alte NTFS-Komprimierung zuerst
entfernen" in its menu, enabled where compressing is. It queues the game alone
with the setting's algorithm and Item::remove_lznt1, which the worker hands to
the shared run; nothing else asks for it.
A separate program with the manifest requireAdministrator (build.rs
writes it as a .res file for that program alone), started through UAC
for one game per run. Its command line carries only a key of the game,
which it looks up in the launcher data of the user who started it, read
by that user's SID; it refuses folders outside the known libraries,
links and junctions, paths with dots, profiles and Windows' folders.
Its reports go over two pipes only the caller's SID may open, served
only to the caller's process; it writes nothing of the user's.
Remote is the engine the caller runs the shared compress_folder with.
It compresses the one game named through ogc-elevated.exe, which
Windows asks the user to allow, and records it as any other run. The
needs-administrator message names it now. shell32.dll is loaded only
for the start, so ogc.exe still imports nothing that loads user32.dll.
The Wine test checks the refusals of both programs.
Plan 3.3: a small built-in list (wofrun::known) of the games CompactGUI's
issue #101 reports: Guild Wars 2 and Secret World Legends decompress
themselves at start and hang meanwhile, Ghost Recon Wildlands stops at its
main menu, The Lord of the Rings Online hangs while patching. A Steam game is
known by its app id, another launcher's or an own folder's by its program.

ogc analyze and ogc compress warn of such a game and still compress it when
asked; ogc auto leaves it alone with the reason noted once a day, and
--include-new does not take it up, as for DirectStorage games. The community
database's future known_issue field is to extend the list.
A card in the state "Benötigt Administratorrechte" offers "Als
Administrator komprimieren" as its button while the game is ready, not
excluded and not compressed and up to date. The job compresses it
through ogc-elevated.exe and records it as any other; once compressed,
its chip tells what it saved.
On Windows each card of a game in the built-in list says so in its meta
line, with what goes wrong and that the background task leaves it alone,
beside DirectStorage's hint. A Steam game is looked up by its app id, which
costs no look at the disk while the cards are built. Linux shows nothing.
The bundle, the portable zip and the installer carry it. check.sh and
check-bundle-windows.sh check its imports, that it is a program of the
Windows subsystem and that its manifest requires administrator rights,
and that ogc.exe and ogc-background.exe carry no manifest.
Both pieces of W4 part 2 together: the elevated program takes
--remove-lznt1 like the other options of a run, so that
ogc compress --elevated --remove-lznt1 removes LZNT1 as administrator,
and its outcome carries the files LZNT1 was removed from. Known issues
are told by the shared folder run, which elevated runs go through too.
The card's "Remove old NTFS compression first" asks for the game as
administrator where its files need the rights, and the elevated program
removes LZNT1 as the card asked. Known issues are warned of also for a
game the elevated program compresses. Tests for the card, the job, the
command line and the caller's run.
The elevated program checked the game's folder by its path once and then
compressed by path, so a junction swapped into the folder after the
check could lead it to files elsewhere. Now it holds the folder open by
a handle, opened without following a link and refused unless it is a
folder whose final path is the one checked, and held without
FILE_SHARE_DELETE so that it cannot be renamed and replaced meanwhile.
Its walk follows no link, also not at the top, and each file is
compressed only where the final path of the handle the engine opened,
and compresses through, lies strictly inside the held folder's final
path; otherwise it is neither read nor changed and is reported as
failed. The same holds for the handle that removes LZNT1.

The elevated run also no longer opens the skip cache: it is the user's
state, in a folder the user's programs can redirect, and the elevated
program writes nothing of the user's.
Once LZNT1 is removed, a file is plain for good. When reopening it,
reading what it takes or WOF then failed, the engine returned a bare
error, so the file was booked as found: before and after both its
length, lznt1_removed not counted, and the growth missing from the
summary and the history. Such a file is now counted as one LZNT1 was
removed from that failed, with what it took before and takes plain now.
A full drive there still stops the run, after the file is counted.
The elevated program reported the folder it found for the key and
started compressing at once; the caller's check that it is the game's
folder could only fail the run afterwards. Now it waits for the
caller's answer on the stop pipe: only "go" lets it compress, while
"stop", anything else, the pipe's end or no answer within a minute
ends it with nothing done. The caller answers "go" only for the folder
it asked for, and declines a second folder too. The test stand-in runs
the elevated side in a thread, as the program runs beside its caller.
The elevated program's game watch read the launchers and the Steam
registry value of the account it runs as, an administrator's after
over-the-shoulder elevation, so a game the calling user started did
not pause the run. GameWatch::start_with takes the libraries already
read for that user and asks Steam by their registry.
The refusal of Windows' own folders and of the other machine places
rested on the elevated process' environment, which under elevation of
the user's own account is built partly from the user's writable
HKCU\Environment. The elevated program now takes Windows' folder from
GetSystemWindowsDirectoryW and Program Files, ProgramData and Public
from HKEY_LOCAL_MACHINE, refuses anything inside Windows' folder
whatever the variables say, and reads its own user's places by its SID
as it reads the caller's.
With --remove-lznt1, files can still keep LZNT1: WOF would not store
them in less room, or they cannot be changed (WOF over LZNT1, a reparse
point). The summary told the user to run ogc compress --remove-lznt1
for them, the command just run. Such a run now says why they kept it
and that --force compresses those WOF would not make smaller, in all
13 languages.
ARCHITECTURE.md and the module notes now describe how ogc-elevated is
kept to the game's folder by a handle, with the risk that remains, that
it compresses only once the caller confirms the folder, that it opens
no skip cache, where it reads the machine's places, its game watch, and
how a file that fails once its LZNT1 is removed is counted.
Say why ogc-elevated may lie in the per-user install folder
All checks were successful
CI / Windows, desktop app (clippy, script tests) (pull_request) Successful in 1m26s
CI / Windows, MSYS2 sysroot (pull_request) Successful in 21s
CI / Windows, command line (clippy with tests) (pull_request) Successful in 43s
CI / Windows, command line (tests under Wine) (pull_request) Successful in 1m53s
CI / Windows, installer (tests under Wine) (pull_request) Successful in 2m30s
CI / Windows, GTK with AccessKit (pull_request) Successful in 46s
CI / Format, lint and test (pull_request) Successful in 5m53s
CI / Windows, desktop app (tests under Wine) (pull_request) Successful in 2m48s
CI / Arch package (pull_request) Successful in 3m7s
CI / Bundle programs, command line and helper (pull_request) Successful in 2m35s
CI / Bundle programs, desktop app (pull_request) Successful in 3m12s
CI / Windows, installer and portable zip (pull_request) Successful in 6m30s
CI / Bundle for any distribution (pull_request) Successful in 41s
CI / Release (pull_request) Has been skipped
VM test / Migrations in a VM (pull_request) Successful in 17m43s
3f8aa7e7ce
SkyfaR manually merged commit 53ad419c7f into main 2026-10-06 08:49:37 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
LevelXStudios/OpenGameCompressor!37
No description provided.