Skip to content

Fix/issue 4 hardening sweep - #6

Merged
otectus merged 8 commits into
mainfrom
fix/issue-4-hardening-sweep
Jun 15, 2026
Merged

Fix/issue 4 hardening sweep#6
otectus merged 8 commits into
mainfrom
fix/issue-4-hardening-sweep

Conversation

@otectus

@otectus otectus commented Jun 15, 2026

Copy link
Copy Markdown
Owner

No description provided.

otectus and others added 8 commits April 30, 2026 22:29
Closes the install half of #4: the new dbus.service reload after copying
io.otectus.Archer1.conf removes the "had to reboot to connect" symptom.

  - modules/gui.sh: reload dbus.service (with kill -HUP fallback) on
    install and uninstall so the policy file takes effect immediately.
  - modules/gui.sh: pre-flight existence checks for every source file
    so a missing .conf or .svg fails the module instead of silently
    leaving the system half-installed.
  - install.sh, uninstall.sh: source new lib/modules.sh which holds the
    canonical MODULE_IDS / MODULE_LABELS arrays and an is_known_module
    allowlist. The uninstall manifest source loop now refuses to source
    untrusted module names (e.g. "../../tmp/evil") that a tampered
    install-manifest.json could otherwise inject.
  - lib/manifest.sh: move manifest to /var/lib/archer/install-manifest.json
    (root-owned, 0644). Resolves the install-as-user / uninstall-as-sudo
    HOME mismatch and stops local users tampering with manifest entries
    that uninstall later sources. migrate_legacy_manifest() handles
    existing installs from $HOME or /root, including the legacy DAMX path.
  - install.sh: per-module rollback. INSTALLED_FILES / DKMS / PACKAGES
    are snapshotted before each module_install and restored on failure,
    so the saved manifest only lists modules that actually installed.
  - uninstall.sh: guard rm -rf "$HOME/.local/share/..." against unset
    or "/" HOME (sudo can leave HOME unset).
  - tests/install.bats: cover is_known_module against path-traversal,
    shell metacharacters, well-formed-but-unknown IDs, and every
    canonical module ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Unix-socket fallback in the daemon was unreachable from the user-mode
GUI (the socket was created with mode 0o660 root:root). Removing it means
D-Bus is now mandatory, and a real D-Bus startup failure produces an
actionable error message instead of a misleading "deprecated" warning.

  - gui/archer-daemon.service: switch PID/socket scratch to
    RuntimeDirectory=archer (systemd creates /run/archer/ at every start
    with 0755 root:root and removes it on stop). Add ReadWritePaths=
    /etc/archer because ProtectSystem=full would otherwise block the
    settings.json writes. Add Requires=dbus.service so the daemon won't
    start before the bus.
  - gui/archer_daemon.py: drop the DaemonServer class and its 30+
    _cmd_* dispatch methods. Drop SOCKET_PATH and the socket import.
    Move PID_FILE to /run/archer/daemon.pid. Rewrite main() so a missing
    or failed D-Bus registration logs a clear error pointing at the
    policy file + dbus reload, then exits.
  - gui/archer/pages/internals.py: replace the stale
    "Daemon Socket: /var/run/archer.sock" UI string with the actual
    D-Bus name "io.otectus.Archer1 (system bus)".
  - gui/install-gui.sh: removed (legacy wrapper, no in-tree references;
    use ./install.sh --modules gui).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the first half of the issue #4 user-visible fix. The GUI no
longer hangs when the daemon is slow, and the user sees an actionable
message when D-Bus is genuinely down.

  - gui/archer/client.py: every D-Bus method invocation now passes
    timeout=TIMEOUT (5s default) including the startup Ping(). Setters
    that legitimately take longer (envycontrol, fwupd, restart) get
    LONG_TIMEOUTS overrides. Without this, dbus-python defaulted to no
    timeout and a single hung sysfs read in the daemon would freeze the
    polling thread forever.
  - gui/archer/client.py: drop the dead Unix-socket fallback and its
    234 lines of retry/backoff. The daemon socket has been root:root
    0o660 since the move to D-Bus, so the user-mode GUI was never
    actually able to fall back. _init_dbus failures are now stored on
    self._init_error (and logged at WARNING) so the window can show
    them in a toast instead of silently flipping to "Daemon Offline".
    Add reconnect() so the window's backoff loop can re-handshake after
    a daemon restart (the old proxy is invalidated).
  - gui/archer/window.py: replace the fixed-5s reconnect with an
    exponential backoff sequence (5, 10, 20, 60s) so a long-down daemon
    doesn't get hammered. Reset to step 0 on the first successful
    settings load. Show the client's init_error in a toast at the
    moment the GUI first detects the offline daemon.
  - gui/archer/application.py: tray import / startup failures are
    logged at WARNING with the exception (was bare 'except: pass').
    First time the window is shown after a tray-init failure, a one-
    shot toast tells the user that close-to-tray won't work this
    session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second half of the issue #4 user-visible fix. Telemetry now arrives via
a D-Bus signal instead of being polled from the GUI thread, so a hanging
daemon can never freeze the dashboard.

  - gui/archer_dbus.py: ArcherDBusService now arms a 2s GLib timer that
    serializes get_monitoring_data() into the existing TelemetryUpdated
    signal (declared but never emitted in the previous version). Returns
    True so GLib re-arms; transient sysfs errors are logged but don't
    kill the timer.
  - gui/archer/window.py: rip out _poll_monitoring / _fetch_monitoring
    (and the associated thread spawning) and replace with a signal
    subscription on TelemetryUpdated. _on_telemetry_signal updates the
    last-seen monotonic timestamp and pushes data into the dashboard;
    _check_staleness flips the status label to "Stale" if no signal
    arrives within 6s and kicks the existing reconnect/backoff path.
  - gui/archer/window.py: re-subscribe on every _start_monitoring call
    by removing the prior SignalMatch first, so a daemon restart (which
    invalidates the old proxy) doesn't leave the GUI permanently
    listening on a dead handle.
  - gui/archer/window.py: _on_settings_loaded clears _is_stale on
    success so the state machine can't get stuck "Stale" if the next
    signal is delayed.

GetMonitoringData stays on the wire for the initial settings fetch and
backward compat with older clients; the GUI subscribes after the first
successful settings load.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  - run_cmd now refuses cmds containing shell metacharacters (;, &&,
    ||, $(, backtick) unless the caller passes shell_meta_ok=True.
    No present callsite is exploitable, but the guard catches future
    regressions where a user-supplied value flows into a shell string.
    All legitimate static-pipeline callsites (awk, lspci|grep, the
    systemd-run wrappers) are tagged explicitly.
  - _TtlCache (5s) wraps every nvidia-smi, lspci, and `which envycontrol`
    invocation. The GUI's monitoring loop hits get_gpu_temp /
    get_gpu_usage / get_display_mode every 2s; without the cache that
    spawns nvidia-smi 30+ times per minute, which is both slow and a
    common hang source on systems with troubled NVIDIA drivers. All
    cached probes also pick up timeout=3 so a hung subprocess can't
    starve the telemetry timer.
  - get_fan_rpm now matches hwmon devices against an Acer-relevant
    chipset allowlist (linuwu_sense, acer_wmi, nct67xx, it87,
    dell_smm_hwmon) before falling back to "first device with
    fan1_input". Previously the first-found rule could return RPMs from
    e.g. a discrete GPU sensor instead of the platform fan controller.
  - restart_daemon and restart_drivers_and_daemon use systemd-run
    --on-active=2s --no-block so the D-Bus reply for the Restart*
    method has time to flush before SIGTERM lands. The previous
    synchronous systemctl restart killed the daemon in-flight and the
    GUI saw a "connection lost" toast on every legitimate restart.
  - gui/archer_dbus.py: hoist `import os` and `import threading` out of
    method bodies. Cosmetic; keeps imports in one place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… failure

  - gui/archer_dbus.py: declare AudioEnhancementChanged(b) and emit it
    after the noise-suppress filter chain rename. Removes the broken
    `systemctl --user restart pipewire.service` from inside the root
    daemon (it targeted root's systemd-user instance and never restarted
    the actual user's pipewire — the noise toggle was effectively a
    no-op until the next pipewire restart).
  - gui/archer/window.py: subscribe to AudioEnhancementChanged on the
    same path that subscribes to TelemetryUpdated; on receipt, run
    `systemctl --user restart pipewire.service` from the GUI process
    so the restart hits the actual user instance, then toast the user.
    Resubscribe after each daemon restart by tracking _audio_signal_match
    alongside _telemetry_signal_match.
  - gui/archer/widgets/async_set.py: new helper. Standard
    async_set(setter_fn, args, on_success, on_failure) wraps a daemon
    call in a thread, marshals the result onto the GLib main thread,
    and runs on_failure with the daemon's error string when the call
    times out, gets denied by polkit, or returns success=False.
  - gui/archer/pages/battery.py, system.py, keyboard.py: convert the
    fire-and-forget `threading.Thread(target=lambda: client.set_X(...))`
    pattern to async_set with revert-on-failure. Switches and combos
    flip back to the previous value with an explanatory toast when the
    daemon refuses; the keyboard color/effect setters get a toast (no
    revert because the UI doesn't track committed colour state).

The display, performance, gamemode, audio_enhance, and firmware pages
already had local _apply / _revert paths and were left alone — converting
them to async_set is mechanical churn for no behavioural gain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  - tests/dbus_smoke.py + tests/dbus_smoke.sh: headless smoke harness
    that runs ArcherDBusService against an ephemeral session bus with a
    mocked HardwareManager. Asserts Ping, the required methods (incl.
    GetMonitoringData, SetAudioEnhancement) and signals (TelemetryUpdated,
    AudioEnhancementChanged, ProfileChanged) appear in the introspection
    XML, and waits up to 5s for the first TelemetryUpdated emit. Exits
    nonzero on any miss so CI fails loudly.
  - .github/workflows/ci.yml: new python-syntax job that py_compiles
    every gui/**/*.py. New dbus-introspect-smoke job that boots a
    private session bus via dbus-run-session and runs tests/dbus_smoke.sh.
    Runs in parallel with the existing ShellCheck/bats/flake8 jobs.
  - .github/ISSUE_TEMPLATE/gui-not-updating.md: structured template for
    the exact failure mode in #4. Asks for distro+kernel, daemon status,
    journal, busctl introspect, GUI logs, and the 'did you try' boxes.
    Would have closed #4 in one round trip.
  - README.md: add a Troubleshooting section covering "Daemon Offline /
    Stale" and "GUI hangs forever". Update the Install Manifest note to
    point at the new /var/lib/archer location and mention the legacy-
    path migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardening-sweep release (closes #4 + audit findings).

  - lib/utils.sh: INSTALLER_VERSION 2.0.0 -> 2.0.1
  - gui/archer_daemon.py: VERSION 2.0.0 -> 2.0.1
  - gui/archer_dbus.py: Ping fallback 2.0.0 -> 2.0.1
  - gui/archer/__init__.py: __version__ 1.0.0 -> 1.0.1
  - gui/archer/pages/system.py: GUI Version row 1.0.0 -> 1.0.1
  - CHANGELOG.md: new file. Categorized Fixed / Changed / Security /
    Added / Removed entries with file references; migration notes for
    upgraders from 2.0.0 covering the manifest path move,
    RuntimeDirectory rename, and the brief dbus.service reload.
  - README.md: link the changelog from the new "Changelog" section
    above Contributing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@otectus
otectus merged commit 220c801 into main Jun 15, 2026
5 of 6 checks passed
@otectus
otectus deleted the fix/issue-4-hardening-sweep branch June 16, 2026 08:06
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