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

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

このコミットは、GoランタイムにおけるLinuxカーネルのセキュリティ拡張であるgrsec (grsecurity) のサポートを修正するものです。具体的には、mmapシステムコールが要求されたアドレスを返さない場合に発生する問題を解決し、Goランタイムのメモリ管理がgrsecパッチが適用されたカーネルでも正しく機能するようにします。

コミット

commit 8eee153bc81680a6115dc8e1f2661ee51d5c7383
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Tue Feb 14 22:09:02 2012 -0200

    runtime: fix grsec support
    
    Changeset 36c9c7810f14 broke support for grsec-patched kernels.
    Those do not give back the address requested without MAP_FIXED,
    so when verifying an mmap without this flag for success, the
    resulting address must not be compared against the requested
    address since it may have succeeded at a different location.
    
    R=golang-dev, rsc, gustavo, iant
    CC=golang-dev
    https://golang.org/cl/5650072

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

https://github.com/golang/go/commit/8eee153bc81680a6115dc8e1f2661ee51d5c7383

元コミット内容

このコミットは、src/pkg/runtime/mem_linux.c ファイルに対して変更を加えています。主な変更点は、mmap_fixed という新しいヘルパー関数の導入と、既存の runtime·SysReserve および runtime·SysMap 関数における mmap の呼び出し方をこの新しいヘルパー関数を使用するように変更したことです。

具体的には、以下の変更が行われています。

  1. mmap_fixed という静的ヘルパー関数が追加されました。この関数は、指定されたアドレス v にメモリをマップしようと試み、もし mmapv 以外のアドレスを返した場合でも、そのアドレス空間がまだ利用可能であれば MAP_FIXED フラグを付けて再試行します。
  2. runtime·SysReserve 関数内で、mmap の直接呼び出しが mmap_fixed の呼び出しに置き換えられました。
  3. runtime·SysMap 関数内で、mmap の直接呼び出しが mmap_fixed の呼び出しに置き換えられました。特に、64-bitシステムにおける特定の条件下のmmap呼び出しが変更されています。

変更の背景

このコミットの背景には、以前のコミット 36c9c7810f14 がgrsecurity (grsec) パッチが適用されたLinuxカーネルでのGoランタイムの動作を壊したという問題があります。

grsecurityは、Linuxカーネルに高度なセキュリティ機能を追加するパッチセットです。その機能の一つに、メモリ割り当てに関する厳格なポリシーがあります。通常、mmapシステムコールは、要求されたアドレス(ヒントアドレス)を渡すことができますが、カーネルはそのアドレスを保証しません。つまり、要求されたアドレスが利用できない場合、mmapは別の利用可能なアドレスを返します。

しかし、grsecパッチが適用されたカーネルでは、MAP_FIXEDフラグなしでmmapを呼び出した場合、要求されたアドレスを「返さない」という挙動を示すことがあります。Goランタイムは、mmapが要求されたアドレスを返したかどうかをチェックすることで、メモリ領域が正しく予約またはマップされたかを検証していました。grsec環境下では、MAP_FIXEDなしでmmapが成功しても、要求アドレスとは異なるアドレスが返されるため、Goランタイムの検証ロジックが誤って失敗と判断し、結果としてGoプログラムが正常に動作しないという問題が発生していました。

このコミットは、このgrsec環境下でのmmapの挙動の違いに対応し、Goランタイムが正しくメモリを管理できるようにするために導入されました。

前提知識の解説

このコミットを理解するためには、以下の概念について知っておく必要があります。

  • grsecurity (grsec): Linuxカーネルのセキュリティ強化パッチセットです。メモリ保護、権限昇格の防止、ファイルシステムアクセス制御など、様々なセキュリティ機能を提供します。特に、メモリ割り当てに関する挙動が標準のLinuxカーネルとは異なる場合があります。
  • mmapシステムコール: Unix系OSで利用されるシステムコールで、ファイルやデバイス、または匿名メモリ領域をプロセスのアドレス空間にマップするために使用されます。
    • void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
    • addr: マップしたいアドレスのヒント。NULLの場合、カーネルが適切なアドレスを選択します。
    • length: マップするバイト数。
    • prot: メモリ領域の保護(読み取り、書き込み、実行など)。
      • PROT_NONE: アクセス不可。
      • PROT_READ: 読み取り可能。
      • PROT_WRITE: 書き込み可能。
      • PROT_EXEC: 実行可能。
    • flags: マップの挙動を制御するフラグ。
      • MAP_ANON (または MAP_ANONYMOUS): ファイルではなく、匿名メモリ領域をマップします。
      • MAP_PRIVATE: マップされた領域への書き込みは、呼び出し元プロセスにのみ見え、基になるファイルや他のプロセスには影響しません(コピーオンライト)。
      • MAP_FIXED: addrで指定されたアドレスに正確にマップすることを要求します。もしそのアドレスが利用できない場合、mmapは失敗します。このフラグがない場合、addrは単なるヒントとして扱われ、カーネルは別のアドレスを返すことがあります。
  • munmapシステムコール: mmapによってマップされたメモリ領域をアンマップ(解放)するために使用されます。
    • int munmap(void *addr, size_t length);
  • Goランタイムのメモリ管理: Go言語は独自のランタイムを持っており、その中でメモリの確保、解放、ガベージコレクションなどを行います。runtime·SysAllocruntime·SysReserveruntime·SysMapは、GoランタイムがOSからメモリを要求したり、マップしたりするための内部的な抽象化関数です。
    • runtime·SysAlloc: OSからメモリを割り当てます。
    • runtime·SysReserve: 将来使用するためにアドレス空間を予約しますが、まだ物理メモリは割り当てません。
    • runtime·SysMap: 予約されたアドレス空間に物理メモリをマップし、アクセス可能にします。
  • addrspace_free: Goランタイムの内部関数で、指定されたアドレス範囲が現在利用可能(解放されている)かどうかをチェックします。

技術的詳細

このコミットの技術的な核心は、mmapシステムコールの挙動が、MAP_FIXEDフラグの有無とカーネルのパッチ(特にgrsec)によって異なるという点にあります。

標準的なLinuxカーネルでは、MAP_FIXEDなしでmmap(v, ...)を呼び出した場合、vがヒントとして使われ、もしvが利用可能であればそのアドレスが返されることが期待されます。しかし、grsecパッチが適用されたカーネルでは、MAP_FIXEDなしではvがヒントとしてほとんど無視され、mmapが成功してもvとは異なるアドレスが返されることが頻繁にあります。

Goランタイムの既存のロジックでは、mmapの戻り値が要求されたアドレスvと一致するかどうかをチェックしていました。一致しない場合は、mmapが失敗したと判断していました。このロジックは、grsec環境下ではmmapが成功しているにもかかわらず、誤って失敗と判断してしまう原因となっていました。

この問題を解決するために、新しいヘルパー関数mmap_fixedが導入されました。この関数は以下のロジックで動作します。

  1. まず、通常のmmap呼び出し(MAP_FIXEDなし)を試みます。
  2. もしmmapが成功し、かつ返されたアドレスpが要求されたアドレスvと異なる場合、そしてaddrspace_free(v, n)が真(つまり、vからv+nまでのアドレス空間がまだ利用可能)であれば、以下の処理を行います。
    • pが有効なアドレス(p > (void*)4096)であれば、最初にマップされた領域pmunmapで解放します。これは、mmapが別の場所をマップしてしまったため、その領域をクリーンアップするためです。
    • 次に、MAP_FIXEDフラグを付けてmmap(v, ...)を再試行します。MAP_FIXEDを使用することで、カーネルはvに正確にマップするか、さもなければ失敗するかのどちらかになります。これにより、Goランタイムが期待するアドレスにメモリがマップされることが保証されます。

このmmap_fixed関数をruntime·SysReserveruntime·SysMapで使用することで、Goランタイムはgrsec環境下でもmmapの挙動の違いを吸収し、正しくメモリを予約・マップできるようになりました。

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

変更は src/pkg/runtime/mem_linux.c ファイルに集中しています。

--- a/src/pkg/runtime/mem_linux.c
+++ b/src/pkg/runtime/mem_linux.c
@@ -34,6 +34,21 @@ addrspace_free(void *v, uintptr n)
 	return 1;
 }
 
+static void *
+mmap_fixed(byte *v, uintptr n, int32 prot, int32 flags, int32 fd, uint32 offset)
+{
+	void *p;
+
+	p = runtime·mmap(v, n, prot, flags, fd, offset);
+	if(p != v && addrspace_free(v, n)) {
+		// On some systems, mmap ignores v without
+		// MAP_FIXED, so retry if the address space is free.
+		if(p > (void*)4096)
+			runtime·munmap(p, n);
+		p = runtime·mmap(v, n, prot, flags|MAP_FIXED, fd, offset);
+	}
+	return p;
+}
 
 void*
 runtime·SysAlloc(uintptr n)
@@ -76,20 +91,16 @@ runtime·SysReserve(void *v, uintptr n)
 	// if we can reserve at least 64K and check the assumption in SysMap.
 	// Only user-mode Linux (UML) rejects these requests.
 	if(sizeof(void*) == 8 && (uintptr)v >= 0xffffffffU) {
-		p = runtime·mmap(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
-		if (p != v) {
+		p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
+		if (p != v)
 			return nil;
-		}
 		runtime·munmap(p, 64<<10);
-		
-		
 		return v;
 	}
 	
 	p = runtime·mmap(v, n, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
-	if((uintptr)p < 4096 || -(uintptr)p < 4096) {
+	if((uintptr)p < 4096 || -(uintptr)p < 4096)
 		return nil;
-	}
 	return p;
 }
 
@@ -102,15 +113,7 @@ runtime·SysMap(void *v, uintptr n)
 
 	// On 64-bit, we don't actually have v reserved, so tread carefully.
 	if(sizeof(void*) == 8 && (uintptr)v >= 0xffffffffU) {
-		p = runtime·mmap(v, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);
-		if(p != v && addrspace_free(v, n)) {
-			// On some systems, mmap ignores v without
-			// MAP_FIXED, so retry if the address space is free.
-			if(p > (void*)4096) {
-				runtime·munmap(p, n);
-			}
-			p = runtime·mmap(v, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_FIXED|MAP_PRIVATE, -1, 0);
-		}
+		p = mmap_fixed(v, n, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);
 		if(p == (void*)ENOMEM)
 			runtime·throw("runtime: out of memory");
 		if(p != v) {

コアとなるコードの解説

mmap_fixed 関数

static void *
mmap_fixed(byte *v, uintptr n, int32 prot, int32 flags, int32 fd, uint32 offset)
{
	void *p;

	p = runtime·mmap(v, n, prot, flags, fd, offset); // 1. 最初にMAP_FIXEDなしでmmapを試行
	if(p != v && addrspace_free(v, n)) { // 2. 返されたアドレスが要求と異なり、かつアドレス空間が利用可能なら
		// On some systems, mmap ignores v without
		// MAP_FIXED, so retry if the address space is free.
		if(p > (void*)4096) // 3. マップされたアドレスが有効なら解放
			runtime·munmap(p, n);
		p = runtime·mmap(v, n, prot, flags|MAP_FIXED, fd, offset); // 4. MAP_FIXEDを付けて再試行
	}
	return p;
}

この関数がこのコミットの肝です。

  1. まず、引数で渡されたflagsMAP_FIXEDを含まない可能性がある)でruntime·mmapを呼び出します。これは、Goランタイムが通常行うmmapの呼び出し方です。
  2. runtime·mmapが返したアドレスpが、要求したアドレスvと異なる場合、かつvからv+nまでのアドレス空間がまだ解放されている(addrspace_free(v, n)が真)場合に、grsec環境下での問題が発生していると判断します。
  3. この場合、最初にmmapがマップしてしまった領域pruntime·munmapで解放します。p > (void*)4096というチェックは、mmapがエラーを示すために返す可能性のある小さな負の値(例えば-1)やNULLを避けるためのものです。
  4. 最後に、元のflagsMAP_FIXEDを追加してruntime·mmapを再試行します。これにより、カーネルはvに正確にマップするか、失敗するかのどちらかになり、Goランタイムの期待する挙動が保証されます。

runtime·SysReserve および runtime·SysMap での利用

以前は、これらの関数内で直接runtime·mmapが呼び出され、その戻り値がvと一致するかどうかで成功を判断していました。このコミットでは、その直接のruntime·mmap呼び出しがmmap_fixedの呼び出しに置き換えられました。

例えば、runtime·SysReserveの変更箇所は以下のようになります。

// 変更前
// p = runtime·mmap(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
// if (p != v) {
//     return nil;
// }

// 変更後
p = mmap_fixed(v, 64<<10, PROT_NONE, MAP_ANON|MAP_PRIVATE, -1, 0);
if (p != v)
    return nil;

これにより、runtime·SysReserveruntime·SysMapがメモリを予約・マップする際に、grsec環境下でのmmapの特殊な挙動をmmap_fixedが透過的に処理し、Goランタイムが常に正しいアドレスにメモリを確保できるようになりました。

関連リンク

参考にした情報源リンク

  • mmap(2) man page: https://man7.org/linux/man-pages/man2/mmap.2.html
  • grsecurity: https://grsecurity.net/ (現在は一般公開されていませんが、過去の情報は参照可能です)
  • Linuxカーネルのメモリ管理に関する一般的な情報源 (例: LWN.netの記事など)
  • Goランタイムのメモリ管理に関するドキュメントやブログ記事 (Goの内部構造に関する深い理解が必要なため、公式ドキュメントやGoのソースコード自体が最も信頼できる情報源となります)
  • Goのコミット履歴と関連する議論