CVE-2026-46529: 10-year-old RCE in Linux PDF Viewer (XReader/Evince/Atril)
A short post about how claude help me to find a RCE in XReader/Evince/Atril CVE-2026-46529.
Introduction
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.
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/.
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.
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.
Fuzzing Evince/Atril
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.
The Injection
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.
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.
After a few days analyzing crashes, we started looking for other vectors, which eventually led us to the ev_spawn function.
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.
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.
While analyzing the function file, after a few rounds, Claude noticed the following:
Source-code:
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.
g_shell_quoteis 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.
And so, when the string is passed to the g_app_info_create_from_commandline function, it will receive:
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.
Escalate with GIMP Toolkit (GTK3)
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.
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 can use. The process kind of “inherits” these flags. One of them, the one we are interested in, is the --gtk-module flag.
This means that hundreds of binaries on Linux accept a standardized catalog of inherited GTK flags: --display, --screen, --gtk-debug, --name, --class, --sync, and the one we care about, --gtk-module=PATH.
Its implementation helps us. In GTK3, when the gtk_init function processes argv and finds --gtk-module=, eventually this will reach this code:
The g_module_open function is a function that uses the dlopen(3) system call from GLIBC. And here is where the most interesting part comes in: when loading the shared ELF file, dlopen automatically executes all symbols marked with __attribute__((constructor)). This means that even without exporting any function, all code inside the constructor will be executed.
Therefore, we already have our flag to escalate. We need to build a malicious .so library that will be processed by the --gtk-module flag and execute our malicious code.
Knowing this, the first part of the exploitation is complete. Instead of --inject, we can pass the --gtk-module=/tmp/evil.so flag containing the path to the malicious .so library, and this will force the execution of our code.
The execution of the child process with the injected flag would look like this:
Let’s gooo! We have command execution!
Polyglot File PDF+ELF
Now we have another problem. An attacker would not want to send a zip file containing a PDF with a .so library in the same directory; obviously, that is very suspicious, and we need to remove this requirement.
Talking with Claude, it gave me an option: creating a Polyglot File.
A polyglot file is a technique where a file is built in such a way that it can be recognized by the system as two different file types. In this case, we would need a file that is recognized by the system both as a PDF and is also loadable as a .so library: two files in one.
This way, instead of the --gtk-module flag pointing to a .so file, it would point directly to itself, the PDF file, and would be loaded by the system as a library.
A single file that is simultaneously a PDF (for Atril to open) and an ELF (for dlopen to load). The %PDF-1.4 magic is embedded in the .note.gnu.build-id section at offset 0x1d8, an informational 20-byte slot that ld.so does not validate, but still inside the 1024-byte window.
Since I understand nothing about polyglot files, I delegated the script 100% to Claude, and it then returned build_polyglot.py, where I pass the malicious library and the name of the PDF that will be generated.
Done. Now we have a PDF that, when using file on it, is seen as an ELF executable, but when viewed by the system, is considered a PDF.

Problem solved. Claude generated a script for me that creates dual files, both .so and .pdf. Now what we have to do is insert the injection into the PDF with --gtk-module= pointing to the PDF itself.
Now we have one last problem. We pass the direct path to the PDF file into --gtk-module. If we want to send it to a victim, we would need to know where the file is located on the system, and since the path includes the Linux home directory, we would also need to know the username. This leads to the third problem.
Where am i?
I tried some manual attempts using /proc/self, where the files of its own process stay in the Linux system, and so on, but none of the techniques actually worked. I asked Claude to analyze it, but even so, it also could not take advantage of /proc.
In another attempt, I gave Claude the task of reading the main ev-window.c code and trying to check for ways to solve this problem. And after some time, it actually brought a solution that, in general, was right in front of me.
Claude found in shell/ev-window.c:6347-6350 the point where open_remote_link processes /GoToR:
And it also discovered the detail we had missed: the constructed string ("/usr/bin/atril --named-des...") does not receive the URI/PATH directly. It is passed separately using the g_app_info_launch_uris function, which, when executed, makes GLib replace placeholders such as %u (URI form) and %f (local path) in the command line at runtime.
This means that, instead of passing the literal path, I only need to pass the placeholder, following the same standardization used by Linux .desktop files.
However, there was another problem:
While investigating, Claude found in ev-application.c:596:
The if above basically says that, if the value of the /F argument is identical to the document PATH, ev_spawn, the function we need to trigger, does not fire.
The logic is simple: the /GoToR argument points to another document, so it makes sense to spawn a new process to open the other document, since you want to read both. However, if it points to the same document you are already reading, it will navigate internally inside the document, without executing ev_spawn, and therefore, without RCE.
Claude solved this in a simple way, changing /F = "x.pdf" to /F = "x.pdf?1". The ?1 is an empty query string; it will be ignored by GLib when being resolved at runtime by the system, however, when doing the comparison inside the if, the result will be that the two are different, which will force the execution of ev_spawn.
strcmp() → different → spawn run
When it is now resolved by GLib, through the g_app_info_launch_uris function mentioned above, it will ignore the ?1, which prevents our exploit from failing.
This finalizes the parameter in the PDF this way.
Perfect. Now we have a complete exploit, and we solved the following problems:
- We do not need to know the literal PATH of the file on the system; it will be resolved at runtime.
- We do not need an arbitrary
.sofile; it is contained inside the PDF itself, which points to itself.
Now, when the user opens the PDF and clicks anywhere on the page, the malicious command will be executed.
Proof Of Concept
Report
The vulnerability was reported to Evince and Atril.
https://github.com/mate-desktop/atril/security/advisories/GHSA-vgv2-m826-8f6f
You can find the source codes here: