POSIX 仕様には関数mprotectがあります。 mprotect
メモリの個々のページのパーミッション (読み取り/書き込み/実行) を変更できます。
ヒープの一部を読み取り専用としてマークアップするために使用する際の問題mprotect
は、最高の粒度が単一ページであり、通常は 4k (OS/アーキテクチャに依存) であるという事実です。すべてのヒープに割り当てられた構造体を 4k の倍数にパディングすると、大量のメモリが膨張します。
したがって、mprotect
ケース 1) を使用するには、保護したいすべてのデータをメモリの 1 つの連続した領域で取得する必要があります。
タロックがお手伝いします。 talloc プールはスラブ割り当ての一種であり、正しく使用するとパフォーマンスが大幅に向上し、(十分なサイズがあれば) プール内のすべての割り当てを 1 つの連続したメモリ領域で行うことができます。
すごい!問題を解決し、talloc メモリ プールを割り当て、すべてのインスタンス化と解析作業を行い、使用mprotect
してプールを読み取り専用としてマークし、完了です! 残念ながら、それほど単純ではありません...
さらに解決すべき問題が 3 つあります。
mprotect
ページ サイズの倍数のメモリが必要です。
mprotect
開始アドレスをページに揃える必要があります。
- プールに割り当てるメモリの量はわかりません。
問題 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 で実行している場合は、不正な書き込みがどこにあったかを正確に示す完全なバックトレースが得られます。