0

I'm working on a kernel live-patch, some code in the live-patch module looks like this:

void list_checker() {
    struct list_head *head, *iter;
    head = (struct list_head *)kallsyms_lookup_name("module_name:symbol_name");
    for (iter = head->next; iter != head; iter = iter->next) {
        // do something.
    }
}

This code gets the address of a kernel symbol (which is type struct list_head) and tries to iterate the list. But for some reason, some nodes in the list may be broken, resulting in the next pointer of some node being invalid (like NULL, 0xABABABAB, or other random numbers), and dereferencing the next pointer may cause the kernel to crash.

So is there a way to check whether a pointer is safe to access?

I have checked two previous answers:

How to check a memory address is valid inside Linux kernel?

How can I tell if a virtual address has valid mapping in ARM linux kernel?

They tell me to use virt_addr_valid. I have some surely accessible address, like 0xFFFFFFFFA032C040, but virt_addr_valid always returns false, which makes me unable to distinguish "accessible" and "non-accessible" addresses in my live-patch module.

0andriy
  • 4,183
  • 1
  • 24
  • 37
Dillion Wang
  • 362
  • 3
  • 18
  • I can't look into this much right now, but 1) arch/x86/mm/physaddr.c is where virt_addr_valid lives, I think. You can try to play with it to see why it's returning False for everything. 2) Look for the copy_to/from_user() functions. They might help you identify how the kernel verifies that an address is legit before going through the copy. – wxz Mar 12 '21 at 03:59
  • My address is 0x 0xFFFFFFFFA032C040 (in the module area), virt_addr_valid checks whether an address belongs to 0xFFFFFFFF80000000 - 0xFFFFFFFFA0000000 (base + 512M) (in the kernel image area), so it returns false. – Dillion Wang Mar 12 '21 at 07:36
  • 1
    Could the list be changing dynamically while you iterate over it? That could explain the bad pointers. In that case, just because a pointer appears to be safe to access, that doesn't mean it is actually pointing to a valid list entry. – Ian Abbott Mar 12 '21 at 13:12
  • Yeah, I would insert printk statements into the virt_addr_valid function to check that the pointer address has stayed the same and is still being rejected by the function. Could give you insight to what's going on. – wxz Mar 12 '21 at 14:21
  • 1
    Adding the check is the papering over the real issue. You clearly have race condition there and the code is simply broken. – 0andriy Mar 12 '21 at 17:05
  • @0andriy I'm going to do this by checking whether the address is mapped in the page table. To avoid the race condition, I can hold lock the page table when accessing. Seems no other easy ways. – Dillion Wang Mar 12 '21 at 18:04
  • And what will you do when interrupt happens? Dead lock? – 0andriy Mar 12 '21 at 18:14
  • @0andriy disable interrupt and spin_lock? The code won’t last too long. – Dillion Wang Mar 13 '21 at 01:27
  • And what will you do, if interrupts are disabled for too long for some critical cases? What about NMI? It is not so easy task. Spin lock disables local IRQ, do you have UP or SMP system? – 0andriy Mar 13 '21 at 08:37
  • @0andriy A SMP system with 128 cores. I think its just like manipulating page table in the kernel (e.g., setup memory mapping in page fault exceptions), which also need to lock the page table for a while. I think I also need to make sure no page fault will happen when running the code in my module. – Dillion Wang Mar 14 '21 at 04:21

3 Answers3

1

In my case, the memory address I want to check should be allocated using kmalloc(), but may be polluted (i.e., random values) due to some bugs.

virt_addr_valid() checks whether the address resides in the "kernel code area" or in the "direct mapping area" in the kernel (check this link for x86_64 memory layout). And the memory which is allocated by kmalloc() resides in the "direct mapping area", so using virt_addr_valid() on kmalloc'ed memories is always true. But on the other side, due to my experiment, some address may get virt_addr_valid=true, but is not accessible, and dereferencing the address may cause the machine to crash. So I also need to make sure the address is correctly mapped in the page table in order not to crash the machine.

So the solution contains two steps:

  1. whether virt_addr_valid() returns true on the address
  2. if true, then do a page-table-walk to check whether the address is correctly mapped

Since the memory mapping in the "virt_addr_valid-is-true" area does not change, there is no need to hold a lock.

below is the code for x86_64 with 4-level page tables.

static bool page_mapping_exist(unsigned long addr, size_t size) {
    pgd_t *pgd;
    pmd_t *pmd;
    pud_t *pud;
    pte_t *pte;
    struct mm_struct *mm = current->mm;
    unsigned long end_addr;
    pgd = pgd_offset(mm, addr);
    if (unlikely(!pgd) || unlikely(pgd_none(*pgd)) || unlikely(!pgd_present(*pgd)) )
        return false;
    
    pud = pud_offset(pgd, addr);
    if (unlikely(!pud) || unlikely(pud_none(*pud)) || unlikely(!pud_present(*pud)))
        return false;

    pmd = pmd_offset(pud, addr);
    if (unlikely(!pmd) || unlikely(pmd_none(*pmd)) || unlikely(!pmd_present(*pmd)))
        return false;

    if (pmd_trans_huge(*pmd)) {
        end_addr = (((addr >> PMD_SHIFT) + 1) << PMD_SHIFT) - 1;
        goto end;
    }
    pte = pte_offset_map(pmd, addr);
    if (unlikely(!pte) || unlikely(!pte_present(*pte)))
        return false;
    end_addr = (((addr >> PAGE_SHIFT) + 1) << PAGE_SHIFT) - 1;
end:
    if (end_addr >= addr + size - 1)
        return true;
    return page_mapping_exist(end_addr + 1, size - (end_addr - addr + 1));
}

static bool addr_valid(unsigned long addr, size_t size) {
    int i;
    for (i = 0; i < size; i++) {
        if (!virt_addr_valid(addr + i))
            return false;
    }
    if (!page_mapping_exist(addr, size))
        return false;
    return true;
}
0andriy
  • 4,183
  • 1
  • 24
  • 37
Dillion Wang
  • 362
  • 3
  • 18
  • This is exactly what kern_addr_valid(addr) in source/arch/x86/mm/init_64.c does, except kern_addr_valid also supports 5-level page tables if need be. Your implementation is correct for older kernels but won't work on newer kernels with 5-level page tables. So you almost got it! – David Yeager Aug 14 '21 at 03:23
0

As noted here, kallsyms_lookup_name() posed some license concerns and it is being unexported on current kernels.

You may want to check livepatch.

Fusho
  • 1,469
  • 1
  • 10
  • 22
0

It took me a while but I found kern_addr_valid(addr) in source/arch/x86/mm/init_64.c does the trick. It walks the page tables, accounting for large pages to make sure the address is valid.

David Yeager
  • 596
  • 5
  • 9