2

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

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

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

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

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

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

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

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

4

1 回答 1

3

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

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

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

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

伟大的!问题解决了,分配一个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 下运行,您将获得完整的回溯,显示错误写入的确切位置。

于 2018-08-06T19:46:40.817 回答