[インデックス 11160] ファイルの概要
このコミットは、Go言語の実験的なSQLパッケージ (exp/sql
) におけるステートメントリークの修正を目的としています。このリークは、特にMySQLのようなデータベースにおいてリソース枯渇の問題を引き起こしていました。また、この修正は gosqlite3
ドライバーにおける二重解放バグも露呈させました。
コミット
exp/sql: fix statement leak
Also verified in external test suite that this fixes MySQL
resource exhaustion problems, and also exposed a double-free
bug in the gosqlite3 driver (where gosqlite3 either got lucky
before, or was working around this bug)
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5544057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1c441e259f66bc2594cb8b0a95bf6cc0847e2bd8
元コミット内容
commit 1c441e259f66bc2594cb8b0a95bf6cc0847e2bd8
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Jan 13 15:25:07 2012 -0800
exp/sql: fix statement leak
Also verified in external test suite that this fixes MySQL
resource exhaustion problems, and also exposed a double-free
bug in the gosqlite3 driver (where gosqlite3 either got lucky
before, or was working around this bug)
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5544057
変更の背景
このコミットの主な背景は、Go言語の exp/sql
パッケージ(後の database/sql
パッケージの原型)において、データベースステートメントが適切にクローズされないことによるリソースリークが発生していたことです。特に、Query
メソッドがエラーを返した場合に、内部で作成されたプリペアドステートメントが閉じられずに残り、データベースサーバー側のリソース(カーソル、ハンドルなど)を消費し続ける問題がありました。
このようなステートメントリークは、アプリケーションがデータベースに対して多数のクエリを実行する際に、データベースサーバーのリソースを枯渇させ、最終的には新しい接続やクエリの処理を妨げる原因となります。コミットメッセージにもあるように、MySQL環境でこの問題が顕著に現れ、リソース枯渇を引き起こしていました。
また、この修正の過程で、gosqlite3
ドライバーにおいて二重解放(double-free)のバグが偶然露呈しました。これは、ステートメントが複数回クローズされようとした際に発生する可能性のある深刻なバグであり、メモリ破損やクラッシュにつながる可能性があります。このコミットは、exp/sql
パッケージ自体のリークを修正するだけでなく、関連するドライバーの潜在的な問題も明らかにするという副次的な効果ももたらしました。
前提知識の解説
Go言語の database/sql
パッケージ (当時は exp/sql
)
Go言語の database/sql
パッケージは、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバーを含まず、データベース固有の操作はドライバーによって実装されます。アプリケーションは database/sql
のAPIを通じてデータベースと対話することで、ドライバーの実装詳細から抽象化されます。
主要な概念:
DB
: データベースへの接続プールを表します。Conn
: データベースへの単一の接続を表します。Stmt
: プリペアドステートメントを表します。SQLクエリを事前に準備(プリコンパイル)することで、繰り返し実行する際のパフォーマンスを向上させ、SQLインジェクション攻撃を防ぐのに役立ちます。Rows
: クエリ結果の行セットを表します。結果をイテレートし、各行のデータをスキャンするために使用されます。Tx
: トランザクションを表します。
データベースリソースの管理
データベースとのやり取りでは、接続(Connection)、ステートメント(Statement)、結果セット(ResultSet/Rows)といったリソースが使用されます。これらのリソースは、使用後に適切に解放(クローズ)される必要があります。解放を怠ると、データベースサーバー側のリソースが消費され続け、最終的にはリソース枯渇を引き起こし、アプリケーションやデータベースのパフォーマンス低下、さらにはサービス停止につながる可能性があります。
defer
ステートメント
Go言語の defer
ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールします。リソースの解放(ファイルクローズ、ロック解除など)を確実に行うためによく使用されます。しかし、このコミットが示すように、defer
の配置によっては、特定のエラーパスでリソースが解放されないケースが発生する可能性があります。
プリペアドステートメントのライフサイクル
プリペアドステートメントは、通常以下のライフサイクルを持ちます。
- 準備 (Prepare): SQLクエリがデータベースに送信され、解析・コンパイルされます。データベースはステートメントハンドルを返します。
- 実行 (Execute): パラメータをバインドしてステートメントを実行します。
- クローズ (Close): ステートメントハンドルを解放し、データベース側のリソースを解放します。
この「クローズ」が適切に行われないと、データベースサーバー上に不要なステートメントが残り続け、リソースリークとなります。
二重解放 (Double-Free)
二重解放は、既に解放されたメモリ領域やリソースを再度解放しようとすることで発生するプログラミングエラーです。これは未定義動作を引き起こし、メモリ破損、プログラムのクラッシュ、またはセキュリティ脆弱性につながる可能性があります。
技術的詳細
このコミットが修正しようとしている問題は、database/sql
パッケージの Query
メソッドが、プリペアドステートメントを内部的に作成し、そのステートメントを defer stmt.Close()
でクローズしようとしていた点にあります。
元のコードでは、db.Query
メソッド内で stmt.Close()
が defer
されていました。
// src/pkg/exp/sql/sql.go (変更前)
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
stmt, err := db.Prepare(query)
if err != nil {
return nil, err
}
defer stmt.Close() // ここが問題
return stmt.Query(args...)
}
この defer stmt.Close()
の問題点は、stmt.Query(args...)
がエラーを返した場合でも stmt.Close()
が実行されることです。一見すると問題ないように見えますが、stmt.Query
が成功して *Rows
オブジェクトを返した場合、その Rows
オブジェクトがクローズされる際に、関連するステートメントもクローズされるべきです。しかし、Query
メソッド内で defer
された stmt.Close()
は、Rows
オブジェクトがまだ使用中であるにもかかわらず、Query
メソッドがリターンする際にステートメントをクローズしてしまう可能性がありました。
より深刻なのは、stmt.Query
がエラーを返した場合です。この場合、Rows
オブジェクトは返されず、defer stmt.Close()
が実行されます。これはステートメントをクローズしますが、もし Rows
オブジェクトが正常に返されたにもかかわらず、その Rows
オブジェクトが適切にクローズされなかった場合(例えば、ユーザーが rows.Close()
を呼び忘れた場合)、ステートメントは開いたままになり、リークが発生します。
このコミットの修正は、ステートメントのクローズの責任を Rows
オブジェクトに移すことで、この問題を解決しています。つまり、Query
メソッドが Rows
オブジェクトを返す場合、その Rows
オブジェクトが最終的にクローズされるときに、関連するステートメントもクローズされるように変更されました。これにより、Rows
オブジェクトのライフサイクルとステートメントのライフサイクルが同期され、リークが防止されます。
また、stmt.Query
がエラーを返した場合は、Rows
オブジェクトが生成されないため、Query
メソッド内で即座に stmt.Close()
を呼び出すように変更されました。これにより、エラーパスでもステートメントが確実にクローズされるようになります。
gosqlite3
ドライバーの二重解放バグが露呈した件については、fakeStmt.Close()
メソッドに s.closed
フラグを追加し、既にクローズされているステートメントを再度クローズしようとしないようにすることで対処されています。これは、exp/sql
パッケージの修正によって、gosqlite3
ドライバーが以前は遭遇しなかったようなステートメントのクローズパターンに直面した結果、既存のバグが顕在化したことを示唆しています。
コアとなるコードの変更箇所
src/pkg/exp/sql/fakedb_test.go
fakeConn
構造体にstmtsMade
とstmtsClosed
という統計情報用のフィールドが追加され、incrStat
ヘルパーメソッドが導入されました。これはテスト目的で、作成されたステートメントとクローズされたステートメントの数を追跡するために使用されます。fakeConn.Prepare
メソッドでステートメントが作成される際にc.incrStat(&c.stmtsMade)
が呼び出されるようになりました。fakeStmt.Close
メソッドにs.closed
フラグによるチェックが追加され、ステートメントが既にクローズされている場合は再度クローズ処理を行わないようになりました。これにより、二重解放を防ぎます。
src/pkg/exp/sql/sql.go
DB.Query
メソッドの変更:- 元の
defer stmt.Close()
が削除されました。 stmt.Query(args...)
の呼び出し結果をrows, err := ...
で受け取るようになりました。err != nil
の場合、つまりstmt.Query
がエラーを返した場合は、stmt.Close()
を即座に呼び出すようになりました。rows.closeStmt = stmt
という行が追加され、Rows
オブジェクトがそのライフサイクル中にクローズすべきStmt
オブジェクトへの参照を持つようになりました。
- 元の
Rows
構造体にcloseStmt *Stmt
フィールドが追加されました。これは、Rows
がクローズされる際にクローズすべきStmt
オブジェクトへのポインタを保持します。Rows.Close
メソッドの変更:rs.closed
フラグによるチェックが追加されました。if rs.closeStmt != nil { rs.closeStmt.Close() }
という行が追加され、Rows
がクローズされる際に、関連するStmt
オブジェクトもクローズされるようになりました。
src/pkg/exp/sql/sql_test.go
TestQueryRowClosingStmt
という新しいテストケースが追加されました。このテストは、QueryRow
を使用した際にステートメントが適切にクローズされることを検証します。fakeConn
のstmtsMade
とstmtsClosed
の数が一致することを確認することで、リークがないことを保証します。
コアとなるコードの解説
このコミットの核心は、DB.Query
メソッドと Rows.Close
メソッドにおけるステートメントのライフサイクル管理の変更です。
変更前:
DB.Query
メソッドは、内部で Prepare
を呼び出して Stmt
を作成し、その Stmt
に対して Query
を実行していました。そして、defer stmt.Close()
を使用して、DB.Query
関数が終了する際に Stmt
をクローズしようとしていました。
このアプローチの問題点は、stmt.Query
が成功して *Rows
オブジェクトを返した場合に、Rows
オブジェクトがまだアクティブであるにもかかわらず、DB.Query
がリターンすると defer
によって Stmt
がクローズされてしまう可能性があったことです。これは、Rows
オブジェクトが後でデータを読み取ろうとした際に、既にクローズされた Stmt
にアクセスしようとしてエラーになるか、あるいは Stmt
が意図せず早くクローズされてしまうという問題を引き起こす可能性がありました。
さらに、stmt.Query
がエラーを返した場合、Rows
オブジェクトは生成されません。この場合、defer stmt.Close()
は実行されますが、もし Rows
オブジェクトが正常に返されたパスでユーザーが rows.Close()
を呼び忘れた場合、ステートメントはリークしたままになります。
変更後:
DB.Query
からdefer stmt.Close()
を削除: これにより、DB.Query
がリターンする際にステートメントが自動的にクローズされるのを防ぎます。- エラーパスでの即時クローズ:
stmt.Query
がエラーを返した場合 (err != nil
)、Rows
オブジェクトは生成されないため、stmt.Close()
をその場で呼び出すように変更されました。これにより、エラーが発生した場合でもステートメントが確実に解放されます。 Rows
オブジェクトへのStmt
参照の追加:Rows
構造体にcloseStmt *Stmt
フィールドが追加されました。DB.Query
が成功してRows
オブジェクトを返す際、rows.closeStmt = stmt
という行で、このRows
オブジェクトがクローズすべきStmt
オブジェクトへの参照を保持するようになりました。Rows.Close
でのStmt
クローズ:Rows.Close
メソッドにif rs.closeStmt != nil { rs.closeStmt.Close() }
というロジックが追加されました。これにより、Rows
オブジェクトがユーザーによって明示的にクローズされるか、ガベージコレクションによって最終的にクローズされる際に、関連するStmt
オブジェクトも確実にクローズされるようになります。
この変更により、ステートメントのライフサイクルが Rows
オブジェクトのライフサイクルと密接に結びつくようになり、Rows
が有効である間は Stmt
も有効であり、Rows
がクローズされるときに Stmt
もクローズされるという、より堅牢なリソース管理が実現されました。これにより、ステートメントリークが防止され、MySQLなどのデータベースにおけるリソース枯渇問題が解決されました。
fakedb_test.go
の変更は、この修正が正しく機能していることを検証するためのテストハーネスの強化です。stmtsMade
と stmtsClosed
のカウンタは、テスト中に作成およびクローズされたステートメントの数を追跡し、リークがないことを数値的に確認するために使用されます。fakeStmt.Close
の s.closed
フラグは、gosqlite3
で露呈した二重解放バグへの直接的な対処であり、既にクローズされたステートメントを再度クローズしようとしないようにすることで、堅牢性を高めています。
関連リンク
- Go CL 5544057: https://golang.org/cl/5544057
参考にした情報源リンク
- Go言語
database/sql
パッケージのドキュメント (当時のexp/sql
に相当する概念): https://pkg.go.dev/database/sql - Go言語における
defer
の使用法: https://go.dev/blog/defer-panic-recover - データベースリソース管理のベストプラクティス (一般的な概念): https://www.ibm.com/docs/en/db2/11.5?topic=applications-resource-management
- 二重解放の脆弱性に関する一般的な情報: https://cwe.mitre.org/data/definitions/416.html