clinuxlow-levelptrace

Ptrace allows to write to executable program segment, but process_vm_writev doesn't


While experimenting with editing opcodes of running process on Linux with C language, I found out that I can't to edit program opcodes by PID with process_vm_writev, but using ptrace and pwrite I can do it. process_vm_writev returns -1 and errno says "Bad address". But when I use ptrace with same address it successes. I run program as root. Is it an issue of GNU/Linux and should I open an issue on GitHub? Code for process_vm_writev (without includes to be short):

ssize_t write_pmem(pid_t pid, off_t addr, void* value, size_t size) {
    struct iovec local[1];
    local[0].iov_base = value;
    local[0].iov_len = size;
    struct iovec remote[1];
    remote[0].iov_base = (void*)addr;
    remote[0].iov_len = size;

    return process_vm_writev(pid, local, 1, remote, 1, 0);
}

int main() {
    pid_t pid;
    off_t opcode_ptr = 0x45B5A7;

    printf("PID: ");
    scanf("%d", &pid);

    char nops[5] = {0x90, 0x90, 0x90, 0x90, 0x90};
    ssize_t res = write_pmem(pid, opcode_ptr, &nops, sizeof(nops));

    printf("%ld\n%s", res, strerror(errno));
    /* and it prints:
       -1
       Bad address */
    return 0;
}

/proc/PID/maps says that 0x45B5A7 is 00406000-004f0000 r-xp 00006000 for that process (no write rights) but I think it's still writable for the root.

I want to use new process_vm Linux API, so I want to fix that code. Any ideas?


Solution

  • Both `process_vm_writev` and `PTRACE_POKETEXT` ultimately rely on `get_user_pages_remote` at the lower level. However, they use different flags: `process_vm_writev` uses `FOLL_WRITE` as the flag, while `PTRACE_POKETEXT` uses `FOLL_WRITE | FOLL_FORCE`. The `FOLL_FORCE` flag allows the `can_follow_write_pte` function to bypass the permission check on the page table entry (PTE), enabling writes to a read-only address.

    Linux kernel 5.10 source code mm/gup.c
    
    /*
     * FOLL_FORCE can write to even unwritable pte's, but only
     * after we've gone through a COW cycle and they are dirty.
     */
    static inline bool can_follow_write_pte(pte_t pte, unsigned int flags)
    {
        return pte_write(pte) ||
            ((flags & FOLL_FORCE) && (flags & FOLL_COW) && pte_dirty(pte));
    }