Skip to content

v1.2.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 09:25
· 54 commits to master since this release
79e8227

Ubuntu Hello v1.2.0 - Keyring-Unseal Fix, Second-Model Wizard, Auth Notifications, GTK 4 + Hardening

This release fixes TPM-sealed keyring unlock under polkit, teaches the setup wizard to enroll a second face model for different lighting, adds native desktop notifications for every face-auth attempt (with the diagnostics behind the result), moves Settings / wizard / overlay to GTK 4, hardens how the root PAM module launches its compare.py helper, and refreshes every product and CI dependency pin to its latest final release. The hardening was in response to an automated vulnerability-scanner report of a potential local privilege escalation via a user-controlled PATH. On review the chain was not exploitable as described (details below), but the code relied on an unspecified corner of libc behaviour, so it is now explicit and no longer depends on it.


πŸ”’ What's in this release

  • Reported: pam_ubuntu_hello.so spawned compare.py as root with posix_spawnp(..., envp = nullptr) and never dropped privileges; compare.py then started the auth overlay with subprocess.Popen(["ubuntu-hello-gtk", ...]) β€” a bare-name PATH lookup. The scanner concluded that a local user could plant ~/.local/bin/ubuntu-hello-gtk and have it executed as root the moment face auth started (e.g. su <enrolled-user>).
  • Why it was not exploitable: on glibc/Linux, posix_spawnp() with a NULL envp gives the child an empty environment, not the caller's β€” verified with a small test program (FOO=bar ./t β†’ env prints nothing). With no PATH set at all, both glibc's execvp() and Python's subprocess fall back to /bin:/usr/bin, so ubuntu-hello-gtk already resolved to /usr/bin/ubuntu-hello-gtk; ~/.local/bin was never searched. The premise of an "inherited environment" was wrong.
  • Hardened anyway β€” PAM module: the helper is now started with posix_spawn() and an explicit minimal environment (PATH=/usr/sbin:/usr/bin:/sbin:/bin only), so the calling user's PATH, PYTHONPATH, LD_PRELOAD, … can never reach the root helper regardless of libc behaviour. The interpreter runs as python3 -E -s (ignore PYTHON* variables, no user site-packages). -I was deliberately not used: it would also drop the script directory from sys.path, which would break compare.py's sibling imports (snapshot, paths_factory, recorders, …).
  • Hardened anyway β€” compare.py: the auth overlay is now spawned via an absolute path (GTK_BIN_PATH = "/usr/bin/ubuntu-hello-gtk"), matching the existing BUSCTL_PATH pattern. No child of the root helper is resolved by bare name any more.
  • Docs: docs/SECURITY.md gains a "No Inherited Environment / PATH" bullet; docs/architecture/README.md Β§3.1 and AGENTS.md now describe the explicit-env spawn and -E -s flags.
  • πŸ”‘ Fix: TPM-sealed keyring unlock failed under polkit (worked at GDM): every pkexec / App Center prompt logged Failed to unseal keyring password from TPM. polkit β‰₯ 126 runs polkit-agent-helper-1 as a hardened transient unit (DevicePolicy=strict, ProtectSystem=strict); the shipped drop-in only re-allowed the camera and /dev/uinput, so /dev/tpmrm0 was Operation not permitted and the transient .ctx files under /etc/ubuntu-hello/tpm-keys/ hit a read-only filesystem. All three drop-in writers (install.sh, package-configure.sh, install_config.py) now add DeviceAllow=char-tpm rw, DeviceAllow=/dev/tpm0 rw, ReadWritePaths=/etc/ubuntu-hello/tpm-keys. Reproduced layer-by-layer with systemd-run under the unit's exact hardening and verified live: PAM_AUTHTOK set successfully for keyring unlocking on a real polkit prompt. Regression tests in test_install_download.py / test_packaging_release.py.
  • πŸ§‘β€πŸŽ¨ Setup wizard: second face model under different lighting: a single model recorded in daylight often lands just past the certainty threshold in the evening under lamps (or vice-versa), which shows up as intermittent "timeout reached" instead of a clear error (root-caused on real hardware: certainty 3.37–3.6 vs 3.5). Slide 4 now runs two passes: after the first scan it re-labels itself "Add a second face model (recommended)", explains why (lighting) and how (change the room light / desk lamp, turn away from the window, put on the glasses you sometimes wear), restarts the live preview and offers Scan second model. Either pass can be skipped, but not both: skipping pass 1 makes pass 2 required (Skip hidden, "Add a face model (required)"); a failed second scan is non-fatal (retry or skip) whereas a failed only-scan still stops the wizard. Models are labelled Setup lighting 1 / Setup lighting 2. Settings also shows a warning bar on the Models tab while exactly one model is enrolled, with an Add a second model action. 18 new strings translated in all 98 catalogs. Unit tests + real-GTK E2E (UH_REAL_GTK=1) cover both passes and both skip paths.
  • πŸ§‘β€πŸŽ¨ Guided multi-sample enrollment (Windows Hello style): a model was one descriptor from one frame, which is why logins landed just past the threshold intermittently. ubuntu-hello add now guides you through six small head movements (about a minute) and keeps up to 13 descriptors per model; the wizard and Settings show the live prompt and progress (the scan no longer blocks the UI), and the same prompt is mirrored on a notification card right under the camera. The optional second scan stays, now recommended for glasses on/off, a change of facial hair or different lighting. Fixes the winning-model lookup for multi-sample models.
  • πŸ”” New: native desktop notifications for every face-auth attempt (notify.py, org.freedesktop.Notifications): one card updated in place β€” "Face authentication Β· sudo β€” πŸ‘€ Looking for your face…" stays on screen for the whole scan (critical urgency: GNOME never auto-hides it), then becomes Face recognized Β· sudo β€” βœ“ Full Name Β· 1.3 s (removed after success_linger = 3 s) or Face not recognized β€” βœ— No match in 8 s Β· πŸ’‘ add a model in this light (stays in the list). Titles name the requester (sudo, su, authorization, screen unlock) because the PAM module now forwards the PAM service to compare.py. [notifications] details = true adds model, certainty/threshold, frames, dark frames, camera, pid, timeout, load. The notification id is persisted per user so consecutive attempts reuse the same card; a detached helper closes the success card and acts as a watchdog for a helper that died mid-scan. Layout follows GNOME Shell 50's rules read from its source (one-line body, <b> only; no desktop-entry/transient; low urgency never banners). Because compare.py runs as root with an empty environment, cards are sent on the target user's session bus by dropping to that uid before exec'ing the absolute /usr/bin/gdbus; greeter logins, missing gdbus, or any error silently disable it β€” never the auth result. Verified live on GNOME 50 / Wayland, including a real polkit prompt: the polkit helper drop-in now sets ProtectHome=tmpfs + BindReadOnlyPaths=-/run/user (yes hid /run/user, so no card could ever be delivered from Settings / App Center / pkexec; /home and /root stay empty inside the unit, only the runtime dir is bound back β€” security review) and ReadWritePaths=-/run/ubuntu-hello for the root-only id state, created at boot by a new tmpfiles.d/ubuntu-hello.conf. Strings translated in all 98 catalogs; libglib2.0-bin / glib2 added to core runtime deps.
  • βš™οΈ Settings β†’ Notifications tab: switches for Show notifications, Notification sounds and Include diagnostics ([notifications] enabled/sound/details), written with a new comment-preserving config_edit.set_option() (the bare-key ubuntu-hello set is ambiguous between sections). "Add new user" removed from the Models tab. Wizard: Back button on every page after the first (Cancel only on page 1) with per-page cleanup β€” camera scan cancelled and single device list on re-entry, chosen camera kept selected, IR test capture released, download not restarted, second scan resumed instead of a third model. First scan is required, only the second can be skipped; the two-scan plan is explained up front.
  • πŸ” New: Settings β†’ Security tab, agreeing-frame matching and recalibrated levels: a face used to be accepted on the first frame that fell under the match threshold. A scan takes dozens of independent looks at the camera, so the deciding frame was the minimum of many draws and a single blurred or oddly lit frame from a stranger could carry the whole attempt. New [video] confirmations requires several separate frames to agree before the match is trusted (Fast 2, Balanced 3, Secure 4); the real user reaches the count in well under a second. The thresholds were also recalibrated from measured logins rather than one sample β€” the previous Secure value matched no frame at all once the room light changed, and with a single enrolled model the distance to the same face drifts by about 0.4 between lighting conditions. Levels are now Fast 3.5 / Balanced 3.0 / Secure 2.6, with strictness carried by the frame count. A new Settings β†’ Security tab exposes the three levels plus an opt-in Ask me to nod to confirm switch, which the setup wizard's security page now offers too (its last page also saves on Finish rather than on arrival, so a level changed there is no longer lost) (a photo held up to the camera cannot nod; a video replay still can, the documented limit of challenge-response). Nod detection was rewritten around anchored nose displacement, the challenge prompt is mirrored to the notification card because the auth overlay can never open a display under PAM, and a liveness check that crashes now denies. Upgrades keep their own threshold and only gain the new key; a hand-tightened config stays on a single frame so the migration cannot break an existing login.
  • πŸ› οΈ Fixes found while auditing translations: a corrupt face model no longer hangs the login with the camera held (the detector thread's lock is now always released and the helper exits with an error); the camera test overlay draws translated labels with a real font instead of question marks (Pillow, now a runtime dependency); command literals, markup tags and English spelled out in other scripts are repaired across the catalogs and guarded by new lint rules; and four catalogs that held the wrong language entirely (Breton, Faroese, Occitan, Bashkir) are rewritten.
  • 🧱 GTK 4 migration: Settings app, setup wizard and auth overlay now run on GTK 4 + GtkBuilder .ui (no Glade). Root cause: the Language combo popup filled the whole screen on GTK 3 with 98 languages. Camera previews/logos are GtkPicture; Settings and wizard apply the user's Yaru accent + light/dark theme even under pkexec, and follow a switch live (no restart). No GTK 4.10-deprecated widgets remain (DropDown, ColumnView, AlertDialog, Revealer banner; guarded by a test) and every wizard page / Settings tab follows one GTK 4 page idiom (glyph, title, hint, cards). Instant language switch no longer freezes (deferred rebuild; .ui strings translated via Python gettext), RTL languages mirror the UI (E2E-audited with Arabic), About links to GitHub issues, cameras are named by brand/model from udev with by-path aliases and metadata nodes de-duplicated. A small gtk4compat.py wraps the removed GTK 3 calls (Gtk.main, Dialog.run, get_children, connect_signals). Runtime dependency is gir1.2-gtk-4.0 / gtk4 on every platform (deb, RPM, Arch, snap, AppImage/Flatpak host install); all UH_CI_DE compat cells build against GTK 4. All real-GTK E2E and unit tests ported.
  • 🎨 New app icon in the Yaru Settings idiom (light tonal badge, dark disc, monochrome face + check glyph): ubuntu-hello-gtk/icons/ubuntu-hello-gtk.svg (+ symbolic) installed to hicolor; logo.png/logo_about.png and the README banner rasterised from it.
  • πŸ” Review fixes (Cursor / Copilot): notification-id state moved from the user-writable /run/user/<uid> to root-owned /run/ubuntu-hello/notify/ (O_NOFOLLOW, no chown β€” closes a symlink privilege escalation); privilege drop via subprocess user=/group= instead of preexec_fn (unsafe with threads); a late in-progress card can no longer overwrite a result (_finalized), and the watchdog is killed by the result instead of a marker file; the success card is sent only after rubberstamp liveness passes (exit 15 β†’ "Liveness check failed"); no_model is reachable (notifier built before the model file is read); PAM forwards validated LANG/LC_* and compare.py applies the target user's language preference so cards and overlay are localised.
  • πŸ“¦ Dependency updates (product + CI), all to latest final releases: dlib 19.24.9 β†’ 20.0.1 (postinst pip build, scripts/package-configure.sh) and 19.24.6 β†’ 20.0.1 (snap payload) β€” verified to build from source on Ubuntu 26.04 / Python 3.14 with the full API intact; the CMAKE_POLICY_VERSION_MINIMUM shim is kept as a harmless no-op for UH_DLIB_PIP_SPEC overrides to 19.24.x. inih Meson wrap r53 β†’ r62 (wrapdb r62-1, verified via --force-fallback-for=inih). Flatpak runtime/SDK 25.08 β†’ 26.08 (manifest + release-builder image). Dockerfile frontend docker/dockerfile:1.26.0 β†’ 1.27.0 (all 16 Dockerfiles). CI coverage 7.15.4 β†’ 7.16.1. archlinux:base-devel snapshot 20260816 β†’ 20260906. GitHub Actions: docker/setup-qemu-action v4.2.0 β†’ v4.3.0, softprops/action-gh-release v3.0.2 β†’ v3.0.3. Already current and left as-is: actions/checkout v7.0.1, actions/upload-artifact v7.0.1, actions/download-artifact v8.0.1, crazy-max/ghaction-github-runtime v4.0.0, docker/setup-buildx-action v4.3.0, appimagetool 1.9.1, pytest 9.1.1, pytest-cov 7.1.0, keyboard 0.13.5, snapcraft rev 18514 (9.0.1), ubuntu:26.04, opensuse/leap:16.0. Deliberately not bumped: fedora:44 (Fedora 45 is still pending in Bodhi, not final) and snap base core24 (a core26 migration changes the build host/plugins and can't be validated locally β€” tracked as follow-up).
  • Verified: new regression test test_auth_overlay_uses_absolute_gtk_path in tests/test_compare_cleanup.py; pam_ubuntu_hello.so builds clean from a fresh Meson configure; Meson C++ tests (pam-face-skip, pam-aes-gcm-uh1) pass; compare test files and the no-suppressions lint pass.

New config section [notifications] (enabled, details); the wizard's face-scan slide now has a second pass; ubuntu-hello-gtk now depends on GTK 4 (gir1.2-gtk-4.0 / gtk4). Desktop / greeter / wallet compatibility is unchanged. Note the dlib major bump: fresh installs now compile dlib 20.x during package configure; existing hosts with the pip marker present are not rebuilt.


πŸ“₯ Installation (from Release assets)

Verify downloads against SHA256SUMS on the Release page before installing. Download this release's assets into an empty/clean directory before running these commands β€” the wildcards below match by filename pattern, and leftover assets from another version in the same directory can make a package manager see multiple matches (or, for the AppImage command, pass a second matched file as a stray argument).

Format Command
Debian (amd64 + arm64) sudo apt install ./ubuntu-hello_*_$(dpkg --print-architecture).deb ./ubuntu-hello-gtk_*_$(dpkg --print-architecture).deb
Fedora (amd64 + arm64) sudo dnf install ./ubuntu-hello-[0-9]*fc44*.$(uname -m).rpm ./ubuntu-hello-gtk-*fc44*.$(uname -m).rpm
openSUSE (amd64 + arm64) sudo zypper install ./ubuntu-hello-[0-9]*lp160*.$(uname -m).rpm ./ubuntu-hello-gtk-*lp160*.$(uname -m).rpm
AppImage (amd64 + arm64) chmod +x Ubuntu-Hello-*-$(uname -m).AppImage && ./Ubuntu-Hello-*-$(uname -m).AppImage --install
Arch (amd64 only) sudo pacman -U ./ubuntu-hello-[0-9]*.pkg.tar.zst ./ubuntu-hello-gtk-*.pkg.tar.zst
Snap (amd64 only) sudo snap install --dangerous --classic ubuntu-hello_*_amd64.snap && sudo snap run ubuntu-hello.host-install
Flatpak (amd64 only) flatpak install --user ./com.github.ventura8.UbuntuHello-*.flatpak && flatpak run --command=ubuntu-hello-host-install com.github.ventura8.UbuntuHello

PPA remains available:

sudo add-apt-repository ppa:ventura8/ubuntu-hello && sudo apt update && sudo apt install ubuntu-hello

Optional source install: download install.sh from the v1.2.0 tag, review it, then run locally β€” do not pipe a moving branch URL directly to bash.

Full Changelog: v1.1.6...v1.2.0