[インデックス 19736] ファイルの概要
このコミットは、Go言語の net
パッケージにおける resolv.conf
ファイルのオプション解析に関するバグ修正です。具体的には、resolv.conf
のオプション(ndots
、timeout
、attempts
など)を解析する際に、文字列スライスの範囲外アクセスが発生する可能性があった問題を修正しています。この修正により、resolv.conf
の不正な形式の行が原因でプログラムがクラッシュする(パニックを起こす)ことを防ぎ、堅牢性が向上しました。
コミット
commit 0a5cb7dc49263ff63e09dfca27df5888e55aeeba
Author: Jakob Borg <jakob@nym.se>
Date: Tue Jul 15 14:49:26 2014 +1000
net: Don't read beyond end of slice when parsing resolv.conf options.
Fixes #8252.
LGTM=adg
R=ruiu, josharian, adg
CC=golang-codereviews
https://golang.org/cl/102470046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a5cb7dc49263ff63e09dfca27df5888e55aeeba
元コミット内容
net: Don't read beyond end of slice when parsing resolv.conf options.
Fixes #8252.
LGTM=adg
R=ruiu, josharian, adg
CC=golang-codereviews
https://golang.org/cl/102470046
変更の背景
このコミットの背景には、Go言語の net
パッケージがUnix系システムでDNSリゾルバの設定を読み込む際に使用する /etc/resolv.conf
ファイルの解析における潜在的な脆弱性がありました。
resolv.conf
には、DNSクエリの動作を制御するための様々なオプションが記述されます。例えば、ndots
はドットの数がいくつ以上であれば完全修飾ドメイン名とみなすか、timeout
はDNSクエリのタイムアウト時間、attempts
はリトライ回数などを指定します。
元のコードでは、これらのオプションを解析する際に、文字列のプレフィックスをチェックするために s[0:N]
のようなスライス操作を使用していました。しかし、このスライス操作を行う前に、文字列 s
の長さが N
以上であることを常に適切に確認していませんでした。特に attempts:
オプションのチェックにおいて、プレフィックスの長さが9文字であるにもかかわらず、文字列の長さチェックが8文字以上で行われていたため、文字列の長さが8文字の場合に s[0:9]
のスライス操作が範囲外アクセス(panic: runtime error: slice bounds out of range
)を引き起こす可能性がありました。
このような範囲外アクセスは、サービス拒否(DoS)攻撃につながる可能性があり、悪意のある、または不正な resolv.conf
ファイルがシステムに存在する場合に、Goアプリケーションがクラッシュする原因となります。このコミットは、この潜在的なクラッシュを防ぎ、resolv.conf
の解析処理の堅牢性を高めることを目的としています。
前提知識の解説
/etc/resolv.conf
/etc/resolv.conf
は、Unix系オペレーティングシステムにおいて、DNS(Domain Name System)リゾルバの設定を記述するファイルです。このファイルには、DNSサーバーのIPアドレス(nameserver
)、検索ドメイン(domain
やsearch
)、そしてDNSクエリの動作を制御する様々なオプション(options
)が含まれます。
一般的な options
の例:
ndots:N
: ホスト名にN個以上のドットが含まれていれば、完全修飾ドメイン名として扱います。それ以下の場合、search
ディレクティブで指定されたドメインが追加されて検索されます。timeout:N
: DNSクエリのタイムアウト時間をN秒に設定します。attempts:N
: DNSクエリのリトライ回数をN回に設定します。
Go言語のスライス (Slice)
Go言語のスライスは、配列の一部を参照する軽量なデータ構造です。スライスは、基となる配列へのポインタ、長さ(len
)、容量(cap
)の3つの要素で構成されます。
len(s)
: スライスs
の現在の要素数を返します。s[low:high]
: スライスs
のlow
インデックスからhigh-1
インデックスまでの要素を含む新しいスライスを作成します。この操作では、0 <= low <= high <= len(s)
という条件が満たされている必要があります。この条件が満たされない場合、Goランタイムはパニック(実行時エラー)を発生させます。
今回のバグは、s[0:N]
のようなスライス操作を行う際に、len(s)
が N
よりも小さい場合に発生する「スライス範囲外アクセス」に起因していました。
文字列のプレフィックスチェック
文字列が特定のプレフィックス(接頭辞)で始まるかどうかをチェックする一般的な方法は、文字列の先頭部分をスライスして、その部分が目的のプレフィックスと一致するかどうかを比較することです。しかし、この方法では、スライスする長さが元の文字列の長さよりも長い場合に、前述のスライス範囲外アクセスが発生する可能性があります。安全なプレフィックスチェックには、まず文字列の長さがプレフィックスの長さ以上であることを確認する必要があります。
技術的詳細
このコミットの技術的な核心は、src/pkg/net/dnsconfig_unix.go
ファイル内の dnsReadConfig
関数における resolv.conf
オプションの解析ロジックの改善です。
元のコードでは、resolv.conf
の options
行を解析する際に、各オプション文字列 s
が特定のプレフィックスで始まるかどうかを以下のようにチェックしていました。
// 例: ndotsオプションのチェック
case len(s) >= 6 && s[0:6] == "ndots:":
// 例: timeoutオプションのチェック
case len(s) >= 8 && s[0:8] == "timeout:":
// 例: attemptsオプションのチェック
case len(s) >= 8 && s[0:9] == "attempts:": // ここが問題
ここで問題となったのは、attempts:
オプションのチェックです。プレフィックス "attempts:"
は9文字の長さですが、その前の長さチェックが len(s) >= 8
となっていました。
もし s
の長さが8文字(例: "attempts"
)であった場合、len(s) >= 8
は true
となりますが、続く s[0:9]
のスライス操作は s
の範囲(0から7)を超えてインデックス8にアクセスしようとするため、panic: runtime error: slice bounds out of range
が発生します。
この問題を解決するために、新しいヘルパー関数 hasPrefix
が導入されました。
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
この hasPrefix
関数は、文字列 s
が prefix
で始まるかどうかを安全にチェックします。重要なのは、s[:len(prefix)]
のスライス操作を行う前に、len(s) >= len(prefix)
という条件を明示的にチェックしている点です。これにより、スライス操作が常に有効な範囲内で行われることが保証されます。
元のコードの各プレフィックスチェックは、この hasPrefix
関数を使用するように変更されました。これにより、resolv.conf
のオプション文字列がプレフィックスの長さよりも短い場合でも、安全に処理され、パニックが回避されるようになりました。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の通りです。
src/pkg/net/dnsconfig_unix.go
src/pkg/net/testdata/resolv.conf
src/pkg/net/dnsconfig_unix.go
の変更点
--- a/src/pkg/net/dnsconfig_unix.go
+++ b/src/pkg/net/dnsconfig_unix.go
@@ -75,19 +75,19 @@ func dnsReadConfig(filename string) (*dnsConfig, error) {
for i := 1; i < len(f); i++ {
s := f[i]
switch {
- case len(s) >= 6 && s[0:6] == "ndots:":
+ case hasPrefix(s, "ndots:"):// ndots: のチェックを hasPrefix に変更
n, _, _ := dtoi(s, 6)
if n < 1 {
n = 1
}
conf.ndots = n
- case len(s) >= 8 && s[0:8] == "timeout:":
+ case hasPrefix(s, "timeout:"):// timeout: のチェックを hasPrefix に変更
n, _, _ := dtoi(s, 8)
if n < 1 {
n = 1
}
conf.timeout = n
- case len(s) >= 8 && s[0:9] == "attempts:":
+ case hasPrefix(s, "attempts:"):// attempts: のチェックを hasPrefix に変更
n, _, _ := dtoi(s, 9)
if n < 1 {
n = 1
@@ -103,3 +103,7 @@ func dnsReadConfig(filename string) (*dnsConfig, error) {
return conf, nil
}
+
+func hasPrefix(s, prefix string) bool {
+ return len(s) >= len(prefix) && s[:len(prefix)] == prefix
+}
src/pkg/net/testdata/resolv.conf
の変更点
--- a/src/pkg/net/testdata/resolv.conf
+++ b/src/pkg/net/testdata/resolv.conf
@@ -3,3 +3,4 @@
domain Home
nameserver 192.168.1.1
options ndots:5 timeout:10 attempts:3 rotate
+options attempts 3 // 新しいテストケースを追加
コアとなるコードの解説
hasPrefix
関数の導入
このコミットの最も重要な変更は、hasPrefix
という新しいヘルパー関数が dnsconfig_unix.go
に追加されたことです。
func hasPrefix(s, prefix string) bool {
return len(s) >= len(prefix) && s[:len(prefix)] == prefix
}
この関数は、Go言語の標準ライブラリ strings.HasPrefix
と同様の機能を提供しますが、このコミットの時点では net
パッケージ内で直接利用できる形ではなかったか、あるいは特定の最適化や依存関係の管理のために内部で定義されたものと考えられます。
この関数のロジックは非常にシンプルかつ効果的です。
len(s) >= len(prefix)
: まず、対象の文字列s
の長さが、チェックしたいプレフィックスprefix
の長さ以上であるかをチェックします。このチェックがfalse
の場合、s
はprefix
で始まることはありえないため、すぐにfalse
を返します。これにより、後続のスライス操作での範囲外アクセスが完全に防止されます。s[:len(prefix)] == prefix
: 最初の条件がtrue
の場合のみ、s
の先頭からprefix
の長さ分の部分スライスを作成し、それがprefix
と完全に一致するかどうかを比較します。
オプション解析ロジックの変更
dnsReadConfig
関数内の switch
ステートメントにおいて、ndots:
、timeout:
、attempts:
の各オプションのプレフィックスチェックが、直接的なスライス比較から hasPrefix
関数を呼び出す形に変更されました。
例えば、元の attempts:
のチェックは以下のようでした。
case len(s) >= 8 && s[0:9] == "attempts:":
これが、以下のように変更されました。
case hasPrefix(s, "attempts:"):// attempts: のチェックを hasPrefix に変更
この変更により、attempts:
のプレフィックス(9文字)に対して、元のコードで誤って len(s) >= 8
となっていた長さチェックが、hasPrefix
関数内で len(s) >= 9
と正しく評価されるようになり、文字列の長さが8文字の場合に発生していたパニックが解消されました。
テストデータの追加
src/pkg/net/testdata/resolv.conf
に options attempts 3
という行が追加されました。これは、attempts
オプションの解析が正しく行われることを確認するためのテストケースです。特に、この行は attempts
の後にスペースが入り、その後に値が続く形式であり、元のバグが顕在化しやすかったケースをカバーしていると考えられます。このテストケースの追加により、修正が意図通りに機能し、将来のリグレッションを防ぐための安全網が提供されます。
関連リンク
- コミットメッセージに記載されているGo issue #8252:
Fixes #8252.
- 注記: このissue番号は、現在のGoのGitHubリポジトリのissueトラッカーでは直接見つけることができませんでした。コミットが2014年のものであるため、当時のissueトラッキングシステムや番号付けが現在とは異なる可能性があります。
- コミットメッセージに記載されているGerrit Change-ID:
https://golang.org/cl/102470046
- 注記: このGerrit CL番号も、現在のGoのコードレビューシステム(go-review.googlesource.com)では直接解決できませんでした。上記と同様に、システム変更の影響が考えられます。
参考にした情報源リンク
- Go言語の公式ドキュメント(スライス、文字列操作など)
/etc/resolv.conf
に関する一般的なUnix/Linuxドキュメント- この解説は、提供されたコミットデータ(コミットメッセージとdiff)およびGo言語とUnixシステムに関する一般的な知識に基づいて作成されました。コミットメッセージに記載された直接のリンクは、現在のシステムでは解決できませんでした。