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

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

このコミットは、Go言語のリンカ (cmd/ld) における cwrite 関数が、長さ0の書き込みを行う際に nil ポインタを渡した場合に、Plan 9カーネル上で「無効なアドレスでのシステムコール」エラーによりプロセスが終了する問題を修正するものです。具体的には、cwrite 関数内で書き込みサイズが0以下の場合に早期リターンすることで、この問題を回避しています。

コミット

cmd/ld: skip 0-length write in cwrite

The 0-length part is fine, but some callers that write 0 bytes
also pass nil as the data pointer, and the Plan 9 kernel kills the
process with 'invalid address in sys call' in that case.

R=ken2
CC=golang-dev
https://golang.org/cl/6862051

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

https://github.com/golang/go/commit/9d2b0e86c8338d8e8f285926752ecca642ce23da

元コミット内容

このコミットは、Go言語のリンカ (cmd/ld) 内の cwrite 関数において、長さ0のデータを書き込む際に、データポインタとして nil が渡されるケースがあることに起因する問題を解決します。特に、Plan 9オペレーティングシステム上でこの状況が発生すると、カーネルが「無効なアドレスでのシステムコール」というエラーでプロセスを強制終了させてしまうため、リンカの動作が不安定になる可能性がありました。このコミットの目的は、cwrite 関数が0バイトの書き込み要求を受けた際に、実際に write システムコールを呼び出す前に処理をスキップすることで、この特定のクラッシュを防ぐことです。

変更の背景

Go言語のリンカは、コンパイルされたオブジェクトファイルを結合して実行可能ファイルを生成する重要なツールです。このリンカの内部では、様々なデータをファイルに書き込むために cwrite のようなヘルパー関数が使用されます。

問題の背景には、以下の2つの要因が絡んでいます。

  1. 0バイト書き込みの正当性: 一般的に、ファイルへの0バイト書き込みは、それ自体は有効な操作であり、多くのシステムではエラーとはなりません。これは、ファイルポインタを移動させたり、特定の状態をリセットしたりする目的で利用されることがあります。
  2. nil ポインタとシステムコールの挙動: write システムコールは、書き込むデータの開始アドレスを指すポインタを引数として取ります。通常、書き込むデータがない(つまりサイズが0)場合でも、有効なポインタを渡すことが期待されます。しかし、一部の呼び出し元では、0バイト書き込みの際に、データポインタとして nil (ヌルポインタ) を渡してしまうケースがありました。

この組み合わせが、特にPlan 9カーネル上で問題を引き起こしました。Plan 9は、ベル研究所で開発された分散システム向けのオペレーティングシステムであり、そのカーネルはシステムコールの引数に対してより厳格なチェックを行う場合があります。write システムコールに nil ポインタと0バイトの長さを渡した場合、他のOSでは問題なく処理されるか、単に何もしないで成功を返すことが多いですが、Plan 9カーネルは nil ポインタを「無効なアドレス」と判断し、システムコールが実行される前にプロセスを終了させてしまう挙動を示しました。

このクラッシュはリンカの安定性を損なうため、GoリンカがPlan 9環境で確実に動作するようにするために、この修正が必要とされました。

前提知識の解説

Go言語のリンカ (cmd/ld)

Go言語のコンパイルプロセスにおいて、cmd/ld はリンカ(linker)の役割を担います。リンカは、Goコンパイラ (cmd/compile) によって生成された複数のオブジェクトファイル(.o ファイル)やアーカイブファイル(.a ファイル)、共有ライブラリなどを結合し、最終的な実行可能ファイルやライブラリを生成します。この過程で、シンボルの解決、アドレスの再配置、セクションの結合などが行われます。cmd/ld はGoツールチェーンの一部であり、Goプログラムのビルドにおいて不可欠なコンポーネントです。

cwrite 関数

cwrite は、Goリンカ (cmd/ld) の内部で使用されるヘルパー関数の一つで、ファイルへの書き込み処理を抽象化しています。この関数は、指定されたバッファから指定されたバイト数のデータを、リンカが現在操作している出力ファイルに書き込む役割を担います。通常、cwrite は低レベルの write システムコールをラップしており、エラーハンドリングやバッファリングなどの追加機能を提供することがあります。

write システムコール

write は、Unix系オペレーティングシステムにおける基本的なシステムコールの一つです。プログラムがファイルディスクリプタを通じてファイルやデバイスにデータを書き込むために使用されます。そのシグネチャは通常、ssize_t write(int fd, const void *buf, size_t count); のようになります。

  • fd: 書き込み先のファイルディスクリプタ。
  • buf: 書き込むデータが格納されているバッファへのポインタ。
  • count: 書き込むバイト数。

write システムコールは、count が0の場合、通常は何も書き込まずに0を返します。このとき bufNULL であっても、多くのシステムではエラーとはなりません。しかし、特定のOS(今回の場合はPlan 9)では、bufNULL であること自体を無効なアドレスと判断し、count の値に関わらずエラーを発生させることがあります。

nil ポインタ (ヌルポインタ)

nil ポインタ(C言語では NULL ポインタ)は、有効なメモリ位置を指していないことを示す特別なポインタ値です。プログラミングにおいて、ポインタがどのオブジェクトも指していない状態を表すために使用されます。nil ポインタを逆参照しようとすると、通常はセグメンテーション違反などの実行時エラーが発生します。しかし、write システムコールのように、ポインタが指す内容ではなくポインタ値自体が引数として渡される場合、そのシステムコールの実装やOSのカーネルの挙動によって、nil ポインタの扱いが異なります。

Plan 9カーネル

Plan 9は、ベル研究所で開発された分散システム向けのオペレーティングシステムです。Unixの概念をさらに推し進め、すべてをファイルとして扱うという哲学を徹底しています。Plan 9カーネルは、その設計思想や実装において、他のUnix系OSとは異なる挙動を示すことがあります。今回のケースでは、write システムコールに nil ポインタが渡された際の挙動が、一般的なLinuxやBSDカーネルとは異なり、より厳格なチェックを行うことで「invalid address in sys call」というエラーを引き起こしました。

技術的詳細

この問題は、Goリンカの cwrite 関数が、0バイトの書き込み要求を受けた際に、データバッファとして nil ポインタを write システムコールに渡してしまうことに起因します。

一般的な write システムコールの実装では、count 引数が0の場合、buf 引数の値は無視されるか、あるいはそのポインタが指すメモリ領域へのアクセスは行われません。そのため、bufnil であっても問題なく処理が完了し、0が返されることがほとんどです。

しかし、Plan 9カーネルの write システムコール実装は、buf 引数に対して、たとえ count が0であっても、有効なメモリ領域を指しているかどうかを事前にチェックする挙動を示しました。nil ポインタは有効なメモリ領域を指していないため、Plan 9カーネルはこのチェックで引っかかり、「invalid address in sys call」(システムコールにおける無効なアドレス)というエラーを発生させ、結果としてリンカプロセスを強制終了させていました。

このコミットによる修正は、このPlan 9カーネルの特定の挙動を回避するためのものです。cwrite 関数内で、実際に write システムコールを呼び出す前に、書き込みサイズ n が0以下であるかをチェックします。もし n が0以下であれば、書き込むべきデータは存在しないため、write システムコールを呼び出すことなく、関数から即座にリターンします。これにより、nil ポインタが write システムコールに渡される状況自体をなくし、Plan 9環境でのクラッシュを防ぎます。

この修正は、Goリンカのクロスプラットフォーム互換性を向上させるための重要なバグ修正であり、特定のOS環境でのみ顕在化するエッジケースへの対応を示しています。

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

変更は src/cmd/ld/lib.c ファイルの cwrite 関数内で行われています。

--- a/src/cmd/ld/lib.c
+++ b/src/cmd/ld/lib.c
@@ -1493,6 +1493,8 @@ void
 cwrite(void *buf, int n)
 {
 	cflush();
+	if(n <= 0)
+		return;
 	if(write(cout, buf, n) != n) {
 		diag("write error: %r");
 		errorexit();

コアとなるコードの解説

追加されたコードは以下の2行です。

	if(n <= 0)
		return;

この if 文は、cwrite 関数が受け取る n (書き込むバイト数) の値が0以下であるかどうかをチェックしています。

  • n が0の場合: 書き込むデータは存在しません。
  • n が負の場合: これは通常、不正な引数ですが、いずれにせよ書き込みはできません。

いずれのケースでも、実際にデータをファイルに書き込む必要はありません。したがって、この条件が真であれば、関数は即座に return し、その後の write(cout, buf, n) システムコールの呼び出しをスキップします。

この変更により、n が0であるにもかかわらず bufnil ポインタであるという、Plan 9カーネルで問題を引き起こしていた特定のシナリオが回避されます。write システムコールが呼び出されないため、nil ポインタがシステムコールに渡されることがなくなり、結果として「invalid address in sys call」エラーによるクラッシュが防止されます。

この修正は、機能的な変更を伴わず、特定の環境での安定性を向上させるための防御的なプログラミングの一例と言えます。

関連リンク

参考にした情報源リンク