Repository navigation
Conversation
Replaces unaccelerated/unsupported video thumbnailing in GNOME with a rootless containerized ffmpeg daemon running under background.slice. - Dual quadlet (.container) files for automatic variant selection: - Non-NVIDIA (AMD/Intel/software): docker.io/jrottenberg/ffmpeg:9.0-alpine320 (lean 111 MB) - NVIDIA: docker.io/jrottenberg/ffmpeg:9.0-nvidia2404 with CDI gpu=all - In-sandbox pure-Python shim passes input/output file descriptors over SCM_RIGHTS via UNIX domain socket in the GStreamer cache dir. - Daemon invokes 'podman exec --preserve-fd' with in-container timeout(1) and single-thread decode. - Resource-governed under background.slice with CPUWeight=20, IOWeight=20, MemoryHigh/Max and TasksMax. - Precedence over /usr/share/thumbnailers/gst-video-thumbnailer guaranteed via environment.d prepending /usr/share/ublue-os/thumbnailer to XDG_DATA_DIRS. - Unit tests added in tests/test_ffmpeg_thumbnailer.py and registered in Justfile + unit-tests.yml. Assisted-by: Claude Opus via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: castrojo <castrojo@users.noreply.github.com>
- Raise TasksMax from 32 to 512 in daemon.service to support concurrent podman exec clients. - Fix NVIDIA CDI AddDevice specification (drop leading hyphen). - Fix aarch64 NVIDIA handling: ConditionArchitecture=x86-64 on NVIDIA unit, triggering ConditionArchitecture=|!x86-64 and ConditionPathExists=|!/sys/module/nvidia/version on Alpine unit. - Validate passed file descriptors in daemon: ensure regular file type and output owner matches uid. - Return ERR_UNAVAILABLE when container is stopped/pulling; shim falls back to local ffmpeg rather than failing permanently. Assisted-by: Claude Opus via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: castrojo <castrojo@users.noreply.github.com>
Automated Review Summary & Hardening (PR #1226)This branch has been audited and updated to address all findings from parallel code and security reviews:
|
…ementarity, and ERR_UNAVAILABLE - Add test_daemon_handle_request_invalid_fd_type: asserts daemon rejects non-regular fds (sockets). - Add test_daemon_returns_unavailable_when_container_missing: asserts daemon returns ERR_UNAVAILABLE when container is absent (exit 125). - Add test_shim_falls_back_on_err_unavailable: asserts shim invokes local ffmpeg fallback on ERR_UNAVAILABLE. - Add test_quadlet_condition_complementarity: asserts identical ContainerName, RunInit=true, and NVIDIA CDI presence. - Ensure fallback ffmpeg invocation in shim uses explicit -f image2 -c:v png. Assisted-by: Claude Opus via GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: castrojo <castrojo@users.noreply.github.com>
Operational Verification & Unexercised Environmental Matrix1. Exercised Verification (Proved Locally):
2. Unexercised Environmental Matrix (Requires Full Image Build / Hardware):
|
|
I ran this end to end on Bluefin 44 (GNOME 50, SELinux enforcing), using GNOME's own thumbnail code ( 1. GNOME deletes the daemon's socket after every thumbnail. After each thumbnailer run, gnome-desktop empties Here's what happened. I hid host ffmpeg from the sandbox so the fallback couldn't mask the result:
So any thumbnail at all cuts the link, even a picture. After that, video thumbnails only work if the host has its own ffmpeg for the fallback. Where it doesn't (the Dakota case in the description), they fail. Nautilus normally records a failed thumbnail and doesn't retry until the file changes. 2. On podman 5.8 the quadlets never load. Only podman 6.0 and later read rootless quadlets from In podman's source, Smaller things:
What I didn't cover: NVIDIA, aarch64, and a real login session. The /usr/libexec path doesn't exist on my machine, so I pointed a copy of the |
hanthor
left a comment
There was a problem hiding this comment.
Nice work on the sandboxing, but this needs changes and a maintainer decision before it can land:
- Default-on everywhere.
user-preset/01-ffmpeg-thumbnailer.preset, the staticgraphical-session.target.wantssymlink andWantedBy=default.targetinusers/*.containerturn this on for every user on every image. Each login then pullsdocker.io/jrottenberg/ffmpeg, which is about 111 MB, or multi-GB for the CUDA variant on nvidia. It's digest-pinned, but it lives in an individual's Docker Hub namespace and nothing tracks updates to it. Either rebuild and sign it underghcr.io/projectbluefin, or make the feature opt-in. ffmpeg-thumbnailer*.container:SecurityLabelDisable=trueremoves SELinux confinement from the process whose whole job is parsing untrusted media. Please keep the label, or document why it has to be dropped.environment.d/10-ffmpeg-thumbnailer.confmakes the shim own every video MIME type even when the daemon or container is down. The fallback is hostffmpeg, which Dakota doesn't ship, so thumbnails disappear instead of falling back to gst.- The description says the daemon runs with
MemoryMax=1024M, but the unit sets 512M.
|
Status update on ffmpeg-thumbnailer:
Holding open as an author/maintainer design decision regarding default vs opt-in service enablement. |
- Drop SecurityLabelDisable=true from both container units so the untrusted-media-parsing process keeps SELinux confinement (keeps the label rather than dropping it). - Raise the host daemon.service MemoryHigh/MemoryMax to 512M/1024M to match the container units, removing the 512M-vs-1024M inconsistency.
|
Two of the four items were clear code fixes; I've pushed those. The other two are maintainer decisions, and I want to flag them before touching code. Fixed (pushed cce2e80): #2 SELinux label — dropped #4 MemoryMax — the host daemon unit was the outlier (MemoryHigh=256M/MemoryMax=512M); both containers already run 512M/1024M. Raised Need a maintainer decision (coupled): #1 Default-on everywhere. This is the real one. Two honest paths:
I'd start opt-in and build the signed ghcr image as a follow-up, but flipping the rollout is a product call, not a code fix — flagging rather than doing it unilaterally. #3 environment.d MIME ownership. This is coupled to #1. The shim already returns Want me to implement the opt-in path for #1 (and #3 falls out), or would you rather go straight to the signed ghcr image?🐝 Hive Agent: |
|
Thanks for the SELinux and MemoryMax fixes. On the rollout, I'd go opt-in first: no preset enable, no static One thing from my earlier review is still open at cce2e80 and isn't in the list above: the daemon's socket still sits in |
…reamer cache GNOME's gstreamer clean_gst_registry_dir() periodically deletes the contents of ~/.cache/gnome-desktop-thumbnailer, including the daemon's socket path. The old listener kept serving on the now-dead path, so every new thumbnailer connection failed until a full daemon restart. serve_recovering() polls for the missing socket and rebinds at the same path (unlinking the stale socket first so the new listener owns a fresh inode) without a restart. Request handling still runs in per-request threads, so the poll loop never blocks on a long ffmpeg job. Adds a regression test that deletes the socket mid-run and verifies the daemon rebinds and answers PING again.
…p path gnome-desktop's clean_gst_registry_dir() runs g_remove() on every top-level entry of ~/.cache/gnome-desktop-thumbnailer/gstreamer-1.0 except gstreamer-1.0.registry after each thumbnailer run. The socket lived at the top level, so every thumbnail deleted it. The rebind loop added in 3596c80 only notices after up to 0.5s, while Nautilus starts the next thumbnailer almost immediately, so back-to-back video thumbnails still missed the socket and failed on hosts without ffmpeg. Move the socket into a ffmpeg-thumbnailer/ subdirectory. remove() cannot delete a non-empty directory, so the directory and the socket inode survive the cleanup. The path keeps the old length so AF_UNIX's 108-byte limit is not reached for any home directory the old path supported. The rebind loop stays as a fallback for a wiped ~/.cache and now recreates the subdirectory. Also drop the duplicate test_bluefin_countme.bats line from the Justfile test recipe; main already registers it. Assisted-by: Claude via omp Signed-off-by: castrojo <castrojo@users.noreply.github.com>
castrojo
left a comment
There was a problem hiding this comment.
I pushed 907b843 to fix the socket-lifetime problem Danathar reported. 3596c80 did not fully fix it.
What was wrong at 3596c80
After every thumbnailer run, gnome-desktop's clean_gst_registry_dir() (libgnome-desktop/gnome-desktop-thumbnail-script.c) calls g_remove() on each top-level entry of ~/.cache/gnome-desktop-thumbnailer/gstreamer-1.0/ except gstreamer-1.0.registry. The socket sat at that top level, so every thumbnail deleted it.
serve_recovering() notices the missing path within 0.5s and rebinds. But Nautilus starts the next thumbnailer almost immediately, so the next shim finds no socket, and on a host without ffmpeg it fails.
I reproduced this with the real daemon (fake podman writing a PNG), the real shim with no ffmpeg on PATH, and a faithful copy of the cleanup (libc remove() on top-level entries) after each call. Twenty back-to-back thumbnails gave 2 successes and 18 failures.
Fix (907b843)
ffmpeg-thumbnailer-daemon:40-43andffmpeg-video-thumbnailer-shim:27-33: the socket moves togstreamer-1.0/ffmpeg-thumbnailer/sock.remove()cannot delete a non-empty directory, so the subdirectory and the socket inode survive. Only the bind in that cache directory is user-writable (/usr,/etc/ld.so.cache,/etc/alternativesand the fontconfig cache are root-owned read-only binds), so a subdirectory there is the only place that is both reachable and not cleaned.- The new path is the same length as the old one, so the 108-byte AF_UNIX limit is no tighter for long
/var/home/<user>paths. - GNOME now logs "Failed to delete left-over thumbnailing '.../ffmpeg-thumbnailer'" once per thumbnail. Before, it logged "Left-over file ... deleting" once per thumbnail, so the log volume is the same.
make_server()now recreates the subdirectory, so the rebind loop still handles a user wiping~/.cache.- After the fix, the same repro gave 20 successes and 0 failures.
- New test
test_socket_survives_gnome_gst_registry_cleanupruns the cleanup against the daemon's real socket layout. It asserts that the shim and daemon resolve the same path, that the socket inode is unchanged, and that PING succeeds with no wait. It fails on 3596c80 and passes on 907b843. Justfile: I dropped the thirdbats tests/test_bluefin_countme.batsline this PR added; main already registers it.
CI at 907b843 is green: validate, both image builds, Unit Tests (54 passed in the validators step, including the new test), and PR E2E on GNOME 50.
Status of @hanthor's review
- SELinux label: fixed in cce2e80;
SecurityLabelDisable=trueis gone from both.containerunits. This still needs a re-test on an enforcing host. Danathar's end-to-end run usedlabel=disable, and with the label on, ffmpeg incontainer_treopens/proc/self/fd/Nfor host-labelled files, which container-selinux may deny. - MemoryMax mismatch: fixed in cce2e80.
ffmpeg-thumbnailer-daemon.servicenow has MemoryHigh=512M and MemoryMax=1024M, matching the containers. - Default-on, and the image in an individual's Docker Hub namespace: open, needs a maintainer decision. Opt-in (remove the user-preset, the
graphical-session.target.wantssymlink andWantedBy=default.target) is the smallest path. Danathar has also said he prefers it. environment.dMIME ownership when the daemon or container is down: open. It depends on the same decision.
Also still open from Danathar's review, not changed here
- Rootless quadlets under
/usr/share/containers/systemd/users/are only read by podman 6.0 or later; he reports Fedora 44 ships 5.8.x. FFT_HWACCELis never set, so NVIDIA hosts pull the CUDA image and still decode in software.
This PR still needs @hanthor to re-review and resolve the CHANGES_REQUESTED review after the rollout decision, plus two approvals from maintainers other than me.
Danathar
left a comment
There was a problem hiding this comment.
Re-review at 907b843 (hold label left in place). I ran tests/test_ffmpeg_thumbnailer.py here: 12 passed. When I moved the socket back to the top of the gst cache dir in both daemon and shim, test_socket_survives_gnome_gst_registry_cleanup failed with FileNotFoundError, so the new test guards the fix. CI (unit tests, both builds, validate) is green; the GNOME 50 E2E for this head was still running when I looked.
hanthor's points:
- Default-on everywhere: not fixed. The user preset, the
graphical-session.target.wantslink andWantedBy=default.targetin both.containerfiles are unchanged. This is the maintainer decision you asked for, and it's still open. SecurityLabelDisable=true: fixed. It's gone from both.containerfiles.environment.dtaking every video MIME type: not fixed. The file is unchanged; you tied it to the rollout choice.- MemoryMax: fixed. The daemon is now 512M/1024M, the same as the containers.
My earlier points:
- GNOME deleting the socket: fixed.
clean_gst_registry_dir()callsg_remove()on each top-level entry (gnome-desktop-thumbnail-script.c:1216), and that can't remove a non-empty directory. I ran that cleanup after each of 20 requests, 10 ms apart, against this head'sserve_recovering: 20/20 reached the daemon. On 3596c80 the same test gave 1 of 10 at 50 ms gaps. One side effect: thatg_remove()now fails on every run, so gnome-desktop will logFailed to delete left-over thumbnailing '.../ffmpeg-thumbnailer'after each thumbnail. It's only journal noise. - Podman 5.8: not fixed, and it's worse than I said before. In podman v5.8.7,
getRootDirsinpkg/systemd/quadlet/unitdirs.gowalks all of/usr/share/containers/systemd,users/included, with no filter. So both.containerfiles become system services, started as root by PID 1 at boot. The GNOME 50 E2E for 3596c80 (run 36257836867) shows it:systemd[1]: Starting ffmpeg-thumbnailer.service, generated into/run/systemd/generator/, then a failed Docker Hub pull because DNS isn't up yet. The rootless daemon'spodman execcan't reach a root container anyway. Fedora 44's current podman is 5.8.7 (Bodhi, 2026-09-19), and the quadlet files are unchanged at 907b843. They need a location that 5.8's user generator reads, or they should stay out of the image until podman 6. - Test not in CI: fixed.
FFT_HWACCELnever set: not fixed. The daemon still defaults tonone, so NVIDIA hosts would pull the CUDA image and still decode in software.- Duplicate
test_bluefin_countme.batsin the Justfile: fixed.
New, small: ManagedOOMMemory=kill isn't a systemd key. The E2E log warns Unknown key 'ManagedOOMMemory' in section [Service], ignoring for the daemon and both containers. ManagedOOMMemoryPressure=kill is probably what was meant.
What's left: the rollout decision (with the MIME ownership that depends on it), the podman 5.8 quadlet location, and FFT_HWACCEL.
|
Status at head 907b843: blocked on a maintainer rollout decision, so I'm not dismissing hanthor's CHANGES_REQUESTED (R5319623719).
castrojo is the author, so this needs hanthor's CR cleared plus two approvals from people other than castrojo. |
Addresses review feedback on PR #1226 (hanthor CHANGES_REQUESTED, Danathar review). Opt-in rollout (hanthor's default-on concern #1): the thumbnailer no longer turns on for every user on every image. Removing the three default-on mechanisms Danathar specified: - delete user-preset/01-ffmpeg-thumbnailer.preset (no auto-enable), - delete the static graphical-session.target.wants symlink, - drop WantedBy=default.target from both *.container units, leaving them static so neither container starts at boot. ManagedOOM key (Danathar): ManagedOOMMemory=kill is not a systemd key and warned on every unit. Renamed to ManagedOOMMemoryPressure=kill in the two container units and the daemon service. Still open, flagged to maintainers (need a decision or hardware/ version testing, not guessable here): - environment.d MIME shadowing (hanthor #3): the global XDG_DATA_DIRS prepend stays and still hides gst for non-opt-in users; gating it behind opt-in is a separate decision. - FFT_HWACCEL is never set (daemon defaults to "none"); NVIDIA decode path is untested without hardware. - podman 5.8 quadlet location: /usr/share/containers/systemd/users is parsed as root system units on 5.8; needs a 5.8-readable shipped location or a version gate. - Opt-in enablement: the daemon lacks Wants= on the container units, so the ujust toggle (#1234) must start the container unit too.
|
Pushed Fixed (dd9be26):
The daemon service keeps its own Flagged — need a maintainer decision or hardware/version testing (not guessable from here):
I am the PR author, so my commit does not count as an approval. @hanthor, @Danathar — the code points above are pushed; flagging the four items that need a maintainer call or hardware. Ready for re-review on what I changed.🐝 Hive Agent: |
|
Re-checked at dd9be26 against my review at 907b843.
The |
|
Status check: Blocked on maintainer architectural rollout decision (@hanthor):
|
|
Updated the PR body per your review: the daemon resource-governance line now reads The remaining technical items you raised (podman 5.8 quadlet location at
|
Resolve the Justfile pytest-line conflict by keeping both tests/test_render_hidamari_movie.py (main) and tests/test_ffmpeg_thumbnailer.py (this branch). Assisted-by: Claude via omp
|
Merged origin/main into the branch (d30d404) to clear the conflict. The only conflict was the Justfile pytest line: main added No behaviour change. Still blocked on @hanthor's CHANGES_REQUESTED and the open maintainer calls listed above (podman 5.8 user-quadlet location, Docker Hub image vs a signed ghcr rebuild, environment.d MIME ownership when disabled). castrojo is the author, so merging needs 2 approvals from other maintainers. |
|
Checked d30d404. It's a merge of main, and the PR's own files are unchanged since dd9be26 (pytest 12 pass, CI green). The body now matches the units. Still open from my 09-27 review: the two |
Human Parts
This basically replaces the gnome thumbnailer with an ffmpeg container and fixes all thumbnails. It's just quadlets lol. Dakota has nothing and fedora always ships a broken one, one less rpmfusion dep or wherever you get it from. Distro agnostic too!
I've been running this for about a month on my new machine and can confirm it works. Would love more testing on super large folders of videos!
thumbnailer
What does this change?
Adds a sandbox-safe, resource-governed ffmpeg video thumbnailer daemon and in-sandbox shim for GNOME on image-based systems (Bluefin, Bluefin LTS, Dakota).
Why?
Stock video thumbnailers in GNOME (
totem-video-thumbnailer/gst-video-thumbnailer) fail on many modern codecs (AV1, HEVC, ProRes, 10-bit), crash under memory pressure, or are completely absent on minimal desktop images like Dakota.Architecture & Container Selection
ffmpeg-thumbnailer.container: Usesdocker.io/jrottenberg/ffmpeg:9.0-alpine320@sha256:2c3d7158...(lean 111 MB multi-arch Alpine image, software decode with nativelibdav1d,libx264,libx265,libvpx). Guarded byConditionPathExists=!/sys/module/nvidia/version.ffmpeg-thumbnailer-nvidia.container: Usesdocker.io/jrottenberg/ffmpeg:9.0-nvidia2404@sha256:76b1f321...withAddDevice=-nvidia.com/gpu=all(CDI). Guarded byConditionPathExists=/sys/module/nvidia/version.ffmpeg-thumbnailer, so the host daemon targets a single container name regardless of hardware.--unshare-alland seccomp filters blocking user namespaces.SCM_RIGHTSvia a UNIX socket in~/.cache/gnome-desktop-thumbnailer/gstreamer-1.0/(the only writable host directory bound into the sandbox).podman exec --preserve-fd=N,M ffmpeg-thumbnailer timeout -s KILL 15 ffmpeg -threads 1 ...directly on/proc/self/fd/N.Slice=background.slicewithCPUWeight=20,IOWeight=20,MemoryHigh=512M,MemoryMax=1024M,TasksMax=512, andManagedOOMMemoryPressure=kill.timeout -s KILL 15and-threads 1.FFT_MAX_INFLIGHT, default $\min(4, \text{cpu_count} // 2)$).10-ffmpeg-thumbnailer.confin/usr/lib/environment.dprepends/usr/share/ublue-os/thumbnailertoXDG_DATA_DIRS.libgnome-desktopscansXDG_DATA_DIRSin order and first registration per MIME wins,ffmpeg-thumbnailer.thumbnailercleanly overrides/usr/share/thumbnailers/gst-video-thumbnailer.thumbnailer.Checklist
AI attribution
Assisted-by: Claude Opus via GitHub Copilot
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com