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

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

このコミットは、Goランタイムにおけるtimediv関数の呼び出しに関する修正です。特にNetBSDとOpenBSD環境での問題を対象としており、timespec構造体のtv_nsecフィールドの扱いにおけるエンディアンネスの仮定を明示しています。

コミット

commit 98cc58e2c71284c1f56cf27758091f6f1d7992bf
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jul 29 16:31:42 2013 -0400

    runtime: fix timediv calls on NetBSD, OpenBSD
    
    Document endian-ness assumption.
    
    R=dvyukov
    CC=golang-dev
    https://golang.org/cl/12056044

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

https://github.com/golang/go/commit/98cc58e2c71284c1f56cf27758091f6f1d7992bf

元コミット内容

このコミットの元のメッセージは以下の通りです。

runtime: fix timediv calls on NetBSD, OpenBSD

Document endian-ness assumption.

R=dvyukov
CC=golang-dev
https://golang.org/cl/12056044

これは、GoランタイムがNetBSDおよびOpenBSD上でtimediv関数を呼び出す際の問題を修正し、エンディアンネスに関する仮定をドキュメント化したことを示しています。

変更の背景

このコミットの背景には、Goランタイムがシステムコールを通じて時間関連の操作を行う際の、特定のOS(NetBSD、OpenBSD)におけるtimediv関数の誤った使用がありました。特に、timespec構造体のtv_nsecフィールド(ナノ秒部分)の型と、timediv関数への引数の渡し方に問題がありました。

timespec構造体は、秒とナノ秒で時間を表現するための標準的なC言語の構造体です。通常、tv_sec(秒)とtv_nsec(ナノ秒)の2つのフィールドを持ちます。tv_nsecはナノ秒を表し、0から999,999,999の範囲の値を取ります。

amd64アーキテクチャ(x64としても知られる)では、tv_nsecフィールドは通常long型であり、これは64ビット整数(int64)として機能します。しかし、Goランタイム内のtimediv関数への呼び出しにおいて、このtv_nsecの値を32ビット整数へのポインタとして渡そうとしていました。

元のコードでは、timediv関数にts.tv_nsecint32へのポインタとしてキャストして渡していました(例: (int32*)ts.tv_nsec)。これは、tv_nsecの実際の値(ナノ秒数)をメモリアドレスとして解釈し、そのアドレスが指す場所を32ビット整数として扱おうとする、根本的に誤った型キャストです。このような操作は、無効なメモリアクセスを引き起こし、プログラムのクラッシュ(セグメンテーション違反など)や未定義の動作につながる可能性が非常に高いです。特に、時間計算のようなクリティカルな部分でのこのようなバグは、システム全体の不安定性やデッドロックなどの深刻な問題を引き起こす可能性があります。

この問題は、NetBSDとOpenBSD環境で顕在化していたようです。Goランタイムにおけるtimediv関連のバグは、これらのOSでゼロ除算エラーとして現れることがあり、パニックやプログラムの誤動作につながっていました。このコミットは、この根本的な型キャストの誤りを修正し、同時にamd64システムがリトルエンディアンであることを前提としていることを明示することで、コードの意図を明確にしています。

前提知識の解説

1. timespec構造体

timespecは、POSIX標準で定義されている時間構造体で、秒とナノ秒の精度で時間を表現します。

struct timespec {
    time_t tv_sec;  // 秒
    long   tv_nsec; // ナノ秒 (0から999,999,999)
};
  • tv_sec: エポック(通常は1970年1月1日00:00:00 UTC)からの経過秒数。
  • tv_nsec: tv_secで表される秒の後のナノ秒部分。

2. timediv関数(Goランタイム内部関数)

Goランタイム内部のtimediv関数は、おそらく大きな時間値(ナノ秒単位のint64)を、秒とナノ秒に分割するためのユーティリティ関数です。例えば、nsナノ秒を1000000000(10億)で割って秒数と残りのナノ秒数を計算するような処理を行います。 一般的なシグネチャは以下のようになっていると推測されます。 func timediv(val int64, div int64, rem *int32) int64

  • val: 割られる値(例: 合計ナノ秒数)
  • div: 割る値(例: 10億)
  • rem: 剰余(ナノ秒部分)を格納するためのint32へのポインタ。

3. エンディアンネス (Endianness)

エンディアンネスとは、マルチバイトのデータをメモリに格納する際のバイト順序のことです。

  • リトルエンディアン (Little-endian): 最下位バイト (Least Significant Byte, LSB) が最も小さいメモリアドレスに格納されます。amd64アーキテクチャはリトルエンディアンです。
  • ビッグエンディアン (Big-endian): 最上位バイト (Most Significant Byte, MSB) が最も小さいメモリアドレスに格納されます。

このコミットでは、tv_nsecint64であるにもかかわらず、timediv関数が剰余をint32へのポインタで受け取る設計になっているため、エンディアンネスが重要になります。リトルエンディアンシステムでは、64ビット整数の下位32ビットがメモリの先頭に位置するため、int64の先頭アドレスをint32*としてキャストしても、そのint32int64の下位32ビットを正しく指すことになります。tv_nsecの値(0から999,999,999)は32ビット整数に収まるため、このアプローチが機能します。

4. ポインタと型キャスト

C言語(Goランタイムの一部はCで書かれているか、Cの概念を強く反映している)において、ポインタはメモリ上のアドレスを指します。型キャストは、ある型の値を別の型として解釈する操作です。

  • (int32*)ts.tv_nsec: ts.tv_nsecint32へのポインタとして解釈しようとする。これは、ts.tv_nsecがメモリアドレスであるかのように扱うため、通常は誤りであり、クラッシュの原因となります。
  • (int32*)&ts.tv_nsec: ts.tv_nsecアドレスint32へのポインタとして解釈しようとする。これは、ts.tv_nsecが格納されているメモリ位置を、int32が格納されているかのように扱うことを意味します。tv_nsecint64である場合、このキャストはint64のメモリ領域の先頭をint32として指すことになります。リトルエンディアンシステムでは、int64の下位32ビットがその先頭に位置するため、timedivがそこに32ビットの剰余を書き込むと、int64tv_nsecの下位32ビットが更新されることになります。

技術的詳細

このコミットの核心は、Goランタイムがtimediv関数を呼び出す際の引数の渡し方の修正と、その背後にあるエンディアンネスの仮定の明示です。

Goランタイムは、OSのシステムコール(FreeBSDのumtx_op、Linuxのfutex、NetBSD/OpenBSDのlwp_park/thrsleepなど)と連携して、スレッドの待機やタイムアウト処理を行います。これらのシステムコールは、タイムアウト値をtimespec構造体で受け取ることが一般的です。

問題となっていたのは、timespec構造体のtv_nsecフィールドがamd64アーキテクチャではint64型であるにもかかわらず、Goランタイム内部のtimediv関数にその剰余部分を格納するための引数として、誤ったポインタ型を渡していた点です。

修正前: ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)ts.tv_nsec); このコードでは、ts.tv_nsecint32へのポインタとしてキャストしていました。これは、ts.tv_nsecに格納されているナノ秒の数値(例えば500,000,000)を、メモリ上のアドレス0x1dcd6500として解釈し、そのアドレスにtimedivが計算した剰余を書き込もうとします。しかし、0x1dcd6500は有効なメモリ領域ではない可能性が高く、結果としてセグメンテーション違反やメモリ破壊を引き起こします。これは非常に危険なバグであり、プログラムのクラッシュや予測不能な動作の原因となります。

修正後: ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec); この修正では、ts.tv_nsecアドレスint32へのポインタとしてキャストしています。&ts.tv_nsecint64*型ですが、これを(int32*)にキャストすることで、timediv関数はts.tv_nsecがメモリに格納されている場所の先頭32ビットに剰余を書き込むことになります。

この修正が正しく機能するためには、以下の2つの条件が満たされる必要があります。

  1. 剰余が32ビット整数に収まること: tv_nsecはナノ秒を表し、その値は0から999,999,999の範囲です。この値は32ビット符号付き整数(最大約20億)に収まるため、この条件は満たされます。
  2. システムがリトルエンディアンであること: amd64アーキテクチャはリトルエンディアンです。リトルエンディアンシステムでは、64ビット整数(int64)がメモリに格納される際、その最下位バイト(LSB)が最も低いアドレスに配置されます。したがって、int64のアドレスをint32*としてキャストすると、そのポインタはint64の下位32ビットが格納されているメモリ領域の先頭を指すことになります。timedivがそこに32ビットの剰余を書き込むと、int64であるts.tv_nsecのナノ秒部分が正しく更新されます。

このコミットは、このエンディアンネスの仮定をコードコメントとして明示することで、将来の読者や異なるアーキテクチャへの移植性を考慮した際に、この挙動が意図的であることを明確にしています。

このバグは、特にNetBSDとOpenBSDで問題を引き起こしていたとコミットメッセージに記載されています。これは、これらのOSの特定のシステムコールやランタイムの挙動が、この誤ったポインタキャストによるメモリ破壊をより顕著に表面化させたためと考えられます。

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

変更は以下の4つのファイルにわたります。

  1. src/pkg/runtime/os_freebsd.c
  2. src/pkg/runtime/os_linux.c
  3. src/pkg/runtime/os_netbsd.c
  4. src/pkg/runtime/os_openbsd.c

各ファイルでの変更は非常に類似しており、主にruntime·timediv関数の第3引数の渡し方と、エンディアンネスに関するコメントの追加です。

src/pkg/runtime/os_freebsd.c および src/pkg/runtime/os_linux.c:

--- a/src/pkg/runtime/os_freebsd.c
+++ b/src/pkg/runtime/os_freebsd.c
@@ -54,6 +54,7 @@ runtime·futexsleep(uint32 *addr, uint32 val, int64 ns)
 			return;
 		goto fail;
 	}
+	// NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
 	ts.tv_nsec = 0;
 	ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec);
 	ret = runtime·sys_umtx_op(addr, UMTX_OP_WAIT_UINT, val, nil, &ts);

src/pkg/runtime/os_netbsd.c および src/pkg/runtime/os_openbsd.c:

--- a/src/pkg/runtime/os_netbsd.c
+++ b/src/pkg/runtime/os_netbsd.c
@@ -95,8 +95,9 @@ runtime·semasleep(int64 ns)
 			        runtime·lwp_park(nil, 0, &m->waitsemacount, nil);
 			} else {
 			        ns += runtime·nanotime();
+			        // NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
 			        ts.tv_nsec = 0;
-			        ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)ts.tv_nsec);
+			        ts.tv_sec = runtime·timediv(ns, 1000000000, (int32*)&ts.tv_nsec);
 			        // TODO(jsing) - potential deadlock!
 			        // See above for details.
 			        runtime·atomicstore(&m->waitsemalock, 0);

コアとなるコードの解説

各ファイルにおける変更は、以下の2点に集約されます。

  1. timediv関数の第3引数の修正:

    • 変更前: (int32*)ts.tv_nsec
      • これはts.tv_nsecint32へのポインタとしてキャストしていました。例えば、ts.tv_nsec500000000という値を持っていた場合、コードはメモリアドレス0x500000000(約21GB)にアクセスしようとします。これは通常、無効なメモリアクセスであり、プログラムのクラッシュ(セグメンテーション違反)を引き起こします。
    • 変更後: (int32*)&ts.tv_nsec
      • これはts.tv_nsecアドレスint32へのポインタとしてキャストしています。&ts.tv_nsects.tv_nsec変数がメモリに格納されている実際の場所を指すポインタです。ts.tv_nsecint64型ですが、amd64アーキテクチャがリトルエンディアンであるため、その下位32ビットがメモリの先頭に位置します。timediv関数が剰余を32ビット整数としてそのポインタ先に書き込むことで、ts.tv_nsecのナノ秒部分が正しく更新されます。
  2. エンディアンネスに関するコメントの追加:

    • // NOTE: tv_nsec is int64 on amd64, so this assumes a little-endian system.
      • このコメントは、tv_nsecamd64上でint64型であること、そして上記のポインタキャストが正しく機能するためにはシステムがリトルエンディアンであるという前提があることを明示しています。これにより、コードの意図が明確になり、将来的に異なるアーキテクチャへの移植や、エンディアンネスに関するデバッグを行う際に役立ちます。

この修正により、Goランタイムはtimespec構造体のナノ秒部分を正しく計算し、OSのシステムコールに渡すことができるようになり、NetBSDやOpenBSD環境での時間関連のバグ(特にゼロ除算エラーなど)が解消されました。

関連リンク

  • Go issue tracker: https://golang.org/cl/12056044 (コミットメッセージに記載されているGoのコードレビューシステムへのリンク)

参考にした情報源リンク