【问题标题】:How to check an address is accessible in the kernel space如何检查内核空间中的地址是否可访问
【发布时间】:2021-06-10 02:32:09
【问题描述】:

我正在开发内核实时补丁,实时补丁模块中的一些代码如下所示:

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.
    }
}

此代码获取内核符号的地址(类型为struct list_head)并尝试迭代列表。但是由于某些原因,链表中的某些节点可能会被破坏,导致某些节点的next 指针无效(如NULL、0xABABABAB 或其他随机数),取消引用next 指针可能会导致内核崩溃。

那么有没有办法检查指针是否可以安全访问?

我已经检查了之前的两个答案:

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?

他们告诉我使用virt_addr_valid。我有一些肯定可以访问的地址,例如 0xFFFFFFFFA032C040,但 virt_addr_valid 总是返回 false,这使我无法区分实时补丁模块中的“可访问”和“不可访问”地址。

【问题讨论】:

  • 我现在不能调查这么多,但是 1) 我认为 arch/x86/mm/physaddr.c 是 virt_addr_valid 所在的位置。您可以尝试使用它来查看为什么它对所有内容都返回 False。 2) 查找 copy_to/from_user() 函数。它们可能会帮助您确定内核如何在复制之前验证地址是否合法。
  • 我的地址是0x 0xFFFFFFFFA032C040(在模块区),virt_addr_valid检查一个地址是否属于0xFFFFFFFF80000000 - 0xFFFFFFFFA0000000(base + 512M)(在内核映像区),所以返回false。跨度>
  • 列表在您迭代时会动态变化吗?这可以解释不好的指针。在这种情况下,仅仅因为指针看起来可以安全访问,并不意味着它实际上指向了一个有效的列表条目。
  • 是的,我会将 printk 语句插入到 virt_addr_valid 函数中,以检查指针地址是否保持不变并且仍然被函数拒绝。可以让您了解正在发生的事情。
  • 添加支票是真正的问题。你显然有竞争条件,代码被破坏了。

标签: linux memory-management linux-kernel patch kernel-module


【解决方案1】:

如here 所述,kallsyms_lookup_name() 提出了一些许可问题,并且在当前内核上未导出。

您可能需要查看livepatch。

【讨论】:

    【解决方案2】:

    在我的情况下,我要检查的内存地址应该使用kmalloc() 分配,但可能由于一些错误而被污染(即随机值)。

    virt_addr_valid() 检查地址是驻留在内核中的“内核代码区域”还是“直接映射区域”中(检查此link 以了解 x86_64 内存布局)。并且kmalloc() 分配的内存位于“直接映射区域”中,因此在kmalloc 的内存上使用virt_addr_valid() 总是正确的。但是另一方面,由于我的实验,一些地址可能会得到virt_addr_valid=true,但是无法访问,取消引用地址可能会导致机器崩溃。所以我还需要确保地址在页表中正确映射,以免机器崩溃。

    所以解决方案包含两个步骤:

    1. virt_addr_valid() 在地址上是否返回 true
    2. 如果为 true,则执行 page-table-walk 以检查地址是否正确映射

    由于“virt_addr_valid-is-true”区域的内存映射没有改变,所以不需要持有锁。

    下面是带有 4 级页表的 x86_64 的代码。

    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;
    }
    

    【讨论】:

    • 这正是 source/arch/x86/mm/init_64.c 中的 kern_addr_valid(addr) 所做的,除了 kern_addr_valid 如果需要还支持 5 级页表。您的实现对于较旧的内核是正确的,但不适用于具有 5 级页表的较新内核。所以你几乎明白了!
    【解决方案3】:

    我花了一些时间,但我发现 source/arch/x86/mm/init_64.c 中的 kern_addr_valid(addr) 可以解决问题。它遍历页表,占大页以确保地址有效。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-04
      • 2014-04-27
      • 1970-01-01
      • 2018-04-23
      • 1970-01-01
      • 1970-01-01
      • 2019-02-19
      • 2013-05-30
      相关资源
      最近更新 更多