【问题标题】:Protecting talloced memory shared by multiple threads against writes保护由多个线程共享的talloced内存免受写入
【发布时间】:2019-01-13 19:53:24
【问题描述】:

在我们的应用程序(一个网络守护程序)中,堆分配的内存大致有三种用途。

  1. 启动时分配的内存用于保存应用程序全局配置的解析结果。

  2. 创建线程时为线程特定数据分配的内存(并在销毁线程时释放)。

  3. 为请求提供服务时分配的内存并绑定到请求的生命周期。

我们在所有三种情况下都使用talloc来管理内存。

我们最近遇到了一些内存损坏问题,其中错误的指针值意味着一个或多个线程正在写入全局配置并导致崩溃。

由于应用程序的结构方式,在应用程序开始处理请求后,任何内容都不应写入在情况 1) 中分配的内存。

有没有办法将情况 1) 中分配的内存标记为只读?

【问题讨论】:

    标签: c mprotect talloc


    【解决方案1】:

    在 POSIX 规范中有一个函数,mprotect。 mprotect 允许更改单个内存页面的权限(读/写/执行)。

    使用mprotect 将部分堆标记为只读的问题在于,最高粒度是单个页面,通常为 4k(取决于操作系统/架构)。将所有堆分配的结构填充到 4k 的倍数会导致大量内存膨胀,嘘。

    因此,为了在案例 1) 中使用 mprotect,我们需要在一个连续的内存区域中获取我们想要保护的所有数据。

    Talloc 可以在这里提供帮助。 talloc pools 是一种平板分配类型,在正确使用时可以带来很大的性能提升,并且(如果大小足够)允许池中的所有分配在单个连续内存区域中完成。

    太棒了!问题解决了,分配一个talloc内存池,做所有的实例化和解析工作,使用mprotect将池标记为只读,完成!不幸的是,事情并没有那么简单……

    还有另外三个问题需要解决:

    1. mprotect 需要内存为页面大小的倍数。
    2. mprotect 需要起始地址进行页面对齐。
    3. 我们不知道要为池分配多少内存。

    问题1很简单,我们只需要四舍五入到页面大小的倍数(可以方便地用getpagesize检索)。

    size_t rounded;
    size_t page_size;
    
    page_size = (size_t)getpagesize();
    rounded = (((((_num) + ((page_size) - 1))) / (page_size)) * (page_size));
    

    问题 2 也很简单。如果我们在池中分配一个字节,我们可以预测第一个“真正的”分配将发生在哪里。我们还可以从分配的地址中减去池的地址,以计算出块头将使用多少内存talloc。

    有了这些信息,我们可以(如果需要)执行第二次分配以将池内存填充到下一页,确保“真正的”分配发生在受保护区域内。然后我们可以返回下一页的地址以供在mprotect 中使用。这里唯一的小问题是我们需要将池过度分配一页以确保有足够的内存。

    问题 3 很烦人,不幸的是,解决方案是特定于应用程序的。如果在案例 1) 中执行所有实例化没有副作用,并且使用的内存量是一致的,则可以使用两遍方法来确定要分配给池的内存量。 第 1 步将使用 talloc_init 获取顶级块,talloc_total_size 显示正在使用的内存量,第 2 步将分配一个适当大小的池。

    对于我们的特定用例,我们只允许用户确定池大小。这是因为我们使用受保护的内存作为调试功能,因此用户也是开发人员,分配 1G 的内存以确保有足够的配置进行配置是没有问题的。

    那么这一切是什么样的呢?那么这是我想出的功能:

    /** Return a page aligned talloc memory pool
     *
     * Because we can't intercept talloc's malloc() calls, we need to do some tricks
     * in order to get the first allocation in the pool page aligned, and to limit
     * the size of the pool to a multiple of the page size.
     *
     * The reason for wanting a page aligned talloc pool, is it allows us to
     * mprotect() the pages that belong to the pool.
     *
     * Talloc chunks appear to be allocated within the protected region, so this should
     * catch frees too.
     *
     * @param[in] ctx   to allocate pool memory in.
     * @param[out] start    A page aligned address within the pool.  This can be passed
     *          to mprotect().
     * @param[out] end  of the pages that should be protected.
     * @param[in] size  How big to make the pool.  Will be corrected to a multiple
     *          of the page size.  The actual pool size will be size
     *          rounded to a multiple of the (page_size), + page_size
     */
    TALLOC_CTX *talloc_page_aligned_pool(TALLOC_CTX *ctx, void **start, void **end, size_t size)
    {
        size_t      rounded, page_size = (size_t)getpagesize();
        size_t      hdr_size, pool_size;
        void        *next, *chunk;
        TALLOC_CTX  *pool;
    
    #define ROUND_UP(_num, _mul) (((((_num) + ((_mul) - 1))) / (_mul)) * (_mul))
    
        rounded = ROUND_UP(size, page_size);            /* Round up to a multiple of the page size */
        if (rounded == 0) rounded = page_size;
    
        pool_size = rounded + page_size;
        pool = talloc_pool(ctx, pool_size);         /* Over allocate */
        if (!pool) return NULL;
    
        chunk = talloc_size(pool, 1);               /* Get the starting address */
        assert((chunk > pool) && ((uintptr_t)chunk < ((uintptr_t)pool + rounded)));
        hdr_size = (uintptr_t)chunk - (uintptr_t)pool;
    
        next = (void *)ROUND_UP((uintptr_t)chunk, page_size);   /* Round up address to the next page */
    
        /*
         *  Depending on how talloc allocates the chunk headers,
         *  the memory allocated here might not align to a page
         *  boundary, but that's ok, we just need future allocations
         *  to occur on or after 'next'.
         */
        if (((uintptr_t)next - (uintptr_t)chunk) > 0) {
            size_t  pad_size;
            void    *padding;
    
            pad_size = ((uintptr_t)next - (uintptr_t)chunk);
            if (pad_size > hdr_size) {
                pad_size -= hdr_size;           /* Save ~111 bytes by not over-padding */
            } else {
                pad_size = 1;
            }
    
            padding = talloc_size(pool, pad_size);
            assert(((uintptr_t)padding + (uintptr_t)pad_size) >= (uintptr_t)next);
        }
    
        *start = next;                      /* This is the address we feed into mprotect */
        *end = (void *)((uintptr_t)next + (uintptr_t)rounded);
    
        talloc_set_memlimit(pool, pool_size);           /* Don't allow allocations outside of the pool */
    
        return pool;
    }
    

    上面还使用talloc_set_memlimit 来确保不会在连续区域之外发生分配。

    TALLOC_CTX *global_ctx;
    size_t      pool_size = 1024;
    void        *pool_page_start = NULL, *pool_page_end = NULL;
    
    global_ctx = talloc_page_aligned_pool(talloc_autofree_context(), &pool_page_start, &pool_page_end, pool_size);
    
    /* Allocate things in global_ctx */
    
    ...
    
    /* Done allocating/writing - protect */
    
    if (mprotect(pool_page_start, (uintptr_t)pool_page_end - (uintptr_t)pool_page_start, PROT_READ) < 0) {
        exit(1);
    }
    
    /* Process requests */
    
    ...
    
    /* Done processing - unprotect (so we can free) */
    
    mprotect(pool_page_start, (uintptr_t)pool_page_end - (uintptr_t)pool_page_start,
             PROT_READ | PROT_WRITE);
    

    当在 macOS 上对受保护内存进行错误写入时,您会看到一个 SEGV,如果在 lldb 下运行,您将获得完整的回溯,显示错误写入的确切位置。

    【讨论】:

      猜你喜欢
      • 2012-02-09
      • 2014-02-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-29
      • 2020-02-23
      • 2012-06-14
      相关资源
      最近更新 更多