2

私たちのアプリケーション (ネットワーク デーモン) では、ヒープに割り当てられたメモリをおよそ 3 つの用途で使用しています。

  1. アプリケーションのグローバル構成の解析結果を保持するために起動時に割り当てられるメモリ。

  2. スレッドが作成されるときにスレッド固有のデータに割り当てられるメモリ (およびスレッドが破棄されるときに解放される)。

  3. リクエストを処理するときに割り当てられ、リクエストの有効期間にバインドされたメモリ。

3 つのケースすべてで、talloc を使用してメモリを管理します。

最近、1 つ以上のスレッドがグローバル構成に書き込みを行ってクラッシュを引き起こしていることを意味する不適切なポインター値が原因で、メモリ破損の問題が発生しました。

アプリケーションが構造化されているため、アプリケーションがリクエストの処理を開始した後は、ケース 1) で割り当てられたメモリに何も書き込まないでください。

ケース1)で割り当てられたメモリを読み取り専用としてマークする方法はありますか?

4

1 に答える 1

3

POSIX 仕様には関数mprotectがあります。 mprotectメモリの個々のページのパーミッション (読み取り/書き込み/実行) を変更できます。

ヒープの一部を読み取り専用としてマークアップするために使用する際の問題mprotectは、最高の粒度が単一ページであり、通常は 4k (OS/アーキテクチャに依存) であるという事実です。すべてのヒープに割り当てられた構造体を 4k の倍数にパディングすると、大量のメモリが膨張します。

したがって、mprotectケース 1) を使用するには、保護したいすべてのデータをメモリの 1 つの連続した領域で取得する必要があります。

タロックがお手伝いします。 talloc プールはスラブ割り当ての一種であり、正しく使用するとパフォーマンスが大幅に向上し、(十分なサイズがあれば) プール内のすべての割り当てを 1 つの連続したメモリ領域で行うことができます。

すごい!問題を解決し、talloc メモリ プールを割り当て、すべてのインスタンス化と解析作業を行い、使用mprotectしてプールを読み取り専用としてマークし、完了です! 残念ながら、それほど単純ではありません...

さらに解決すべき問題が 3 つあります。

  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 も非常に簡単です。プール内で 1 バイトを割り当てると、最初の「実際の」割り当てがどこで行われるかを予測できます。また、割り当てのアドレスからプールのアドレスを減算して、talloc がチャンク ヘッダーに使用するメモリ量を計算することもできます。

この情報を使用して、(必要に応じて) 2 番目の割り当てを実行して、プール メモリを次のページにパディングし、保護領域内で「実際の」割り当てが行われるようにします。次に、 で使用する次のページのアドレスを返すことができますmprotect。ここでのわずかな問題は、十分なメモリを確保するために、プールを 1 ページ分過剰に割り当てる必要があることです。

問題 3 は煩わしく、解決策は残念ながらアプリケーション固有です。ケース 1) ですべてのインスタンス化を実行しても副作用がなく、使用されるメモリの量が一貫している場合は、プールに割り当てるメモリの量を把握するために 2 パス アプローチを使用できます。パス 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 に答える