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

[インデックス 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)な権限を持っている必要があります。

元のコミット 3c2cddfbdaecVirtualAlloc のメモリ保護フラグを 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では、makenew などの組み込み関数や、関数の引数や戻り値がスタックに収まらない場合にヒープが使用されます。syscall.NewCallback が生成する実行可能コードも、このヒープ領域に配置されることがあります。

技術的詳細

このコミットの技術的詳細は、src/pkg/runtime/mem_windows.c ファイル内の VirtualAlloc 関数の呼び出しにおけるメモリ保護フラグの変更に集約されます。

元のコミット 3c2cddfbdaec は、おそらくセキュリティ上の理由から、メモリ領域に実行権限を与える PAGE_EXECUTE_READWRITE フラグの使用を避け、より制限的な PAGE_READWRITE フラグを使用しようとしました。しかし、Goランタイムの特定の機能、特に syscall.NewCallback が動的に実行可能コードを生成し、それをヒープに配置する性質上、そのメモリ領域には実行権限が必要でした。

このコミットでは、以下の関数内で VirtualAlloc の呼び出しにおけるメモリ保護フラグを PAGE_READWRITE から PAGE_EXECUTE_READWRITE に戻しています。

  1. runtime·SysAlloc: 新しいメモリ領域をシステムから割り当てる際に使用されます。
  2. runtime·SysReserve: メモリ領域を予約する際に使用されます。
  3. runtime·SysMap: 予約されたメモリ領域をコミットし、マップする際に使用されます。

これらの変更により、GoランタイムがWindows上で確保するメモリ領域が、必要に応じて実行可能コードを格納し、実行できるようになります。これは、syscall.NewCallback のような機能が正しく動作するために不可欠です。

セキュリティの観点からは、PAGE_EXECUTE_READWRITEPAGE_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 関数を呼び出す際に渡されるメモリ保護フラグの定義と使用方法です。

  1. PAGE_READWRITE の削除:

    - 	PAGE_READWRITE = 0x0004,
    

    この行は、PAGE_READWRITE という定数の定義を削除しています。これは、以前のコミットでこのフラグが使用されようとしていたことを示唆しています。このリバートにより、PAGE_READWRITE はGoランタイムのWindowsメモリ管理コードでは直接使用されなくなります。

  2. 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 に変更されています。これにより、新しく割り当てられるメモリ領域は、読み込み、書き込み、実行のすべての権限を持つようになります。

  3. 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 に変更されています。これは、予約されたメモリ領域が後でコミットされ、実行可能コードを格納する可能性があるためです。

  4. 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プログラムの安定した動作に直接影響します。

関連リンク

参考にした情報源リンク