home intel cve-2026-0008-confused-deputy-privilege-escalation
CVE Analysis 2026-03-02 · 8 min read

CVE-2026-0008: Confused Deputy Leads to Local Privilege Escalation

A confused deputy vulnerability in multiple system locations allows local privilege escalation with no additional permissions or user interaction required. CVSS 8.4 HIGH.

#privilege-escalation#confused-deputy#local-attack#authorization-bypass#cross-platform
Technical mode — for security professionals
▶ Privilege escalation — CVE-2026-0008
USER SPACELow privilegeVULNERABILITYCVE-2026-0008 · Cross-platformKERNEL / ROOTFull system accessNo confirmed exploits · HIGH

Vulnerability Overview

CVE-2026-0008 describes a confused deputy problem affecting multiple locations within a cross-platform codebase. A confused deputy vulnerability occurs when a privileged component — the "deputy" — is manipulated by an unprivileged caller into performing operations on that caller's behalf using the deputy's elevated rights, without properly validating the caller's authority to request such operations.

The vulnerability requires no additional execution privileges and no user interaction, making it particularly dangerous in local attack scenarios. An unprivileged process can trigger the privileged deputy to act on attacker-controlled parameters, resulting in privilege escalation to a higher integrity context.

Root cause: A privileged service accepts operation requests carrying attacker-controlled resource identifiers and acts on them using its own elevated credentials without verifying that the requesting caller has the authority to access the target resource.

Affected Component

The CVE description specifies "multiple locations" — a hallmark pattern in Android platform security bulletins indicating the same logical flaw is replicated across more than one privileged service or daemon. Based on the vulnerability class (confused deputy, local privilege escalation, no additional privileges needed, no user interaction), the affected components are consistent with a system service or privileged daemon that brokers access to protected resources on behalf of calling processes.

Typical confused deputy sinks in this class include:

  • Permission-gated file or device node operations performed via a privileged service (e.g., installd, vold, mediaserver-class daemons)
  • IPC interfaces (Binder, AIDL) that accept resource handles without re-checking caller UID/PID against the target resource's access policy
  • Kernel driver ioctl handlers that trust a userspace-supplied capability token rather than re-deriving it from the calling process credentials

Root Cause Analysis

The canonical confused deputy pattern in Android system services follows this structure. A privileged Binder service receives a transaction, extracts a resource descriptor from the Parcel, and operates on that descriptor using its own uid=1000 (system) or uid=0 (root) credentials — never re-checking whether the calling UID has rights to that resource.

// Pseudocode: Privileged system service IPC handler
// Represents the "multiple locations" pattern described in CVE-2026-0008

status_t PrivilegedService::onTransact(
    uint32_t code,
    const Parcel& data,
    Parcel* reply,
    uint32_t flags)
{
    switch (code) {
        case TRANSACTION_performResourceOp: {
            // Read attacker-controlled resource path from the Parcel
            String16 resourcePath = data.readString16();   // attacker-controlled
            int32_t  opCode       = data.readInt32();       // attacker-controlled

            // BUG: caller UID is never validated against resourcePath ownership.
            // The service holds elevated credentials (AID_SYSTEM or AID_ROOT)
            // and performs the operation unconditionally on behalf of the caller.
            uid_t callerUid = IPCThreadState::self()->getCallingUid();

            // MISSING: checkCallerPermission(callerUid, resourcePath)
            // The service is the "deputy": it has the privilege; the caller
            // tricks it into exercising that privilege on an arbitrary target.

            status_t result = performOpAsSystem(resourcePath, opCode);
            // BUG: result of privileged op written back to untrusted caller
            reply->writeInt32(result);
            return NO_ERROR;
        }

        case TRANSACTION_accessProtectedNode: {
            // Second affected location: device node / protected file access
            int32_t nodeId    = data.readInt32();   // attacker-controlled index
            int32_t accessMode = data.readInt32();

            // BUG: nodeId is used as a direct index into a privileged resource
            // table without verifying the calling process has SELinux label or
            // DAC permission to reference nodeId at all.
            ProtectedResource* res = gResourceTable[nodeId];  // no bounds check, no ownership check

            // Service opens the resource using its own credentials
            int fd = open(res->path, accessMode);  // opened as AID_SYSTEM
            reply->writeDupFileDescriptor(fd);      // fd handed back to unprivileged caller
            close(fd);
            return NO_ERROR;
        }
    }
}

// The privileged operation executes as the service's UID, not the caller's
status_t performOpAsSystem(const String16& path, int32_t op) {
    // Runs with effective UID = AID_SYSTEM (1000) or AID_ROOT (0)
    // No credential downgrade before acting on caller-supplied path
    switch (op) {
        case OP_READ:  return readProtectedResource(path);   // bypasses DAC
        case OP_WRITE: return writeProtectedResource(path);  // bypasses DAC + MAC
        case OP_CHMOD: return chmodResource(path, 0777);     // CRITICAL: world-writable
    }
}

The second location follows the same structural pattern but at a kernel driver interface level:

// Kernel-side ioctl handler — second "location" in the confused deputy chain
// Privileged character device accessible to a system service but not to apps directly

static long privileged_drv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct op_request req;

    if (copy_from_user(&req, (void __user *)arg, sizeof(req)))
        return -EFAULT;

    switch (cmd) {
        case IOCTL_DELEGATE_OP:
            // BUG: req.target_uid and req.resource_id are fully attacker-controlled.
            // The driver checks only that the *calling process* can reach this ioctl,
            // not that the calling process is entitled to act on req.target_uid's resources.
            // A system service forwarding this ioctl is the confused deputy.
            return execute_privileged_op(req.resource_id, req.target_uid, req.flags);
            //                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
            //                           BUG: no caller-to-target ownership check
    }
}

Exploitation Mechanics

EXPLOIT CHAIN — CVE-2026-0008:

1. Attacker process (uid=10xxx, unprivileged app or shell) connects to the
   privileged Binder service via ServiceManager lookup.
   No special permissions required — service is exported without permission check
   on the relevant transaction codes.

2. Craft a Parcel targeting a protected resource (e.g., /data/system/packages.xml,
   a protected device node, or a root-owned file):
     resourcePath = "/data/system/packages.xml"
     opCode       = OP_WRITE  (or OP_CHMOD -> 0777)

3. Send TRANSACTION_performResourceOp to the privileged service.
   Service executes performOpAsSystem("/data/system/packages.xml", OP_WRITE)
   using its own AID_SYSTEM credentials. DAC and MAC checks pass because
   *the service* has access — the caller does not.

4. [Second location] Attacker-controlled app or shell sends IOCTL_DELEGATE_OP
   via the system service proxy with:
     req.resource_id = 
     req.target_uid  = 0  (root)
     req.flags       = EXEC_AS_TARGET

5. Kernel driver executes execute_privileged_op() in the context of the driver,
   operating on the root-owned resource on behalf of the unprivileged caller.

6. Outcome: attacker writes to /data/system/packages.xml granting their package
   system-level permissions, OR attacker gains an open fd to a root-owned
   device node enabling further kernel exploitation.
   Effective privilege: AID_SYSTEM (1000) or AID_ROOT (0).

Memory Layout

For the Binder transaction path, the critical state is the Parcel buffer layout and the resource table dereference. The gResourceTable confused deputy dereference is the key memory primitive:

// Struct layout for ProtectedResource table entries
struct ProtectedResource {
    /* +0x00 */ uint32_t  resource_id;     // index key
    /* +0x04 */ uid_t     owner_uid;       // DAC owner — NEVER CHECKED vs caller
    /* +0x08 */ gid_t     owner_gid;
    /* +0x0C */ uint32_t  access_flags;
    /* +0x10 */ char      path[256];       // path opened by privileged deputy
    /* +0x110 */ uint32_t  refcount;
    /* +0x114 */ uint32_t  reserved;
};

// Binder op_request struct passed through the confused deputy chain
struct op_request {
    /* +0x00 */ uint32_t  resource_id;    // attacker-controlled: arbitrary table index
    /* +0x04 */ uid_t     target_uid;     // attacker-controlled: desired target UID
    /* +0x08 */ uint32_t  flags;          // attacker-controlled: operation flags
    /* +0x0C */ uint32_t  reserved;
};
BINDER TRANSACTION STATE — confused deputy activation:

Caller Parcel (attacker-controlled, uid=10234):
  [ String16 length  = 0x002A                         ]
  [ String16 data    = "/data/system/packages.xml\0"  ]  <-- target resource
  [ int32_t opCode   = 0x00000002 (OP_WRITE)          ]

Service Parcel processing (executing as uid=1000/AID_SYSTEM):
  callerUid  = 10234   <-- retrieved but NEVER used for authz
  resourcePath = "/data/system/packages.xml"
  // No ownership check performed here
  // Service proceeds to open+write as uid=1000

Effective open() call:
  path  = "/data/system/packages.xml"
  flags = O_WRONLY | O_TRUNC
  euid  = 1000 (AID_SYSTEM)   <-- deputy's credentials, not caller's
  // DAC: passes (system owns this file)
  // SELinux: passes (service has system_data_file:write in policy)
  // Caller's label: u:r:untrusted_app:s0 — would be DENIED if called directly

Patch Analysis

The correct fix requires the privileged service to perform an authorization check binding the caller's identity to the requested resource before exercising its own elevated credentials. Two complementary fixes are required:

// BEFORE (vulnerable): caller UID retrieved but ignored
status_t PrivilegedService::onTransact(...) {
    String16 resourcePath = data.readString16();
    int32_t  opCode       = data.readInt32();

    uid_t callerUid = IPCThreadState::self()->getCallingUid();
    // callerUid is read but never used to gate access
    status_t result = performOpAsSystem(resourcePath, opCode);
    reply->writeInt32(result);
    return NO_ERROR;
}

// AFTER (patched):
status_t PrivilegedService::onTransact(...) {
    String16 resourcePath = data.readString16();
    int32_t  opCode       = data.readInt32();

    uid_t callerUid = IPCThreadState::self()->getCallingUid();
    pid_t callerPid = IPCThreadState::self()->getCallingPid();

    // FIX 1: Verify caller has DAC/MAC rights to the target resource
    // by temporarily dropping to caller credentials before the check.
    if (!callerHasAccessToResource(callerUid, callerPid, resourcePath, opCode)) {
        ALOGE("Confused deputy check failed: uid=%d pid=%d -> %s",
              callerUid, callerPid, String8(resourcePath).c_str());
        reply->writeInt32(PERMISSION_DENIED);
        return PERMISSION_DENIED;
    }

    // FIX 2: Validate opCode is within the defined enum range
    if (opCode < OP_MIN || opCode > OP_MAX) {
        return BAD_VALUE;
    }

    status_t result = performOpAsSystem(resourcePath, opCode);
    reply->writeInt32(result);
    return NO_ERROR;
}

// BEFORE (vulnerable kernel ioctl):
static long privileged_drv_ioctl(...) {
    struct op_request req;
    copy_from_user(&req, (void __user *)arg, sizeof(req));
    return execute_privileged_op(req.resource_id, req.target_uid, req.flags);
    // BUG: no ownership verification
}

// AFTER (patched kernel ioctl):
static long privileged_drv_ioctl(...) {
    struct op_request req;
    kuid_t caller_kuid = current_uid();

    copy_from_user(&req, (void __user *)arg, sizeof(req));

    // FIX: verify calling UID owns or has explicit grant on resource_id
    if (!resource_caller_authorized(req.resource_id, caller_kuid)) {
        pr_warn("privileged_drv: deputy abuse attempt by uid=%u on resource %u\n",
                from_kuid(&init_user_ns, caller_kuid), req.resource_id);
        return -EPERM;
    }

    // FIX: reject target_uid escalation — operation runs as caller, not target
    if (!uid_eq(make_kuid(&init_user_ns, req.target_uid), caller_kuid) &&
        !capable(CAP_SYS_ADMIN)) {
        return -EPERM;
    }

    return execute_privileged_op(req.resource_id, req.target_uid, req.flags);
}

Detection and Indicators

Exploitation attempts will surface in system logs before a patch is applied. Key indicators:

// logcat indicators of confused deputy abuse attempt:
E PrivilegedService: performOpAsSystem called for path=/data/system/packages.xml caller_uid=10234
W Binder          : Unexpected caller uid=10234 accessing system resource
// SELinux denials if partial mitigations are in place:
avc: denied { write } for pid= comm="attacker_proc"
    scontext=u:r:untrusted_app:s0:c,c
    tcontext=u:object_r:system_data_file:s0
    tclass=file permissive=0

// audit trail for ioctl path:
kernel: privileged_drv: deputy abuse attempt by uid=2000 on resource 7
kernel: privileged_drv: rejected target_uid escalation (2000 -> 0)

At the system level, monitor for:

  • Unprivileged UIDs (uid >= 10000) sending Binder transactions to system services that operate on /data/system/ paths
  • packages.xml or packages.list modification timestamps changing without a corresponding PackageManager install event
  • Repeated PERMISSION_DENIED returns from system services originating from a single PID in a short window (probe phase)
  • Unexpected setuid or capability grant events in audit logs following system service transactions

Remediation

Apply the vendor security update addressing CVE-2026-0008 as listed in the NVD advisory. For defenders operating prior to patch availability:

  • SELinux policy hardening: Add neverallow rules preventing untrusted_app and shell domains from reaching the affected service transaction codes directly or through intermediary services.
  • Binder interface audit: Audit all exported onTransact handlers that call IPCThreadState::self()->getCallingUid() and verify the returned UID is used in an access control decision — not merely logged.
  • Principle of least privilege: System services should drop to caller credentials via seteuid(callerUid) before performing any filesystem operation on caller-supplied paths, restoring elevated credentials only for operations that genuinely require them.
  • Capability token validation: Any kernel driver accepting a capability token or resource handle via ioctl must re-derive authorization from current_uid() / current_cred(), never from a field in the userspace-supplied struct.
CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →