[インデックス 17616] ファイルの概要
コミット
このコミットは、Go言語のcmd/cgo
ツールとruntime/cgo
パッケージにおけるC言語のmalloc(0)
呼び出しの挙動を修正するものです。具体的には、C.malloc(0)
が常に成功するように変更し、malloc(0)
がNULL
を返した場合でもプログラムがクラッシュしないように対処しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/647eaed93b2aba07d4d4d032d5f58bf6dbc06e5a
元コミット内容
commit 647eaed93b2aba07d4d4d032d5f58bf6dbc06e5a
Author: Russ Cox <rsc@golang.org>
Date: Mon Sep 16 14:04:55 2013 -0400
cmd/cgo: allow C.malloc(0) always
Because we can, and because it otherwise might crash
the program if we think we're out of memory.
Fixes #6390.
R=golang-dev, iant, minux.ma
CC=golang-dev
https://golang.org/cl/13345048
---
misc/cgo/test/cgo_test.go | 1 +
src/cmd/cgo/out.go | 2 ++
src/pkg/runtime/cgo/gcc_util.c | 2 ++
3 files changed, 5 insertions(+)
diff --git a/misc/cgo/test/cgo_test.go b/misc/cgo/test/cgo_test.go
index e36f93597c..38151abca8 100644
--- a/misc/cgo/test/cgo_test.go
+++ b/misc/cgo/test/cgo_test.go
@@ -46,5 +46,6 @@ func Test3250(t *testing.T) { test3250(t) }\n func TestCallbackStack(t *testing.T) { testCallbackStack(t) }\n func TestFpVar(t *testing.T) { testFpVar(t) }\n func Test4339(t *testing.T) { test4339(t) }\n+func Test6390(t *testing.T) { test6390(t) }\n \n func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) }\ndiff --git a/src/cmd/cgo/out.go b/src/cmd/cgo/out.go
index efa55a335b..9cf8dc55be 100644
--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -1225,6 +1225,8 @@ Slice GoBytes(char *p, int32_t n) {
extern void runtime_throw(const char *):\n void *Cmalloc(size_t n) {\n void *p = malloc(n);\n+ if(p == NULL && n == 0)\n+ p = malloc(1);\n if(p == NULL)\n runtime_throw(\"runtime: C malloc failed\");\n return p;\ndiff --git a/src/pkg/runtime/cgo/gcc_util.c b/src/pkg/runtime/cgo/gcc_util.c
index 20913d7369..143734e94b 100644
--- a/src/pkg/runtime/cgo/gcc_util.c
+++ b/src/pkg/runtime/cgo/gcc_util.c
@@ -14,6 +14,8 @@ x_cgo_malloc(void *p)\n \t} *a = p;\n \n \ta->ret = malloc(a->n);\n+\tif(a->ret == NULL && a->n == 0)\n+\t\ta->ret = malloc(1);\n }\n \n /* Stub for calling free from Go */\n```
## 変更の背景
このコミットは、Go言語のIssue #6390「`C.malloc(0)` should not crash」を修正するために行われました。
一般的なC言語の標準ライブラリ関数である`malloc(0)`の挙動は、C標準(ISO/IEC 9899:1999, 7.20.3.3 The `malloc` function)において「サイズが0の場合、`malloc`はNULLポインタまたはそのオブジェクトを解放できるようなユニークなポインタを返す」と規定されています。つまり、`malloc(0)`が`NULL`を返すことは標準で許容される挙動です。
しかし、Goの`cgo`パッケージを通じてCの`malloc`を呼び出す際、`C.malloc(0)`が`NULL`を返した場合、Goランタイムはこれをメモリ不足と誤解し、`runtime_throw("runtime: C malloc failed")`を呼び出してプログラムをパニックさせていました。これは、`malloc(0)`が`NULL`を返すことが必ずしもエラーではないにもかかわらず、Goランタイムがそれをエラーとして扱ってしまうという問題でした。
この挙動は、特にCライブラリが`malloc(0)`を呼び出す可能性のあるシナリオで、Goプログラムが予期せずクラッシュする原因となっていました。このコミットは、`malloc(0)`が`NULL`を返した場合でも、それをメモリ不足とは見なさず、代わりに1バイトのメモリを割り当てることで、この問題を解決しようとしています。これにより、`C.malloc(0)`が常に有効な(非`NULL`の)ポインタを返すようになり、Goランタイムが誤ってパニックを起こすことを防ぎます。
## 前提知識の解説
### Cgo
Cgoは、GoプログラムからC言語のコードを呼び出すためのGoの機能です。GoとCの間の相互運用を可能にし、既存のCライブラリをGoプロジェクトで利用したり、パフォーマンスが重要な部分をCで記述したりする際に使用されます。Cgoを使用すると、Goのコード内で`import "C"`と記述し、Cの関数や変数に`C.`プレフィックスを付けてアクセスできます。
### `malloc`関数
`malloc`はC言語の標準ライブラリ関数で、指定されたサイズのメモリブロックをヒープから動的に割り当てます。成功すると、割り当てられたメモリブロックの先頭へのポインタを返します。失敗すると`NULL`を返します。
`malloc(0)`の挙動は、C標準で定義されており、`NULL`を返すか、または後で`free`できる有効なポインタを返すかのどちらかです。多くのシステムでは、`malloc(0)`は有効な(非`NULL`の)ポインタを返しますが、`NULL`を返す実装も存在します。
### Goランタイムのメモリ管理とパニック
Goランタイムは独自のメモリ管理システムを持っています。Cgoを通じてCのメモリ割り当て関数(`malloc`など)を呼び出す場合、Goランタイムはその結果を監視します。もし`malloc`が`NULL`を返した場合、Goランタイムは通常、メモリ割り当てに失敗したと判断し、`runtime_throw`関数を呼び出してプログラムをパニックさせます。`runtime_throw`は、回復不可能なエラーが発生した際にGoプログラムを終了させるための内部関数です。
### `size_t`型
`size_t`はC言語で定義されている符号なし整数型で、メモリのサイズや配列のインデックスを表すために使用されます。`malloc`関数の引数として使用され、割り当てるメモリのバイト数を指定します。
## 技術的詳細
このコミットの技術的詳細は、`C.malloc(0)`が`NULL`を返した場合のGoランタイムの挙動を修正することにあります。
Goの`cgo`ツールは、GoコードからC関数を呼び出すためのラッパーコードを生成します。`C.malloc`のような呼び出しは、最終的にCの`malloc`関数に変換されます。
変更は主に2つのファイルで行われています。
1. **`src/cmd/cgo/out.go`**:
このファイルは、`cgo`ツールがGoコードからC関数を呼び出すためのCラッパーコードを生成する部分です。`Cmalloc`という関数は、Goの`C.malloc`呼び出しに対応するC側のラッパー関数です。
元のコードでは、`malloc(n)`の呼び出し結果が`NULL`だった場合、無条件に`runtime_throw`を呼び出してパニックさせていました。
このコミットでは、`if(p == NULL && n == 0)`という条件が追加されました。これは、`malloc`が`NULL`を返し、かつ要求されたサイズ`n`が0であった場合にのみ適用されます。この条件が真の場合、`p = malloc(1)`が実行され、1バイトのメモリを割り当て直します。これにより、`Cmalloc`は`n=0`の場合でも`NULL`以外のポインタを返すことが保証されます。
2. **`src/pkg/runtime/cgo/gcc_util.c`**:
このファイルは、GoランタイムがCgo関連のユーティリティ関数を実装している部分です。`x_cgo_malloc`は、GoランタイムがCの`malloc`を呼び出すための内部的なヘルパー関数です。
ここでも同様に、`malloc(a->n)`の呼び出し結果が`NULL`だった場合に、`if(a->ret == NULL && a->n == 0)`という条件が追加されました。`a->n`は要求されたサイズを表します。この条件が真の場合、`a->ret = malloc(1)`が実行され、1バイトのメモリを割り当て直します。
この変更により、`malloc(0)`が`NULL`を返すというC標準で許容される挙動が発生した場合でも、Goランタイムはそれをメモリ不足とは判断せず、代わりに1バイトのメモリを割り当てることで、プログラムのクラッシュを防ぎます。これは、`malloc(0)`が返すポインタは通常、実際にメモリを使用しないため、1バイトを割り当てることで実質的なオーバーヘッドはほとんどなく、安全性を高めるための実用的な解決策です。
## コアとなるコードの変更箇所
### `src/cmd/cgo/out.go`
```diff
--- a/src/cmd/cgo/out.go
+++ b/src/cmd/cgo/out.go
@@ -1225,6 +1225,8 @@ Slice GoBytes(char *p, int32_t n) {
extern void runtime_throw(const char *):\n void *Cmalloc(size_t n) {\n void *p = malloc(n);\n+ if(p == NULL && n == 0)\n+ p = malloc(1);\n if(p == NULL)\n runtime_throw(\"runtime: C malloc failed\");
return p;
src/pkg/runtime/cgo/gcc_util.c
--- a/src/pkg/runtime/cgo/gcc_util.c
+++ b/src/pkg/runtime/cgo/gcc_util.c
@@ -14,6 +14,8 @@ x_cgo_malloc(void *p)\n \t} *a = p;\n \n \ta->ret = malloc(a->n);\n+\tif(a->ret == NULL && a->n == 0)\n+\t\ta->ret = malloc(1);\n }\n \n /* Stub for calling free from Go */\n```
### `misc/cgo/test/cgo_test.go`
```diff
--- a/misc/cgo/test/cgo_test.go
+++ b/misc/cgo/test/cgo_test.go
@@ -46,5 +46,6 @@ func Test3250(t *testing.T) { test3250(t) }\n func TestCallbackStack(t *testing.T) { testCallbackStack(t) }\n func TestFpVar(t *testing.T) { testFpVar(t) }\n func Test4339(t *testing.T) { test4339(t) }\n+func Test6390(t *testing.T) { test6390(t) }\n \n func BenchmarkCgoCall(b *testing.B) { benchCgoCall(b) }\n```
テストファイルに`Test6390`が追加され、この修正が正しく機能するかを検証するためのテストケースが導入されたことを示唆しています。
## コアとなるコードの解説
### `src/cmd/cgo/out.go` の変更
`Cmalloc`関数は、Goコードから`C.malloc`が呼び出されたときに実際にC側でメモリ割り当てを行うラッパーです。
追加されたコード:
```c
if(p == NULL && n == 0)
p = malloc(1);
このif
文は、malloc(n)
の呼び出し結果p
がNULL
であり、かつ要求されたサイズn
が0
であった場合にのみ実行されます。この条件が満たされた場合、malloc(1)
を呼び出して1バイトのメモリを割り当て直します。これにより、Cmalloc
はn=0
の場合でもNULL
以外の有効なポインタを返すようになります。その後のif(p == NULL)
チェックは、malloc(1)
も失敗した場合(真のメモリ不足の場合)にのみruntime_throw
を呼び出すことになります。
src/pkg/runtime/cgo/gcc_util.c
の変更
x_cgo_malloc
関数は、Goランタイム内部でCのmalloc
を呼び出す際に使用されるユーティリティ関数です。
追加されたコード:
if(a->ret == NULL && a->n == 0)
a->ret = malloc(1);
これはsrc/cmd/cgo/out.go
の変更と全く同じロジックです。a->ret
はmalloc
の戻り値を、a->n
は要求されたサイズを保持しています。ここでも、malloc(0)
がNULL
を返した場合に1バイトのメモリを割り当て直すことで、Goランタイムが誤ってパニックを起こすのを防ぎます。
これらの変更により、GoプログラムがCgoを通じてC.malloc(0)
を呼び出した際に、Cのmalloc
がNULL
を返しても、Goランタイムがそれをメモリ不足と誤解してパニックを起こすことがなくなりました。代わりに、1バイトのメモリが割り当てられ、有効なポインタがGo側に返されるようになります。これは、malloc(0)
が返すポインタが通常は実際にメモリを使用しないという特性を利用した、堅牢性を高めるための実用的な修正です。
関連リンク
- Go Issue #6390: https://github.com/golang/go/issues/6390
- Go CL 13345048: https://golang.org/cl/13345048
参考にした情報源リンク
- C Standard (ISO/IEC 9899:1999) - 7.20.3.3 The
malloc
function: (具体的なURLは標準文書のため提供できませんが、C標準のmalloc
関数の定義を参照しました。) - Go Programming Language Documentation - Cgo: https://go.dev/blog/c-go-cgo
malloc(0)
behavior: https://stackoverflow.com/questions/1120471/what-does-malloc0-return- Go runtime source code (relevant versions for context)
- Go issue tracker for #6390 discussion.