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

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

このコミットは、Go言語の net パッケージにおける resolv.conf ファイルのオプション解析に関するバグ修正です。具体的には、resolv.conf のオプション(ndotstimeoutattemptsなど)を解析する際に、文字列スライスの範囲外アクセスが発生する可能性があった問題を修正しています。この修正により、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)、検索ドメイン(domainsearch)、そして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]: スライス slow インデックスから high-1 インデックスまでの要素を含む新しいスライスを作成します。この操作では、0 <= low <= high <= len(s) という条件が満たされている必要があります。この条件が満たされない場合、Goランタイムはパニック(実行時エラー)を発生させます。

今回のバグは、s[0:N] のようなスライス操作を行う際に、len(s)N よりも小さい場合に発生する「スライス範囲外アクセス」に起因していました。

文字列のプレフィックスチェック

文字列が特定のプレフィックス(接頭辞)で始まるかどうかをチェックする一般的な方法は、文字列の先頭部分をスライスして、その部分が目的のプレフィックスと一致するかどうかを比較することです。しかし、この方法では、スライスする長さが元の文字列の長さよりも長い場合に、前述のスライス範囲外アクセスが発生する可能性があります。安全なプレフィックスチェックには、まず文字列の長さがプレフィックスの長さ以上であることを確認する必要があります。

技術的詳細

このコミットの技術的な核心は、src/pkg/net/dnsconfig_unix.go ファイル内の dnsReadConfig 関数における resolv.conf オプションの解析ロジックの改善です。

元のコードでは、resolv.confoptions 行を解析する際に、各オプション文字列 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) >= 8true となりますが、続く 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 関数は、文字列 sprefix で始まるかどうかを安全にチェックします。重要なのは、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 パッケージ内で直接利用できる形ではなかったか、あるいは特定の最適化や依存関係の管理のために内部で定義されたものと考えられます。

この関数のロジックは非常にシンプルかつ効果的です。

  1. len(s) >= len(prefix): まず、対象の文字列 s の長さが、チェックしたいプレフィックス prefix の長さ以上であるかをチェックします。このチェックが false の場合、sprefix で始まることはありえないため、すぐに false を返します。これにより、後続のスライス操作での範囲外アクセスが完全に防止されます。
  2. 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.confoptions 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システムに関する一般的な知識に基づいて作成されました。コミットメッセージに記載された直接のリンクは、現在のシステムでは解決できませんでした。