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

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

このコミットは、Go言語のコンパイラ (cmd/cc) とリンカ (cmd/ld) におけるシンボルルックアップ処理のバグ修正に関するものです。具体的には、シンボル名の比較において memcmp 関数が誤った長さで呼び出されていたために発生する可能性があった文字列のオーバーフロー(境界外読み取り)を修正し、より安全な strcmp 関数に置き換えることで問題を解決しています。

コミット

commit 77e7e4c329f44353d6d11eb0adee2d83437ce5ea
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Mon Mar 25 08:20:22 2013 +0100

    cmd/cc, cmd/ld: do not overflow strings in symbol lookup.
    
    R=golang-dev, dave, minux.ma
    CC=golang-dev
    https://golang.org/cl/7876044

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

https://github.com/golang/go/commit/77e7e4c329f44353d6d11eb0adee2d83437ce5ea

元コミット内容

このコミットの元の内容は、Go言語のコンパイラ (src/cmd/cc/lexbody) とリンカ (src/cmd/ld/lib.c) のシンボルルックアップ関数において、文字列比較に memcmp を使用していた箇所を strcmp に変更するというものです。

具体的には、以下の変更が行われています。

  • src/cmd/cc/lexbodylookup 関数内で、memcmp(s->name, symb, l) == 0strcmp(s->name, symb) == 0 に変更。
  • src/cmd/ld/lib.c_lookup 関数内で、l 変数の宣言と l = (p - symb) + 1; の計算を削除し、memcmp(s->name, symb, l) == 0strcmp(s->name, symb) == 0 に変更。

変更の背景

この変更の背景には、Go言語のコンパイラとリンカがシンボルテーブル内でシンボル名を検索する際に使用していた文字列比較ロジックに潜在的なバグが存在したことが挙げられます。

従来のコードでは、memcmp 関数を使用してシンボル名を比較していました。memcmp は指定されたバイト数だけメモリ領域を比較する関数であり、比較するバイト数 (l) が正確に計算されていない場合、または比較対象の文字列がその長さよりも短い場合に、メモリの境界を越えて読み取ってしまう(オーバーフロー)可能性がありました。これは、セキュリティ上の脆弱性やプログラムのクラッシュにつながる可能性があります。

特に、src/cmd/ld/lib.c_lookup 関数では、l = (p - symb) + 1; という計算で比較長を決定していましたが、この計算が常に正しい文字列長を保証するものではなかった可能性があります。シンボル名がヌル終端されていることを前提とするならば、strcmp を使用する方がより安全で意図に沿った比較方法となります。

このコミットは、このような潜在的な問題を解消し、シンボルルックアップの堅牢性を向上させることを目的としています。

前提知識の解説

Go言語のツールチェイン

Go言語は、コンパイラ、リンカ、アセンブラなどのツールチェインを自身で実装しています。

  • cmd/cc: Go言語のコンパイラの一部であり、C言語のコードをコンパイルする際に使用されることがあります。Goの初期のツールチェインはC言語で書かれており、その名残が見られます。
  • cmd/ld: Go言語のリンカです。コンパイルされたオブジェクトファイルを結合し、実行可能なバイナリを生成する役割を担います。シンボル解決(どの関数や変数がどこにあるかを特定するプロセス)はリンカの主要な機能の一つです。

シンボルルックアップ

シンボルルックアップとは、プログラム内で使用される関数名、変数名などの「シンボル」が、メモリ上のどこに配置されているかを特定するプロセスです。コンパイラやリンカは、このシンボルルックアップを通じて、コード内の参照を実際のメモリアドレスに解決します。シンボルテーブルと呼ばれるデータ構造にシンボル名とそのアドレスが格納されており、ルックアップ時にはこのテーブルを検索します。

C言語の文字列比較関数

  • memcmp(const void *s1, const void *s2, size_t n):

    • s1s2 が指すメモリ領域の最初の n バイトを比較します。
    • ヌル終端文字 (\0) を文字列の終端とはみなしません。指定された n バイトを厳密に比較します。
    • バイナリデータの比較に適しています。
    • n の値が不正確だと、バッファオーバーフロー(境界外読み取り)を引き起こす可能性があります。
  • strcmp(const char *s1, const char *s2):

    • s1s2 が指すヌル終端文字列を比較します。
    • ヌル終端文字 (\0) を文字列の終端とみなし、その文字までを比較します。
    • 文字列の比較に適しています。
    • 比較する文字列がヌル終端されていることを前提とします。

オーバーフロー(境界外読み取り)

プログラミングにおける「オーバーフロー」は、通常、数値がデータ型の最大値を超えてしまうことを指しますが、この文脈では「バッファオーバーフロー」の一種である「境界外読み取り (Out-of-bounds read)」を指します。これは、プログラムが割り当てられたメモリ領域の境界を越えてデータを読み取ろうとするときに発生します。

memcmp の場合、比較するバイト数 l が実際の文字列の長さよりも大きいと、文字列の終端を超えてメモリを読み取ってしまう可能性があります。これにより、予期せぬデータが比較に含まれたり、不正なメモリアクセスが発生してプログラムがクラッシュしたり、セキュリティ上の脆弱性につながったりすることがあります。

技術的詳細

このコミットの技術的な核心は、シンボルルックアップにおける文字列比較の安全性の向上です。

src/cmd/cc/lexbodylookup 関数と src/cmd/ld/lib.c_lookup 関数は、どちらもハッシュテーブルを使用してシンボルを検索するロジックを持っています。ハッシュ衝突が発生した場合、リンクリストを辿って目的のシンボルを見つける必要があります。この際、リスト内の各シンボルの名前 (s->name) と検索対象のシンボル名 (symb) を比較して一致するかどうかを確認します。

変更前は、この比較に memcmp(s->name, symb, l) が使用されていました。ここで l は比較するバイト数を指定します。

  • src/cmd/cc/lexbodylookup 関数では、l の値は明示的に計算されていませんが、おそらく symb の長さとして使用されていたと考えられます。
  • src/cmd/ld/lib.c_lookup 関数では、l = (p - symb) + 1; という計算で l が決定されていました。psymb の終端(ヌル終端文字の次)を指すポインタであり、symb から p までの距離に1を加えることで、symb の長さ(ヌル終端文字を含む)を計算しようとしています。

しかし、この l の計算が常に正確である保証はありませんでした。特に、symb がヌル終端されていない場合や、p の計算が何らかの理由で誤っていた場合、l が実際の文字列長を超えてしまう可能性があります。その結果、memcmps->namesymb のバッファ境界を越えてメモリを読み取り、比較を行ってしまうことになります。これが「overflow strings in symbol lookup」という問題の本質です。

この問題を解決するために、開発者は memcmpstrcmp に置き換えました。strcmp はヌル終端文字列を比較するように設計されているため、比較する文字列が適切にヌル終端されていれば、比較長を明示的に指定する必要がなく、境界外読み取りのリスクがなくなります。この変更は、s->namesymb が常にヌル終端文字列であることを前提としています。これはC言語の標準的な文字列の扱いに合致しており、より安全で堅牢な比較方法と言えます。

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

src/cmd/cc/lexbody

--- a/src/cmd/cc/lexbody
+++ b/src/cmd/cc/lexbody
@@ -263,7 +263,7 @@ lookup(void)
 	for(s = hash[h]; s != S; s = s->link) {
 		if(s->name[0] != c)
 			continue;
-		if(memcmp(s->name, symb, l) == 0)
+		if(strcmp(s->name, symb) == 0)
 			return s;
 	}
 	s = alloc(sizeof(*s));

src/cmd/ld/lib.c

--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -846,17 +846,16 @@ _lookup(char *symb, int v, int creat)\n \tSym *s;\n \tchar *p;\n \tint32 h;\n-\tint l, c;\n+\tint c;\n \n \th = v;\n \tfor(p=symb; c = *p; p++)\n \t\th = h+h+h + c;\n-\tl = (p - symb) + 1;\n \t// not if(h < 0) h = ~h, because gcc 4.3 -O2 miscompiles it.\n \th &= 0xffffff;\n \th %= NHASH;\n \tfor(s = hash[h]; s != S; s = s->hash)\n-\t\tif(memcmp(s->name, symb, l) == 0)\n+\t\tif(strcmp(s->name, symb) == 0)\n \t\t\treturn s;\n \tif(!creat)\n \t\treturn nil;\

コアとなるコードの解説

src/cmd/cc/lexbodylookup 関数

この関数は、コンパイラの字句解析器の一部で、シンボルテーブルからシンボルを検索する役割を担っています。

  • for(s = hash[h]; s != S; s = s->link): ハッシュ値 h に対応するハッシュテーブルのエントリから、リンクリストを辿ってシンボルを検索します。
  • if(s->name[0] != c) continue;: 最初の文字が一致しない場合は、すぐに次のシンボルへスキップします。これは、比較の最適化です。
  • if(memcmp(s->name, symb, l) == 0) (変更前): s->namesymbl バイトだけ比較していました。l の値がどこから来るのかは、このスニペットだけでは不明ですが、おそらく symb の長さに関連する値が渡されていたと考えられます。
  • if(strcmp(s->name, symb) == 0) (変更後): memcmpstrcmp に置き換えることで、s->namesymb がヌル終端文字列として比較されるようになりました。これにより、比較長を明示的に指定する必要がなくなり、l の誤った値による境界外読み取りのリスクが排除されました。

src/cmd/ld/lib.c_lookup 関数

この関数は、リンカのライブラリの一部で、シンボルテーブルからシンボルを検索する役割を担っています。

  • int l, c; (変更前): 比較長を格納する l 変数と、文字を格納する c 変数が宣言されていました。
  • int c; (変更後): l 変数が不要になったため、宣言から削除されました。
  • for(p=symb; c = *p; p++) h = h+h+h + c;: シンボル名 symb を走査しながらハッシュ値を計算しています。psymb のポインタです。
  • l = (p - symb) + 1; (変更前): symb の開始ポインタから p の現在位置までの距離(文字列長)に1を加えることで、ヌル終端文字を含む文字列の長さを計算しようとしていました。この計算が、前述の通り、潜在的な問題の原因となっていました。
  • if(memcmp(s->name, symb, l) == 0) (変更前): s->namesymb を計算された l バイトだけ比較していました。
  • if(strcmp(s->name, symb) == 0) (変更後): memcmpstrcmp に置き換えることで、s->namesymb がヌル終端文字列として比較されるようになりました。これにより、l の計算と使用が不要になり、コードが簡潔かつ安全になりました。

この変更は、Go言語のツールチェインがC言語で書かれていた時代のコードベースにおける、C言語の文字列操作のベストプラクティスへの移行を示しています。

関連リンク

参考にした情報源リンク