CVE-2026-46529: 10-year-old RCE in Linux PDF Viewer (XReader/Evince/Atril) - medeiros.zip
Introduction<br>Some time ago I started feeling the urge to analyze Open Source application code looking for vulnerabilities, mainly in applications where I could explore more about fuzzing, heap overflows, OOBs, among others.<br>And what pushed me the most was the series of articles from the Calif blog, a very cool cybersecurity company, that has been finding really interesting vulnerabilities by exploring the use of current AIs, such as ChatGPT 5.5 and Claude Opus. I recommend reading the articles at https://blog.calif.io/.<br>I decided to focus my research on popular Linux PDF viewers, so I chose xreader/evince/atril. You will always find these slashes because they were my main focus, since they also share the same codebase, the generic reader XREADER.<br>Evince is a very popular PDF reader used with the GNOME interface, and Atril comes from the MATE interface, widely used in Linux Mint and Ubuntu LTS.<br>Fuzzing Evince/Atril<br>Well, unfortunately, or maybe fortunately, this is not going to be a long story. Me, together with little Claude, performed fuzzing against many components of the readers, however, I was not able to elevate any of the bugs found into a possible RCE. I do not know whether it was actually a technical limitation, or an issue with what sits between the monitor and the chair. So I gradually started focusing on enumerating the readers’ functionalities and whipping Claude into performing code review.<br>The Injection<br>Unfortunately, the narrative will not be 100% faithful because I lost the vulnerability prompt history/flow showing how I got there using Claude. But I believe what is really worth it here is the explanation of the technical content itself.<br>With the difficulty in finding memory corruption vulnerability vectors, the AI analysis flow started shifting back into analyzing the application wrappers, the part responsible for executing the application logic.<br>After a few days analyzing crashes, we started looking for other vectors, which eventually led us to the ev_spawn function.<br>The ev_spawn function is an internal function located in shell/ev-application.c; it is the function responsible for creating a new viewer process when the program needs to open a remote document: a reference to another PDF inside a PDF, like a link between PDFs. So the user clicks this link inside the PDF, and the /GoToR (Go to Remote) action executes the function responsible for opening the other document.<br>When the user clicks the link, the program spawns a new instance of itself to open the target document, and that is where ev_spawn gets executed.<br>While analyzing the function file, after a few rounds, Claude noticed the following:
Terminal
▎ "ev_spawn at ev-application.c:235-275 has THREE unquoted %s injections:<br>▎ - --page-label=%s (line 239) — from PDF link dest<br>▎ - --named-dest=%s (line 247) — from PDF link dest<br>▎ - --find=%s (line 257) — from CLI
Source-code:
Terminal
g_string_append_printf (cmd, " --page-label=%s", ev_link_dest_get_page_label (dest));<br>g_string_append_printf (cmd, " --named-dest=%s", ev_link_dest_get_named_dest (dest));<br>g_string_append_printf (cmd, " --find=%s", search_string);<br>...<br>app = g_app_info_create_from_commandline (cmdline, NULL, ...);<br>g_app_info_launch_uris (app, files, ...);
Claude identified that there are 3 parameters in the code without the g_shell_quote function. This means that attacker-controlled strings are being passed without protection.<br>g_shell_quote is a function from GLib (the core library of the GNOME/GTK ecosystem) that escapes a string so it will be interpreted as a single argv element when passed through a shell parser.
In practice, this means that, instead of interpreting the entire provided string as a single argument, each space will be interpreted as a new flag. Below is an example using a fictional flag called inject.
Terminal
const char *named_dest = ev_link_dest_get_named_dest (dest);<br>// named_dest = "namerandom --inject"
g_string_append_printf (cmd, " --named-dest=%s", named_dest);<br>// cmd agora contém: " /usr/bin/atril --named-dest=namerandom --inject" ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
And so, when the string is passed to the g_app_info_create_from_commandline function, it will receive:
Terminal
/usr/bin/atril --named-dest=namerandom --inject
This way, we identified that we were able to inject flags as arguments into the child process spawned after the click. What Claude needed now was to find a flag that would allow command execution.<br>Escalate with GIMP Toolkit (GTK3)<br>GTK3 is a graphical interface library maintained by GNOME and very popular on Linux. It provides the visual building blocks for graphical interfaces, such as windows, buttons, menus, etc. Several distros and desktops are built on top of it, such as Cinnamon, MATE, XFCE, etc.<br>The point about this library here is that it loads, in every process that uses it, its own flags, which any application running on it...