[インデックス 14064] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
(Goコンパイラのフロントエンド部分)におけるクラッシュバグを修正するものです。具体的には、fmt.c
ファイル内の書式設定関数において、特定の条件下で%N
書式指定子を使用した際に発生する可能性のあるクラッシュを回避するための変更が加えられています。この修正は、nil
リテラルや型情報の処理に関するロジックの堅牢性を高めることを目的としています。
コミット
commit 54191126e49bb6504012fc8aacdadf273683750a
Author: Russ Cox <rsc@golang.org>
Date: Sun Oct 7 15:35:01 2012 -0400
cmd/gc: avoid crash in %N print
R=ken2
CC=golang-dev
https://golang.org/cl/6609052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/54191126e49bb6504012fc8aacdadf273683750a
元コミット内容
cmd/gc: avoid crash in %N print
R=ken2
CC=golang-dev
https://golang.org/cl/6609052
変更の背景
このコミットは、Goコンパイラのcmd/gc
部分で発生していたクラッシュバグを修正するために導入されました。具体的には、コンパイラが抽象構文木(AST)のノードを文字列として表現する際に使用する%N
書式指定子(fmt.c
内のexprfmt
関数で処理される)が、特定の条件下で不正なメモリ参照を引き起こし、コンパイラがクラッシュする問題がありました。
このクラッシュは、主にnil
リテラルが型情報を持つ場合や、ノードの型情報が欠落している場合に発生したと考えられます。コンパイラがASTノードの情報を出力しようとした際に、期待されるNode
構造体のフィールド(特にorig
やtype
)がNULL
または無効な値であったため、デリファレンス時にセグメンテーション違反などのクラッシュが発生していました。
この修正は、このような不正な状態を事前にチェックし、安全な処理経路を確保することで、コンパイラの安定性と堅牢性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラに関する基本的な知識が必要です。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラは、主にcmd/compile
(旧cmd/gc
)として知られています。これはGo言語で書かれたソースコードを機械語に変換する役割を担います。cmd/gc
は、字句解析、構文解析、型チェック、中間表現の生成、最適化、コード生成といったコンパイルの各段階を実行します。 - 抽象構文木 (AST): コンパイラはソースコードを解析し、その構造を抽象構文木(AST)として内部的に表現します。ASTは、プログラムの構造を木構造で表現したもので、各ノードは変数、関数呼び出し、リテラルなどのプログラム要素に対応します。
Node
構造体:cmd/gc
では、ASTの各要素を表現するためにNode
というC言語の構造体が使用されます。このNode
構造体には、ノードの種類(Op
)、関連するシンボル(Sym
)、型情報(Type
)、値(Val
)、元のノード(Orig
)など、様々な情報が含まれています。n->val.ctype == CTNIL
:Node
がnil
リテラルを表す場合に、その値の型がCTNIL
(Constant Type NIL)であることを示します。Goではnil
は特定の型を持たず、インターフェース、ポインタ、スライス、マップ、チャネルなどのゼロ値として使用されます。n->orig
:Node
が別のノードから派生した場合に、元のノードへのポインタを保持します。例えば、型付けされたnil
リテラルが、型付けされていない元のnil
リテラルを参照する場合があります。N
:Node
型のグローバルなNULL
または無効なノードを表す定数です。n->orig != N
は、orig
フィールドが有効なノードを指しているかどうかをチェックします。T
:Type
型のグローバルなNULL
または無効な型を表す定数です。n->type != T
は、type
フィールドが有効な型を指しているかどうかをチェックします。
fmt.c
とexprfmt
関数:fmt.c
は、Goコンパイラ内でデバッグ出力やエラーメッセージ生成のために、ASTノードや型などの内部構造を文字列に書式設定する機能を提供します。exprfmt
関数は、特に式(Node
)を文字列に変換する役割を担っています。%N
書式指定子は、このexprfmt
関数を通じてASTノードの情報を出力するために使用されます。- 型システムと
types
配列: Goコンパイラは、言語の型システムを内部的に表現するために、Type
構造体と、組み込み型(int
,string
,bool
など)を表すtypes
配列を使用します。n->type != types[n->type->etype]
のような比較は、ノードの型がその基本型(etype
)に対応する組み込み型と異なるかどうかをチェックします。これは、カスタム型やエイリアス型などの場合に重要になります。idealbool
,idealstring
: Goの型システムにおける「理想型」と呼ばれる概念の一部です。これらは、型付けされていないブールリテラルや文字列リテラルが持つ一時的な型を表します。
技術的詳細
このコミットの技術的詳細は、src/cmd/gc/fmt.c
ファイル内のexprfmt
関数の2つの変更点に集約されます。
変更点1: nil
リテラルの処理 (OLITERAL
ケース)
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1086,9 +1086,9 @@ exprfmt(Fmt *f, Node *n, int prec)
case OLITERAL: // this is a bit of a mess
\t\tif(fmtmode == FErr && n->sym != S)\
\t\t\treturn fmtprint(f, "%S", n->sym);\
-\t\t\tif(n->val.ctype == CTNIL)\
+\t\t\tif(n->val.ctype == CTNIL && n->orig != N)\
\t\t\tn = n->orig; // if this node was a nil decorated with at type, print the original naked nil
-\t\t\tif(n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring) {
+\t\t\tif(n->type != T && n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring) {
\t\t\t// Need parens when type begins with what might
\t\t\t// be misinterpreted as a unary operator: * or <-.
\t\t\tif(isptr[n->type->etype] || (n->type->etype == TCHAN && n->type->chan == Crecv))\
元のコードでは、OLITERAL
(リテラルノード)がnil
リテラル(n->val.ctype == CTNIL
)である場合、そのノードが型付けされたnil
であったとしても、元の型付けされていないnil
ノード(n->orig
)を参照するようにしていました。これは、出力時に「裸のnil
」として表示するためです。
しかし、ここで問題となるのは、n->orig
が常に有効なノードを指しているとは限らない点です。もしn->orig
がNULL
ポインタや無効なメモリを指していた場合、n = n->orig;
の代入後にn
が不正な状態となり、その後のn
へのアクセス(例えばn->type
など)がクラッシュを引き起こす可能性がありました。
この修正では、n->val.ctype == CTNIL
の条件に加えて、n->orig != N
というチェックが追加されました。N
はGoコンパイラにおける無効なNode
ポインタを表す定数です。これにより、n->orig
が有効なノードを指している場合にのみ、n
をn->orig
に置き換えるようになります。n->orig
が無効な場合は、現在のn
ノードをそのまま使用し、不正なポインタのデリファレンスを回避します。
変更点2: 型情報のチェック (n->type
の検証)
同じOLITERAL
ケースのブロック内で、ノードの型が特定の組み込み型や理想型と異なる場合に、括弧を追加するかどうかのロジックがあります。
元のコード:
if(n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)
この条件式は、n->type
が有効なポインタであることを前提としています。しかし、何らかの理由でn->type
がNULL
ポインタや無効なメモリを指していた場合、n->type->etype
へのアクセスがクラッシュを引き起こす可能性がありました。
この修正では、条件式の先頭にn->type != T
というチェックが追加されました。T
はGoコンパイラにおける無効なType
ポインタを表す定数です。
修正後のコード:
if(n->type != T && n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)
これにより、n->type
が有効な型ポインタである場合にのみ、その後のn->type->etype
へのアクセスが試みられるようになります。n->type
が無効な場合は、この条件式全体が短絡評価され、クラッシュが回避されます。
これらの変更は、Goコンパイラの内部データ構造(特にASTノードと型情報)の整合性が常に保証されるわけではないという現実に対応し、不正なポインタ参照によるクラッシュを防ぐための防御的なプログラミングの例と言えます。
コアとなるコードの変更箇所
diff --git a/src/cmd/gc/fmt.c b/src/cmd/gc/fmt.c
index 5a1f679301..61709c2862 100644
--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -1086,9 +1086,9 @@ exprfmt(Fmt *f, Node *n, int prec)
case OLITERAL: // this is a bit of a mess
\t\tif(fmtmode == FErr && n->sym != S)\
\t\t\treturn fmtprint(f, "%S", n->sym);\
-\t\t\tif(n->val.ctype == CTNIL)\
+\t\t\tif(n->val.ctype == CTNIL && n->orig != N)\
\t\t\tn = n->orig; // if this node was a nil decorated with at type, print the original naked nil
-\t\t\tif(n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring) {\
+\t\t\tif(n->type != T && n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring) {\
\t\t\t// Need parens when type begins with what might
\t\t\t// be misinterpreted as a unary operator: * or <-.\
\t\t\tif(isptr[n->type->etype] || (n->type->etype == TCHAN && n->type->chan == Crecv))\
コアとなるコードの解説
このコミットでは、src/cmd/gc/fmt.c
ファイルのexprfmt
関数内の2行が変更されています。
-
if(n->val.ctype == CTNIL && n->orig != N)
:- 変更前:
if(n->val.ctype == CTNIL)
- 変更後:
if(n->val.ctype == CTNIL && n->orig != N)
- この行は、現在のASTノード
n
がnil
リテラルを表すかどうかをチェックしています。 - 変更前は、単に
n
がnil
リテラルであれば、そのorig
フィールド(元のノード)にn
を置き換えていました。これは、型付けされたnil
(例:var p *int = nil
)を、型付けされていない「裸のnil
」として出力するためです。 - しかし、
n->orig
が不正なポインタ(NULL
や未初期化)である場合、n = n->orig;
の代入後にn
が不正な状態になり、その後のn
へのアクセスがクラッシュを引き起こす可能性がありました。 - 変更後は、
n->orig != N
という条件が追加されました。N
はGoコンパイラで定義されている「無効なノードポインタ」を表す定数です。これにより、n->orig
が有効なノードを指している場合にのみ、n
をn->orig
に置き換えるようになります。n->orig
が無効な場合は、この処理はスキップされ、現在のn
ノードがそのまま使用されるため、不正なポインタのデリファレンスが回避されます。
- 変更前:
-
if(n->type != T && n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)
:- 変更前:
if(n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)
- 変更後:
if(n->type != T && n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)
- この行は、ノード
n
の型(n->type
)が、その基本型(n->type->etype
で示される)に対応する組み込み型や、idealbool
、idealstring
といった特殊な理想型と異なる場合に、出力に括弧を追加するかどうかを判断する条件式です。これは、例えば*int
のようなポインタ型や、チャネル型など、単項演算子と誤解される可能性のある型を正しく表示するために行われます。 - 変更前は、
n->type
が有効なポインタであることを前提としていました。もしn->type
が不正なポインタである場合、n->type->etype
へのアクセスがクラッシュを引き起こす可能性がありました。 - 変更後は、条件式の先頭に
n->type != T
という条件が追加されました。T
はGoコンパイラで定義されている「無効な型ポインタ」を表す定数です。これにより、n->type
が有効な型ポインタである場合にのみ、その後のn->type->etype
へのアクセスが試みられます。n->type
が無効な場合は、この条件式全体が短絡評価され、クラッシュが回避されます。
- 変更前:
これらの変更は、コンパイラの内部処理において、ASTノードや型情報が常に期待通りの状態であるとは限らないという現実に対応するための、堅牢性向上のための修正です。不正なポインタ参照を事前にチェックすることで、コンパイラのクラッシュを防ぎ、安定性を高めています。
関連リンク
- Go Code Review: https://golang.org/cl/6609052
参考にした情報源リンク
- Go言語のソースコード(
cmd/gc
ディレクトリ内の関連ファイル) - Goコンパイラの内部構造に関する一般的なドキュメントやブログ記事(具体的なURLは割愛しますが、GoコンパイラのAST、型システム、
Node
構造体に関する情報源を参照しました。) - Go言語の
nil
に関するドキュメント - Go言語の型システムに関するドキュメント
git diff
コマンドの出力- GitHubのコミットページ
- Goコンパイラの開発に関するメーリングリストやIssueトラッカー(
golang-dev
メーリングリストなど)