[インデックス 16328] ファイルの概要
このコミットは、GoランタイムのWindows向けメモリ管理に関する変更を元に戻す(リバートする)ものです。具体的には、以前のコミット 3c2cddfbdaec
で導入された変更が、syscall.NewCallback
が実行可能コードをヒープに保存する際に問題を引き起こすことが判明したため、その変更を取り消しています。これにより、VirtualAlloc
関数呼び出しにおけるメモリページの保護フラグが PAGE_READWRITE
から PAGE_EXECUTE_READWRITE
に戻されています。
コミット
commit c15ca825ade48dd37ac024a9aae45d26fcdfb251
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri May 17 14:23:29 2013 +1000
runtime: revert 3c2cddfbdaec
It appears, syscall.NewCallback still
uses heap to store executable code.
R=golang-dev, khr
CC=golang-dev
https://golang.org/cl/9060046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c15ca825ade48dd37ac024a9aae45d26fcdfb251
元コミット内容
このコミットは、ハッシュ 3c2cddfbdaec
のコミットをリバート(元に戻す)するものです。元のコミット 3c2cddfbdaec
の具体的な内容は、このコミットメッセージからは直接読み取れませんが、このリバートコミットの目的から推測すると、おそらく VirtualAlloc
のメモリ保護フラグを PAGE_EXECUTE_READWRITE
から PAGE_READWRITE
に変更する試みであったと考えられます。これは、セキュリティ強化やメモリ使用効率の改善を目的としたものであった可能性があります。しかし、その変更が意図しない副作用を引き起こしたため、元に戻す必要が生じました。
変更の背景
この変更の背景には、Go言語の syscall.NewCallback
関数が関連しています。syscall.NewCallback
は、Goの関数をWindows APIのコールバックとして登録するために使用されます。この機能は、GoのコードからC/C++のライブラリやWindowsのシステムコールを呼び出す際に、Goの関数がコールバックとして渡される必要がある場合に重要です。
コミットメッセージによると、以前のコミット 3c2cddfbdaec
が適用された後も、syscall.NewCallback
が「実行可能コードをヒープに保存している」ことが判明しました。これは、コールバックとして使用されるGoの関数が、実行時に動的に生成されるコードとしてメモリ上に配置されることを意味します。このような実行可能コードは、そのメモリ領域が実行可能(Executable)な権限を持っている必要があります。
元のコミット 3c2cddfbdaec
が VirtualAlloc
のメモリ保護フラグを PAGE_READWRITE
に変更したと仮定すると、これはメモリ領域に書き込みと読み込みの権限のみを与え、実行権限を剥奪したことになります。しかし、syscall.NewCallback
が生成するコードは実行される必要があるため、実行権限がないメモリ領域に配置されると、プログラムはクラッシュするか、正しく動作しなくなります。
この問題が発覚したため、Goランタイムの安定性と syscall.NewCallback
の正しい動作を保証するために、3c2cddfbdaec
の変更をリバートし、メモリ領域に実行権限を再度付与する必要がありました。
前提知識の解説
Goランタイムとメモリ管理
Go言語は独自のランタイムを持っており、ガベージコレクションを含むメモリ管理を自動的に行います。Goプログラムがメモリを要求すると、ランタイムはオペレーティングシステム(OS)からメモリを確保し、それを管理します。Windows環境では、このメモリ確保にWindows APIの VirtualAlloc
関数が使用されます。
Windowsメモリ管理 (VirtualAlloc)
VirtualAlloc
は、Windowsオペレーティングシステムが提供する低レベルのメモリ管理APIです。この関数は、プロセスのアドレス空間内でメモリ領域を予約、コミット、または解放するために使用されます。VirtualAlloc
の重要な引数の一つに、確保するメモリ領域の「保護(Protection)」フラグがあります。このフラグは、そのメモリ領域に対してどのようなアクセス権限(読み込み、書き込み、実行など)を許可するかを定義します。
MEM_COMMIT
: 予約されたメモリ領域を物理ストレージ(RAMまたはページファイル)にコミットし、実際に使用可能にします。MEM_RESERVE
: プロセスのアドレス空間内にメモリ領域を予約しますが、物理ストレージは割り当てません。後でMEM_COMMIT
でコミットできます。PAGE_READWRITE
(0x0004): メモリ領域に対して読み込みと書き込みのアクセスを許可します。実行は許可しません。PAGE_EXECUTE_READWRITE
(0x40): メモリ領域に対して読み込み、書き込み、および実行のアクセスを許可します。これは、コードが動的に生成され、その場で実行されるようなシナリオ(JITコンパイラ、動的コード生成など)で必要となります。
syscall.NewCallback
Go言語の syscall
パッケージは、OSの低レベルな機能にアクセスするためのインターフェースを提供します。syscall.NewCallback
は、Goの関数ポインタをWindows APIが期待するコールバック関数ポインタに変換するために使用されます。内部的には、Goの関数を呼び出すための小さなスタブコードが動的に生成され、メモリ上に配置されます。このスタブコードは、Windows APIからの呼び出しを受け取り、Goのランタイムに制御を渡し、最終的にGoの関数を実行します。
ヒープメモリ
ヒープメモリは、プログラムが実行時に動的にメモリを確保するために使用される領域です。Goでは、make
や new
などの組み込み関数や、関数の引数や戻り値がスタックに収まらない場合にヒープが使用されます。syscall.NewCallback
が生成する実行可能コードも、このヒープ領域に配置されることがあります。
技術的詳細
このコミットの技術的詳細は、src/pkg/runtime/mem_windows.c
ファイル内の VirtualAlloc
関数の呼び出しにおけるメモリ保護フラグの変更に集約されます。
元のコミット 3c2cddfbdaec
は、おそらくセキュリティ上の理由から、メモリ領域に実行権限を与える PAGE_EXECUTE_READWRITE
フラグの使用を避け、より制限的な PAGE_READWRITE
フラグを使用しようとしました。しかし、Goランタイムの特定の機能、特に syscall.NewCallback
が動的に実行可能コードを生成し、それをヒープに配置する性質上、そのメモリ領域には実行権限が必要でした。
このコミットでは、以下の関数内で VirtualAlloc
の呼び出しにおけるメモリ保護フラグを PAGE_READWRITE
から PAGE_EXECUTE_READWRITE
に戻しています。
runtime·SysAlloc
: 新しいメモリ領域をシステムから割り当てる際に使用されます。runtime·SysReserve
: メモリ領域を予約する際に使用されます。runtime·SysMap
: 予約されたメモリ領域をコミットし、マップする際に使用されます。
これらの変更により、GoランタイムがWindows上で確保するメモリ領域が、必要に応じて実行可能コードを格納し、実行できるようになります。これは、syscall.NewCallback
のような機能が正しく動作するために不可欠です。
セキュリティの観点からは、PAGE_EXECUTE_READWRITE
は PAGE_READWRITE
よりも広い権限を与えるため、潜在的なリスクが増加します。しかし、この場合はGoランタイムの内部的な要件を満たすために必要な変更であり、Goの設計上、この種の動的コード生成は避けられないため、適切なバランスが取られています。
コアとなるコードの変更箇所
diff --git a/src/pkg/runtime/mem_windows.c b/src/pkg/runtime/mem_windows.c
index 7ac0c6aaf1..7840daa22c 100644
--- a/src/pkg/runtime/mem_windows.c
+++ b/src/pkg/runtime/mem_windows.c
@@ -13,7 +13,6 @@ enum {
MEM_RESERVE = 0x2000,
MEM_RELEASE = 0x8000,
- PAGE_READWRITE = 0x0004,
PAGE_EXECUTE_READWRITE = 0x40,
};
@@ -26,7 +25,7 @@ void*
runtime·SysAlloc(uintptr n)
{
mstats.sys += n;
- return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)(MEM_COMMIT|MEM_RESERVE), (uintptr)PAGE_READWRITE);\n+\treturn runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)(MEM_COMMIT|MEM_RESERVE), (uintptr)PAGE_EXECUTE_READWRITE);
}
void
@@ -52,12 +51,12 @@ runtime·SysReserve(void *v, uintptr n)
{
// v is just a hint.
// First try at v.
- v = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_READWRITE);\n+\tv = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_EXECUTE_READWRITE);
if(v != nil)
return v;
// Next let the kernel choose the address.
- return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_READWRITE);\n+\treturn runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_EXECUTE_READWRITE);
}
void
@@ -66,7 +65,7 @@ runtime·SysMap(void *v, uintptr n)
void *p;
mstats.sys += n;
- p = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_COMMIT, (uintptr)PAGE_READWRITE);\n+\tp = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_COMMIT, (uintptr)PAGE_EXECUTE_READWRITE);
if(p != v)
runtime·throw(\"runtime: cannot map pages in arena address space\");
}
コアとなるコードの解説
このコミットは、src/pkg/runtime/mem_windows.c
ファイルに対して行われた変更です。このファイルは、GoランタイムがWindowsオペレーティングシステム上でメモリを管理するための低レベルな関数を含んでいます。
変更の核心は、VirtualAlloc
関数を呼び出す際に渡されるメモリ保護フラグの定義と使用方法です。
-
PAGE_READWRITE
の削除:- PAGE_READWRITE = 0x0004,
この行は、
PAGE_READWRITE
という定数の定義を削除しています。これは、以前のコミットでこのフラグが使用されようとしていたことを示唆しています。このリバートにより、PAGE_READWRITE
はGoランタイムのWindowsメモリ管理コードでは直接使用されなくなります。 -
runtime·SysAlloc
の変更:- return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)(MEM_COMMIT|MEM_RESERVE), (uintptr)PAGE_READWRITE); + return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)(MEM_COMMIT|MEM_RESERVE), (uintptr)PAGE_EXECUTE_READWRITE);
runtime·SysAlloc
は、Goランタイムが新しいメモリ領域をシステムから割り当てる際に使用する関数です。ここでは、VirtualAlloc
の最後の引数であるメモリ保護フラグがPAGE_READWRITE
からPAGE_EXECUTE_READWRITE
に変更されています。これにより、新しく割り当てられるメモリ領域は、読み込み、書き込み、実行のすべての権限を持つようになります。 -
runtime·SysReserve
の変更:- v = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_READWRITE); + v = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_EXECUTE_READWRITE); // ... - return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_READWRITE); + return runtime·stdcall(runtime·VirtualAlloc, 4, nil, n, (uintptr)MEM_RESERVE, (uintptr)PAGE_EXECUTE_READWRITE);
runtime·SysReserve
は、メモリ領域を予約する際に使用されます。ここでも、VirtualAlloc
の呼び出しにおいて、予約されるメモリ領域の保護フラグがPAGE_READWRITE
からPAGE_EXECUTE_READWRITE
に変更されています。これは、予約されたメモリ領域が後でコミットされ、実行可能コードを格納する可能性があるためです。 -
runtime·SysMap
の変更:- p = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_COMMIT, (uintptr)PAGE_READWRITE); + p = runtime·stdcall(runtime·VirtualAlloc, 4, v, n, (uintptr)MEM_COMMIT, (uintptr)PAGE_EXECUTE_READWRITE);
runtime·SysMap
は、予約されたメモリ領域をコミットし、実際に使用可能にする際に使用されます。ここでも、コミットされるメモリ領域の保護フラグがPAGE_READWRITE
からPAGE_EXECUTE_READWRITE
に変更されています。
これらの変更は、GoランタイムがWindows上でメモリを確保する際に、そのメモリ領域が実行可能コードを格納し、実行できることを保証するために不可欠です。特に syscall.NewCallback
のような機能が、動的に生成されたコードをヒープに配置し、それを実行する必要があるため、この変更はGoプログラムの安定した動作に直接影響します。
関連リンク
- Go CL 9060046: https://golang.org/cl/9060046
参考にした情報源リンク
- Microsoft Docs - VirtualAlloc function: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
- Microsoft Docs - Memory Protection Constants: https://learn.microsoft.com/en-us/windows/win32/memory/memory-protection-constants
- Go Programming Language Documentation: https://go.dev/
- Go
syscall
package documentation: https://pkg.go.dev/syscall - Go runtime source code (relevant to Windows memory management): https://github.com/golang/go/tree/master/src/runtime (特に
mem_windows.go
やmem_windows.c
など)