Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15404] ファイルの概要

コミット

commit 5a8b7dc6d009bd95f5904b4250107ef57c163c22
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Sun Feb 24 22:47:22 2013 +0800

    runtime: remove PROT_EXEC from mmap calls.
    Executable heap is gone on Unix!
    
    R=golang-dev, dave, bradfitz
    CC=golang-dev
    https://golang.org/cl/7405045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/5a8b7dc6d009bd95f5904b4250107ef57c163c22

元コミット内容

このコミットは、GoランタイムにおけるmmapシステムコールからPROT_EXECフラグを削除することを目的としています。コミットメッセージには「Executable heap is gone on Unix!」と明記されており、これはUnix系システムにおいて実行可能なヒープメモリが廃止されたことを示唆しています。具体的には、src/pkg/runtime/mem_darwin.csrc/pkg/runtime/mem_freebsd.csrc/pkg/runtime/mem_linux.csrc/pkg/runtime/mem_netbsd.csrc/pkg/runtime/mem_openbsd.cの5つのファイルで変更が行われ、PROT_READ|PROT_WRITE|PROT_EXECPROT_READ|PROT_WRITEに置き換えられています。

変更の背景

この変更の背景には、セキュリティの強化と、現代のオペレーティングシステムにおけるメモリ管理の進化があります。かつては、JIT (Just-In-Time) コンパイルや動的なコード生成を行うアプリケーションでは、ヒープ領域に実行可能なメモリを確保することが一般的でした。しかし、これにより攻撃者が悪意のあるコードをヒープに注入し、それを実行させるという「コードインジェクション」や「Return-to-libc攻撃」などのセキュリティ脆弱性が生じる可能性がありました。

オペレーティングシステムベンダーは、このような攻撃を防ぐために、データ領域とコード領域を厳密に分離する方針を強化してきました。特に、ヒープ領域を「実行不可」とマークすることで、データとして扱われるべきメモリ領域からコードが実行されることを防ぐようになりました。Goランタイムもこのセキュリティ強化のトレンドに追随し、ヒープメモリにPROT_EXECフラグを付与する必要がなくなったため、このフラグを削除する変更が行われました。

この変更は、Go言語のセキュリティモデルを強化し、より堅牢なアプリケーションを構築するための重要なステップと言えます。

前提知識の解説

1. mmapシステムコール

mmap (memory map) は、Unix系オペレーティングシステムで利用されるシステムコールの一つで、ファイルやデバイス、または匿名メモリ領域をプロセスのアドレス空間にマッピングするために使用されます。これにより、ファイルの内容をメモリとして直接アクセスしたり、プロセス間でメモリを共有したりすることが可能になります。

mmapの主な引数は以下の通りです。

  • addr: マッピングを開始するアドレスのヒント。通常はNULLを指定し、システムに適切なアドレスを選択させます。
  • len: マッピングするバイト数。
  • prot: メモリ領域の保護(パーミッション)フラグ。
    • PROT_READ: 読み取り可能
    • PROT_WRITE: 書き込み可能
    • PROT_EXEC: 実行可能
    • PROT_NONE: アクセス不可
  • flags: マッピングの動作を制御するフラグ。
    • MAP_ANON (または MAP_ANONYMOUS): ファイルではなく匿名メモリ領域をマッピングします。
    • MAP_PRIVATE: プライベートなコピーオンライトマッピングを作成します。元のファイルや他のプロセスとの変更は共有されません。
    • MAP_SHARED: 共有マッピングを作成します。元のファイルや他のプロセスとの変更が共有されます。
    • MAP_FIXED: addrで指定されたアドレスに厳密にマッピングを試みます。
  • fd: マッピングするファイルのファイルディスクリプタ。匿名マッピングの場合は-1
  • offset: ファイル内のマッピング開始オフセット。

2. PROT_EXECフラグ

PROT_EXECは、mmapシステムコールでメモリ領域に実行権限を与えるための保護フラグです。このフラグが設定されたメモリ領域は、CPUが命令として解釈し、実行することができます。

3. 実行可能なヒープ (Executable Heap)

ヒープは、プログラムが実行時に動的にメモリを確保するために使用される領域です。通常、ヒープはデータ格納のために使用され、実行可能なコードを格納するようには設計されていません。しかし、JITコンパイラや一部の動的コード生成技術では、実行時に生成されたコードをヒープに配置し、それを実行する必要がありました。このようなヒープ領域を「実行可能なヒープ」と呼びます。

4. DEP (Data Execution Prevention) / NX bit (No-Execute bit)

DEP(データ実行防止)またはNX bit(No-Execute bit)は、CPUレベルで実装されたセキュリティ機能です。これは、データ領域としてマークされたメモリページからのコード実行を防止します。これにより、バッファオーバーフローなどの脆弱性を悪用して、攻撃者がデータ領域に注入した悪意のあるコードを実行することを困難にします。現代のほとんどのオペレーティングシステムとCPUは、この機能をサポートし、デフォルトで有効にしています。

技術的詳細

このコミットの技術的な核心は、Goランタイムがメモリを確保する際に使用するmmapシステムコールのprot引数からPROT_EXECフラグを削除した点にあります。

Goランタイムは、ガベージコレクションやスケジューラなどの内部処理のために、独自のメモリ管理メカニズムを持っています。このメモリ管理の一部として、runtime·SysAlloc(新しいメモリ領域をシステムから割り当てる)やruntime·SysMap(既存のメモリ領域をプロセスのアドレス空間にマッピングする)といった関数がmmapシステムコールを呼び出しています。

変更前は、これらのmmap呼び出しにおいて、PROT_READ|PROT_WRITE|PROT_EXECというフラグが設定されていました。これは、割り当てられたメモリ領域が読み取り、書き込み、そして実行可能であることを意味します。しかし、前述の通り、現代のUnix系OSではセキュリティ上の理由から、ヒープ領域に実行権限を与えることは推奨されません。

このコミットでは、PROT_EXECフラグが削除され、PROT_READ|PROT_WRITEのみが設定されるようになりました。これにより、Goランタイムが確保するヒープメモリは、デフォルトで実行不可となります。

この変更は、GoランタイムがJITコンパイルや動的なコード生成を必要としない、あるいはそのための特別なメモリ領域を別途確保するようになったことを示唆しています。Go言語のコンパイラは、通常、事前にコンパイルされたバイナリを生成するため、実行時にヒープ上でコードを生成して実行する必要性は低いと考えられます。もし実行時にコードを生成する必要がある場合でも、それは特定の目的のために明示的に実行可能なメモリ領域を確保する形で行われるべきであり、一般的なヒープ領域が実行可能である必要はありません。

この変更は、Goプログラムのセキュリティを向上させるだけでなく、オペレーティングシステムのメモリ保護機能との整合性を高めることにも貢献しています。

コアとなるコードの変更箇所

変更は、Goランタイムのメモリ管理に関連する以下の5つのファイルで行われています。

  • src/pkg/runtime/mem_darwin.c (macOS)
  • src/pkg/runtime/mem_freebsd.c (FreeBSD)
  • src/pkg/runtime/mem_linux.c (Linux)
  • src/pkg/runtime/mem_netbsd.c (NetBSD)
  • src/pkg/runtime/mem_openbsd.c (OpenBSD)

これらのファイルでは、runtime·mmap関数の呼び出しにおいて、prot引数からPROT_EXECが削除されています。

例: src/pkg/runtime/mem_darwin.c の変更

--- a/src/pkg/runtime/mem_darwin.c
+++ b/src/pkg/runtime/mem_darwin.c
@@ -14,7 +14,7 @@ runtime·SysAlloc(uintptr n)
 	void *v;
 
 	mstats.sys += n;
-	v = runtime·mmap(nil, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);
+	v = runtime·mmap(nil, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
 	if(v < (void*)4096)
 		return nil;
 	return v;
@@ -51,7 +51,7 @@ runtime·SysMap(void *v, uintptr n)
 	void *p;
 	
 	mstats.sys += n;
-	p = runtime·mmap(v, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
+	p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
 	if(p == (void*)-ENOMEM)
 		runtime·throw("runtime: out of memory");
 	if(p != v)

同様の変更が他のUnix系OS向けのファイルでも行われています。

コアとなるコードの解説

上記のコードスニペットは、Goランタイムがシステムからメモリを割り当てる際のmmapシステムコールの呼び出しを示しています。

  • runtime·SysAlloc(uintptr n): この関数は、指定されたサイズnの新しいメモリ領域をシステムから割り当てます。

    • 変更前: v = runtime·mmap(nil, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);
    • 変更後: v = runtime·mmap(nil, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
    • ここで、PROT_EXECフラグが削除されています。MAP_ANONは匿名メモリ(ファイルに関連付けられていないメモリ)を意味し、MAP_PRIVATEはそのマッピングがプロセスにプライベートであることを意味します。
  • runtime·SysMap(void *v, uintptr n): この関数は、既存のメモリ領域vを、指定されたサイズnでプロセスのアドレス空間にマッピングします。これは、Goランタイムが既に確保したメモリ領域を再マッピングする際などに使用されます。

    • 変更前: p = runtime·mmap(v, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
    • 変更後: p = runtime·mmap(v, n, PROT_READ|PROT_WRITE, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
    • ここでもPROT_EXECフラグが削除されています。MAP_FIXEDは、vで指定されたアドレスに厳密にマッピングを試みることを意味します。

これらの変更により、GoランタイムがUnix系システムで確保するメモリ領域は、デフォルトで実行権限を持たなくなりました。これは、データ実行防止(DEP)などのセキュリティメカニズムと整合性が取れており、悪意のあるコードがヒープ領域から実行されるリスクを低減します。

関連リンク

参考にした情報源リンク