This is a booby-trapped PDF that turns the viewer itself into the launcher
CVE-2026-46529 is an argument/command-injection bug in Linux desktop document viewers derived from the Evince code path ev_spawn(). A malicious PDF can smuggle attacker-controlled values from /GoToR actions into the viewer's command line, then abuse --gtk-module so GTK dlopen() loads attacker code from the same file. Upstream disclosures say Atril is fixed in 1.28.4 and 1.26.3, Xreader in 4.6.4 and 3.6.7, and that Evince and Papers are also affected; distro advisories show backported fixes in Debian and Ubuntu even when the package version stays below the upstream tag.
Technically this is nasty: it is reliable user-context code execution from a single malicious file, with public exploit-building details and no special configuration required on stock installs. But the blast radius is narrower than a server-side RCE because the attacker needs a Linux desktop target using one of these viewers, plus a user who opens the PDF and clicks the embedded link. That keeps it out of *CRITICAL* territory for most enterprises, but it is still solidly *HIGH* for Linux workstation fleets.
4 steps from start to impact.
Build a PDF/ELF polyglot with build_polyglot.py from the GHSA writeup
- Attacker can generate and deliver a malicious PDF attachment or download
- Target architecture is known or the attacker ships the right build for the victim
- This is not a wormable network path; it starts with social engineering or file delivery
- Architecture-specific payloads add some attacker prep work
ELF magic plus embedded %PDF- is useful.Deliver to a Linux desktop user and get a click on the PDF link
/A << /S /GoToR ... /D (...) >>. Exploitation requires the user to open the file in a vulnerable viewer and actively click the link inside the document.- Victim uses Evince, Atril, Xreader, or Papers on a vulnerable build
- Victim opens the file in that viewer rather than a browser sandbox or different renderer
- Victim clicks the malicious link
- User interaction is mandatory
- Many enterprises are Windows-heavy, so the exposed population is much smaller than the total fleet
- Browser-based PDF viewing, sandboxing, or alternate readers break this chain
/GoToR, suspicious /D fields, and link annotations spanning the full page. User-reporting telemetry matters here.Abuse ev_spawn() to inject --gtk-module
g_app_info_create_from_commandline(), which reparses it into argv. That turns the injected --gtk-module=/path/to/file into a real startup argument for the spawned viewer instance.- Viewer build includes the vulnerable
ev_spawn()path - The injected field reaches command construction unchanged
- This is a local application exploit path, not remotely reachable over the network
- Some downstream builds may have already backported the fix without matching upstream version numbers
--gtk-module, especially when pointed at user-writable paths like /tmp, ~/Downloads, or the opened PDF itself.GTK dlopen() executes the embedded constructor payload
- GTK honors the supplied module path
- The payload library is readable and valid for the victim architecture
- Execution is in user context, not instant root
- Application confinement such as AppArmor, SELinux policy, or aggressive EDR policy can reduce post-exploit options
dlopen of unusual modules from user-writable locations, module loads from PDFs, or immediate shell/network activity from the viewer process.The supporting signals.
| In-the-wild status | No CISA KEV listing was found, and the searched primary sources do not claim active exploitation. This looks like public exploitability, not confirmed campaign use. |
|---|---|
| PoC availability | Public exploit-building details are available in the GitHub advisory and the follow-up oss-sec thread, including a build_polyglot.py workflow and a --gtk-module abuse path. |
| EPSS | No authoritative FIRST EPSS entry was located in the searched sources, so treat predictive exploit probability as unknown, not low. |
| KEV status | Not listed in CISA KEV per the user-supplied intel block; no contradictory source was found. |
| Published severity data | A GitHub security advisory for Atril published on 2026-05-21 labels the issue High 8.4 with CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. I am not treating that as the noisgate comparison baseline because NVD/MITRE lacked a normalized record when requested. |
| Affected products | Evince, Atril, Xreader, and Papers are affected through the shared vulnerable code path in ev_spawn(). This is primarily a Linux desktop exposure, not a server exposure. |
| Fixed versions | Upstream disclosures say Atril 1.28.4 and 1.26.3, Xreader 4.6.4 and 3.6.7, and Evince has a fix upstream while Debian/Ubuntu shipped backports such as Debian bookworm 43.1-2+deb12u1, Debian trixie 48.1-3+deb13u1, Ubuntu noble 46.3.1-0ubuntu1.1, and Ubuntu jammy 42.3-0ubuntu3.2. |
| Version ambiguity to watch | Primary sources disagree on the raw upstream Evince fixed tag: oss-sec says 48.2, while the GHSA page lists 48.4. For enterprise verification, trust the distro advisory package version rather than upstream semver alone. |
| Exposure telemetry | Internet scan data is largely not applicable here: these viewers are endpoint applications, not listening services. Shodan/Censys-style counts would understate risk because the exposure surface is local user workflow, especially email and downloads. |
| Disclosure and credit | Initial public coordination appeared on 2026-05-19 via oss-sec; the GitHub advisory credits N1et as finder and shows the write-up published on 2026-05-21. |
noisgate verdict.
The decisive factor is that exploitation yields real user-context code execution on stock Linux desktops with public weaponization details, so this is not a harmless parser crash. It stays below *CRITICAL* because the attack chain still depends on a narrower population of Linux desktop viewers and explicit user interaction inside the document.
Why this verdict
- Real code execution, not just a viewer crash: the
--gtk-modulepath reachesdlopen()and runs attacker code as the logged-in user. - Public exploit path lowers attacker effort: the advisory and follow-up discussion disclose a practical polyglot PDF/ELF technique, shrinking the gap between bug and weaponization.
- User interaction is the main downward pressure: the victim must open the PDF and click the embedded link, which makes this more phishing-driven than self-propagating.
- Attacker position is effectively pre-initial-access content delivery: there is no unauthenticated network service to hit, so exposure is gated by email/download/USB workflows.
- Reachable population is narrower than generic endpoint bugs: only Linux desktops running Evince-family viewers are meaningfully exposed, which is usually a minority slice of a 10,000-host enterprise.
Why not higher?
This is not an internet-facing service flaw and it is not zero-click. Every prerequisite narrows the real attack set: Linux desktop target, vulnerable viewer, successful file delivery, and a user click inside the PDF. Those compounding frictions keep it out of *CRITICAL* even though the impact after exploitation is serious.
Why not lower?
Downgrading to *MEDIUM* would ignore the quality of the exploit path. The bug is reliable enough for single-file user-context RCE on stock installs, and public write-ups remove a lot of engineering work for attackers. For Linux workstation fleets, that is above backlog-only risk.
What to do — in priority order.
- Force safer PDF handling paths — Route untrusted PDFs to a browser sandbox, remote viewer, or hardened alternate renderer instead of Evince-family local desktop apps. For a *HIGH* verdict, deploy this control where feasible within 30 days if patch rollout will lag.
- Block risky PDF features at ingress — Tune mail and web security controls to quarantine PDFs containing
/GoToR, suspicious/Daction fields, or obvious PDF/ELF polyglot indicators. This cuts the easiest delivery channel and should be in place within 30 days. - Hunt for
--gtk-moduleabuse — Add endpoint detections for document viewers spawning with--gtk-moduleor loading modules from user-writable paths such as/tmp,/var/tmp,~/Downloads, or the opened file itself. Detection engineering is a viable temporary risk reducer within 30 days. - Constrain desktop execution — Use AppArmor/SELinux, application control, or EDR prevention to block document viewers from loading arbitrary shared objects out of user-writable locations. This does not fix the bug, but it meaningfully reduces post-click reliability within 30 days.
- Perimeter vulnerability scanning does not help much; these are local desktop viewers, not remotely fingerprintable services.
- Network segmentation is weak compensation here; the trigger is a local file open and click, not east-west network reachability.
- Generic PDF attachment blocking alone is too blunt and often bypassed by downloads, chat attachments, or shared storage links.
Crowdsourced verification payload.
Run this on the target Linux host as a normal user; root is not required. Invoke it with bash cve-2026-46529-check.sh. It checks installed package versions against known Debian/Ubuntu fixed builds where available, then falls back to upstream semver checks for Atril/Xreader; unsupported package layouts return UNKNOWN.
#!/usr/bin/env bash
# CVE-2026-46529 verification helper
# Checks common Linux package installations of evince/atril/xreader/papers.
# Output: VULNERABLE / PATCHED / UNKNOWN
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
set -u
status="PATCHED"
notes=()
found_any=0
have() { command -v "$1" >/dev/null 2>&1; }
add_note() {
notes+=("$1")
}
mark_vuln() {
status="VULNERABLE"
add_note "$1"
}
mark_unknown() {
if [ "$status" != "VULNERABLE" ]; then
status="UNKNOWN"
fi
add_note "$1"
}
check_dpkg_pkg() {
local pkg="$1"
dpkg-query -W -f='${Version}' "$pkg" 2>/dev/null || return 1
}
check_rpm_pkg() {
local pkg="$1"
rpm -q --qf '%{VERSION}-%{RELEASE}' "$pkg" 2>/dev/null || return 1
}
check_dpkg_threshold() {
local pkg="$1" installed="$2" fixed="$3"
if dpkg --compare-versions "$installed" ge "$fixed"; then
add_note "$pkg $installed >= fixed $fixed"
else
mark_vuln "$pkg $installed < fixed $fixed"
fi
}
check_semver_fallback() {
local name="$1" version="$2" fixed1="$3" fixed2="${4:-}"
if printf '%s\n%s\n' "$fixed1" "$version" | sort -V -C; then
add_note "$name $version >= upstream fixed $fixed1"
return 0
fi
if [ -n "$fixed2" ] && printf '%s\n%s\n' "$fixed2" "$version" | sort -V -C; then
add_note "$name $version >= upstream fixed $fixed2"
return 0
fi
mark_vuln "$name $version is below upstream fixed versions $fixed1${fixed2:+ / $fixed2}"
}
# Debian/Ubuntu path
if have dpkg-query && have dpkg; then
if [ -r /etc/os-release ]; then
. /etc/os-release
fi
os_id="${ID:-unknown}"
version_id="${VERSION_ID:-unknown}"
# evince on Debian/Ubuntu with distro-fixed package versions
ev_ver=$(check_dpkg_pkg evince) || ev_ver=""
if [ -n "$ev_ver" ]; then
found_any=1
case "$os_id:$version_id" in
ubuntu:22.04) check_dpkg_threshold evince "$ev_ver" '42.3-0ubuntu3.2' ;;
ubuntu:24.04) check_dpkg_threshold evince "$ev_ver" '46.3.1-0ubuntu1.1' ;;
ubuntu:25.10) check_dpkg_threshold evince "$ev_ver" '48.1-3ubuntu2.1' ;;
ubuntu:26.04) check_dpkg_threshold evince "$ev_ver" '49~alpha-2ubuntu2.1' ;;
debian:11) check_dpkg_threshold evince "$ev_ver" '3.38.2-1+deb11u1' ;;
debian:12) check_dpkg_threshold evince "$ev_ver" '43.1-2+deb12u1' ;;
debian:13) check_dpkg_threshold evince "$ev_ver" '48.1-3+deb13u1' ;;
*) mark_unknown "evince $ev_ver installed, but no authoritative distro threshold encoded for $os_id $version_id" ;;
esac
fi
# atril: upstream fixed versions known; distro backports vary widely
atril_ver=$(check_dpkg_pkg atril) || atril_ver=""
if [ -n "$atril_ver" ]; then
found_any=1
upstream_guess=$(printf '%s' "$atril_ver" | sed 's/+.*$//' | sed 's/-.*$//')
if [[ "$upstream_guess" =~ ^[0-9]+(\.[0-9]+)+$ ]]; then
check_semver_fallback atril "$upstream_guess" '1.28.4' '1.26.3'
else
mark_unknown "atril $atril_ver installed; package version is distro-specific and cannot be safely mapped"
fi
fi
papers_ver=$(check_dpkg_pkg papers) || papers_ver=""
if [ -n "$papers_ver" ]; then
found_any=1
case "$os_id:$version_id" in
ubuntu:25.10) check_dpkg_threshold papers "$papers_ver" '48.0-1ubuntu1.25.10.4' ;;
ubuntu:26.04) check_dpkg_threshold papers "$papers_ver" '50.1-0ubuntu1.1' ;;
*) mark_unknown "papers $papers_ver installed; searched sources did not provide a reliable fixed threshold for $os_id $version_id" ;;
esac
fi
fi
# RPM fallback for common package names; authoritative thresholds not encoded except via upstream semver best effort
if [ "$found_any" -eq 0 ] && have rpm; then
for pkg in evince atril xreader papers; do
ver=$(check_rpm_pkg "$pkg") || ver=""
[ -z "$ver" ] && continue
found_any=1
base=$(printf '%s' "$ver" | sed 's/-.*$//')
case "$pkg" in
atril) check_semver_fallback atril "$base" '1.28.4' '1.26.3' ;;
xreader) check_semver_fallback xreader "$base" '4.6.4' '3.6.7' ;;
evince) mark_unknown "evince $ver installed via RPM; distro backport policy varies, confirm against vendor advisory" ;;
papers) mark_unknown "papers $ver installed via RPM; confirm against distro advisory" ;;
esac
done
fi
if [ "$found_any" -eq 0 ]; then
echo "UNKNOWN"
echo "No supported package installation of evince/atril/xreader/papers was detected."
exit 2
fi
echo "$status"
printf '%s\n' "${notes[@]}"
case "$status" in
PATCHED) exit 0 ;;
VULNERABLE) exit 1 ;;
*) exit 2 ;;
esac
If you remember one thing.
--gtk-module abuse; the noisgate remediation SLA is within 180 days for the vendor or distro patch, using distro-fixed package builds rather than upstream version strings alone.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.