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

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

このコミットは、Go言語のunicode/utf8パッケージにおいて、UTF-8デコード関数が範囲外のルーン(Unicodeコードポイント)を拒否するように修正するものです。特に、MaxRune(Unicodeの最大有効コードポイントであるU+10FFFF)を超える値が不正なルーンとして扱われるようになります。また、サロゲートペアの扱いについても言及されており、関連する議論がgolang-devメーリングリストで行われていることが示唆されています。

コミット

commit fc360f238137717e7246cc0fde908b71a3f1e5c7
Author: Rob Pike <r@golang.org>
Date:   Thu Jul 19 11:58:14 2012 -0700

    unicode/utf8: reject out-of-range runes.
    Surrogates are still admitted, but I have sent mail to golang-dev on that topic.
    
    Fixes #3785.
    
    R=golang-dev, rogpeppe, iant
    CC=golang-dev
    https://golang.org/cl/6398049

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

https://github.com/golang/go/commit/fc360f238137717e7246cc0fde908b71a3f1e5c7

元コミット内容

このコミットは、Go言語の標準ライブラリであるunicode/utf8パッケージにおけるUTF-8デコード処理の改善を目的としています。具体的には、デコードされたルーンがUnicodeの有効な範囲(U+0000からU+10FFFF)を超えている場合に、それを不正なルーンとして扱うように変更が加えられました。コミットメッセージでは、サロゲートペア(U+D800からU+DFFF)については引き続き許容されるものの、この点についてはgolang-devメーリングリストで議論が開始されたことが述べられています。この変更は、Issue #3785を修正するものです。

変更の背景

この変更の背景には、Go言語のunicode/utf8パッケージが、本来UTF-8として不正であるべき「範囲外のルーン」をデコードしてしまうという問題がありました。Issue #3785「unicode/utf8: decoding functions accept out-of-range input」で報告されたように、utf8.DecodeRuneのような関数が、Unicodeの有効なコードポイント範囲外の値を誤って受け入れてしまうことが指摘されていました。特に、Unicodeのサロゲートペア(U+D800-U+DFFF)は、UTF-8では直接エンコードされるべきではないにもかかわらず、デコードされてしまうケースがありました。

この問題は、GoがUTF-8の厳密な仕様に準拠し、不正な入力を適切に処理することを保証するために重要でした。不正なルーンを拒否することで、アプリケーションが予期せぬデータやセキュリティ上の脆弱性に晒されるリスクを低減し、より堅牢なUTF-8処理を実現することが目的です。

前提知識の解説

UTF-8 (Unicode Transformation Format - 8-bit)

UTF-8は、Unicode文字を可変長バイトシーケンスでエンコードするための文字エンコーディング方式です。ASCII文字は1バイトで表現され、それ以外の文字は2バイトから4バイトで表現されます。UTF-8は、インターネット上で最も広く使用されている文字エンコーディングであり、後方互換性、効率性、多言語対応のバランスが取れています。

Unicode

Unicodeは、世界中のあらゆる文字を統一的に扱うための文字コード標準です。各文字には一意の「コードポイント」が割り当てられます。コードポイントは通常、U+XXXXの形式で表記され、XXXXは16進数です。

ルーン (Rune)

Go言語において、「ルーン」はUnicodeのコードポイントを表すために使用される用語です。Goの組み込み型runeは、実際にはint32のエイリアスであり、単一のUnicodeコードポイントを保持します。

コードポイント (Code Point)

Unicodeにおける文字の抽象的な数値表現です。例えば、文字'A'のコードポイントはU+0041です。Unicodeの有効なコードポイントの範囲はU+0000からU+10FFFFまでです。

サロゲートペア (Surrogate Pairs)

UnicodeのU+D800からU+DFFFの範囲は、サロゲートコードポイントとして予約されています。これらは単独では有効な文字を表さず、UTF-16エンコーディングにおいて、基本多言語面(BMP、U+0000からU+FFFF)外の文字(U+10000からU+10FFFF)を表現するために2つのコードポイントのペア(サロゲートペア)として使用されます。UTF-8では、サロゲートコードポイントは直接エンコードされるべきではありません。UTF-8でサロゲートコードポイントをエンコードしようとすると、それは不正なUTF-8シーケンスとみなされます。

RuneError

Go言語のunicode/utf8パッケージでは、不正なUTF-8シーケンスをデコードしようとした場合に、RuneErrorという特別なルーン(通常はU+FFFD、REPLACEMENT CHARACTER)と、そのシーケンスの幅(通常は1バイト)を返します。これは、不正な入力を安全に処理するためのメカニズムです。

MaxRune

Go言語のunicode/utf8パッケージで定義されている定数で、Unicodeの最大有効コードポイントであるU+10FFFFを表します。

技術的詳細

このコミットの主要な変更は、src/pkg/unicode/utf8/utf8.go内のdecodeRuneInternalおよびdecodeRuneInStringInternal関数の修正です。これらの関数は、UTF-8バイトシーケンスをルーンにデコードする内部ヘルパー関数です。

変更前は、4バイトのUTF-8シーケンスをデコードする際に、デコードされたルーンrrune3Max(3バイトUTF-8シーケンスで表現できる最大値、U+FFFF)以下である場合にのみRuneErrorを返していました。これは、4バイトシーケンスが実際には3バイト以下で表現できる値である場合に、最短形式ではないため不正とみなすためのチェックでした。

変更後、この条件に|| MaxRune < rが追加されました。これにより、デコードされたルーンrMaxRune(U+10FFFF)を超える場合も、不正なルーンとしてRuneErrorが返されるようになりました。これは、Unicodeの有効なコードポイント範囲外の値を明示的に拒否するための重要な変更です。

また、DecodeRune, DecodeRuneInString, DecodeLastRune, DecodeLastRuneInStringといった公開APIのコメントが更新され、不正なエンコーディングの定義に「encodes a rune that is out of range」(範囲外のルーンをエンコードしている場合)が追加されました。これにより、これらの関数がどのような場合にRuneErrorを返すのかがより明確になりました。

src/pkg/unicode/utf8/utf8_test.goには、この変更を検証するための新しいテストケースが追加されました。 {string("\\xFB\\xBF\\xBF\\xBF\\xBF"), false}, // 0x3FFFFFF; out of range このテストケースは、MaxRuneを超える値(0x3FFFFFF)が不正なUTF-8シーケンスとして正しく認識されることを確認します。

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

diff --git a/src/pkg/unicode/utf8/utf8.go b/src/pkg/unicode/utf8/utf8.go
index 57ea19e96d..cd9c80c5a5 100644
--- a/src/pkg/unicode/utf8/utf8.go
+++ b/src/pkg/unicode/utf8/utf8.go
@@ -102,7 +102,7 @@ func decodeRuneInternal(p []byte) (r rune, size int, short bool) {
 	// 4-byte, 21-bit sequence?
 	if c0 < t5 {
 		r = rune(c0&mask4)<<18 | rune(c1&maskx)<<12 | rune(c2&maskx)<<6 | rune(c3&maskx)
-		if r <= rune3Max {
+		if r <= rune3Max || MaxRune < r {
 			return RuneError, 1, false
 		}
 		return r, 4, false
@@ -177,7 +177,7 @@ func decodeRuneInStringInternal(s string) (r rune, size int, short bool) {
 	// 4-byte, 21-bit sequence?
 	if c0 < t5 {
 		r = rune(c0&mask4)<<18 | rune(c1&maskx)<<12 | rune(c2&maskx)<<6 | rune(c3&maskx)
-		if r <= rune3Max {
+		if r <= rune3Max || MaxRune < r {
 			return RuneError, 1, false
 		}
 		return r, 4, false
@@ -202,6 +202,9 @@ func FullRuneInString(s string) bool {
 
 // DecodeRune unpacks the first UTF-8 encoding in p and returns the rune and its width in bytes.
 // If the encoding is invalid, it returns (RuneError, 1), an impossible result for correct UTF-8.
+// An encoding is invalid if it is incorrect UTF-8, encodes a rune that is
+// out of range, or is not the shortest possible UTF-8 encoding for the
+// value. No other validation is performed.
 func DecodeRune(p []byte) (r rune, size int) {
 	r, size, _ = decodeRuneInternal(p)
 	return
@@ -209,6 +212,9 @@ func DecodeRune(p []byte) (r rune, size int) {
 
 // DecodeRuneInString is like DecodeRune but its input is a string.
 // If the encoding is invalid, it returns (RuneError, 1), an impossible result for correct UTF-8.
+// An encoding is invalid if it is incorrect UTF-8, encodes a rune that is
+// out of range, or is not the shortest possible UTF-8 encoding for the
+// value. No other validation is performed.
 func DecodeRuneInString(s string) (r rune, size int) {
 	r, size, _ = decodeRuneInStringInternal(s)
 	return
@@ -216,6 +222,9 @@ func DecodeRuneInString(s string) (r rune, size int) {
 
 // DecodeLastRune unpacks the last UTF-8 encoding in p and returns the rune and its width in bytes.
 // If the encoding is invalid, it returns (RuneError, 1), an impossible result for correct UTF-8.
+// An encoding is invalid if it is incorrect UTF-8, encodes a rune that is
+// out of range, or is not the shortest possible UTF-8 encoding for the
+// value. No other validation is performed.
 func DecodeLastRune(p []byte) (r rune, size int) {
 	end := len(p)
 	if end == 0 {
@@ -250,6 +259,9 @@ func DecodeLastRune(p []byte) (r rune, size int) {
 
 // DecodeLastRuneInString is like DecodeLastRune but its input is a string.
 // If the encoding is invalid, it returns (RuneError, 1), an impossible result for correct UTF-8.
+// An encoding is invalid if it is incorrect UTF-8, encodes a rune that is
+// out of range, or is not the shortest possible UTF-8 encoding for the
+// value. No other validation is performed.
 func DecodeLastRuneInString(s string) (r rune, size int) {
 	end := len(s)
 	if end == 0 {
diff --git a/src/pkg/unicode/utf8/utf8_test.go b/src/pkg/unicode/utf8/utf8_test.go
index 4f73c8fb81..65e6c7e8b3 100644
--- a/src/pkg/unicode/utf8/utf8_test.go
+++ b/src/pkg/unicode/utf8/utf8_test.go
@@ -311,6 +311,11 @@ var validTests = []ValidTest{\n 	{string([]byte{66, 250}), false},\n 	{string([]byte{66, 250, 67}), false},\n 	{\"a\\uFFFDb\", true},\n+\t{string(\"\\xF7\\xBF\\xBF\\xBF\"), true},      // U+1FFFFF\n+\t{string(\"\\xFB\\xBF\\xBF\\xBF\\xBF\"), false}, // 0x3FFFFFF; out of range\n+\t{string(\"\\xc0\\x80\"), false},             // U+0000 encoded in two bytes: incorrect\n+\t// TODO {string(\"\\xed\\xa0\\x80\"), false },\t// U+D800 high surrogate (sic)\n+\t// TODO {string(\"\\xed\\xbf\\xbf\"), false },\t// U+DFFF low surrogate (sic)\n }\n \n func TestValid(t *testing.T) {\n```

## コアとなるコードの解説

### `src/pkg/unicode/utf8/utf8.go`

-   **`decodeRuneInternal` および `decodeRuneInStringInternal` 関数の変更**:
    -   これらの関数は、UTF-8バイトシーケンスをルーンにデコードするGo内部のヘルパー関数です。
    -   4バイトのUTF-8シーケンスを処理する部分で、以下の条件が追加されました。
        ```go
        if r <= rune3Max || MaxRune < r {
            return RuneError, 1, false
        }
        ```
    -   `r <= rune3Max` は、デコードされたルーン`r`が3バイトUTF-8で表現できる最大値(U+FFFF)以下であるにもかかわらず、4バイトでエンコードされている場合に、最短形式ではないため不正とみなす既存のチェックです。
    -   新たに追加された `|| MaxRune < r` は、デコードされたルーン`r`が`MaxRune`(U+10FFFF、Unicodeの最大有効コードポイント)を超える場合に、そのルーンがUnicodeの有効な範囲外であるため不正とみなすためのチェックです。これにより、例えばU+110000のような存在しないコードポイントが入力された場合でも、正しく`RuneError`が返されるようになります。

-   **公開APIのコメント更新**:
    -   `DecodeRune`, `DecodeRuneInString`, `DecodeLastRune`, `DecodeLastRuneInString`といった公開関数のドキュメンテーションコメントが更新されました。
    -   `If the encoding is invalid, it returns (RuneError, 1), an impossible result for correct UTF-8.` の後に、不正なエンコーディングの定義が追加されました。
    -   `An encoding is invalid if it is incorrect UTF-8, encodes a rune that is out of range, or is not the shortest possible UTF-8 encoding for the value. No other validation is performed.`
    -   この追加により、「範囲外のルーンをエンコードしている場合」も不正なエンコーディングとして扱われることが明示されました。

### `src/pkg/unicode/utf8/utf8_test.go`

-   **新しいテストケースの追加**:
    -   `validTests`というテストスライスに、新しいテストケースが追加されました。
    -   `{string("\\xFB\\xBF\\xBF\\xBF\\xBF"), false}, // 0x3FFFFFF; out of range`
    -   このテストケースは、`\xFB\xBF\xBF\xBF\xBF`というバイトシーケンスが、デコードすると`0x3FFFFFF`というルーンになることを想定しています。この値は`MaxRune`(U+10FFFF)をはるかに超えるため、`false`(不正なUTF-8)として評価されることを期待しています。これにより、範囲外のルーンが正しく拒否されることが検証されます。

## 関連リンク

-   GitHub Issue: [Fixes #3785](https://github.com/golang/go/issues/3785)
-   Go Code Review: [https://golang.org/cl/6398049](https://golang.org/cl/6398049)

## 参考にした情報源リンク

-   [Go issue 3785, titled \"unicode/utf8: decoding functions accept out-of-range input\"](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQEZwSUHllRKA2XRoimJuC1HNpI5ozzsukjsTAC1xMyuGV08Owg5PV0D_D3Qfnc0Z7lf4K812XKzaKFLPPTdNa5UwooCCVGgMiU4cHSvLCV9VTCEI7IoKlDghihfMQ9KP6fZUZQ=)
-   [Rob Pike, a key figure in Go\'s development, noted that it is correct behavior to reject these surrogate ranges in UTF-8 encoding.](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHZEKbhMb6NzO0dJ6XGWVZW6YOR8IJWeUtZ3110tlJY7KWlkOmYG-i3M8mIH74JcV7rog3ZqxIgN8_Bz6wQUMMGPfOMS0YD2P1oQD0_LQhvg7-_X2MUUiC_VJmsDzScb0rFxA-1KUAUxx3MxNCYnFWX0Xyg5BPXhEH24d37lg==)