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

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

このコミットは、Go言語のビルドツールである cmd/dist におけるディレクトリチェックの堅牢性を向上させるための変更です。具体的には、GOROOT 環境変数の末尾のスラッシュの扱いと、シンボリックリンクの解決に関する問題に対処しています。

コミット

commit 710d0540e27f57b2589552965418b95b88187fa7
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Feb 17 11:29:34 2012 -0500

    cmd/dist: make dir check in defaulttarg() more robust
    
    1, strip last path separator from $GOROOT
       The user might define GOROOT=/path/to/go/, but then the dir
       check in defaulttarg() will always complain the current dir
       is not within $GOROOT/src/.\n
    2, resolve symlinks in the default goroot
       Or if getcwd() returns a fully-resolved path, the check in
       defaulttarg() will always fail.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5649073

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

https://github.com/golang/go/commit/710d0540e27f57b2589552965418b95b88187fa7

元コミット内容

cmd/dist: make dir check in defaulttarg() more robust

1, strip last path separator from $GOROOT
   The user might define GOROOT=/path/to/go/, but then the dir
   check in defaulttarg() will always complain the current dir
   is not within $GOROOT/src/.
2, resolve symlinks in the default goroot
   Or if getcwd() returns a fully-resolved path, the check in
   defaulttarg() will always fail.

R=rsc
CC=golang-dev
https://golang.org/cl/5649073

変更の背景

このコミットは、Go言語のビルドシステムにおけるパス解決の堅牢性を高めることを目的としています。Goのビルドプロセスでは、GOROOT 環境変数がGoのインストールディレクトリを指し、その中の src ディレクトリがソースコードの基準点となります。cmd/dist はGoの配布ツールであり、ビルドやインストールに関連するタスクを管理します。

defaulttarg() 関数は、現在の作業ディレクトリが GOROOT/src の下にあるかどうかを検証する役割を担っていました。しかし、この検証ロジックには2つの既知の問題がありました。

  1. GOROOT の末尾スラッシュ問題: ユーザーが GOROOT 環境変数を /path/to/go/ のように末尾にパスセパレータ(スラッシュ)を付けて定義した場合、defaulttarg() 内のディレクトリチェックが常に失敗していました。これは、パスの比較が厳密に行われるため、末尾のスラッシュの有無が不一致を引き起こしていたためです。
  2. シンボリックリンクの解決問題: getcwd()(現在の作業ディレクトリを取得する関数)がシンボリックリンクを完全に解決したパス(実パス)を返す場合、もし GOROOT 自体にシンボリックリンクが含まれていると、defaulttarg() 内のチェックが常に失敗していました。これは、GOROOT/src のパスがシンボリックリンクを解決しないまま構築され、実パスと比較されることで不一致が生じていたためです。

これらの問題は、ユーザーがGoのビルドシステムを特定の環境設定(GOROOT の定義方法やシンボリックリンクの使用)で使用する際に、予期せぬエラーやビルドの失敗を引き起こす可能性がありました。このコミットは、これらの一般的なシナリオに対応し、cmd/dist のパスチェックをより柔軟で堅牢なものにすることで、ユーザーエクスペリエンスを向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下の概念について理解しておく必要があります。

  • Go言語のビルドシステム (cmd/dist):

    • cmd/dist は、Go言語のソースコードからGoツールチェイン自体をビルドするためのツールです。Goのコンパイラ、リンカ、標準ライブラリなどを構築する際に使用されます。これは、Goのユーザーが日常的に go buildgo install を使うのとは異なり、Goの開発者がGoの新しいバージョンをビルドする際や、Goをソースからインストールする際に主に利用されます。
    • cmd/dist は、Goのソースツリーの構造に深く依存しており、GOROOT 環境変数によって指定されるGoのルートディレクトリ内の src ディレクトリを基準として動作します。
  • GOROOT 環境変数:

    • GOROOT は、Goのインストールディレクトリ(またはGoのソースコードのルートディレクトリ)を指す環境変数です。Goのツールチェインは、この変数を使って標準ライブラリやその他の必要なファイルを見つけます。
    • 例えば、/usr/local/go/home/user/go などが GOROOT として設定されることがあります。
  • パスの正規化とシンボリックリンク:

    • ファイルシステムにおけるパスは、絶対パス、相対パス、シンボリックリンクなど、様々な形式で表現されます。
    • パスの正規化: パスを比較する際には、末尾のスラッシュの有無や、. (カレントディレクトリ) や .. (親ディレクトリ) のような特殊な要素を解決して、一意の形式に変換する「正規化」の概念が重要になります。
    • シンボリックリンク (Symbolic Link / Symlink): あるファイルやディレクトリを指し示す別のファイルです。シンボリックリンクを介してアクセスすると、実際にはリンクが指す元のファイルやディレクトリにリダイレクトされます。
    • getcwd() (Get Current Working Directory): 現在の作業ディレクトリのパスを返します。システムによっては、この関数がシンボリックリンクを解決した「実パス」を返す場合と、シンボリックリンクを含む「論理パス」を返す場合があります。
    • real_path() / xrealwd(): シンボリックリンクを完全に解決し、ファイルシステム上の実際の物理的なパス(正規化された絶対パス)を返す関数です。このコミットでは、xrealwd というGoのビルドシステム内部のヘルパー関数が使用されています。
  • hasprefix() 関数:

    • 文字列が特定のプレフィックス(接頭辞)で始まるかどうかをチェックする一般的な関数です。このコミットでは、現在の作業ディレクトリのパスが GOROOT/src のパスで始まるかどうかを検証するために使用されています。パスの比較において、末尾のスラッシュやシンボリックリンクの解決が適切に行われていないと、この hasprefix() チェックが誤った結果を返す可能性があります。

これらの概念は、ファイルシステム操作、パス解決、およびGoのような複雑なビルドシステムがどのようにパスを解釈し、検証するかを理解する上で不可欠です。

技術的詳細

このコミットは、src/cmd/dist/build.c ファイル内の init() 関数と defaulttarg() 関数に焦点を当てています。

init() 関数における GOROOT の処理

変更前は、GOROOT 環境変数を取得した後、末尾のスラッシュを削除する処理がありませんでした。

// 変更前
if(b.len > 0)
    goroot = btake(&b);

変更後は、GOROOT の長さが2以上で、かつ末尾がパスセパレータ(/)である場合に、そのセパレータを削除するロジックが追加されました。これにより、ユーザーが GOROOT=/path/to/go/ のように設定しても、内部的には /path/to/go として扱われるようになります。

// 変更後
if(b.len > 0) {
    // if not "/", then strip trailing path separator
    if(b.len >= 2 && b.p[b.len - 1] == slash[0])
        b.len--;
    goroot = btake(&b);
}

この変更は、GOROOT のパスが常に正規化された形式(末尾にスラッシュがない形式)で goroot 変数に格納されることを保証し、後続のパス比較処理での不整合を防ぎます。

defaulttarg() 関数におけるディレクトリチェックの改善

defaulttarg() 関数は、現在の作業ディレクトリが GOROOT/src の下にあることを確認する主要なロジックを含んでいます。

変更前は、現在の作業ディレクトリ (pwd) を取得し、GOROOT/src/ のパスを構築し、単純に hasprefix() で比較していました。

// 変更前
xgetwd(&pwd); // get current working directory
p = btake(&pwd);
bpathf(&src, "%s/src/", goroot); // construct GOROOT/src/ path
if(!hasprefix(p, bstr(&src))) // check if current directory starts with GOROOT/src/
    fatal("current directory %s is not under %s", p, bstr(&src));
p += src.len;

このアプローチには、前述のシンボリックリンクの問題がありました。xgetwd() が実パスを返す一方で、bstr(&src) はシンボリックリンクを解決しない論理パスを返す可能性があるため、比較が失敗することがありました。

変更後は、この問題を解決するために xrealwd() 関数が導入されました。

// 変更後
// xgetwd might return a path with symlinks fully resolved, and if
// there happens to be symlinks in goroot, then the hasprefix test
// will never succeed. Instead, we use xrealwd to get a canonical
// goroot/src before the comparison to avoid this problem.
xgetwd(&pwd);
p = btake(&pwd);
bpathf(&src, "%s/src/", goroot);
xrealwd(&real_src, bstr(&src)); // Get the real path of GOROOT/src/
if(!hasprefix(p, bstr(&real_src))) // Compare current directory with the real path of GOROOT/src/
    fatal("current directory %s is not under %s", p, bstr(&real_src));
p += real_src.len;
// guard againt xrealwd return the directory without the trailing /
if(*p == slash[0])
    p++;

ここで重要なのは、xrealwd(&real_src, bstr(&src)) の呼び出しです。これは、GOROOT/src のパス(bstr(&src))を、シンボリックリンクを解決した「実パス」に変換し、real_src に格納します。その後、hasprefix() は、現在の作業ディレクトリの実パス (p) と、GOROOT/src の実パス (bstr(&real_src)) を比較します。これにより、シンボリックリンクの有無に関わらず、正確なパス比較が可能になります。

また、xrealwd が末尾のスラッシュなしでディレクトリを返す可能性に対するガードとして、if(*p == slash[0]) p++; という行が追加されています。これは、real_src.lenp を進めた後、もし p がまだスラッシュを指している場合(つまり、xrealwd が末尾のスラッシュを削除してパスを返した場合)、さらに1文字進めて、実際のサブパスの開始位置を正確に指すようにするためのものです。

これらの変更により、cmd/dist は、ユーザーが GOROOT をどのように設定しているか、あるいはファイルシステムにシンボリックリンクが存在するかに関わらず、現在の作業ディレクトリがGoのソースツリーの正しい場所にあるかをより堅牢に判断できるようになりました。

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

変更は src/cmd/dist/build.c ファイルに集中しています。

diff --git a/src/cmd/dist/build.c b/src/cmd/dist/build.c
index f31c83ea7a..6cb33ab10f 100644
--- a/src/cmd/dist/build.c
+++ b/src/cmd/dist/build.c
@@ -77,8 +77,12 @@ init(void)\n \tbinit(&b);\n \n \txgetenv(&b, "GOROOT");\n-\tif(b.len > 0)\n+\tif(b.len > 0) {\n+\t\t// if not "/", then strip trailing path separator\n+\t\tif(b.len >= 2 && b.p[b.len - 1] == slash[0])\n+\t\t\tb.len--;\n \t\tgoroot = btake(&b);\n+\t}\n \n \txgetenv(&b, "GOBIN");\n \tif(b.len == 0)\n@@ -1373,20 +1377,30 @@ static char*\n defaulttarg(void)\n {\n \tchar *p;\n-\tBuf pwd, src;\n+\tBuf pwd, src, real_src;\n \t\n \tbinit(&pwd);\n \tbinit(&src);\n+\tbinit(&real_src);\n \n+\t// xgetwd might return a path with symlinks fully resolved, and if\n+\t// there happens to be symlinks in goroot, then the hasprefix test\n+\t// will never succeed. Instead, we use xrealwd to get a canonical\n+\t// goroot/src before the comparison to avoid this problem.\n \txgetwd(&pwd);\n \tp = btake(&pwd);\n \tbpathf(&src, "%s/src/", goroot);\n-\tif(!hasprefix(p, bstr(&src)))\n-\t\tfatal("current directory %s is not under %s", p, bstr(&src));\n-\tp += src.len;\n+\txrealwd(&real_src, bstr(&src));\n+\tif(!hasprefix(p, bstr(&real_src)))\n+\t\tfatal("current directory %s is not under %s", p, bstr(&real_src));\n+\tp += real_src.len;\n+\t// guard againt xrealwd return the directory without the trailing /\n+\tif(*p == slash[0])\n+\t\tp++;\n \n \tbfree(&pwd);\n \tbfree(&src);\n+\tbfree(&real_src);\n \t\n \treturn p;\n }\n```

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

### `init()` 関数内の変更

```c
+\tif(b.len > 0) {\n+\t\t// if not "/", then strip trailing path separator\n+\t\tif(b.len >= 2 && b.p[b.len - 1] == slash[0])\n+\t\t\tb.len--;\n \t\tgoroot = btake(&b);\n+\t}

このコードブロックは、GOROOT 環境変数から取得したパス b の末尾にパスセパレータ(/)が存在する場合、それを削除する処理です。

  • b.len >= 2: パスの長さが少なくとも2文字以上であることを確認します。これは、ルートディレクトリ / のような単一のセパレータパスを誤って処理しないためのガードです。
  • b.p[b.len - 1] == slash[0]: パスの最後の文字がパスセパレータであるかをチェックします。slash[0] はシステムに応じたパスセパレータ(Unix系では /)を表します。
  • b.len--: 条件が真の場合、パスの長さを1減らすことで、末尾のセパレータを論理的に削除します。これにより、goroot 変数には常に末尾にスラッシュがない正規化されたパスが格納されます。

defaulttarg() 関数内の変更

+\tBuf pwd, src, real_src;\n \t\n \tbinit(&pwd);\n \tbinit(&src);\n+\tbinit(&real_src);\n \n+\t// xgetwd might return a path with symlinks fully resolved, and if\n+\t// there happens to be symlinks in goroot, then the hasprefix test\n+\t// will never succeed. Instead, we use xrealwd to get a canonical\n+\t// goroot/src before the comparison to avoid this problem.\n \txgetwd(&pwd);\n \tp = btake(&pwd);\n \tbpathf(&src, "%s/src/", goroot);\n+\txrealwd(&real_src, bstr(&src));\n+\tif(!hasprefix(p, bstr(&real_src)))\n+\t\tfatal("current directory %s is not under %s", p, bstr(&real_src));\n+\tp += real_src.len;\n+\t// guard againt xrealwd return the directory without the trailing /\n+\tif(*p == slash[0])\n+\t\tp++;

この部分が、シンボリックリンクの問題を解決する主要な変更です。

  • Buf real_src;: 新たに real_src という Buf 型の変数が宣言され、初期化されます。これは GOROOT/src の実パスを格納するために使用されます。
  • xrealwd(&real_src, bstr(&src));: ここが最も重要な変更点です。
    • bstr(&src) は、goroot"/src/" を結合して構築された GOROOT/src/ の論理パスです。
    • xrealwd は、この論理パスを受け取り、ファイルシステム上のシンボリックリンクをすべて解決した「実パス」(canonical path)を real_src に書き込みます。
  • if(!hasprefix(p, bstr(&real_src))): hasprefix 関数による比較が、現在の作業ディレクトリの実パス p と、GOROOT/src の実パス bstr(&real_src) の間で行われるようになりました。これにより、シンボリックリンクの有無によるパスの不一致が解消されます。
  • p += real_src.len;: p は、GOROOT/src の部分を除いた、現在の作業ディレクトリの相対パスの開始位置を指すように更新されます。
  • if(*p == slash[0]) p++;: xrealwd が返す実パスが末尾にスラッシュを含まない場合があるため、p がまだスラッシュを指している場合は、さらに1文字進めて、正確な相対パスの開始位置を確保します。これは、パスの正規化をさらに確実にするためのものです。

これらの変更により、cmd/dist は、GOROOT の設定方法やファイルシステム上のシンボリックリンクの存在に関わらず、現在の作業ディレクトリがGoのソースツリーの正しい場所にあるかをより正確かつ堅牢に判断できるようになりました。

関連リンク

参考にした情報源リンク

  • Goのコミットメッセージとコード変更: https://github.com/golang/go/commit/710d0540e27f57b2589552965418b95b88187fa7
  • Goのコードレビュー (CL 5649073): https://golang.org/cl/5649073
  • Unix/Linuxにおけるパスとシンボリックリンクの概念に関する一般的な情報源 (例: Wikipedia, man pages for readlink, realpath, getcwd)
  • C言語における文字列操作とパス操作に関する一般的な情報源 (例: string.h の関数、ファイルシステムAPIのドキュメント)
  • Goのビルドシステムに関する一般的な議論やドキュメント (Goの公式ブログやメーリングリストのアーカイブなど)