[インデックス 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環境での安定性を確保することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
-
システムコール (System Call): オペレーティングシステム(OS)のカーネルが提供するサービスを、ユーザー空間のプログラムが利用するためのインターフェースです。ファイルI/O、メモリ管理、プロセス制御など、OSの基本的な機能はシステムコールを通じてアクセスされます。Go言語の
syscall
パッケージは、これらのシステムコールをGoプログラムから呼び出すためのラッパーを提供します。 -
Getdirentries
/getdirentries64
: ディレクトリの内容(ファイルやサブディレクトリのエントリ)を読み取るためのシステムコールです。getdirentries
: 一般的なディレクトリ読み取りシステムコール。getdirentries64
: 64ビットのオフセット(ファイルシステム内の位置)を扱うことができるバージョン。特に32ビットシステム上で大きなファイルシステムを扱う際に重要になります。このコミットの文脈では、Darwin/386(32ビット)環境でも内部的にgetdirentries64
が使用されており、これが問題の原因となっていました。
-
uintptr
: Go言語の組み込み型の一つで、ポインタを保持できるだけの大きさを持つ符号なし整数型です。そのサイズは、実行されているシステムのポインタサイズ(32ビットシステムでは32ビット、64ビットシステムでは64ビット)に依存します。このコミットの文脈では、32ビットシステム(386)ではuintptr
が32ビットであるため、64ビットの値を格納するには不十分でした。 -
unsafe.Pointer
: Go言語のunsafe
パッケージに含まれる特殊なポインタ型です。Goの型安全性をバイパスして、任意の型のポインタを保持したり、ポインタとuintptr
の間で変換を行ったりすることができます。これは、C言語のポインタのような低レベルなメモリ操作を可能にしますが、誤用するとメモリ破壊やクラッシュを引き起こす可能性があるため、「unsafe」と名付けられています。このコミットでは、uint64
型のメモリを確保し、それをuintptr
としてシステムコールに渡すためにunsafe.Pointer
が使用されています。 -
new(T)
: Go言語の組み込み関数で、型T
の新しいゼロ値のインスタンスを割り当て、そのインスタンスへのポインタを返します。例えば、new(uintptr)
はuintptr
型のゼロ値が格納されたメモリ領域へのポインタを返します。 -
スタック (Stack): プログラムの実行中に、関数呼び出しの管理やローカル変数の格納に使用されるメモリ領域です。関数が呼び出されるたびに、その関数のローカル変数や引数、リターンアドレスなどがスタックに積まれ(プッシュ)、関数が終了するとそれらがスタックから取り除かれます(ポップ)。スタックはLIFO(Last-In, First-Out)の原則で動作します。
-
スタック破壊 (Stack Smashing): スタックに割り当てられたメモリ領域を超えてデータが書き込まれることで、スタック上の他のデータ(特にリターンアドレス)が上書きされてしまう現象です。これにより、関数からのリターン時に不正なアドレスにジャンプしたり、プログラムがクラッシュしたりします。これはセキュリティ上の脆弱性としても悪用されることがあります。
-
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ビットのオフセット値を扱います。
このミスマッチが問題を引き起こしました。
new(uintptr)
は32ビットのメモリ領域を割り当てます。getdirentries64
システムコールは、この32ビットの領域に64ビットのオフセット値を書き込もうとします。- 結果として、割り当てられた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)
}
この変更では、以下のステップが実行されています。
new(uint64)
:uint64
型の新しいゼロ値へのポインタを生成します。uint64
は常に64ビット幅であるため、これによりbasep
が期待する64ビットのメモリ領域が確保されます。unsafe.Pointer(...)
:new(uint64)
が返した*uint64
型のポインタを、型安全性をバイパスして汎用ポインタであるunsafe.Pointer
に変換します。(*uintptr)(...)
:unsafe.Pointer
を*uintptr
型にキャストします。これは、Getdirentries
関数が*uintptr
型の引数を期待しているためです。このキャストにより、Goの型システム上は*uintptr
として扱われますが、その実体は64ビットのメモリ領域を指しています。var base = ...
: この結果をbase
変数に代入します。return Getdirentries(fd, buf, base)
: 最後に、適切に64ビットのメモリが割り当てられたbase
ポインタをGetdirentries
に渡します。
この修正により、32ビットシステム上でもgetdirentries64
システムコールが64ビットのオフセット値を安全に書き込めるようになり、スタック破壊によるクラッシュが解消されました。コメントも追加され、なぜ64ビットを割り当てる必要があるのかが明確に説明されています。
関連リンク
- Go CL (Code Review) リンク: https://golang.org/cl/53840043
参考にした情報源リンク
getdirentries
man page: https://www.polarhome.com/service/man/?q=getdirentries&t=a- Apple Developer Documentation -
getdirentries
: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getdirentries.2.html - Go Issue #32498:
syscall: getdirentries on 32-bit platforms needs 64-bit basep
: https://github.com/golang/go/issues/32498 - Go Issue #28984:
x/mobile/cmd/gomobile: App Store submission rejected due to use of private API ___getdirentries64
: https://github.com/golang/go/issues/28984 - Go
syscall
package source (darwin_386): https://go.googlesource.com/go/+/refs/heads/master/src/syscall/syscall_darwin_386.go - Stack Overflow -
Is macOS Catalina the last version to support 32-bit apps?
: https://stackoverflow.com/questions/58292099/is-macos-catalina-the-last-version-to-support-32-bit-apps - Wikipedia - Darwin (operating system): https://en.wikipedia.org/wiki/Darwin_(operating_system)
- Go Blog - Go 1.15 Release Notes (mentioning 32-bit macOS deprecation): https://go.dev/doc/go1.15