Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
12 KiB
Bug bash — 2026-09-18
Fix pass 2026-09-18: every item carries its outcome; ✅ resolved and verified live, ➖ no change with reason. Found by a headless Chromium pass (iPhone and 1440px desktop viewports) over every page on the hub and on each remote machine, an offline-node scenario (wyse stopped for the test, then restarted), direct API probing with hostile inputs, and a code read. Ordered by severity within each group.
Security / robustness
- ✅ RESOLVED 2026-09-18 — only regular files are readable; the byte cap applies to what is sent, not to
stat./dev/zero,/dev/nulland FIFOs answer 400 in 30 ms;/proc/cpuinfostill reads. Reading a special file hangs the request forever.GET /api/files/read?path=/dev/zeronever ends: themaxcap compares againststatsize, which is 0 for device files, so the cap never triggers and the handler streams/dev/zerountil the client gives up. Same for FIFOs (blocks on open) and/proc/self/mem. Opening one in the Files viewer would hang the tab. Needs: refuse non-regular files for read/edit, and cap the copy withio.LimitReaderregardless ofstat. - ✅ RESOLVED — per-client buckets (first X-Forwarded-For hop or the peer) with the same 5-strike schedule, plus a global backstop of 100 failures per 5 min → 1 min. Verified: core's public IP locked while grid logged in fine. Login lockout is global, not per client. Five bad passwords from anyone on the
internet lock the real user out for up to 10 minutes (
auth.Limiteris one bucket). The bash locked the hub for the full 10 minutes twice. Also locksPOST /api/tokens, so hub pairing fails during a lockout. Needs per-IP buckets with a modest global cap. - ✅ RESOLVED — server rejects refreshMs outside 100–60000 and non-identifier themes (400); client ignores out-of-range rates from any source. UI prefs from the server are not validated. A stored
refreshMsof-5made the client callsetInterval(-5)on every live view: a request storm that took the browser toERR_INSUFFICIENT_RESOURCESand hammered the daemon.themeis validated only by falling back. Needs range checks on load (and on save server-side). - ✅ RESOLVED — every response carries X-Frame-Options DENY, CSP frame-ancestors none, nosniff, no-referrer. No anti-framing / CSP headers. Only HSTS (from NPM). A root-capable console should
send
X-Frame-Options: DENY,Content-Security-Policy: frame-ancestors 'none',Referrer-Policy: no-referrer,X-Content-Type-Options: nosniffon every response. - ✅ RESOLVED — kernel threads (empty cmdline) and the daemon's own PID are refused. Signals can be sent to kernel threads.
POST /api/procs/2/signal(kthreadd) returns 204. Harmless in practice, but the guard should refuse PIDs with an empty cmdline (kernel threads) and our own PID. - ✅ RESOLVED — unit runs with
SupplementaryGroups=systemd-journal admso journal reads need no sudo, and the installer drops/etc/sudoers.d/hamlab-quiet(no syslog/pam session for the read-only polling commands). Verified: 0 sudo lines in 60 s of 250 ms polling (was 839/10 min). Applied on all four hosts. Journal spam from the daemon's own sudo calls. Every sudo'djournalctl,smartctlorpwrstatwrites threepam_unix(sudo:session)lines into the journal underhamlab.service: 839 lines in 10 minutes at a 250 ms refresh, and the journal is already 4 GB. It also buries Ham Lab's own log entries. Fix options: add the service user tosystemd-journal(andadm) so journal reads don't need sudo, and/or a sudoersDefaults!/usr/bin/journalctl !syslog, !pam_sessionline in the installer. - ✅ RESOLVED — server answers 409 unless
overwrite=1; the UI asks "Replace it?" first. Uploading over an existing file silently overwrites it. No confirmation, no "replace?" prompt. Same for "+ file" when the name exists (server refuses; UI just shows the error, fine) — upload is the gap. - ➖ NO CHANGE — Go's mux redirects the unclean path to
/etc/passwd, which is an ordinary deep link the SPA must serve; there is no API prefix left by the time the static handler sees it. Harmless./api/<anything>/../../xfalls through to the SPA and returnsindex.htmlwith 200 instead of a JSON 404. Harmless, but sloppy for an API prefix.
Functional bugs
- ✅ RESOLVED — 401 carries the server message; the form shows "wrong password" (verified headless). Wrong password gives no feedback at all. The 401 is thrown as
Unauthenticatedwith an empty message, and the login form only renderserrwhen non-empty. The button just re-enables. Reproduced on phone and desktop. - ✅ RESOLVED — hub-only pages bounce to the dashboard on switch (verified headless). Switching machines while on Settings or Machines keeps showing the hub's page.
switchMachineonly resets the view for Machines; Settings stays, so "mac" shows core's account/token/about panels. Both are hub-only pages and should bounce to the dashboard (or be clearly labelled as hub pages). - ✅ RESOLVED — escaped; no console error. Machines add-form
patternis an invalid regex in Chromium.[a-z0-9][a-z0-9-]*fails under thevflag ("Invalid character class"), logs a console error and the browser skips validation. Needs[a-z0-9][a-z0-9\-]*. - ✅ RESOLVED — refresh from the newest repodata under libdnf5/dnf caches (Mac: 9 d), last upgrade from
dnf history(194 d), label reads "from dnf history". Updates tab on dnf hosts shows nonsense. Mac: "Index refreshed 561d ago" (mtime of/var/cache/dnf, which Fedora 41+ no longer uses;libdnf5is the live cache) and "Last upgrade never · from apt history" (label and source are apt-specific). Needsdnf historyfor last upgrade, metadata timestamps for refresh, and neutral labels. - ✅ RESOLVED — journal read incrementally by cursor, timers from one
systemctl show, services' two calls run in parallel. Cron 3.6 s → 0.08 s, services 0.6 s → 0.05 s, with last-run and next-run data intact. Cron overview is slow: 3.6 s per request on core. At any refresh rate the page is constantly busy. The 30-day, 20 000-line journal scan for "last run" is rebuilt every 5 s; timers make threesystemctlcalls. Needs an incremental journal cursor and a singlesystemctlcall. Services list is 0.6 s for the same reason (two calls). - ✅ RESOLVED — heap profile showed a 48 MB
bytes.Bufferfrom bufferingjournalctldumps; those are now streamed line by line, plus a 160 MB soft memory limit for the GC. RSS 22 MB fresh, 27 MB after 25× cron/logs/procs/services (was 180 MB). pprof stays available behind the login at/api/debug/pprof/. Daemon memory grows. RSS on core went 22 → 85 → 216 MB during the day (136 MB at rest after the bash). Suspects: per-pid caches in the process table (procInfoonly prunes on the next walk), task output buffers, SMART cache, xterm scrollback rings that are never freed for exited sessions. Needs a heap profile under load. - ✅ RESOLVED —
--dirtydropped from the version and deploys now follow commits; binaries report e.g.v0.1.0-38-gc1805f6. Version string is always-dirty.make deploybuilds before the commit, so every installed binary reports e.g.v0.1.0-32-ga372378-dirty, and the Machines page compares with-dirtystripped. Build after commit, or tag releases. - ✅ RESOLVED — hash routing
#/<machine|hub>/<view>: reload restores machine and tab, Back/Forward work, links can point at a page on a machine (verified headless). View resets to Dashboard on reload; no deep links; browser Back leaves the app. The selected machine survives a reload (sessionStorage) but the tab does not, and there is no URL routing at all. Needs hash or path routing (/#/grid/services). - ✅ RESOLVED — newest first. Cron log tab lists oldest first and starts at the top. 200 lines from Sep 10 with the newest at the bottom, off-screen. Should show newest first or scroll to the end.
- ✅ RESOLVED — "Nothing matches." Processes: empty filter result shows nothing. No "nothing matches" message.
- ✅ RESOLVED — exited shells remove themselves 10 minutes after exit (verified live: seeded 17:53:56, gone at 18:03:59 with no daemon restart in between). Found and fixed alongside: a daemon restart used to re-spawn exited shells as fresh running ones; only running shells are persisted now. Idle running shells are deliberately kept. Terminals accumulate. Nothing ever reaps exited or idle sessions; the bash left several "bug-bash-term" shells on the hub until deleted by hand. Exited sessions should auto-remove after a while; idle ones at least be counted somewhere.
- ✅ RESOLVED — a node answers 409 "busy" when a package task is running or a terminal is attached; the Machines page offers Force,
hamlab push-nodes --force/make push-nodes-forcedo the same. Verified: 409 with a terminal attached on grid, 200 with force. Self-update restarts without warning running work. A push to a node kills its open terminals and any running task (upgrade in progress). The hub should refuse or warn when the node reports a running task or attached terminals. - ✅ RESOLVED — PathError and LinkError are unwrapped to the bare reason. Error text leaks raw Go errors. e.g. rename shows "rename /tmp/x /tmp/y: no such file or directory". Fine for a lab, ugly for a product; strip the syscall prefix.
Mobile / UX
- ✅ RESOLVED — on coarse pointers every button, chip, icon button, link, header and select is at least 36 px tall (verified: no control under 32 px on the dashboard or processes pages). Tap targets under 28 px on phone: header ⏻ (25), logout (22), customize ▲ ▼ hide
(23), terminal leaf ✎ ✕ (24), chips (25), table headers (27). Apple's guideline is
44 pt. Systemic: the small
.btn/.chip/.icon-btnsizes need a touch variant. - ✅ RESOLVED — the dialog asks for the machine name (
mac); the node still checks its own hostname under the hood. Power dialog on the Mac asks you to type5ce91ee54af6. The confirmation uses the node's hostname, which on the Mac is the unset default. Should offer the machine name (mac) as the confirmation word on remote machines. - ✅ RESOLVED — down interfaces collapse to one line marked "down"; modules with nothing to show (fans, UPS, temps, disks) hide unless customizing. Dashboard shows dead interfaces and empty modules. wlp1s0 (down) gets a full sparkline block of flat lines; "Fans: no fan sensors" is a permanent empty card on hosts without fans. Down interfaces should collapse to one line; empty modules should auto-hide unless customizing.
- ✅ RESOLVED — "load 1.39 1.56 1.55" with a tooltip. Header load average is unlabelled ("1.39 1.56 1.55" floating in the header).
- ➖ NOT FIXING — the list is intentionally a plain table; the menu anchors inside the row. Cosmetic taste, not a defect. Files list on desktop is a narrow strip in a sea of empty space for small folders; the row menu opens off the right edge of the row. Cosmetic.
- ✅ RESOLVED — after a 502 the client leaves that machine alone for 5 s: 2 requests in 10 s at a 250 ms refresh (would have been ~80), badge and message intact. Offline machine keeps polling every view at the refresh rate, producing a 502 per request per page (and console errors). The UI copes — badge, message — but the hub should back off to the prober's cadence while a node is down.
- ✅ RESOLVED with 6. Services logs are noise. The default view of
hamlab.servicelogs is the sudo session spam from bug 6, three lines per poll.
Confirmed working under abuse (for the record)
Path traversal in files, services, cron, ZFS, procs and updates all rejected. /etc
and / protected from delete/rename as root. Unit, pool, dataset, snapshot and
package names validated. Token issue refused with a wrong password. Oversized prefs
rejected. Duplicate machine add refused. Unicode + spaces in file names round-trip.
Upload byte-exact. Terminal scrollback survives switching. Offline node: switcher and
header show it, pages show the reason. Remote terminals, logs, tasks and files through
the proxy all work on all three nodes.