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

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

このコミットは、Go言語のsyscallパッケージにおけるDarwin/386アーキテクチャでのGetdirentriesシステムコール呼び出しに関するバグ修正です。具体的には、Getdirentriesシステムコールの最後の引数であるbasep(ディレクトリ内のオフセットを示すポインタ)に割り当てるメモリサイズが不足していたために発生していたスタック破壊の問題を解決します。この問題は、32ビットアーキテクチャである386上で、実際には64ビットの値を扱うgetdirentries64システムコールが内部的に使用されていることに起因していました。

コミット

commit 451667a67f5b7765bb4d1d5e94e12ea1b18cfe23
Author: Rob Pike <r@golang.org>
Date:   Fri Jan 17 13:19:00 2014 -0800

    syscall: allocate 64 bits of "basep" for Getdirentries
    Recent crashes on 386 Darwin appear to be caused by this system call
    smashing the stack. Phenomenology shows that allocating more data
    here addresses the probem.
    The guess is that since the actual system call is getdirentries64, 64 is
    what we should allocate.
    
    Should fix the darwin/386 build.
    
    R=rsc
    CC=golang-codereviews
    https://golang.org/cl/53840043

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

https://github.com/golang/go/commit/451667a67f5b7765bb4d1d5e94e12ea1b18cfe23

元コミット内容

このコミットは、Go言語のsyscallパッケージにおいて、Darwinオペレーティングシステム上の386(32ビット)アーキテクチャでGetdirentriesシステムコールを呼び出す際に発生していたクラッシュを修正します。クラッシュの原因は、このシステムコールがスタックを破壊しているように見えることでした。より多くのメモリを割り当てることでこの問題が解決されることが経験的に示されました。実際のシステムコールがgetdirentries64であるため、64ビットを割り当てるべきであるという推測に基づいています。この変更により、darwin/386ビルドの問題が修正されるはずです。

変更の背景

Go言語のsyscallパッケージは、オペレーティングシステムの低レベルな機能にアクセスするためのインターフェースを提供します。ReadDirent関数は、ディレクトリの内容を読み取るために内部的にGetdirentriesシステムコールを使用しています。

問題は、Darwinオペレーティングシステム上の386(Intel 80386互換の32ビット)アーキテクチャで発生していました。この環境でGetdirentriesを呼び出すと、アプリケーションがクラッシュするという報告がありました。調査の結果、このクラッシュはシステムコールがスタックを破壊している("smashing the stack")ように見えることが判明しました。

スタック破壊は、関数がローカル変数やリターンアドレスを格納するスタック領域に、意図しないデータが書き込まれることで発生します。これは通常、バッファオーバーフローや、ポインタの誤った使用によって引き起こされます。このケースでは、Getdirentriesシステムコールの最後の引数であるbasepに割り当てられるメモリサイズが不適切であったことが原因と推測されました。

特に、32ビットシステムであるにもかかわらず、内部的に呼び出される実際のシステムコールがgetdirentries64であるという点が重要でした。getdirentries64は、ディレクトリ内のオフセットを64ビットで扱うことを想定しています。しかし、Goのsyscallパッケージでは、この引数に対して32ビットのuintptr(ポインタのサイズに合わせた整数型)を割り当てていたため、64ビットのデータが32ビットの領域に書き込まれようとし、結果としてスタックが破壊されていたと考えられます。

このコミットは、このメモリ割り当ての不一致を修正し、basepに適切な64ビットの領域を確保することで、Darwin/386環境での安定性を確保することを目的としています。

前提知識の解説

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

  1. システムコール (System Call): オペレーティングシステム(OS)のカーネルが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイルI/O、メモリ管理、プロセス制御など、OSの基本的な機能はシステムコールを通じてアクセスされます。Go言語のsyscallパッケージは、これらのシステムコールをGoプログラムから呼び出すためのラッパーを提供します。

  2. Getdirentries / getdirentries64: ディレクトリの内容(ファイルやサブディレクトリのエントリ)を読み取るためのシステムコールです。

    • getdirentries: 一般的なディレクトリ読み取りシステムコール。
    • getdirentries64: 64ビットのオフセット(ファイルシステム内の位置)を扱うことができるバージョン。特に32ビットシステム上で大きなファイルシステムを扱う際に重要になります。このコミットの文脈では、Darwin/386(32ビット)環境でも内部的にgetdirentries64が使用されており、これが問題の原因となっていました。
  3. uintptr: Go言語の組み込み型の一つで、ポインタを保持できるだけの大きさを持つ符号なし整数型です。そのサイズは、実行されているシステムのポインタサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)に依存します。このコミットの文脈では、32ビットシステム(386)ではuintptrが32ビットであるため、64ビットの値を格納するには不十分でした。

  4. unsafe.Pointer: Go言語のunsafeパッケージに含まれる特殊なポインタ型です。Goの型安全性をバイパスして、任意の型のポインタを保持したり、ポインタとuintptrの間で変換を行ったりすることができます。これは、C言語のポインタのような低レベルなメモリ操作を可能にしますが、誤用するとメモリ破壊やクラッシュを引き起こす可能性があるため、「unsafe」と名付けられています。このコミットでは、uint64型のメモリを確保し、それをuintptrとしてシステムコールに渡すためにunsafe.Pointerが使用されています。

  5. new(T): Go言語の組み込み関数で、型Tの新しいゼロ値のインスタンスを割り当て、そのインスタンスへのポインタを返します。例えば、new(uintptr)uintptr型のゼロ値が格納されたメモリ領域へのポインタを返します。

  6. スタック (Stack): プログラムの実行中に、関数呼び出しの管理やローカル変数の格納に使用されるメモリ領域です。関数が呼び出されるたびに、その関数のローカル変数や引数、リターンアドレスなどがスタックに積まれ(プッシュ)、関数が終了するとそれらがスタックから取り除かれます(ポップ)。スタックはLIFO(Last-In, First-Out)の原則で動作します。

  7. スタック破壊 (Stack Smashing): スタックに割り当てられたメモリ領域を超えてデータが書き込まれることで、スタック上の他のデータ(特にリターンアドレス)が上書きされてしまう現象です。これにより、関数からのリターン時に不正なアドレスにジャンプしたり、プログラムがクラッシュしたりします。これはセキュリティ上の脆弱性としても悪用されることがあります。

  8. Darwin/386: DarwinはmacOSの基盤となるオープンソースのUNIX系オペレーティングシステムです。386はIntel 80386互換の32ビットCPUアーキテクチャを指します。このコミットは、2014年時点でのGo言語がサポートしていた、32ビット版macOS環境での問題に対処しています。現代のmacOSは64ビットアーキテクチャに完全に移行しており、32ビットアプリケーションのサポートは終了しています。

技術的詳細

このコミットの核心は、Getdirentriesシステムコールに渡されるbasep引数のメモリ割り当てサイズが、実際のシステムコールの期待するサイズと異なっていた点にあります。

ReadDirent関数は、ディレクトリの内容を読み取るためにGetdirentriesを呼び出します。Getdirentriesの最後の引数は、basep *uintptrとコメントされており、これはディレクトリ内の次のエントリのオフセットを示すポインタです。Goのsyscallパッケージでは、この引数に対して当初new(uintptr)を使用していました。

しかし、Darwin/386環境では、Goのuintptrは32ビット幅です。一方、この環境で実際に呼び出されるカーネルのシステムコールはgetdirentries64であり、これは64ビットのオフセット値を扱います。

このミスマッチが問題を引き起こしました。

  1. new(uintptr)は32ビットのメモリ領域を割り当てます。
  2. getdirentries64システムコールは、この32ビットの領域に64ビットのオフセット値を書き込もうとします。
  3. 結果として、割り当てられた32ビットの領域を超えてデータが書き込まれ、スタック上の隣接するメモリ領域が上書きされてしまいます。これが「スタック破壊」として観測され、アプリケーションのクラッシュにつながっていました。

コミットの修正は、このbasep引数に割り当てるメモリサイズを明示的に64ビットにすることで、この問題を解決します。具体的には、new(uintptr)の代わりにnew(uint64)で64ビットのメモリを割り当て、そのポインタをunsafe.Pointerを介して*uintptr型にキャストしてGetdirentriesに渡しています。

// 変更前:
// return Getdirentries(fd, buf, new(uintptr))

// 変更後:
// var base = (*uintptr)(unsafe.Pointer(new(uint64)))
// return Getdirentries(fd, buf, base)

これにより、getdirentries64システムコールが期待する64ビットのオフセット値を安全に書き込むための十分なメモリ領域が確保され、スタック破壊が防止されます。この修正は、32ビットシステム上で64ビットの値を扱う際のポインタとメモリ割り当ての厳密な管理の重要性を示しています。

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

--- a/src/pkg/syscall/syscall_bsd.go
+++ b/src/pkg/syscall/syscall_bsd.go
@@ -64,8 +64,11 @@ func Setgroups(gids []int) (err error) {
 
 func ReadDirent(fd int, buf []byte) (n int, err error) {
 	// Final argument is (basep *uintptr) and the syscall doesn't take nil.
+	// 64 bits should be enough. (32 bits isn't even on 386). Since the
+	// actual system call is getdirentries64, 64 is a good guess.
 	// TODO(rsc): Can we use a single global basep for all calls?
-	return Getdirentries(fd, buf, new(uintptr))
+	var base = (*uintptr)(unsafe.Pointer(new(uint64)))
+	return Getdirentries(fd, buf, base)
 }
 
 // Wait status is 7 bits at bottom, either 0 (exited),

コアとなるコードの解説

変更はsrc/pkg/syscall/syscall_bsd.goファイルのReadDirent関数内で行われています。

変更前:

func ReadDirent(fd int, buf []byte) (n int, err error) {
	// Final argument is (basep *uintptr) and the syscall doesn't take nil.
	// TODO(rsc): Can we use a single global basep for all calls?
	return Getdirentries(fd, buf, new(uintptr))
}

ここでは、Getdirentriesシステムコールの最後の引数basepに対して、new(uintptr)を使ってuintptr型の新しいゼロ値へのポインタを生成し、それを渡していました。Darwin/386のような32ビットシステムでは、uintptrは32ビット幅です。

変更後:

func ReadDirent(fd int, buf []byte) (n int, err error) {
	// Final argument is (basep *uintptr) and the syscall doesn't take nil.
	// 64 bits should be enough. (32 bits isn't even on 386). Since the
	// actual system call is getdirentries64, 64 is a good guess.
	// TODO(rsc): Can we use a single global basep for all calls?
	var base = (*uintptr)(unsafe.Pointer(new(uint64)))
	return Getdirentries(fd, buf, base)
}

この変更では、以下のステップが実行されています。

  1. new(uint64): uint64型の新しいゼロ値へのポインタを生成します。uint64は常に64ビット幅であるため、これによりbasepが期待する64ビットのメモリ領域が確保されます。
  2. unsafe.Pointer(...): new(uint64)が返した*uint64型のポインタを、型安全性をバイパスして汎用ポインタであるunsafe.Pointerに変換します。
  3. (*uintptr)(...): unsafe.Pointer*uintptr型にキャストします。これは、Getdirentries関数が*uintptr型の引数を期待しているためです。このキャストにより、Goの型システム上は*uintptrとして扱われますが、その実体は64ビットのメモリ領域を指しています。
  4. var base = ...: この結果をbase変数に代入します。
  5. return Getdirentries(fd, buf, base): 最後に、適切に64ビットのメモリが割り当てられたbaseポインタをGetdirentriesに渡します。

この修正により、32ビットシステム上でもgetdirentries64システムコールが64ビットのオフセット値を安全に書き込めるようになり、スタック破壊によるクラッシュが解消されました。コメントも追加され、なぜ64ビットを割り当てる必要があるのかが明確に説明されています。

関連リンク

参考にした情報源リンク