Repository navigation
v1.2.0
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.sospawnedcompare.pyas root withposix_spawnp(..., envp = nullptr)and never dropped privileges;compare.pythen started the auth overlay withsubprocess.Popen(["ubuntu-hello-gtk", ...])β a bare-namePATHlookup. The scanner concluded that a local user could plant~/.local/bin/ubuntu-hello-gtkand 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 aNULLenvpgives the child an empty environment, not the caller's β verified with a small test program (FOO=bar ./tβenvprints nothing). With noPATHset at all, both glibc'sexecvp()and Python'ssubprocessfall back to/bin:/usr/bin, soubuntu-hello-gtkalready resolved to/usr/bin/ubuntu-hello-gtk;~/.local/binwas 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:/binonly), so the calling user'sPATH,PYTHONPATH,LD_PRELOAD, β¦ can never reach the root helper regardless of libc behaviour. The interpreter runs aspython3 -E -s(ignorePYTHON*variables, no user site-packages).-Iwas deliberately not used: it would also drop the script directory fromsys.path, which would breakcompare.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 existingBUSCTL_PATHpattern. No child of the root helper is resolved by bare name any more. - Docs:
docs/SECURITY.mdgains a "No Inherited Environment / PATH" bullet;docs/architecture/README.mdΒ§3.1 andAGENTS.mdnow describe the explicit-env spawn and-E -sflags. - π Fix: TPM-sealed keyring unlock failed under polkit (worked at GDM): every
pkexec/ App Center prompt loggedFailed to unseal keyring password from TPM. polkit β₯ 126 runspolkit-agent-helper-1as a hardened transient unit (DevicePolicy=strict,ProtectSystem=strict); the shipped drop-in only re-allowed the camera and/dev/uinput, so/dev/tpmrm0wasOperation not permittedand the transient.ctxfiles 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 addDeviceAllow=char-tpm rw,DeviceAllow=/dev/tpm0 rw,ReadWritePaths=/etc/ubuntu-hello/tpm-keys. Reproduced layer-by-layer withsystemd-rununder the unit's exact hardening and verified live:PAM_AUTHTOK set successfully for keyring unlockingon a real polkit prompt. Regression tests intest_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 addnow 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 aftersuccess_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 tocompare.py.[notifications] details = trueadds 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; nodesktop-entry/transient; low urgency never banners). Becausecompare.pyruns 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 setsProtectHome=tmpfs+BindReadOnlyPaths=-/run/user(yeshid/run/user, so no card could ever be delivered from Settings / App Center /pkexec;/homeand/rootstay empty inside the unit, only the runtime dir is bound back β security review) andReadWritePaths=-/run/ubuntu-hellofor the root-only id state, created at boot by a newtmpfiles.d/ubuntu-hello.conf. Strings translated in all 98 catalogs;libglib2.0-bin/glib2added 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-preservingconfig_edit.set_option()(the bare-keyubuntu-hello setis 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] confirmationsrequires 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 areGtkPicture; 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,Revealerbanner; 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;.uistrings 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 smallgtk4compat.pywraps the removed GTK 3 calls (Gtk.main,Dialog.run,get_children,connect_signals). Runtime dependency isgir1.2-gtk-4.0/gtk4on every platform (deb, RPM, Arch, snap, AppImage/Flatpak host install); allUH_CI_DEcompat 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.pngand 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, nochownβ closes a symlink privilege escalation); privilege drop viasubprocessuser=/group=instead ofpreexec_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_modelis reachable (notifier built before the model file is read); PAM forwards validatedLANG/LC_*andcompare.pyapplies the target user's language preference so cards and overlay are localised. - π¦ Dependency updates (product + CI), all to latest final releases:
dlib19.24.9β20.0.1(postinst pip build,scripts/package-configure.sh) and19.24.6β20.0.1(snap payload) β verified to build from source on Ubuntu 26.04 / Python 3.14 with the full API intact; theCMAKE_POLICY_VERSION_MINIMUMshim is kept as a harmless no-op forUH_DLIB_PIP_SPECoverrides to 19.24.x.inihMeson wrapr53βr62(wrapdbr62-1, verified via--force-fallback-for=inih). Flatpak runtime/SDK25.08β26.08(manifest + release-builder image). Dockerfile frontenddocker/dockerfile:1.26.0β1.27.0(all 16 Dockerfiles). CIcoverage7.15.4β7.16.1.archlinux:base-develsnapshot20260816β20260906. GitHub Actions:docker/setup-qemu-actionv4.2.0βv4.3.0,softprops/action-gh-releasev3.0.2βv3.0.3. Already current and left as-is:actions/checkoutv7.0.1,actions/upload-artifactv7.0.1,actions/download-artifactv8.0.1,crazy-max/ghaction-github-runtimev4.0.0,docker/setup-buildx-actionv4.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 stillpendingin Bodhi, not final) and snap basecore24(acore26migration 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_pathintests/test_compare_cleanup.py;pam_ubuntu_hello.sobuilds clean from a fresh Meson configure; Meson C++ tests (pam-face-skip,pam-aes-gcm-uh1) pass;comparetest 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-helloOptional 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