Share
## https://sploitus.com/exploit?id=74806B76-D6CE-5827-A658-E3BBD64B6543
# CVE-2022-22706 - Page-Cache Write

The Arm Mali GPU driver hands userspace a CPU-writable mapping of pages it pinned read-only,
so an unprivileged process gets a writable alias of the page cache backing a file it can
only open `O_RDONLY`.

`exploit.c` blanks root's password field in the `/etc/passwd` page cache and execs
`su root`. The on-disk file is never modified.

> PoC for the driver tree and QEMU target in this repo (`mali_kbase` **r35p0-01eac0**,
> `CONFIG_MALI_NO_MALI=y`, x86_64 GKI 5.15). Run it in the VM.

## The bug

Two decisions disagree about who may write the imported pages.

The CPU mapping's writability comes from `KBASE_REG_CPU_WR`, but the pin asks for write
access based on `KBASE_REG_GPU_WR` alone:

```c
/* mali_kbase_mem.c */
pinned_pages = pin_user_pages_remote(
    mm, address, alloc->imported.user_buf.nr_pages,
    reg->flags & KBASE_REG_GPU_WR ? FOLL_WRITE : 0, pages, NULL, NULL);
```

Import with `CPU_WR` set and `GPU_WR` clear and you get both halves at once: a writable CPU
mapping of the import, and a `get_user_pages` pin taken without `FOLL_WRITE`. Without
`FOLL_WRITE`, `get_user_pages` never breaks COW on a read-only file mapping, it returns the
page-cache page itself. The driver then maps those exact pages back to userspace as writable.

Fixed by [`5381ff7`](https://android.googlesource.com/kernel/google-modules/gpu/+/5381ff7b4106b277ff207396e293ede2bf959f0c%5E%21/)
("GPUCORE-32592 Fix userbuf imports to respect RO memory"), which derives write access from
`KBASE_REG_CPU_WR | KBASE_REG_GPU_WR` instead of `GPU_WR` alone.

## Exploit flow

```mermaid
sequenceDiagram
    participant U as unprivileged process
    participant K as mali_kbase
    participant PC as page cache

    U->>U: mmap /etc/passwd O_RDONLY, PROT_READ
    U->>K: MEM_IMPORT(anon page, CPU_RD|CPU_WR|GPU_RD)
    Note over K: address recorded, nothing pinned yet
    U->>U: munmap(anon) + mremap file mapping onto that VA
    U->>K: mmap(import cookie) โ†’ writable CPU mapping
    U->>K: JOB_SUBMIT(EXTERNAL_RESOURCES)
    K->>PC: pin_user_pages_remote() without FOLL_WRITE
    U->>PC: memcpy() through the writable mapping
    U->>U: execl("/bin/su", "su", "root")
```

`MEM_IMPORT` only records an address; the pin happens later, at `JOB_SUBMIT`. That gap is
what lets the anonymous page be swapped for the file mapping in between.

The edit is length-preserving, so nothing after root's line shifts:

```
root:x:0:0:root:/root:/bin/sh      โ† before
root::0:0:rootx:/root:/bin/sh      โ† after (empty password)
```

busybox `su` returns `CHECKPASS_PW_HAS_EMPTY_PASSWORD` before prompting when the password
field is empty, and only reads `/etc/shadow` when it is exactly `x`.

## Build and run

```sh
gcc -static -o exploit exploit.c
```

Copy it into the VM and run it as an unprivileged user:

```sh
$ ./exploit
```

## Demo

![demo](demo.gif)

Recorded over SSH into the QEMU target as `user` (uid 1000). `./exploit` patches root's line
in the `/etc/passwd` page cache and execs `su root`, which drops straight to a root shell
without prompting.

Leaving that shell and running `su` again still gives root: the page stays cached, so every
later `open()`/`read()` of `/etc/passwd` sees the patched bytes.

After `echo 1 > /proc/sys/vm/drop_caches` evicts the page and the file is re-read from storage, 
`su` asks for a password again. The bytes on disk were never touched.

## References

* [Arm โ€” Mali GPU driver vulnerabilities](https://developer.arm.com/Arm%20Security%20Center/Mali%20GPU%20Driver%20Vulnerabilities)
* [STAR Labs โ€” Mali-cious Intent: Exploiting GPU Vulnerabilities (CVE-2022-22706 / CVE-2021-39793)](https://starlabs.sg/blog/2025/02-mali-cious-intent-exploiting-gpu-vulnerabilities-cve-2022-22706-/-cve-2021-39793/)
* [Upstream fix โ€” `5381ff7`](https://android.googlesource.com/kernel/google-modules/gpu/+/5381ff7b4106b277ff207396e293ede2bf959f0c%5E%21/)