# Code Review

**Scope:** entire codebase (src/, tests/, scripts/, scripts-arch/, build.sh, release.sh)
**Date:** 2026-07-06
**Verdict:** The privilege-escalation core (Cmd, ElevationBroker, helper) is sound and consistently argv-based, but one shell-string code path was missed by the earlier "pass user-controlled values as arguments, not interpolated into bash" hardening pass and is exploitable; a handful of smaller correctness/robustness gaps round out the rest.

## Action items

1. - [x] **Command injection via `-f/--file` snapshot name in `savePackageList()`** — `src/work.cpp:1259-1261`
   `Work::savePackageList()` builds `dpkg -l | awk '...' > '%1'` by interpolating `fi.completeBaseName()` (derived from `settings->snapshotName`, i.e. the CLI `-f/--file <name>` argument or the GUI output filename) directly into a string passed to `shell.run()`, which executes it via `/bin/bash -c`. The only validation, `settings.cpp:378`'s `QRegularExpression("[<>:\"/\\|?*]")`, blocks filesystem-invalid characters but not shell metacharacters (`'`, `;`, `` ` ``, `$`, `&`, spaces). A name like `-f "test'; touch pwned #"` passes validation and breaks out of the quoted redirect target, letting arbitrary shell commands run with whatever privilege the process already has (root, when the CLI is invoked via `sudo`, which is the documented escalation path). Fix by using the same positional-parameter (`$1`) pattern already used elsewhere in this file (e.g. `makeChecksum`, `createIso`) instead of string interpolation, or by tightening the `snapshotName` validation regex to an allow-list of safe characters.

2. - [x] **`resetCustomExcludes()` deletes the user's custom excludes file even when the safety backup fails** — `src/excludesutils.cpp:98-106`
   When resetting to default excludes (triggered from the "excludes changed upstream, reset?" flow in `mainwindow.cpp:709/975` and `batchprocessing.cpp:286`), the function attempts to back up the existing file via `QFile::copy(configuredPath, backupPath)`. If that copy fails (destination unwritable, disk full, or a same-second collision with a prior backup filename since the name is only second-resolution), the failure is only logged with `qWarning()` and the code falls through to unconditionally `QFile::remove(configuredPath)` anyway — permanently deleting the user's customized exclude list with no recoverable backup. Make the removal conditional on backup success (or skip the reset and return `false` if the backup can't be created).

3. - [x] **Inconsistent config validation lets one bad `throttle` value block the app from starting, while `cores` self-heals** — `src/settings.cpp:1176-1186` vs `src/settings.cpp:359-361`
   `loadConfig()` clamps an out-of-range `cores` value to `maxCores`, logs a warning, and persists the correction (`settings.cpp:1178-1183`) — the app still starts. `throttle` is read with no validation at all (`settings.cpp:1186`) and is only checked later in `checkConfiguration()` (`settings.cpp:359`), where a value `> 99` causes `Settings::Settings()` to throw `std::runtime_error("Configuration validation failed")`, refusing to launch at all (both GUI and CLI). A hand-edited or stale config with `throttle=100` permanently locks the user out of the app until they manually find and fix the config file, whereas the equivalent `cores` mistake self-corrects silently. Clamp `throttle` the same way `cores` is clamped in `loadConfig()`.

4. - [x] **`getEditor()` unconditionally prepends the elevation tool for ordinary GUI/CLI editors even when not root** — `src/settings.cpp:559-578`
   Only the `isEditorThatElevates && !isRoot` branch (line 565) returns without an elevation prefix. The CLI-editor branch (line 575) and the generic fallback (line 578) always prepend `elevate` (pkexec/sudo) regardless of `isRoot`. Both `editBootMenu()` and `btnEditExclude_clicked()` (`mainwindow.cpp:920-963`) target files that are extracted/chowned to the invoking user without root (`tar xf` via unprivileged `shell.run()` at `work.cpp:518/634`, and the excludes file is chowned to the logged-in user). Concrete scenario: normal unprivileged GUI session with a default editor like `mousepad` or `gedit` (matches neither the elevating nor CLI-editor regex) — clicking "Edit Exclusions" or "Edit Boot Menu" needlessly triggers a pkexec polkit prompt to edit a file the user already owns, and can fail outright in environments without a polkit authentication agent. Skip the `elevate` prefix in these branches when `!isRoot`, mirroring the logic already used for `isEditorThatElevates`.

5. - [x] **`release.sh`'s `check_tag_exists()` treats remote-lookup failures as "tag available"** — `release.sh:71`
   `git ls-remote --tags mxlinux 2>/dev/null | grep -q "refs/tags/${version}$"` discards stderr from `git ls-remote`, so any failure unrelated to "tag doesn't exist" (network outage, auth failure, wrong remote name) makes `grep` see empty input and return non-zero, and the function falls through to `return 0` (available). The script then creates a real annotated local tag (`release.sh:159`) before the actual problem surfaces at `git push mxlinux "$version"` (`release.sh:390`), leaving an orphaned local tag to clean up by hand. Check `git ls-remote`'s own exit status before trusting the `grep` result.

6. - [x] **`release.sh`'s annotation "cannot be empty" check doesn't catch an unedited placeholder** — `release.sh:129-146`
   `prompt_annotation()` seeds `$tmpfile` with boilerplate (`## Release $version`, `- Feature 1`, `- Bug fix 2`, `- Other changes`) before invoking `$EDITOR`. If the maintainer saves without changing anything, or presses Enter at the no-`$EDITOR` prompt, the placeholder text is not empty, so the `[ -z "$annotation" ]` guard at line 143 never fires — the boilerplate becomes the real annotated-tag message and AUR commit message. The later confirmation prompt (`release.sh:375-381`) does echo the annotation, so this is a soft safety-net gap rather than a silent failure, but the intended "abort if not filled in" check should detect the unedited placeholder specifically (e.g. compare against the template text), not just emptiness.
