C语言 保护多个线程共享的 talloced 内存免受写入



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

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

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

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

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

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

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

有没有办法将案例 1( 中分配的内存标记为只读?

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

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

因此,为了将mprotect用于情况 1(,我们需要将要保护的所有数据都放在一个连续的内存区域中。

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

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

最新更新