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

[インデックス 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構造体のフィールド(特にorigtype)が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: Nodenilリテラルを表す場合に、その値の型が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.cexprfmt関数: 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->origNULLポインタや無効なメモリを指していた場合、n = n->orig;の代入後にnが不正な状態となり、その後のnへのアクセス(例えばn->typeなど)がクラッシュを引き起こす可能性がありました。

この修正では、n->val.ctype == CTNILの条件に加えて、n->orig != Nというチェックが追加されました。NはGoコンパイラにおける無効なNodeポインタを表す定数です。これにより、n->origが有効なノードを指している場合にのみ、nn->origに置き換えるようになります。n->origが無効な場合は、現在のnノードをそのまま使用し、不正なポインタのデリファレンスを回避します。

変更点2: 型情報のチェック (n->typeの検証)

同じOLITERALケースのブロック内で、ノードの型が特定の組み込み型や理想型と異なる場合に、括弧を追加するかどうかのロジックがあります。

元のコード: if(n->type != types[n->type->etype] && n->type != idealbool && n->type != idealstring)

この条件式は、n->typeが有効なポインタであることを前提としています。しかし、何らかの理由でn->typeNULLポインタや無効なメモリを指していた場合、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行が変更されています。

  1. if(n->val.ctype == CTNIL && n->orig != N):

    • 変更前: if(n->val.ctype == CTNIL)
    • 変更後: if(n->val.ctype == CTNIL && n->orig != N)
    • この行は、現在のASTノードnnilリテラルを表すかどうかをチェックしています。
    • 変更前は、単にnnilリテラルであれば、そのorigフィールド(元のノード)にnを置き換えていました。これは、型付けされたnil(例: var p *int = nil)を、型付けされていない「裸のnil」として出力するためです。
    • しかし、n->origが不正なポインタ(NULLや未初期化)である場合、n = n->orig;の代入後にnが不正な状態になり、その後のnへのアクセスがクラッシュを引き起こす可能性がありました。
    • 変更後は、n->orig != Nという条件が追加されました。NはGoコンパイラで定義されている「無効なノードポインタ」を表す定数です。これにより、n->origが有効なノードを指している場合にのみ、nn->origに置き換えるようになります。n->origが無効な場合は、この処理はスキップされ、現在のnノードがそのまま使用されるため、不正なポインタのデリファレンスが回避されます。
  2. 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で示される)に対応する組み込み型や、idealboolidealstringといった特殊な理想型と異なる場合に、出力に括弧を追加するかどうかを判断する条件式です。これは、例えば*intのようなポインタ型や、チャネル型など、単項演算子と誤解される可能性のある型を正しく表示するために行われます。
    • 変更前は、n->typeが有効なポインタであることを前提としていました。もしn->typeが不正なポインタである場合、n->type->etypeへのアクセスがクラッシュを引き起こす可能性がありました。
    • 変更後は、条件式の先頭にn->type != Tという条件が追加されました。TはGoコンパイラで定義されている「無効な型ポインタ」を表す定数です。これにより、n->typeが有効な型ポインタである場合にのみ、その後のn->type->etypeへのアクセスが試みられます。n->typeが無効な場合は、この条件式全体が短絡評価され、クラッシュが回避されます。

これらの変更は、コンパイラの内部処理において、ASTノードや型情報が常に期待通りの状態であるとは限らないという現実に対応するための、堅牢性向上のための修正です。不正なポインタ参照を事前にチェックすることで、コンパイラのクラッシュを防ぎ、安定性を高めています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(cmd/gcディレクトリ内の関連ファイル)
  • Goコンパイラの内部構造に関する一般的なドキュメントやブログ記事(具体的なURLは割愛しますが、GoコンパイラのAST、型システム、Node構造体に関する情報源を参照しました。)
  • Go言語のnilに関するドキュメント
  • Go言語の型システムに関するドキュメント
  • git diffコマンドの出力
  • GitHubのコミットページ
  • Goコンパイラの開発に関するメーリングリストやIssueトラッカー(golang-devメーリングリストなど)