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

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

コミット

commit 8cb916f71d7d397905b9d8a5a0ea5c22871ac867
Author: Peter Collingbourne <pcc@google.com>
Date:   Sat Apr 26 22:16:38 2014 -0700

    cmd/cgo: fix C.CString for strings containing null terminators under gccgo
    
    Previously we used strndup(3) to implement C.CString for gccgo. This
    is wrong because strndup assumes the string to be null terminated,
    and stops at the first null terminator. Instead, use malloc
    and memmove to create a copy of the string, as we do in the
    gc implementation.
    
    LGTM=iant
    R=iant
    CC=golang-codereviews
    https://golang.org/cl/96790047

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

https://github.com/golang/go/commit/8cb916f71d7d397905b9d8a5a0ea5c22871ac867

元コミット内容

cmd/cgo: fix C.CString for strings containing null terminators under gccgo

Previously we used strndup(3) to implement C.CString for gccgo. This is wrong because strndup assumes the string to be null terminated, and stops at the first null terminator. Instead, use malloc and memmove to create a copy of the string, as we do in the gc implementation.

変更の背景

このコミットは、gccgoコンパイラを使用している場合に、GoのC.CString関数がヌル終端文字を含む文字列を正しく処理できないというバグを修正するものです。

GoのC.CString関数は、Goの文字列をC言語の文字列(char*)に変換するために使用されます。C言語の文字列は通常、ヌル終端文字(\0)で文字列の終わりを示します。しかし、Goの文字列はバイナリセーフであり、内部にヌル終端文字を含むことができます。

以前のgccgoの実装では、C.CStringの内部でC標準ライブラリ関数であるstrndup(3)を使用していました。strndupは指定された長さまで文字列を複製しますが、その名前が示す通り、内部的にはヌル終端文字列を扱うことを前提としています。具体的には、strndupは指定された長さの範囲内であっても、最初のヌル終端文字で文字列のコピーを停止してしまいます。

この挙動により、Goの文字列が途中にヌル終端文字を含んでいた場合、C.CStringによって変換されたC文字列は、元のGo文字列の途中で切り詰められてしまい、データが欠損するという問題が発生していました。これは、特にバイナリデータや、意図的にヌル文字を含む文字列をC関数に渡す必要がある場合に深刻なバグとなります。

Goの標準コンパイラであるgcの実装では、この問題は発生せず、mallocmemmoveを組み合わせて文字列全体を正確にコピーしていました。このコミットは、gccgoの実装をgcの実装と整合させることで、この不整合とバグを解消することを目的としています。

前提知識の解説

Cgo

Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。また、C言語のコードからGoの関数を呼び出すことも可能です。Cgoを使用することで、既存のCライブラリをGoプロジェクトで再利用したり、パフォーマンスが重要な部分をCで記述したりすることができます。

Cgoを使用する際には、Goの型とCの型の間の変換が重要になります。特に文字列の扱いは注意が必要です。Goの文字列はバイト列であり、その長さは別途管理されますが、Cの文字列は慣習的にヌル終端文字(\0)でその終わりを示します。

C.CString

C.CStringはCgoが提供する関数の一つで、Goのstring型をCのchar*型に変換するために使用されます。この関数は、Goの文字列の内容をCのメモリ領域にコピーし、そのメモリへのポインタを返します。返されたC文字列は、Cgoによって管理されるメモリに割り当てられるため、使用後はC.freeで解放する必要があります。

ヌル終端文字列 (Null-terminated string)

C言語において、文字列は通常、一連の文字の後にヌル文字(ASCII値0、\0)が続くバイト配列として表現されます。ヌル文字は文字列の終端を示すマーカーとして機能します。多くのC標準ライブラリ関数(例: strlen, strcpy, printf%sフォーマット指定子)は、このヌル終端文字を文字列の終わりとして認識します。

strndup(3)

strndupは、C標準ライブラリ(通常はPOSIX標準の一部)で提供される関数です。そのプロトタイプは通常 char *strndup(const char *s, size_t n); のようになります。 この関数は、sが指す文字列の先頭から最大nバイトを複製し、新しく割り当てられたメモリにそのコピーを格納します。複製された文字列はヌル終端されます。 しかし、strndupの重要な特性は、nバイトに達する前にヌル終端文字が見つかった場合、そこでコピーを停止し、そのヌル終端文字を含めて複製することです。つまり、strndupは「最大nバイト、または最初のヌル文字まで」という挙動をします。

malloc

mallocはC標準ライブラリ(stdlib.h)で提供される関数で、指定されたサイズのメモリブロックをヒープから動的に割り当てます。割り当てられたメモリは初期化されません。成功すると、割り当てられたメモリブロックの先頭へのポインタ(void*)を返します。メモリが割り当てられなかった場合はNULLを返します。

memmove

memmoveはC標準ライブラリ(string.h)で提供される関数で、指定されたバイト数のデータを、ソースメモリ領域からデスティネーションメモリ領域へコピーします。そのプロトタイプは通常 void *memmove(void *dest, const void *src, size_t n); のようになります。 memcpyと異なり、memmoveはソースとデスティネーションのメモリ領域がオーバーラップしている場合でも正しく動作することを保証します。これは、データが一時的なバッファを介してコピーされることで実現されます。この特性により、任意のバイト列を正確にコピーするのに適しています。

gcgccgo

Go言語には主に2つのコンパイラ実装があります。

  • gc: Goチームが開発している公式のコンパイラです。Goのソースコードを直接機械語にコンパイルします。
  • gccgo: GCC(GNU Compiler Collection)のフロントエンドとしてGoをサポートするコンパイラです。GoのソースコードをGCCの中間表現に変換し、その後GCCのバックエンドが機械語にコンパイルします。gccgoはGCCの最適化やターゲットプラットフォームのサポートを利用できるという利点がありますが、gcとは異なる実装の詳細を持つことがあります。本コミットは、C.CStringの実装におけるこの違いに起因する問題に対処しています。

技術的詳細

このコミットの核心は、gccgoにおけるC.CStringの実装が、Goの文字列が持つ「バイナリセーフ」という特性を尊重していなかった点にあります。Goの文字列は、任意のバイト列を格納でき、その中にヌル文字(\0)が含まれていても、文字列の長さは別途管理されるため問題なく扱えます。

しかし、gccgoC.CStringは、内部でC標準ライブラリのstrndup関数を使用していました。strndup(s, n)は、sが指す文字列から最大nバイトをコピーしますが、途中でヌル文字が見つかった場合はそこでコピーを停止し、そのヌル文字を含めて新しい文字列をヌル終端します。

例えば、Goの文字列が"hello\0world"(長さ11)であったとします。これをC.CStringに渡すと、strndup"hello"(長さ5)の時点でヌル文字を見つけ、そこでコピーを終了してしまいます。結果として、C側で受け取る文字列は"hello"となり、元のGo文字列の後半部分("world")が失われてしまいます。これは、Goの文字列がバイナリセーフであるという期待に反する動作です。

この問題を解決するため、コミットではstrndupの使用を廃止し、gcコンパイラの実装と同様に、mallocmemmoveを組み合わせる方法に変更しました。

  1. malloc(s.__length + 1): まず、Goの文字列の実際の長さ(s.__length)に1バイトを追加したサイズのメモリをmallocで動的に確保します。この追加の1バイトは、C文字列の慣習に従って最後にヌル終端文字を配置するために使用されます。
  2. memmove(p, s.__data, s.__length): 次に、Goの文字列の生データ(s.__data)を、mallocで確保した新しいメモリ領域pに、Goの文字列の実際の長さ(s.__length)分だけ正確にコピーします。memmoveは、ソースとデスティネーションがオーバーラップしていても安全にコピーできるため、このようなバイト列のコピーに適しています。
  3. p[s.__length] = 0: 最後に、コピーされた文字列の末尾(s.__lengthの位置)に明示的にヌル終端文字(0)を書き込みます。これにより、C言語の規約に準拠したヌル終端文字列が完成します。

この変更により、Goの文字列が内部にヌル文字を含んでいても、C.CStringは常にGoの文字列全体を正確にCのメモリにコピーし、その後にヌル終端文字を追加するようになります。これにより、gccgogcの間でC.CStringの挙動が統一され、Goの文字列のバイナリセーフティがCgoのコンテキストでも維持されるようになりました。

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

--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -1225,7 +1225,10 @@ struct __go_string __go_byte_array_to_string(const void* p, intgo len);\
 struct __go_open_array __go_string_to_byte_array (struct __go_string str);\
 
 const char *_cgoPREFIX_Cfunc_CString(struct __go_string s) {
-	return strndup((const char*)s.__data, s.__length);\
+	char *p = malloc(s.__length+1);\
+	memmove(p, s.__data, s.__length);\
+	p[s.__length] = 0;\
+	return p;\
 }\
 
 struct __go_string _cgoPREFIX_Cfunc_GoString(char *p) {\

コアとなるコードの解説

変更はsrc/cmd/cgo/out.goファイル内の_cgoPREFIX_Cfunc_CString関数にあります。この関数は、Goの文字列(内部的には__go_string構造体として表現される)をCのconst char*に変換するCgoの内部ヘルパー関数です。

変更前:

const char *_cgoPREFIX_Cfunc_CString(struct __go_string s) {
	return strndup((const char*)s.__data, s.__length);
}

変更前は、strndup関数が使用されていました。s.__dataはGo文字列のバイトデータへのポインタ、s.__lengthはその長さです。前述の通り、strndupはヌル文字で途中で停止する可能性があるため、Goの文字列が内部にヌル文字を含む場合に問題がありました。

変更後:

const char *_cgoPREFIX_Cfunc_CString(struct __go_string s) {
	char *p = malloc(s.__length+1);
	memmove(p, s.__data, s.__length);
	p[s.__length] = 0;
	return p;
}

変更後では、以下の3ステップで文字列のコピーが行われます。

  1. char *p = malloc(s.__length+1);

    • s.__lengthは元のGo文字列のバイト長です。
    • +1は、C文字列の終端を示すヌル文字(\0)のために追加されるバイトです。
    • mallocによって、この長さのメモリブロックがヒープに確保され、その先頭アドレスがポインタpに格納されます。
  2. memmove(p, s.__data, s.__length);

    • s.__dataは、Go文字列の実際のバイトデータが格納されているメモリ領域の先頭ポインタです。
    • s.__lengthは、コピーするバイト数です。
    • memmove関数は、s.__dataからs.__lengthバイトを、新しく確保したメモリ領域pに正確にコピーします。これにより、Go文字列のすべてのバイト(ヌル文字を含む場合でも)が忠実に複製されます。
  3. p[s.__length] = 0;

    • コピーされた文字列の直後、つまりs.__lengthのインデックス位置に、明示的にヌル文字(0)を書き込みます。これにより、C言語の規約に準拠したヌル終端文字列が完成します。

この修正により、C.CStringはGoの文字列のバイナリセーフな性質を完全に尊重し、ヌル文字を含むGo文字列もC側で正しく扱えるようになりました。

関連リンク

参考にした情報源リンク

  • 特になし