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

[インデックス 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 の配置によっては、特定のエラーパスでリソースが解放されないケースが発生する可能性があります。

プリペアドステートメントのライフサイクル

プリペアドステートメントは、通常以下のライフサイクルを持ちます。

  1. 準備 (Prepare): SQLクエリがデータベースに送信され、解析・コンパイルされます。データベースはステートメントハンドルを返します。
  2. 実行 (Execute): パラメータをバインドしてステートメントを実行します。
  3. クローズ (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 構造体に stmtsMadestmtsClosed という統計情報用のフィールドが追加され、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 を使用した際にステートメントが適切にクローズされることを検証します。fakeConnstmtsMadestmtsClosed の数が一致することを確認することで、リークがないことを保証します。

コアとなるコードの解説

このコミットの核心は、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() を呼び忘れた場合、ステートメントはリークしたままになります。

変更後:

  1. DB.Query から defer stmt.Close() を削除: これにより、DB.Query がリターンする際にステートメントが自動的にクローズされるのを防ぎます。
  2. エラーパスでの即時クローズ: stmt.Query がエラーを返した場合 (err != nil)、Rows オブジェクトは生成されないため、stmt.Close() をその場で呼び出すように変更されました。これにより、エラーが発生した場合でもステートメントが確実に解放されます。
  3. Rows オブジェクトへの Stmt 参照の追加: Rows 構造体に closeStmt *Stmt フィールドが追加されました。DB.Query が成功して Rows オブジェクトを返す際、rows.closeStmt = stmt という行で、この Rows オブジェクトがクローズすべき Stmt オブジェクトへの参照を保持するようになりました。
  4. Rows.Close での Stmt クローズ: Rows.Close メソッドに if rs.closeStmt != nil { rs.closeStmt.Close() } というロジックが追加されました。これにより、Rows オブジェクトがユーザーによって明示的にクローズされるか、ガベージコレクションによって最終的にクローズされる際に、関連する Stmt オブジェクトも確実にクローズされるようになります。

この変更により、ステートメントのライフサイクルが Rows オブジェクトのライフサイクルと密接に結びつくようになり、Rows が有効である間は Stmt も有効であり、Rows がクローズされるときに Stmt もクローズされるという、より堅牢なリソース管理が実現されました。これにより、ステートメントリークが防止され、MySQLなどのデータベースにおけるリソース枯渇問題が解決されました。

fakedb_test.go の変更は、この修正が正しく機能していることを検証するためのテストハーネスの強化です。stmtsMadestmtsClosed のカウンタは、テスト中に作成およびクローズされたステートメントの数を追跡し、リークがないことを数値的に確認するために使用されます。fakeStmt.Closes.closed フラグは、gosqlite3 で露呈した二重解放バグへの直接的な対処であり、既にクローズされたステートメントを再度クローズしようとしないようにすることで、堅牢性を高めています。

関連リンク

参考にした情報源リンク