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

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

このコミットは、Goコンパイラ(gc)において、インポートパスに特殊文字が含まれる場合にそれを拒否する機能を追加し、また単一ファイル内で複数の無効なインポート文を許可するように変更したものです。これは、Go言語のインポートパスの堅牢性を高め、不正なパスによるコンパイルエラーや予期せぬ動作を防ぐことを目的としています。

コミット

gc: reject import paths containing special characters

Also allow multiple invalid import statements in a single file.

Fixes #3021. The changes to go/parser and the language specifcation have already been committed.

R=rsc, gri CC=golang-dev https://golang.org/cl/5672084

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

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

元コミット内容

commit dc38756ce12e83c9466afaa4d439da03303fec7b
Author: Anthony Martin <ality@pbrane.org>
Date:   Fri Feb 24 14:48:36 2012 -0500

    gc: reject import paths containing special characters
    
    Also allow multiple invalid import statements in a
    single file.
    
    Fixes #3021. The changes to go/parser and the
    language specifcation have already been committed.
    
    R=rsc, gri
    CC=golang-dev
    https://golang.org/cl/5672084
---
 src/cmd/gc/go.h    |    1 +
 src/cmd/gc/go.y    |   10 +-\
 src/cmd/gc/lex.c   |   23 +-\
 src/cmd/gc/subr.c  |   44 +-\
 src/cmd/gc/y.tab.c | 3767 ++++++++++++++++++++++++++--------------------------
 src/cmd/gc/y.tab.h |   14 +-\
 src/cmd/gc/yerr.h  |   28 +-\
 test/import5.go    |   44 +-\
 8 files changed, 2040 insertions(+), 1891 deletions(-)

変更の背景

このコミットは、Go言語のIssue 3021「cmd/go, cmd/gc: reject import paths containing special characters」を修正するために行われました。以前のGoコンパイラ(gc)および関連ツールは、インポートパスに特殊文字が含まれている場合に適切に処理できず、予期せぬ動作やコンパイルエラーを引き起こす可能性がありました。

Go言語のインポートパスは、パッケージを一意に識別し、コードの整理と参照に不可欠です。健全なインポートパスは、予測可能で安全なビルドプロセスを保証するために重要です。特殊文字の混入は、ファイルシステムやオペレーティングシステムの違いによるパス解釈の不一致、セキュリティ上の脆弱性、または単にコードの可読性と保守性の低下につながる可能性があります。

この変更の主な目的は、インポートパスの検証を強化し、不正な文字を含むパスをコンパイル時に明確に拒否することです。これにより、開発者はより早期に問題を発見し、Goプログラムの堅牢性と移植性が向上します。また、単一ファイル内で複数の無効なインポート文を許可する変更は、エラー報告の改善に寄与し、開発者が一度のコンパイルで複数のインポート関連の問題を特定できるようにします。

前提知識の解説

Go言語のインポートパス

Go言語において、インポートパスはパッケージを一意に識別するための文字列です。例えば、"fmt"は標準ライブラリのフォーマットパッケージを、"github.com/user/repo/pkg"はサードパーティのパッケージを指します。インポートパスは、Goコンパイラがソースコード内のimport文を解決し、必要なパッケージを見つけるために使用されます。

Go Modulesが導入される以前(このコミットの時点ではGOPATHが主流)、インポートパスはGOPATH内のディレクトリ構造に直接マッピングされていました。Go Modulesが標準となった現在では、モジュールパスがインポートパスのプレフィックスとなり、より柔軟なパッケージ管理が可能になっています。しかし、インポートパス自体の文字要件は、Go言語の仕様とコンパイラの設計に深く根ざしています。

Goコンパイラ (gc) のアーキテクチャと字句解析・構文解析

Goコンパイラはgcとして知られ、Go言語で書かれたソースコードを機械語に変換する役割を担います。コンパイルプロセスは複数のフェーズに分かれていますが、このコミットに関連するのは主に**字句解析(Lexical Analysis)構文解析(Syntax Analysis)**の初期フェーズです。

  1. 字句解析(Lexical Analysis): ソースコードを読み込み、意味のある最小単位である「トークン」に分割します。例えば、import "path/to/pkg"というコードは、importキーワード、文字列リテラル"path/to/pkg"、セミコロン(または改行)などのトークンに分割されます。この段階で、文字列リテラル内の文字が有効であるかどうかの基本的なチェックが行われることがあります。
  2. 構文解析(Syntax Analysis): 字句解析で生成されたトークンのストリームを受け取り、Go言語の文法規則に従ってそれらが正しい構造を形成しているかを確認します。このプロセスでは、抽象構文木(AST: Abstract Syntax Tree)が構築されます。go.yファイルは、Go言語の文法を定義するYacc(またはBison)の入力ファイルであり、構文解析器の生成に使用されます。

このコミットでは、インポートパスの文字列リテラルが字句解析および構文解析の段階で適切に検証されるように、コンパイラの内部ロジックが変更されています。特に、go/parser(Go言語のパーサーライブラリ)とGo言語の仕様自体にも、この変更に先行して関連する修正がコミットされていることが言及されており、インポートパスの厳格な検証が言語全体の方針として進められていたことが伺えます。

技術的詳細

このコミットの技術的詳細の核心は、Goコンパイラがインポートパスの文字列リテラルをどのように検証し、不正な文字を検出して拒否するかという点にあります。

isbadimport 関数の導入

主要な変更点の一つは、src/cmd/gc/subr.cisbadimportという新しい関数が導入されたことです。この関数はStrlit *path(文字列リテラルへのポインタ)を引数に取り、インポートパスが不正な文字を含んでいる場合に1(真)を、そうでなければ0(偽)を返します。

isbadimport関数は、以下の種類の不正な文字をチェックします。

  • NULバイト (\0): インポートパスの文字列長が、実際に含まれるNULバイトを含まない文字列長と異なる場合、NULバイトが含まれていると判断し、エラーとします。これは、C言語の文字列終端文字であるNULバイトがパスの途中に存在すると、文字列処理が途中で終了してしまう可能性があるためです。
  • 無効なUTF-8シーケンス: インポートパスが有効なUTF-8エンコーディングではない場合、エラーとします。Go言語はUTF-8を標準の文字エンコーディングとしており、パスもこれに従う必要があります。chartorune関数を使用してUTF-8シーケンスをルーン(Unicodeコードポイント)に変換し、Runeerrorが返された場合に無効と判断します。
  • 制御文字 (0x00-0x1F, 0x7F): ASCIIの制御文字(例: タブ、改行、ベル文字など)やDEL文字(0x7F)が含まれている場合、エラーとします。これらの文字はパスとして意味を持たず、システム間で異なる解釈をされる可能性があるためです。
  • バックスラッシュ (\): インポートパスにバックスラッシュが含まれている場合、エラーとします。Go言語ではパスの区切り文字としてスラッシュ(/)を使用することが標準であり、Windows環境などでのバックスラッシュの使用は推奨されません。このチェックは、クロスプラットフォームでの一貫性を保証します。
  • スペース文字: インポートパスにスペース文字が含まれている場合、エラーとします。スペースはシェルやファイルシステムで特別な意味を持つことがあり、パスの解釈を複雑にするためです。isspacerune関数でチェックされます。
  • その他の無効な特殊文字: "!\"#$%&'()*,:;<=>?[]^{|}~"に含まれる文字がインポートパスに含まれている場合、エラーとします。これらの文字は、URLエンコーディングやシェルスクリプトなどで特別な意味を持つことが多く、パスの曖昧さを避けるために禁止されます。utfrune`関数でこれらの文字のいずれかが含まれているかをチェックします。

fakeimport メカニズム

このコミットのもう一つの重要な変更は、src/cmd/gc/lex.cに導入されたfakeimport関数と、それを利用したエラー処理の改善です。

以前のコンパイラでは、無効なインポートパスが検出されると、errorexit()が呼び出され、コンパイルプロセスが即座に終了していました。これにより、単一ファイル内に複数の無効なインポート文が存在する場合でも、最初に見つかったエラーでコンパイルが停止し、他のエラーが報告されないという問題がありました。

fakeimport関数は、無効なインポートパスが検出された際に、実際には存在しない「fake」という名前のパッケージをインポートしたかのようにコンパイラの内部状態を設定します。具体的には、importpkg = mkpkg(strlit("fake"));cannedimports("fake.6", "$$\n");が呼び出されます。これにより、コンパイラはエラーを報告しつつも、その後の処理を続行できるようになります。

go.yimport_stmtルールにも変更が加えられ、import_here import_thereという新しいプロダクションが追加されました。これは、importfileが無効なインポートパスを受け取った際にyyerrorを呼び出し、その後パッケージステートメントなしで偽のインポートを設定することで、単一ファイル内で複数の無効なインポートステートメントをテストできるようにするためのものです。nerrors == 0の場合にfatal("phase error in import");が呼び出されるのは、この偽のインポートメカニズムが意図通りに機能していることを確認するためのデバッグ的なチェックと考えられます。

このfakeimportメカニズムにより、コンパイラはインポートパスの検証エラーが発生しても即座に終了せず、可能な限り多くのエラーを収集して報告できるようになり、開発者のデバッグ体験が向上します。

Bisonバージョンの更新

src/cmd/gc/y.tab.csrc/cmd/gc/y.tab.hの変更は、GNU Bisonのバージョンが2.4.1から2.5に更新されたことによるものです。BisonはYaccのGNU版であり、Goコンパイラの構文解析器を生成するために使用されています。Bisonのバージョンアップに伴い、生成されるパーサーのCコード(y.tab.c)に大幅な変更が生じています。これは直接的な機能変更ではなく、ツールチェインの更新による副次的な変更ですが、コンパイラのビルドプロセスに影響を与えます。

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

このコミットでは、以下のファイルが変更されています。

  • src/cmd/gc/go.h:
    • isbadimport関数のプロトタイプ宣言が追加されました。
  • src/cmd/gc/go.y:
    • インポート文の構文規則(import_stmt)に、無効なインポートパスを処理するための新しいプロダクションが追加されました。これにより、importfileがエラーを報告した後も、構文解析を継続できるようになります。
  • src/cmd/gc/lex.c:
    • fakeimport関数が追加されました。
    • importfile関数内で、インポートパスの文字列が空であるか、isbadimport関数によって不正と判断された場合にfakeimportを呼び出すように変更されました。
    • 以前のNULバイトやバックスラッシュのチェックがisbadimportに統合され、削除されました。
  • src/cmd/gc/subr.c:
    • isbadimport関数が実装されました。この関数は、インポートパスにNULバイト、無効なUTF-8シーケンス、制御文字、バックスラッシュ、スペース、特定の特殊文字が含まれていないかを検証します。
    • mkpkg関数内のNULバイトチェックがisbadimportの呼び出しに置き換えられました。
  • src/cmd/gc/y.tab.c:
    • GNU Bisonのバージョンが2.4.1から2.5に更新されたことにより、生成されたパーサーのCコードが大幅に変更されました。これは主にコメントや内部構造の変更であり、直接的なロジックの変更ではありません。
  • src/cmd/gc/y.tab.h:
    • y.tab.cと同様に、Bisonのバージョンアップに伴う変更が含まれています。
  • src/cmd/gc/yerr.h:
    • エラーメッセージに関連する変更が含まれている可能性がありますが、具体的な差分からは詳細が読み取れません。
  • test/import5.go:
    • 新しいテストケースが追加されました。このテストファイルは、インポートパスに不正な文字が含まれる場合のコンパイラの挙動、特に単一ファイル内で複数の無効なインポート文が正しく報告されることを検証するために使用されます。

コアとなるコードの解説

src/cmd/gc/subr.c における isbadimport 関数

int
isbadimport(Strlit *path)
{
	char *s;
	Rune r;

	if(strlen(path->s) != path->len) {
		yyerror("import path contains NUL");
		return 1;
	}

	s = path->s;
	while(*s) {
		s += chartorune(&r, s);
		if(r == Runeerror) {
			yyerror("import path contains invalid UTF-8 sequence");
			return 1;
		}
		if(r < 0x20 || r == 0x7f) {
			yyerror("import path contains control character");
			return 1;
		}
		if(r == '\\') {
			yyerror("import path contains backslash; use slash");
			return 1;
		}
		if(isspacerune(r)) {
			yyerror("import path contains space character");
			return 1;
		}
		if(utfrune("!\"#$%&'()*,:;<=>?[]^`{|}~", r)) {
			yyerror("import path contains invalid character '%C'", r);
			return 1;
		}
	}
	return 0;
}

このisbadimport関数は、インポートパスの文字列pathを文字ごとに走査し、Go言語のインポートパスとして許可されない文字が含まれていないかをチェックします。

  • strlen(path->s) != path->len: 文字列リテラルの実際の長さと、C文字列としての長さが異なる場合(途中にNULバイトがある場合)を検出します。
  • chartorune(&r, s): UTF-8シーケンスをルーンに変換します。Runeerrorが返された場合、無効なUTF-8シーケンスです。
  • r < 0x20 || r == 0x7f: 制御文字(ASCII 0x00-0x1F)またはDEL文字(0x7F)を検出します。
  • r == '\\': バックスラッシュを検出します。
  • isspacerune(r): スペース文字を検出します。
  • utfrune("!\"#$%&'()*,:;<=>?[]^{|}~", r)`: 特定の特殊文字を検出します。

いずれかの条件に合致した場合、yyerrorを呼び出してエラーメッセージを報告し、1を返して不正であることを示します。

src/cmd/gc/lex.c における fakeimportimportfile の変更

static void
fakeimport(void)
{
	importpkg = mkpkg(strlit("fake"));
	cannedimports("fake.6", "$$\n");
}

void
importfile(Val *f, int line)
{
	// ... (既存のコード)

	if(f->ctype != CTSTR) {
		yyerror("import statement not a string");
		fakeimport(); // エラー後も処理を継続
		return;
	}

	if(f->u.sval->len == 0) {
		yyerror("import path is empty");
		fakeimport(); // エラー後も処理を継続
		return;
	}

	if(isbadimport(f->u.sval)) {
		fakeimport(); // エラー後も処理を継続
		return;
	}

	// ... (既存のコード)
}

fakeimport関数は、無効なインポートパスが検出された際に、コンパイラがエラーを報告しつつも、その後の構文解析を継続できるようにするための「ダミー」のインポート処理を行います。これにより、コンパイラは単一ファイル内の複数のインポートエラーを報告できるようになります。

importfile関数は、インポート文の処理を担当します。この変更により、インポートパスが文字列リテラルでない場合、空文字列の場合、またはisbadimportによって不正と判断された場合に、yyerrorでエラーを報告した後、fakeimport()を呼び出して処理を継続するようになりました。以前はerrorexit()で即座に終了していました。

src/cmd/gc/go.y における構文規則の変更

import_stmt:
	// ... (既存のプロダクション)

|	import_here import_there
	{
		// When an invalid import path is passed to importfile,
		// it calls yyerror and then sets up a fake import with
		// no package statement. This allows us to test more
		// than one invalid import statement in a single file.
		if(nerrors == 0)
			fatal("phase error in import");
	}

go.yimport_stmtルールに新しいプロダクションが追加されました。これは、importfileyyerrorを呼び出し、fakeimportによって偽のインポートが設定された場合に、構文解析器がその状態を認識し、エラーを報告しつつも次のインポート文の解析に進めるようにするためのものです。if(nerrors == 0)のチェックは、このパスが意図せず実行された場合にフェーズエラーとして致命的なエラーを報告するためのガードです。

これらの変更により、Goコンパイラはインポートパスの検証を大幅に強化し、不正なパスをより適切に処理できるようになりました。また、エラー報告の粒度も向上し、開発者がより効率的に問題を特定・修正できるようになっています。

関連リンク

参考にした情報源リンク