BUNTSTARTER/REVIEW.md
jing 529039dc87 the great unbuntening: ui goes brrr + full review in REVIEW.md
- splash no longer does the 5.5s slow-scroll flex
- apt-get update only runs if lists are >6h old (-r/--refresh to force)
- log() fork-free, debug spam gated behind ULTRABUNT_DEBUG=1
- category menu counts in one pass, no more dpkg fork storm per redraw
- deleted refresh_cache_silent (600 background forks doing literally nothing)
- rc-state packages no longer show as installed; flatpak exact-match
- bulk ops no longer die after first package (((x++)) set -e trap)
- bulk checklists no longer scramble themselves when sorting
- ESC no longer nukes the whole script in buntage/log-viewer menus
- swappiness menu: fixed regex crash + duplicate sysctl lines
- wp hardening: domain validated (no more chown -R /var via ".."), options actually apply now
- fun category (15 buntages) rescued from the void
- --no-media/--no-web/--no-network/-minimal/-core now exclude real categories

REVIEW.md has the full findings list incl. everything not fixed yet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:37:48 +10:00

16 KiB
Raw Blame History

Ultrabunt Code Review — 2026-07-17

Full review of ultrabunt.sh (17k lines) and ultrabunt-accessible.sh. Scope: real breakage and danger only — style ignored, silliness preserved.

Line numbers are approximate (the performance pass shifted things by a few dozen lines in places) — search for the quoted function name if a number is off.


Fixed in this pass (performance + crashers)

These are already applied to ultrabunt.sh:

What Why it was slow/broken
Splash screen printed instantly 78 lines × sleep 0.05 + sleep 1.5 = ~5.5s of dead air at every launch
apt-get update skipped when lists < 6h old (--refresh to force) Ran unconditionally at startup: 1060s per launch on old hardware
log() rewritten fork-free (printf %(...)T + direct append) Old version forked ~3 processes per line ($(date), pipe, tee) and was called per package per menu redraw; also sprayed stderr underneath whiptail. Set ULTRABUNT_DEBUG=1 to mirror logs to stderr
Removed per-check logging from is_package_installed 3+ log lines (≈9 forks) per package, per menu redraw
Category counts computed in ONE pass over PACKAGES Old loop re-walked all ~600 packages once per category (~15,000 iterations) on every main-menu redraw
is_apt_installed cache miss is now authoritative after startup scan Old fallback forked dpkg -l for every NOT-installed package on every redraw — hundreds of forks per screen
build_package_cache always does one full dump per package manager "Selective mode" forked one dpkg-query per package — strictly slower than dumping everything (which is 1 fork)
Deleted refresh_cache_silent + the 🔄 indicator It forked ~600 background processes whose cache writes happened in a child shell and were thrown away — a subshell can't modify the parent's INSTALLED_CACHE. 100% wasted CPU on a machine that has none to spare
Removed log "Menu items array contents: ..." Wrote the entire ~5KB menu array to the log on every redraw
dpkg checks filter on Status-Status == installed dpkg-query -W also lists removed-but-not-purged (rc) packages, so uninstalled packages showed ✓ Installed
Flatpak checks use exact match (grep -Fxq on the app-id column) Bare grep -q "$pkg" matched substrings
((installed++))-style counters → x=$((x+1)) (6 sites in bulk ops) ((x++)) returns exit 1 on the 0→1 increment; under set -e every bulk install/remove killed the whole script after the first package
Bulk-select checklists sort names first, then build triples Old code joined tag/desc/OFF triples into lines and sorted ALL lines, scrambling the checklist alignment
show_buntage_actions menu guarded with || return; install/reinstall success message now conditional on actual success Cancel killed the script via set -e; failures reported "installed successfully!"
view_log_file menu guarded with || return ESC/Cancel in the log viewer killed the whole script
Swappiness regexes fixed (vm\\.swappinessvm\.swappiness) + no-match grep guarded The \\. inside single quotes is a literal backslash: "show entry" crashed the script and a duplicate vm.swappiness line was appended to sysctl.conf on every persist
wordpress_security_hardening: domain validated, wp-config.php required Entering .. made site_dir=/var/www/.. = /var, then "Fix File Permissions" ran chown -R www-data:www-data /varsystem-wrecking
Hardening checklist strips whiptail's literal quotes No case arm ever matched "file-permissions" (with quotes), so every hardening option was silently skipped while reporting success
fun category added to CATEGORIES 15 packages were tagged fun but the category wasn't in the menu — permanently invisible
--no-media/--no-web/--no-network/-minimal/-core use real category ids They referenced ids that don't exist (multimedia, music, web-browsers, network, education…), so the exclusions silently did nothing
Package-list sort uses mapfile instead of unquoted $(sort) Glob chars in descriptions ((_*_)) could expand against files in the cwd

Expected result: startup goes from ~4070s to a few seconds (splash + one dpkg/snap/flatpak dump), and menu redraws go from seconds of fork-storm to near-instant pure-bash.


Outstanding — HIGH (dangerous or feature completely broken)

WordPress / database

  • finalize_wordpress_setup: custom setup's user-entered site_dir is ignored by the actual installers (they all install to /var/www/$domain) but the finalizer still runs chown -R $(whoami):www-data "$site_dir" + recursive chmod on the unvalidated path. Enter /home and it re-owns it. Validate/derive site_dir the same way the installers do.
  • SQL injection everywhere a password/name is interpolated into mysql -e "... IDENTIFIED BY '$pass'" (setup_wordpress_database*, create_database_user_interactive ~L16381, DB repair eval ~L13870). A single ' in a password breaks or injects as MariaDB root. The repair path pipes it through eval too. Fix: validate names as [A-Za-z0-9_]+, pass passwords via --defaults-extra-file or here-doc SET PASSWORD with proper quoting.
  • setup_wordpress_database_custom runs under command substitution but its whiptail dialogs aren't redirected to /dev/tty — dialogs are captured into the result variable instead of being shown; flow appears hung and wp-config gets garbage credentials.
  • wp-config salts: the awk insert anchors on /** MySQL settings which WordPress renamed in 5.8 — the sed first deletes all AUTH_KEY…NONCE_SALT lines, then the insert never fires → modern WP sites end up with no security keys at all, silently.
  • setup_nginx_ssl writes the 443 block to sites-available/$domain while .localhost sites were created under the stripped base name → new file is never enabled; reports success, HTTPS never serves. Also appends a duplicate 443 block on every rerun (tee -a).
  • phpMyAdmin installer writes the MariaDB root password to predictable /tmp/mysql_defaults_phpmyadmin.cnf (pre-creatable by any local user; 644 window before chmod), and the follow-up mysql --defaults-file runs as the wrong user so it silently no-ops. Root password also logged in plaintext to /var/log/ultrabunt.log (~L10883) and wp-config gets loosened to 0664 (group-writable by www-data, world-readable).
  • delete_wordpress_site reads /root/.mysql_root_password without sudo → DB/user never actually dropped while reporting "completely removed including database"; also deletes the nginx vhost matching the first label of the domain (deleting shop.example.com can nuke shop.localhost's config), and will DROP a database shared by other sites without cross-checking.
  • grant_file_access_to_user: chmod -R 2775 /var/www makes every file group-writable+setgid and world-readable — www-data can rewrite all PHP (persistence vector) and wp-config passwords are world-readable. Use 664 files / 2775 dirs.
  • show_individual_site_management uses $site_dir (never set locally — dynamically inherits the caller's last loop value) instead of the site_root it computes → wp-config view/edit/DB-info act on the wrong site whenever >1 site exists.

Cloudflare / firewall

  • cloudflare_create_rule builds its JSON with raw printf and no escaping — every filter expression contains quotes (some contain newlines) → malformed JSON → every rule creation fails, including "light". The whole feature can't work as written. Also uses the deprecated legacy firewall/rules endpoint, and the country code UK should be GB (would block UK visitors incl. you).

Mirror management

  • The entire "fastest mirror" feature is dead: it invokes mirrorselect (a Gentoo tool) which is never installed (install_mirrorselect actually installs netselect-apt). The auto mode uses netselect-apt's Debian mirror list and seds those hosts over archive.ubuntu.com → apt breaks. The "restore" path recomputes $(date +%s)-style backup names so it restores from a filename that doesn't exist and dies under set -e with sources.list already clobbered. use_local_mirror/setup_local_mirror write to /etc/apt/ without sudo → instant permission-denied crash. On 24.04+ the real sources live in /etc/apt/sources.list.d/ubuntu.sources anyway. Recommend: rip this section out or rewrite around netselect-apt-for-Ubuntu with sudo and proper backup naming.
  • run_mirrorselect_custom does eval "sudo mirrorselect $custom_flags" on free-typed input — arbitrary shell as root by design.

Reverse-proxy add-ons

  • Umami: publishes 3000:3000 on 0.0.0.0 (Docker's iptables bypass UFW) with hardcoded APP_SECRET: replace-me-with-a-random-string and default admin/umami login → internet-reachable with forgeable sessions on any VPS.
  • Mailcow: mailcow's own nginx defaults to 80/443 (collides with host nginx) while the proxy snippet assumes 127.0.0.1:8080 which nothing configures — either compose fails or the proxy points at a dead port; completion message advertises https:// URLs no config provides.
  • Both add-on flows have zero error handling: any failed step under set -e kills the whole script with containers half-started and nginx confs already symlinked.

Installers (selection — full list in agent notes)

  • install_arduino-ide: unzip --strip-components=1 — flag doesn't exist; wipes /opt/arduino-ide then always fails, and set -e kills the script.
  • install_mailhog: curl -Lo without -f → 404 page installed as a root systemd-managed binary; also downloads into whatever cwd the script happens to be in (deno/zed installers cd into temp dirs and delete them, leaving a dead cwd).
  • Plex: curl | sudo apt-key add -apt-key is gone on newer Ubuntu, so it aborts after deleting the old sources; key would land in the global trusted keyring.
  • Fixed predictable /tmp paths + sudo mv/dpkg -i (lazydocker, glow, cheat, broot, dog, discord, ums, gollama, koboldcpp, LM Studio…): a local user can pre-plant/race the file and get a root-installed binary. Use mktemp -d.
  • dpkg -i || apt-get install -f -y pattern (6+ sites): non-dependency dpkg failures are masked and reported as success.
  • whisper.cpp builds unpinned master and copies main/quantize — upstream renamed the binaries; always fails now, make 2>/dev/null hides why.
  • Version-pinned URLs that 404 now or soon: ctop (latest/download + 0.7.7), kind v0.20.0, Insomnia 8.6.1, mergerfs jammy-only deb, LM Studio 0.3.5, UMS deb name, balena-etcher remove targets the old package name.
  • curl | bash as root (nodesource, ollama, starship, ddev — ddev also points at the moved drud/ddev repo) — accepted risk for a personal tool, but at least pin/checksum the big ones.
  • gpg --dearmor without --yes (caddy, ngrok, docker, warp): reruns abort mid-way after old keys were already deleted.

Packages that can never install (verified against live registries)

  • apt, wrong/absent name: sqlitestudio (26.04+ only), chromium (→ chromium-browser), asciiquarium, batcat (→ bat, and duplicates the existing bat entry), mysql-workbench-community (needs MySQL repo), pgadmin4 (needs pgadmin repo), bottom, google-drive-ocamlfuse (PPA-only), jellyfin (needs jellyfin repo), citra, dosbox-staging, python3-esptool (→ esptool), webcamize; version-dependent: exa (gone in 24.04+, keep eza), pastel, fastfetch (25.04+), qalculate (→ qalculate-gtk), bsnes, git-delta (23.10+).
  • snap, 404: tableplus, mongodb-compass, nextcloud-server (→ nextcloud), heroic, dosbox, platformio.
  • cargo/npm: qmasa (→ qmassa), dust (→ du-dust), adminmongo (not on npm).
  • flatpak: citra + yuzu delisted from Flathub (Nintendo takedown); freetube/stremio use bare names instead of app IDs.

Outstanding — MEDIUM

  • MariaDB/MySQL passwords passed as -p"$pass" on the command line everywhere (~20 sites) — visible in /proc/*/cmdline to any local user (matters most on the VPS). Standard fix: --defaults-extra-file=<(...) or MYSQL_PWD for a personal box.
  • DB dumps from backup_database_interactive written world-readable (default umask) into $BACKUP_DIR; grep -q "^${database}$" treats the name as a regex.
  • wp-config credential extraction via grep | cut -d"'" -f4 matches commented lines, no head -1, breaks on ' in passwords (DB test/repair flows).
  • restart_web_services probes only php 8.1/8.0/7.4-fpm — never restarts the script's own PHP 8.3 FPM, so "optimize uploads" silently doesn't land. Same class: SQL-import optimizer derives paths from CLI php -v (can be a different version than FPM); client_max_body_size sed lowers larger values across all vhosts; <IfModule mod_php8.c> never matches; mysql.connect_timeout should be mysqli.connect_timeout.
  • MySQL "import optimization" permanently sets innodb_flush_log_at_trx_commit=2 and blind buffer_pool=512M with no revert or RAM check; PHP memory_limit=8G per worker can OOM a small machine.
  • modify_php_setting interpolates unvalidated input into sed (a /, & or \ corrupts php.ini).
  • Bulk ops: install_package ... | tee runs in a pipeline subshell, so cache updates inside are discarded — status can go stale for the session (harmless-ish now that redraws are cheap; re-run build_package_cache after bulk ops to be exact).
  • ui_menu returns the literal string "back" on ESC — any future package named back breaks navigation; bulk category picker can pass "back" into the match loop.
  • Web-config-summary site pickers push 3 args per non-live site into a menu (that's checklist syntax) — misaligned rows; "No live sites" branch unreachable.
  • Log viewer: Ctrl+C on tail -f SIGINTs the whole script (no trap); pager temp file at predictable /tmp/ultrabunt_log_view_$$ copies sudo-read logs world-readable.
  • wordpress_cleanup_menu find: -name "*.localhost" -o -name "*.local" -print0-print0 binds to one branch; .localhost sites never listed.
  • certbot always adds -d www.$domain — fails for domains without a www record.
  • Hotkeys: >24 categories get lowercase tags but the handler uppercases before lookup (opens the wrong category); >48 reuses letters and silently overwrites. The "cycling" code for shared hotkeys is dead (assignment overwrites, never appends). Currently 27 categories, so the first overflow row (a) is already live.
  • ultrabunt-accessible.sh: launches ./ultrabunt.sh relative to caller's cwd; instructs running the whole thing via sudo, which breaks $USER-based ownership logic in the main script and makes TTS silent (root has no Pulse session); timeout 10s echo | festival applies the timeout to echo (festival can hang forever); sudo -u user cat > file redirection still runs as root.

Outstanding — LOW (grab-bag)

  • show_system_info hardcodes a 50×140 dialog — breaks on a stock 80×24 SSH window.
  • Export list writes to predictable /tmp/ultrabunt-packages-<ts>.txt.
  • get_status() is dead code (never called; no arms for custom/npm/cargo methods).
  • silversearcher-ag and ag are duplicate entries for the same package.
  • remove_phpmyadmin only restarts services that are is-enabled — a running-but-disabled DB/web server stays down. --force-yes no longer exists in modern apt.
  • __site_is_live needs host (not on minimal Ubuntu) → everything reports "not live".
  • Keyboard config writes to $HOME/gsettings — configures root's session if run under sudo.
  • bun installer greps live $PATH instead of rc files → duplicate export lines per rerun.
  • README drift: claims pip is removed but install_pip_package/npm/cargo helpers and CUSTOM npm tools are still in the script; claims 25+ categories/560+ apps — counts don't match the code.

Suggested priorities

  1. WordPress DB quoting + the finalize_wordpress_setup chown — it's the feature you actually use, on machines you keep.
  2. Delete or rewrite mirror management — it cannot work and it edits sources.list.
  3. Umami/Mailcow port + secret fixes if you ever run them on the VPS.
  4. Purge the ~25 never-installable package entries (list above) — they just waste menu space and end in failure dialogs.
  5. Sweep the installers for curl -f, mktemp -d, and gpg --dearmor --yes — three mechanical patterns fix most of the installer findings.