/dev/mem可沒那麼簡單

這幾天研究了下/dev/mem。發現功能很是奇妙,經過mmap可以將物理地址映射到用戶空間的虛擬地址上。在用戶空間完畢對設備寄存器的操做,因而上網搜了一些/dev/mem的資料。

網上的說法也很是統一,/dev/mem是物理內存的全映像,可以用來訪問物理內存,通常使用方法是open("/dev/mem",O_RDWR|O_SYNC),接着就可以用mmap來訪問物理內存以及外設的IO資源,這就是實現用戶空間驅動的一種方法。
用戶空間驅動聽起來很是酷。但是對於/dev/mem,我認爲沒那麼簡單,有2個地方引發個人懷疑:
(1)網上資料都說/dev/mem是物理內存的全鏡像。這個概念很是含糊,/dev/mem究竟可以完畢哪些地址的虛實映射?
(2)/dev/mem看似很是強大。但是這也太危急了,黑客全然可以利用/dev/mem對kernel代碼以及IO進行一系列的非法操做,後果不可預測。難道內核開發人員們沒有意識到這點嗎?


網上資料說法都很是泛泛,僅僅對mem設備的使用進行說明,沒有對這些問題進行深究。安全

要搞清這一點,我認爲仍是從/dev/mem驅動開始下手。app



參考內核版本號:3.4.55 
參考平臺:powerpc/arm

mem驅動在drivers/char/mem.c,mmap是系統調用。產生軟中斷進入內核後調用sys_mmap。終於會調用到mem驅動的mmap實現函數。
ide

來看下mem.c中的mmap實現:函數

static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
    size_t size = vma->vm_end - vma->vm_start;

    if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
        return -EINVAL;

    if (!private_mapping_ok(vma))
        return -ENOSYS;

    if (!range_is_allowed(vma->vm_pgoff, size))
        return -EPERM;

    if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,
                        &vma->vm_page_prot))
        return -EINVAL;

    vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
                         size,
                         vma->vm_page_prot);

    vma->vm_ops = &mmap_mem_ops;

    /* Remap-pfn-range will mark the range VM_IO and VM_RESERVED */
    if (remap_pfn_range(vma,
                vma->vm_start,
                vma->vm_pgoff,
                size,
                vma->vm_page_prot)) {
        return -EAGAIN;
    }
    return 0;
}
vma是內核內存管理很是重要的一個結構體。
其結構成員中start end表明要映射到的用戶空間虛擬地址範圍。用戶空間的動態映射是以PAGE_SIZE也就是4K爲一頁,
vma_pgoff是要映射的物理地址。vma_page_prot表明該頁的權限。


這些成員的賦值是在調用詳細驅動的mmap實現函數以前。在sys_mmap中進行的。
在mmap_mem最後調用remap_pfn_range,該函數完畢指定物理地址與用戶空間虛擬地址頁表的創建。
remap_pfn_range參數中vma->vm_pgoff即表明要映射的物理地址,並無範圍限制僅可以操做內存。
mmap系統調用的函數定義例如如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr指定要映射到的虛擬地址。寫NULL則有sys_mmap來分配該虛擬地址。
mmap參數與mem_mmap參數對應關係例如如下:
prot      ===> vma->vma_page_prot
offset    ===> vma->vma_pgoff
length    ===> size
post


從剛纔分析的mem_mmap流程來看,可以得出一個簡單的結論:
mem_mmap可以映射整個處理器的地址空間。而不僅僅是內存。這裏要說明的是,地址空間不等於內存空間。站在處理器角度看。地址空間指處理器總線上的所有可尋址空間。除了內存,還有外設的IO空間。以及其它總線映射過來的mem(如PCI)
個人理解。mem_mmap全然可以映射0-0xffffffff的所有物理地址(填TLB頁表完畢映射)。但前提是保證該物理地址是真實有效的,也就是處理器訪問該總線物理地址可以獲取有效數據。
因此現在看來mmap /dev/mem,僅僅要肯定咱們處理器的地址空間分佈,就可以將咱們需要的地址映射到用戶空間進行操做。
假設地址不是一個有效物理地址(處理器地址空間分佈中該地址沒用)。mmap創建該物理地址與用戶空間虛擬地址的映射。填TLB,CPU通過TLB翻譯後去訪問該不存在的物理地址訪問就有可能致使CPU掛掉。


這也就解釋了我第一個疑問,但是kernel的安全機制不會贊成用戶這麼肆無忌憚的操做。接着來看remap_pfn_range以前mmap_mem怎樣進行防禦。

首先是valid_mmap_phys_addr_range,檢查該物理地址是不是一個有效的mmap地址。假設平臺定義了ARCH_HAS_VALID_PHYS_ADDR_RANGE則會實現該函數,
arm中定義並實現了該函數,在arch/arm/mm/mmap.c中,例如如下:
ui

/*
 * We don't use supersection mappings for mmap() on /dev/mem, which
 * means that we can't map the memory area above the 4G barrier into
 * userspace.
 */
int valid_mmap_phys_addr_range(unsigned long pfn, size_t size)
{
    return !(pfn + (size >> PAGE_SHIFT) > 0x00100000);
}
該函數肯定mmap的範圍是否超過4G,超過4G則爲無效物理地址,這樣的狀況用戶空間通常不會出現。


而對於powerpc,平臺未定義ARCH_HAS_VALID_PHYS_ADDR_RANGE,因此valid_mmap_phys_addr_range在mem.c中定義爲空函數,返回1 表示該物理地址一直有效。
物理地址有效。不會返回-EINVAL。繼續往下走。

接下來是private_mapping_ok,對於有MMU的CPU,實現例如如下:
this

static inline int private_mapping_ok(struct vm_area_struct *vma)
{
    return 1;
}
MMU的權限管理可以支持私有映射,因此該函數一直成功。

接下來是一個最爲關鍵的檢查函數range_is_allowed。定義例如如下:
#ifdef CONFIG_STRICT_DEVMEM
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
    u64 from = ((u64)pfn) << PAGE_SHIFT;
    u64 to = from + size;
    u64 cursor = from;

    while (cursor < to) {
        if (!devmem_is_allowed(pfn)) {
            printk(KERN_INFO
        "Program %s tried to access /dev/mem between %Lx->%Lx.\n",
                current->comm, from, to);
            return 0;
        }
        cursor += PAGE_SIZE;
        pfn++;
    }
    return 1;
}
#else
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
    return 1;
}
#endif
可以看出假設不打開CONFIG_STRICT_DEVMEM,range_is_allowed是返回1,表示該物理地址範圍是被贊成的。查看kconfig文件(在對應平臺文件夾下。如arch/arm/Kconfig.debug中)找到CONFIG_STRICT_DEVMEM說明例如如下

config STRICT_DEVMEM
    def_bool y
    prompt "Filter access to /dev/mem"
    help
      This option restricts access to /dev/mem.  If this option is
      disabled, you allow userspace access to all memory, including
      kernel and userspace memory. Accidental memory access is likely
      to be disastrous.
      Memory access is required for experts who want to debug the kernel.

      If you are unsure, say Y.
該選項menuconfig時在kernel hacking文件夾下。
依聽說明可以理解。CONFIG_STRICT_DEVMEM是嚴格的對/dev/mem訪問檢查,假設關掉該選項,用戶就可以經過mem設備訪問所有地址空間(依據對我提出的第一個問題理解,這裏memory應該理解爲地址空間)。該選項對於調試內核有幫助。


假設打開該選項,內核就會對mem設備訪問加以檢查。檢查函數就是range_is_allowed。spa


range_is_allowed函數對要檢查的物理地址範圍以4K頁爲單位,一頁一頁的調用devmem_is_allowed。假設不一樣意,則會進行打印提示。並返回0,表示該物理地址範圍不被贊成
翻譯



來看devmem_is_allowed.該函數是平臺相關函數,只是arm跟powerpc的實現相差不大,以arm的實現爲例。debug

在arch/arm/mm/mmap.c中。

/*
 * devmem_is_allowed() checks to see if /dev/mem access to a certain
 * address is valid. The argument is a physical page number.
 * We mimic x86 here by disallowing access to system RAM as well as
 * device-exclusive MMIO regions. This effectively disable read()/write()
 * on /dev/mem.
 */
int devmem_is_allowed(unsigned long pfn)
{
    if (iomem_is_exclusive(pfn << PAGE_SHIFT))
        return 0;
    if (!page_is_ram(pfn))
        return 1;
    return 0;
}
首先iomem_is_exclusive檢查該物理地址是否被獨佔保留,實現例如如下:

#ifdef CONFIG_STRICT_DEVMEM
static int strict_iomem_checks = 1;
#else
static int strict_iomem_checks;
#endif

/*
 * check if an address is reserved in the iomem resource tree
 * returns 1 if reserved, 0 if not reserved.
 */
int iomem_is_exclusive(u64 addr)
{
    struct resource *p = &iomem_resource;
    int err = 0;
    loff_t l;
    int size = PAGE_SIZE;

    if (!strict_iomem_checks)
        return 0;

    addr = addr & PAGE_MASK;

    read_lock(&resource_lock);
    for (p = p->child; p ; p = r_next(NULL, p, &l)) {
        /*
         * We can probably skip the resources without
         * IORESOURCE_IO attribute?
         */
        if (p->start >= addr + size)
            break;
        if (p->end < addr)
            continue;
        if (p->flags & IORESOURCE_BUSY &&
             p->flags & IORESOURCE_EXCLUSIVE) {
            err = 1;
            break;
        }
    }
    read_unlock(&resource_lock);

    return err;
}
假設打開了CONFIG_STRICT_DEVMEM,iomem_is_exclusive遍歷iomem_resource鏈表,查看要檢查的物理地址所在resource的flags,假設是bug或者exclusive。則返回1,代表該物理地址是獨佔保留的。

據我瞭解,iomem_resource是來表徵內核iomem資源的鏈表。

對於外設的IO資源,kernel中使用platform device機制來註冊平臺設備(platform_device_register)時調用insert_resource將該設備對應的io資源插入到iomem_resource鏈表中。


假設我要對某外設的IO資源進行保護。防止用戶空間訪問。可以將其resource的flags置位exclusive就能夠。

只是我查看我平臺支持包裏的所有platform device的resource。flags都沒有置位exclusive或者busy。

假設我映射的物理地址範圍是外設的IO。檢查可以經過。

對於內存的mem資源,怎樣註冊到iomem_resource鏈表中。內核代碼中我還沒找到詳細的位置,只是iomem在proc下有對應的表徵文件。可以cat /proc/iomem。


依據個人實際操做測試。內存資源也都沒有exclusive。因此假設我映射地址是內存。檢查也可以經過。




因此這裏iomem_is_exclusive檢查一般是經過的。接下來看page_is_ram。看devmem_is_range的邏輯,假設地址是ram地址。則該地址不被贊成。page_is_ram也是平臺函數,查看powerpc的實現例如如下。


int page_is_ram(unsigned long pfn)
{
#ifndef CONFIG_PPC64    /* XXX for now */
    return pfn < max_pfn;
#else
    unsigned long paddr = (pfn << PAGE_SHIFT);
    struct memblock_region *reg;

    for_each_memblock(memory, reg)
        if (paddr >= reg->base && paddr < (reg->base + reg->size))
            return 1;
    return 0;
#endif
}
max_pfn賦值在在do_init_bootmem中。例如如下.
void __init do_init_bootmem(void)
{
    unsigned long start, bootmap_pages;
    unsigned long total_pages;
    struct memblock_region *reg;
    int boot_mapsize;

    max_low_pfn = max_pfn = memblock_end_of_DRAM() >> PAGE_SHIFT;
    total_pages = (memblock_end_of_DRAM() - memstart_addr) >> PAGE_SHIFT;
max_pfn表明了內核lowmem的頁個數,lowmem在內核下靜態線性映射。系統啓動之初完畢映射以後不會修改。讀寫效率高。內核代碼都是跑在lowmem。
lowmem大小咱們可以經過cmdline的「mem=」來指定。



這裏就明確了假設要映射的物理地址在lowmem範圍內,也是不一樣意被映射的。

這樣range_is_allowed就分析完了。exclusive的iomem以及lowmem範圍內的物理地址是不一樣意被映射的。

接下來phys_mem_access_prot_allowed實現爲空返回1,沒有影響。

phys_mem_access_prot肯定咱們映射頁的權限,該函數也是平臺函數,以powerpc實現爲例,例如如下:

pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
                  unsigned long size, pgprot_t vma_prot)
{
    if (ppc_md.phys_mem_access_prot)
        return ppc_md.phys_mem_access_prot(file, pfn, size, vma_prot);

    if (!page_is_ram(pfn))
        vma_prot = pgprot_noncached(vma_prot);

    return vma_prot;
}
假設有平臺實現的phys_mem_access_prot,則調用之。

假設沒有。對於不是lowmem範圍內的物理地址。權限設置爲uncached。

以上的檢查完畢,最後調用remap_pfn_range完畢頁表設置。



因此假設打開CONFIG_STRICT_DEVMEM,mem驅動會對mmap要映射的物理地址進行範圍和位置的檢查而後才進行映射。檢查條件例如如下:
(1)映射範圍不能超過4G。
(2)該物理地址所在iomem不能exclusive.
(3)該物理地址不能處在lowmem中。

因此說對於網上給出的各類利用/dev/mem來操做內存以及寄存器的文章。假設操做範圍在上述3個條件內,內核必須關閉CONFIG_STRICT_DEVMEM才行。

這樣對於mem設備個人2個疑問算是攻克了。

查看mem.c時我還看到了另一個有趣的設備kmem。這個設備mmap的是哪裏的地址,網上的說法是內核虛擬地址。這個說法我不覺得然,這裏記錄下個人想法。

假設內核打開CONFIG_KMEM。則會建立kmem設備。它與mem設備主要區別在mmap的實現上。kmem的mmap實現例如如下:
#ifdef CONFIG_DEVKMEM
static int mmap_kmem(struct file *file, struct vm_area_struct *vma)
{
    unsigned long pfn;

    /* Turn a kernel-virtual address into a physical page frame */
    pfn = __pa((u64)vma->vm_pgoff << PAGE_SHIFT) >> PAGE_SHIFT;

    /*
     * RED-PEN: on some architectures there is more mapped memory than
     * available in mem_map which pfn_valid checks for. Perhaps should add a
     * new macro here.
     *
     * RED-PEN: vmalloc is not supported right now.
     */
    if (!pfn_valid(pfn))
        return -EIO;

    vma->vm_pgoff = pfn;
    return mmap_mem(file, vma);
}
#endif
引發我注意的是__pa,完畢內核虛擬地址到物理地址的轉換,最後調用mmap_mem,簡單一看kmem的確是映射的內核虛擬地址。
但是搞清楚__pa的實現,我就不這麼認爲了。

以powerpc爲例。在arch/powerpc/include/asm/page.h,定義例如如下:

#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET))
#define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET)
....
#define VIRT_PHYS_OFFSET (KERNELBASE - PHYSICAL_START)
內核中定義了4個變量來表示內核一些主要的物理地址和虛擬地址,例如如下:
KERNELBASE     內核的起始虛擬地址,個人是0xc0000000
PAGE_OFFSET    低端內存的起始虛擬地址,一般是0xc0000000
PHYSICAL_START 內核的起始物理地址。個人是0x80000000
MEMORY_START   低端內存的起始物理地址。個人是0x80000000

內核在啓動過程當中對於lowmem的靜態映射。就是以上述的物理地址和虛擬地址的差值進行線性映射的。
因此__pa __va轉換的是線性映射的內存部分,也就是lowmem。
因此kmem映射的是lowmem。假設個人cmdline參數中mem=512M,這就意味着經過kmem的mmap我最多可以訪問內核地址空間開始的512M內存。

對於超過lowmem範圍,訪問highmem。假設使用__pa訪問,由於highmem是動態映射的,其映射關係不是線性的那麼簡單了,依據__pa獲取的物理地址與咱們想要的內核虛擬地址是不正確應的。

相關文章
相關標籤/搜索