Evince, Flatpak, and GTK print previews

Endless OS is distributed as an immutable OSTree snapshot, with apps added & removed with Flatpak (and podman for power users & developers). Although the snapshot is assembled from Debian packages, it’s not really possible to install additional system packages locally, nor to remove them. Over time, we have tried to remove as many apps out of the immutable OS as possible: Flatpak apps are sandboxed and can be updated at a faster cadence than the OS itself, or removed if not needed.

Evince is one such app built into the OS at present. As a PDF viewer, it handles untrusted input in a complex format with libraries that have historically contained vulnerabilities, so is a good candidate for sandboxing and timely updates. While exploring removing it from the OS in favour of the Flatpak version from Flathub, I learned some things that were non-obvious to me about print preview, and which prevented making this change at the time.

Caveats: the notes below are a simplification, but I believe they are broadly accurate for GNOME on Linux. I’m sure people more familiar with GTK and/or printing already know everything here.

Printing from GTK apps

GTK provides API for applications to print documents. This presents the user with a print dialog, with many knobs to control how your document will be printed. That dialog has a Preview button; when you press it, the dialog vanishes and another one appears, showing a preview of your document. You can press Print on that dialog to print the document, or close it to cancel.

Why does the Preview button close the print dialog and open another one? Why does the preview dialog not have any of the knobs from the print dialog, or a way to return to the print dialog?

The answer lies in the documentation for GtkPrintOperation:

By default GtkPrintOperation uses an external application to do print preview.

The default application is Evince! More specifically, it is the following command:

evince --unlink-tempfile --preview --print-settings %s %f

Cribbing from the manpage:

--preview
Run evince as a previewer.
--unlink-temp-file
If evince is run in preview mode, this will unlink the temporary file created by GTK+.
--print-settings %s %f
This sends the full path of the PDF file, f, and the settings of the print dialog, s, to evince.

So when the user chooses to preview the document, GTK asks the application to render the document with the settings from the dialog, generates a PDF, and then invokes Evince to display that PDF. When you press Print in the preview dialog, it is Evince that sends the job to CUPS and thence to the printer.

What if evince is not present on the $PATH? The button is still displayed, but pressing it does nothing and the following is logged to stderr:

sh: 1: exec: evince: not found

There is code in GTK which attempts to handle this case by logging its own warning and then invoking the default PDF viewer on the generated PDF, but it doesn’t work because GLib actually spawns sh, not evince directly, and then returns success because ‘sh’ was successfully launched.

Printing from sandboxed apps

What happens if the application using the GtkPrintOperation API is a Flatpak app? evince is not part of any runtime, so GTK running in the application process cannot invoke it to preview the document? Well, in the general case the app can’t talk directly to CUPS either. So it uses the print portal’s PreparePrint method to prompt the user to choose a printer & settings, then renders the document and sends it to the portal with the Print method. The desktop portal service, which also uses GTK but is running outside the sandbox, presents the print dialog, and invokes evince if needed. All good, nothing too tricky here.

But notice that a sandboxed app is feeding a PDF to an unsandboxed PDF viewer. If the sandboxed app is malicious and can convince a user to print-preview a document, and there is some arbitrary code execution bug in Evince’s PDF library, then you’re in for a bad day.

What if Evince is a Flatpak?

The Flatpak version of Evince does not put an ‘evince’ command onto the $PATH, by design of Flatpak. So if you remove Evince from the OS and install the Flatpak, print preview stops working.

The evince executable inside the org.gnome.Evince Flatpak supports the --preview flag as normal. So you can put something like the following into ~/.config/gtk-4.0/settings.ini:

# https://docs.gtk.org/gtk4/property.Settings.gtk-print-preview-command.html
[Settings]
gtk-print-preview-command=flatpak run --file-forwarding org.gnome.Evince --unlink-tempfile --preview --print-settings @@ %s %f @@

--file-forwarding triggers special handling of the arguments bracketed by @@:

If this option is specified, the remaining arguments are scanned, and all arguments that are enclosed between a pair of ‘@@’ arguments are interpreted as file paths, exported in the document store, and passed to the command in the form of the resulting document path.

And this does indeed cause Evince to be spawned. However Evince can’t print the document. This is because its previewer tries to talk directly to CUPS, and its sandbox does not allow it to talk to CUPS. You might try punching some crude holes in the sandbox:

[Settings]
gtk-print-preview-command=flatpak run --file-forwarding --socket=system-bus --socket=cups org.gnome.Evince --unlink-tempfile --preview --print-settings @@ %s %f @@

and it seems to get a bit further, but by this point you’ve given up and turned your printer off because you want to go to bed.

What next?

I think it’s desirable for a PDF viewer to be sandboxed. I also think it’s desirable for the print previewer in particular to be sandboxed, or else a malicious-but-sandboxed application could trick the user into printing a PDF that exploits some vulnerability in the previewer and run stuff on the host system.

As I write this up, the gtk-print-preview-command override seems more viable than it did when I first looked into this last year. I think at the time, GTK in the GNOME runtime didn’t have the CUPS backend enabled so it couldn’t print even if you punched the relevant sandbox holes, but apparently it does now, so maybe we can make this change after all. It’s a shame I only realised this after spending hours writing this post!

You could also imagine extending the print portal API to allow an external app to be used for the preview without allowing that app to talk directly to CUPS.

(You could gracefully handle Evince not being installed by putting a wrapper script onto the $PATH which invokes Evince if installed or prompts you to install it if not.)