[インデックス 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
の呼び出し方をこの新しいヘルパー関数を使用するように変更したことです。
具体的には、以下の変更が行われています。
mmap_fixed
という静的ヘルパー関数が追加されました。この関数は、指定されたアドレスv
にメモリをマップしようと試み、もしmmap
がv
以外のアドレスを返した場合でも、そのアドレス空間がまだ利用可能であればMAP_FIXED
フラグを付けて再試行します。runtime·SysReserve
関数内で、mmap
の直接呼び出しがmmap_fixed
の呼び出しに置き換えられました。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·SysAlloc
、runtime·SysReserve
、runtime·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
が導入されました。この関数は以下のロジックで動作します。
- まず、通常の
mmap
呼び出し(MAP_FIXED
なし)を試みます。 - もし
mmap
が成功し、かつ返されたアドレスp
が要求されたアドレスv
と異なる場合、そしてaddrspace_free(v, n)
が真(つまり、v
からv+n
までのアドレス空間がまだ利用可能)であれば、以下の処理を行います。p
が有効なアドレス(p > (void*)4096
)であれば、最初にマップされた領域p
をmunmap
で解放します。これは、mmap
が別の場所をマップしてしまったため、その領域をクリーンアップするためです。- 次に、
MAP_FIXED
フラグを付けてmmap(v, ...)
を再試行します。MAP_FIXED
を使用することで、カーネルはv
に正確にマップするか、さもなければ失敗するかのどちらかになります。これにより、Goランタイムが期待するアドレスにメモリがマップされることが保証されます。
このmmap_fixed
関数をruntime·SysReserve
とruntime·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;
}
この関数がこのコミットの肝です。
- まず、引数で渡された
flags
(MAP_FIXED
を含まない可能性がある)でruntime·mmap
を呼び出します。これは、Goランタイムが通常行うmmap
の呼び出し方です。 runtime·mmap
が返したアドレスp
が、要求したアドレスv
と異なる場合、かつv
からv+n
までのアドレス空間がまだ解放されている(addrspace_free(v, n)
が真)場合に、grsec環境下での問題が発生していると判断します。- この場合、最初に
mmap
がマップしてしまった領域p
をruntime·munmap
で解放します。p > (void*)4096
というチェックは、mmap
がエラーを示すために返す可能性のある小さな負の値(例えば-1
)やNULL
を避けるためのものです。 - 最後に、元の
flags
にMAP_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·SysReserve
やruntime·SysMap
がメモリを予約・マップする際に、grsec環境下でのmmap
の特殊な挙動をmmap_fixed
が透過的に処理し、Goランタイムが常に正しいアドレスにメモリを確保できるようになりました。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Goのコードレビューシステム (Gerrit): https://go-review.googlesource.com/
- このコミットのGerritレビューページ: https://golang.org/cl/5650072
参考にした情報源リンク
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のコミット履歴と関連する議論