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

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

このコミットは、Go言語の標準ライブラリであるbytesパッケージ内のMap関数におけるパニック(panic)を修正するものです。具体的には、utf8.RuneLen関数が不正なルーン(rune)に対して-1を返した場合に、内部バッファの拡張が適切に行われず、結果としてパニックが発生する問題を解決しています。この修正により、不正なルーンがbytes.Map関数によって処理される際に、Unicodeの置換文字である\uFFFD(U+FFFD)に適切に変換され、バッファオーバーフローによるパニックが回避されるようになります。

コミット

commit 1a21dbc5720326b0e325a54c3e01c0e50b32eb03
Author: Rui Ueyama <ruiu@google.com>
Date:   Tue Mar 18 20:52:58 2014 -0700

    bytes: fix panic in Map
    
    utf8.RuneLen returns -1 for an invalid rune. In that case we
    need to extend the internal buffer at least by 3 for \uFFFD.
    
    Fixes #7577.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/77420044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/1a21dbc5720326b0e325a54c3e01c0e50b32eb03

元コミット内容

bytes: fix panic in Map

utf8.RuneLenは不正なルーンに対して-1を返します。その場合、\uFFFDのために内部バッファを少なくとも3バイト拡張する必要があります。

Fixes #7577.

変更の背景

このコミットは、Go言語のbytesパッケージにあるMap関数が、不正なUTF-8シーケンスを含むバイトスライスを処理する際にパニックを引き起こすバグを修正するために行われました。

Go言語では、文字列はUTF-8でエンコードされたバイトスライスとして扱われます。bytes.Map関数は、入力バイトスライス内の各ルーン(Unicodeコードポイント)に対してユーザー定義のマッピング関数を適用し、その結果を新しいバイトスライスとして返します。

問題は、マッピング関数が不正なルーン(例えば、utf8.MaxRune + 1のような範囲外のルーン)を返した場合に発生しました。Goのunicode/utf8パッケージのRuneLen関数は、有効なルーンのUTF-8エンコードに必要なバイト数を返しますが、不正なルーンに対しては-1を返します。

bytes.Map関数は、内部で新しいバイトスライスを構築する際に、各ルーンのエンコードに必要なバイト数に基づいてバッファのサイズを計算していました。しかし、RuneLen-1を返した場合、この計算が負の値となり、結果としてmake関数に不正なサイズが渡され、パニックが発生していました。

このバグは、GoのIssueトラッカーでIssue #7577として報告されていました。このコミットは、その問題を解決することを目的としています。

前提知識の解説

  • Go言語のrunebyte:
    • Go言語において、byteは8ビットのバイトを表し、uint8のエイリアスです。
    • runeはUnicodeコードポイントを表し、int32のエイリアスです。Goの文字列はUTF-8でエンコードされたバイトのシーケンスであり、rangeループで文字列をイテレートすると、各要素はruneとして取得されます。
  • UTF-8エンコーディング:
    • UTF-8はUnicode文字を可変長のバイトシーケンスで表現するエンコーディング方式です。ASCII文字は1バイトで表現され、他の文字は2バイトから4バイトで表現されます。
    • 不正なUTF-8シーケンスは、有効なUnicode文字にデコードできないバイトシーケンスを指します。
  • unicode/utf8パッケージ:
    • Goの標準ライブラリであるunicode/utf8パッケージは、UTF-8エンコードされたテキストを操作するための関数を提供します。
    • utf8.RuneLen(r rune) int: この関数は、与えられたルーンrをUTF-8でエンコードした場合のバイト数を返します。もしルーンrが有効なUnicodeコードポイントの範囲外(0 <= r <= unicode.MaxRune)である場合、この関数は-1を返します。
    • utf8.RuneError (\uFFFD): これはUnicodeの置換文字(Replacement Character)U+FFFDを表すルーンです。不正な文字やデコードできない文字の代わりに表示されます。UTF-8エンコードでは通常3バイト(0xEF 0xBF 0xBD)を占めます。
  • bytes.Map関数:
    • func Map(mapping func(r rune) rune, s []byte) []byte: この関数は、バイトスライスsをルーンのシーケンスとして解釈し、各ルーンにmapping関数を適用します。mapping関数が返したルーンをUTF-8エンコードし、その結果を新しいバイトスライスとして返します。

技術的詳細

bytes.Map関数は、入力バイトスライスsをイテレートし、各UTF-8エンコードされたルーンをデコードします。デコードされたルーンはmapping関数に渡され、その結果として新しいルーンrが返されます。このrを再度UTF-8エンコードして、結果のバイトスライスに追加します。

問題の核心は、mapping関数がutf8.MaxRune + 1のような不正なルーンを返した場合にありました。 元のコードでは、新しいルーンrのUTF-8エンコード長をutf8.RuneLen(r)で取得し、その長さを既存のバッファサイズnbytesに加算して、バッファが最大サイズmaxbytesを超えるかどうかをチェックしていました。

// 変更前
if nbytes+utf8.RuneLen(r) > maxbytes {
    // Grow the buffer.
    // ...
}

ここで、utf8.RuneLen(r)が不正なルーンに対して-1を返すと、nbytes + (-1)という計算が行われ、バッファサイズが実際よりも小さく見積もられる可能性がありました。さらに深刻なのは、この負の値がバッファの再割り当てロジックに影響を与え、最終的にmake関数に不正なサイズが渡されてパニックを引き起こす可能性があったことです。

修正では、utf8.RuneLen(r)の戻り値をrlという変数に格納し、rl-1であるかどうかを明示的にチェックします。もしrl-1であれば、それは不正なルーンであることを意味するため、そのルーンはutf8.RuneError\uFFFD)に置き換えられると仮定し、そのエンコード長である3バイト(len(string(utf8.RuneError)))をrlに設定します。

// 変更後
rl := utf8.RuneLen(r)
if rl < 0 {
    rl = len(string(utf8.RuneError)) // \uFFFD is 3 bytes
}
if nbytes+rl > maxbytes {
    // Grow the buffer.
    // ...
}

これにより、不正なルーンが返された場合でも、バッファの拡張に必要なバイト数が正しく計算され、パニックが回避されます。\uFFFDはUTF-8で3バイトを占めるため、少なくとも3バイトのバッファ拡張が必要であるというコミットメッセージの記述と一致します。

また、テストケースTestMapに、マッピング関数がutf8.MaxRune + 1を返す場合のテストが追加されました。このテストは、bytes.Mapがパニックを起こさずに\uFFFDを返すことを検証します。

コアとなるコードの変更箇所

src/pkg/bytes/bytes.go

--- a/src/pkg/bytes/bytes.go
+++ b/src/pkg/bytes/bytes.go
@@ -356,7 +356,11 @@ func Map(mapping func(r rune) rune, s []byte) []byte {
 		}
 		r = mapping(r)
 		if r >= 0 {
-			if nbytes+utf8.RuneLen(r) > maxbytes {
+			rl := utf8.RuneLen(r)
+			if rl < 0 {
+				rl = len(string(utf8.RuneError))
+			}
+			if nbytes+rl > maxbytes {
 				// Grow the buffer.
 				maxbytes = maxbytes*2 + utf8.UTFMax
 				nb := make([]byte, maxbytes)

src/pkg/bytes/bytes_test.go

--- a/src/pkg/bytes/bytes_test.go
+++ b/src/pkg/bytes/bytes_test.go
@@ -785,6 +785,16 @@ func TestMap(t *testing.T) {\n 	if string(m) != expect {\n 		t.Errorf(\"drop: expected %q got %q\", expect, m)\n 	}\n+\n+\t// 6. Invalid rune
+\tinvalidRune := func(r rune) rune {\n+\t\treturn utf8.MaxRune + 1\n+\t}\n+\tm = Map(invalidRune, []byte(\"x\"))\n+\texpect = \"\\uFFFD\"\n+\tif string(m) != expect {\n+\t\tt.Errorf(\"invalidRune: expected %q got %q\", expect, m)\n+\t}\n }\n \n func TestToUpper(t *testing.T) { runStringTests(t, ToUpper, \"ToUpper\", upperTests) }\

コアとなるコードの解説

src/pkg/bytes/bytes.go の変更

  • 変更前: if nbytes+utf8.RuneLen(r) > maxbytes {
    • ここでは、マッピング関数から返されたルーンrのUTF-8エンコード長を直接utf8.RuneLen(r)で取得し、それをバッファの現在の長さnbytesに加算していました。
    • もしrが不正なルーン(例: utf8.MaxRune + 1)であった場合、utf8.RuneLen(r)-1を返します。この-1nbytesに加算されると、バッファの必要なサイズが過小評価され、結果としてバッファの再割り当てが適切に行われず、後続の書き込みでパニックを引き起こす可能性がありました。
  • 変更後:
    rl := utf8.RuneLen(r)
    if rl < 0 {
        rl = len(string(utf8.RuneError))
    }
    if nbytes+rl > maxbytes {
    
    1. rl := utf8.RuneLen(r): まず、ルーンrのエンコード長をrl変数に格納します。
    2. if rl < 0 { rl = len(string(utf8.RuneError)) }: rlが負の値(つまり-1)である場合、それはrが不正なルーンであることを意味します。この場合、GoのUTF-8デコーダは通常、不正なシーケンスを\uFFFD(Unicode置換文字)に置き換えます。\uFFFDはUTF-8で3バイトでエンコードされるため、rllen(string(utf8.RuneError))(これは3を返す)に設定し直します。これにより、不正なルーンが返された場合でも、バッファの拡張に必要な最小限のサイズ(3バイト)が確保されるようになります。
    3. if nbytes+rl > maxbytes {: 修正されたrlの値を使用して、バッファのサイズチェックと拡張ロジックが実行されます。これにより、不正なルーンが返された場合でも、バッファが適切に拡張され、パニックが回避されます。

src/pkg/bytes/bytes_test.go の変更

  • 新しいテストケースの追加:
    • invalidRuneという匿名関数が定義されています。この関数は、入力されたルーンrに関わらず、常にutf8.MaxRune + 1という不正なルーンを返します。
    • m = Map(invalidRune, []byte("x"))bytes.Map関数にこのinvalidRuneマッピング関数と、任意の入力バイトスライス(ここでは"x")を渡して呼び出します。
    • expect = "\\uFFFD":期待される結果は、入力ルーンが不正なルーンにマッピングされた結果として、\uFFFD(Unicode置換文字)が返されることです。
    • if string(m) != expect { t.Errorf(...) }: 実際の出力mが期待される\uFFFDと一致するかどうかを検証します。このテストケースは、bytes.Mapが不正なルーンを適切に処理し、パニックを起こさずに\uFFFDを返すことを保証します。

これらの変更により、bytes.Map関数はより堅牢になり、不正なルーンの処理においても安定して動作するようになりました。

関連リンク

参考にした情報源リンク