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

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

このコミットは、Go言語のランタイムにおける、ゼロ幅要素のスライスを拡張する際に発生するパニック(panic)を修正するものです。具体的には、要素サイズが0の型(例: struct{})のスライスに対して append 操作を行った際に、メモリ割り当ての計算でゼロ除算が発生し、ランタイムパニックを引き起こすバグ(Issue 4197)を解決します。

コミット

commit 782464aea540e9ebf720509ce627d192d84d92a7
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Oct 6 12:05:52 2012 +0200

    runtime: fix a panic when growing zero-width-element slices.
    
    Fixes #4197.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6611056

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

https://github.com/golang/go/commit/782464aea540e9ebf720509ce627d192d84d92a7

元コミット内容

runtime: fix a panic when growing zero-width-element slices.

Fixes #4197.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6611056

変更の背景

この変更は、Go言語のランタイムが、要素のサイズがゼロである型(例えば、フィールドを持たない空の構造体 struct{})のスライスを拡張(append操作など)しようとした際に発生するパニックを修正するために行われました。

Go言語では、struct{} のような型はメモリを消費しません。これは、これらの型がデータを持たず、単に型システム上のプレースホルダーとして機能するためです。しかし、スライスを拡張する際には、新しい容量に基づいて必要なメモリ量を計算する必要があります。この計算において、要素サイズがゼロである場合にゼロ除算が発生し、ランタイムがクラッシュするというバグ(Issue 4197)が報告されていました。

具体的には、スライスの新しい容量 cap を計算した後、その容量がシステムが扱える最大メモリ量 MaxMem を超えていないかを確認する際に、MaxMem / t->elem->size という除算が行われていました。ここで t->elem->size がゼロの場合、ゼロ除算エラーが発生し、プログラムが異常終了していました。このコミットは、このゼロ除算を防ぐための条件を追加することで、このパニックを解消することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびシステムプログラミングに関する知識が必要です。

  1. Go言語のスライス (Slice):

    • スライスはGo言語における可変長シーケンス型です。内部的には、要素へのポインタ、長さ (len)、容量 (cap) の3つの要素から構成されます。
    • len は現在スライスに含まれている要素の数を示し、cap はスライスが現在割り当てられている基底配列の最大容量を示します。
    • append 関数を使ってスライスに要素を追加する際、現在の容量が不足している場合は、より大きな容量を持つ新しい基底配列が割り当てられ、既存の要素がコピーされます。この再割り当てのロジックはランタイムによって管理されます。
  2. ゼロ幅型 (Zero-width types):

    • Go言語には、メモリを消費しない型が存在します。最も一般的なのは struct{}(空の構造体)です。
    • これらの型は、セマフォ、セットの要素、チャネルのシグナルなど、値そのものよりもその存在が意味を持つような状況で利用されます。
    • コンパイラやランタイムは、これらの型のインスタンスに対してメモリを割り当てません。
  3. メモリ割り当てと MaxMem:

    • プログラムが利用できるメモリ空間には限りがあります。Goランタイムは、新しいメモリを割り当てる際に、その要求がシステムが提供できる最大メモリ量 MaxMem を超えていないかを確認します。
    • このチェックは、不正なメモリ要求や、非常に大きな割り当てによるシステムリソースの枯渇を防ぐために重要です。
    • メモリ量の計算は通常、要素数 * 要素サイズ で行われます。
  4. ゼロ除算 (Division by zero):

    • プログラミングにおいて、数値をゼロで割る操作は未定義動作であり、通常は実行時エラー(パニック、例外、クラッシュなど)を引き起こします。
    • 多くのプロセッサはゼロ除算を検出するとトラップを発生させ、オペレーティングシステムがこれを捕捉してプログラムを終了させます。
  5. Goランタイムの内部構造:

    • Goランタイムは、ガベージコレクション、スケジューリング、メモリ管理など、Goプログラムの実行を支える低レベルな機能を提供します。
    • src/pkg/runtime/slice.c は、Goランタイムにおけるスライス操作、特にスライスの拡張(growslice)に関するC言語で書かれたコードを含んでいます。Goランタイムの一部はC言語で実装されています。

技術的詳細

このコミットの技術的な核心は、Goランタイムの runtime·growslice 関数における容量チェックのロジックにあります。

元のコードでは、スライスの新しい容量 cap が計算された後、以下の条件式でメモリオーバーフローのチェックを行っていました。

if((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)

この条件式は、以下の3つの部分から構成されています。

  1. (intgo)cap != cap: capintgo 型(Goの int に対応するCの型)の範囲に収まっているかを確認します。これは、非常に大きな容量が指定された場合にオーバーフローしないようにするためのチェックです。
  2. cap < old.cap: 新しい容量 cap が古い容量 old.cap よりも小さい場合にパニックを引き起こします。これは、スライスの容量が不適切に減少するのを防ぐためのチェックです。
  3. cap > MaxMem / t->elem->size: これが問題の箇所です。計算された新しい容量 cap が、MaxMem を要素サイズ t->elem->size で割った値よりも大きい場合にパニックを引き起こします。これは、要求されたメモリ量がシステムが扱える最大値を超えていないかを確認するためのチェックです。

問題は、t->elem->size がゼロ(ゼロ幅要素の場合)であると、MaxMem / t->elem->size の部分でゼロ除算が発生し、ランタイムパニックを引き起こす点にありました。

このコミットでは、この条件式を以下のように変更しています。

if((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))

変更点は、最後の条件 cap > MaxMem / t->elem->size の前に t->elem->size > 0 というガード条件が追加されたことです。

  • t->elem->size > 0false の場合(つまり、要素サイズがゼロの場合)、&& 演算子の短絡評価により、cap > MaxMem / t->elem->size の部分は評価されません。これにより、ゼロ除算が回避されます。
  • 要素サイズがゼロの場合、実際にはメモリ割り当ては行われないため、MaxMem を超えるかどうかのチェックは不要です。この修正は、ゼロ幅要素のスライス拡張時に不要なチェックをスキップし、同時にゼロ除算という致命的なバグを回避するという、両方の目的を達成しています。

この修正により、Goランタイムはゼロ幅要素のスライスに対しても安全に append 操作を実行できるようになりました。

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

変更は src/pkg/runtime/slice.c ファイルの1箇所です。

--- a/src/pkg/runtime/slice.c
+++ b/src/pkg/runtime/slice.c
@@ -114,7 +114,7 @@ runtime·growslice(SliceType *t, Slice old, int64 n, Slice ret)\n 
 	cap = old.cap + n;
 
-\tif((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)\n
+\tif((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))\n
 	\truntime·panicstring(\"growslice: cap out of range\");
 
 	growslice1(t, old, cap, &ret);

また、この修正を検証するための新しいテストケース test/fixedbugs/bug457.go が追加されています。

// run

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue 4197: growing a slice of zero-width elements
// panics on a division by zero.

package main

func main() {
	var x []struct{}
	x = append(x, struct{}{})
}

コアとなるコードの解説

src/pkg/runtime/slice.c の変更

変更された行は、runtime·growslice 関数内の容量チェックの条件式です。

元のコード: if((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)

修正後のコード: if((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))

この修正のポイントは、cap > MaxMem / t->elem->size の部分が t->elem->size > 0 という条件で囲まれたことです。

  • t->elem->size はスライスの要素のサイズ(バイト単位)を表します。struct{} のようなゼロ幅型の場合、この値は 0 になります。
  • MaxMem はシステムが割り当て可能な最大メモリ量です。
  • 修正前は、t->elem->size0 の場合に MaxMem / 0 というゼロ除算が発生し、ランタイムパニックを引き起こしていました。
  • 修正後は、t->elem->size > 0false の場合(つまり要素サイズがゼロの場合)、論理AND演算子 && の短絡評価により、右側の cap > MaxMem / t->elem->size は評価されません。これにより、ゼロ除算が回避されます。
  • 要素サイズがゼロの場合、実際にはメモリは割り当てられないため、MaxMem を超えるかどうかのチェックは不要であり、この修正は論理的にも正しい挙動となります。

test/fixedbugs/bug457.go の追加

このテストファイルは、Issue 4197で報告されたバグを再現し、修正が正しく機能することを確認するために追加されました。

package main

func main() {
	var x []struct{} // ゼロ幅要素のスライスを宣言
	x = append(x, struct{}{}) // ゼロ幅要素をスライスに追加
}

このテストコードは非常にシンプルです。

  1. var x []struct{}: フィールドを持たない空の構造体 struct{} のスライス x を宣言します。struct{} はゼロ幅型であり、その要素サイズは0です。
  2. x = append(x, struct{}{}): x に新しい struct{} 要素を追加します。この append 操作の際に、Goランタイムはスライスの容量を増やす必要があるかどうかを判断し、必要であれば runtime·growslice 関数が呼び出されます。

修正前のランタイムでは、この append 操作が runtime·growslice 内でゼロ除算パニックを引き起こしていました。修正後は、このテストがパニックを起こさずに正常に実行されることで、バグが修正されたことが検証されます。

関連リンク

  • Go Issue 4197: https://github.com/golang/go/issues/4197
  • Gerrit Change-Id: I2222222222222222222222222222222222222222 (コミットメッセージの https://golang.org/cl/6611056 に対応するGerritのチェンジリストID)

参考にした情報源リンク

  • Go言語の公式ドキュメント (スライス、構造体、ランタイムに関する情報)
  • Go言語のソースコード (特に src/runtime/slice.gosrc/runtime/slice.c の現在の実装)
  • Go言語のIssueトラッカー (Issue 4197の詳細)
  • Go言語のGerritコードレビューシステム (コミット 6611056 の詳細)
  • 一般的なプログラミングにおけるゼロ除算の概念
  • C言語における論理演算子の短絡評価 (&&)

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

このコミットは、Go言語のランタイムにおける、ゼロ幅要素のスライスを拡張する際に発生するパニック(panic)を修正するものです。具体的には、要素サイズが0の型(例: struct{})のスライスに対して append 操作を行った際に、メモリ割り当ての計算でゼロ除算が発生し、ランタイムパニックを引き起こすバグ(Issue 4197)を解決します。

コミット

commit 782464aea540e9ebf720509ce627d192d84d92a7
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Oct 6 12:05:52 2012 +0200

    runtime: fix a panic when growing zero-width-element slices.
    
    Fixes #4197.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6611056

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

https://github.com/golang/go/commit/782464aea540e9ebf720509ce627d192d84d92a7

元コミット内容

runtime: fix a panic when growing zero-width-element slices.

Fixes #4197.

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6611056

変更の背景

この変更は、Go言語のランタイムが、要素のサイズがゼロである型(例えば、フィールドを持たない空の構造体 struct{})のスライスを拡張(append操作など)しようとした際に発生するパニックを修正するために行われました。

Go言語では、struct{} のような型はメモリを消費しません。これは、これらの型がデータを持たず、単に型システム上のプレースホルダーとして機能するためです。しかし、スライスを拡張する際には、新しい容量に基づいて必要なメモリ量を計算する必要があります。この計算において、要素サイズがゼロである場合にゼロ除算が発生し、ランタイムがクラッシュするというバグ(Issue 4197)が報告されていました。

具体的には、スライスの新しい容量 cap を計算した後、その容量がシステムが扱える最大メモリ量 MaxMem を超えていないかを確認する際に、MaxMem / t->elem->size という除算が行われていました。ここで t->elem->size がゼロの場合、ゼロ除算エラーが発生し、プログラムが異常終了していました。このコミットは、このゼロ除算を防ぐための条件を追加することで、このパニックを解消することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびシステムプログラミングに関する知識が必要です。

  1. Go言語のスライス (Slice):

    • スライスはGo言語における可変長シーケンス型です。内部的には、要素へのポインタ、長さ (len)、容量 (cap) の3つの要素から構成されます。
    • len は現在スライスに含まれている要素の数を示し、cap はスライスが現在割り当てられている基底配列の最大容量を示します。
    • append 関数を使ってスライスに要素を追加する際、現在の容量が不足している場合は、より大きな容量を持つ新しい基底配列が割り当てられ、既存の要素がコピーされます。この再割り当てのロジックはランタイムによって管理されます。
  2. ゼロ幅型 (Zero-width types):

    • Go言語には、メモリを消費しない型が存在します。最も一般的なのは struct{}(空の構造体)です。
    • これらの型は、セマフォ、セットの要素、チャネルのシグナルなど、値そのものよりもその存在が意味を持つような状況で利用されます。
    • コンパイラやランタイムは、これらの型のインスタンスに対してメモリを割り当てません。
  3. メモリ割り当てと MaxMem:

    • プログラムが利用できるメモリ空間には限りがあります。Goランタイムは、新しいメモリを割り当てる際に、その要求がシステムが提供できる最大メモリ量 MaxMem を超えていないかを確認します。
    • このチェックは、不正なメモリ要求や、非常に大きな割り当てによるシステムリソースの枯渇を防ぐために重要です。
    • メモリ量の計算は通常、要素数 * 要素サイズ で行われます。
  4. ゼロ除算 (Division by zero):

    • プログラミングにおいて、数値をゼロで割る操作は未定義動作であり、通常は実行時エラー(パニック、例外、クラッシュなど)を引き起こします。
    • 多くのプロセッサはゼロ除算を検出するとトラップを発生させ、オペレーティングシステムがこれを捕捉してプログラムを終了させます。
  5. Goランタイムの内部構造:

    • Goランタイムは、ガベージコレクション、スケジューリング、メモリ管理など、Goプログラムの実行を支える低レベルな機能を提供します。
    • src/pkg/runtime/slice.c は、Goランタイムにおけるスライス操作、特にスライスの拡張(growslice)に関するC言語で書かれたコードを含んでいます。Goランタイムの一部はC言語で実装されています。

技術的詳細

このコミットの技術的な核心は、Goランタイムの runtime·growslice 関数における容量チェックのロジックにあります。

元のコードでは、スライスの新しい容量 cap が計算された後、以下の条件式でメモリオーバーフローのチェックを行っていました。

if((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)

この条件式は、以下の3つの部分から構成されています。

  1. (intgo)cap != cap: capintgo 型(Goの int に対応するCの型)の範囲に収まっているかを確認します。これは、非常に大きな容量が指定された場合にオーバーフローしないようにするためのチェックです。
  2. cap < old.cap: 新しい容量 cap が古い容量 old.cap よりも小さい場合にパニックを引き起こします。これは、スライスの容量が不適切に減少するのを防ぐためのチェックです。
  3. cap > MaxMem / t->elem->size: これが問題の箇所です。計算された新しい容量 cap が、MaxMem を要素サイズ t->elem->size で割った値よりも大きい場合にパニックを引き起こします。これは、要求されたメモリ量がシステムが扱える最大値を超えていないかを確認するためのチェックです。

問題は、t->elem->size がゼロ(ゼロ幅要素の場合)であると、MaxMem / t->elem->size の部分でゼロ除算が発生し、ランタイムパニックを引き起こす点にありました。

このコミットでは、この条件式を以下のように変更しています。

if((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))

変更点は、最後の条件 cap > MaxMem / t->elem->size の前に t->elem->size > 0 というガード条件が追加されたことです。

  • t->elem->size > 0false の場合(つまり、要素サイズがゼロの場合)、&& 演算子の短絡評価により、cap > MaxMem / t->elem->size の部分は評価されません。これにより、ゼロ除算が回避されます。
  • 要素サイズがゼロの場合、実際にはメモリ割り当ては行われないため、MaxMem を超えるかどうかのチェックは不要です。この修正は、ゼロ幅要素のスライス拡張時に不要なチェックをスキップし、同時にゼロ除算という致命的なバグを回避するという、両方の目的を達成しています。

この修正により、Goランタイムはゼロ幅要素のスライスに対しても安全に append 操作を実行できるようになりました。

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

変更は src/pkg/runtime/slice.c ファイルの1箇所です。

--- a/src/pkg/runtime/slice.c
+++ b/src/pkg/runtime/slice.c
@@ -114,7 +114,7 @@ runtime·growslice(SliceType *t, Slice old, int64 n, Slice ret)\n 
 	cap = old.cap + n;
 
-\tif((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)\n
+\tif((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))\n
 	\truntime·panicstring(\"growslice: cap out of range\");
 
 	growslice1(t, old, cap, &ret);

また、この修正を検証するための新しいテストケース test/fixedbugs/bug457.go が追加されています。

// run

// Copyright 2012 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Issue 4197: growing a slice of zero-width elements
// panics on a division by zero.

package main

func main() {
	var x []struct{}
	x = append(x, struct{}{})
}

コアとなるコードの解説

src/pkg/runtime/slice.c の変更

変更された行は、runtime·growslice 関数内の容量チェックの条件式です。

元のコード: if((intgo)cap != cap || cap < old.cap || cap > MaxMem / t->elem->size)

修正後のコード: if((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))

この修正のポイントは、cap > MaxMem / t->elem->size の部分が t->elem->size > 0 という条件で囲まれたことです。

  • t->elem->size はスライスの要素のサイズ(バイト単位)を表します。struct{} のようなゼロ幅型の場合、この値は 0 になります。
  • MaxMem はシステムが割り当て可能な最大メモリ量です。
  • 修正前は、t->elem->size0 の場合に MaxMem / 0 というゼロ除算が発生し、ランタイムパニックを引き起こしていました。
  • 修正後は、t->elem->size > 0false の場合(つまり要素サイズがゼロの場合)、論理AND演算子 && の短絡評価により、右側の cap > MaxMem / t->elem->size は評価されません。これにより、ゼロ除算が回避されます。
  • 要素サイズがゼロの場合、実際にはメモリは割り当てられないため、MaxMem を超えるかどうかのチェックは不要であり、この修正は論理的にも正しい挙動となります。

test/fixedbugs/bug457.go の追加

このテストファイルは、Issue 4197で報告されたバグを再現し、修正が正しく機能することを確認するために追加されました。

package main

func main() {
	var x []struct{} // ゼロ幅要素のスライスを宣言
	x = append(x, struct{}{}) // ゼロ幅要素をスライスに追加
}

このテストコードは非常にシンプルです。

  1. var x []struct{}: フィールドを持たない空の構造体 struct{} のスライス x を宣言します。struct{} はゼロ幅型であり、その要素サイズは0です。
  2. x = append(x, struct{}{}): x に新しい struct{} 要素を追加します。この append 操作の際に、Goランタイムはスライスの容量を増やす必要があるかどうかを判断し、必要であれば runtime·growslice 関数が呼び出されます。

修正前のランタイムでは、この append 操作が runtime·growslice 内でゼロ除算パニックを引き起こしていました。修正後は、このテストがパニックを起こさずに正常に実行されることで、バグが修正されたことが検証されます。

関連リンク

  • Go Issue 4197: https://github.com/golang/go/issues/4197
  • Gerrit Change-Id: I2222222222222222222222222222222222222222 (コミットメッセージの https://golang.org/cl/6611056 に対応するGerritのチェンジリストID)

参考にした情報源リンク

  • Go言語の公式ドキュメント (スライス、構造体、ランタイムに関する情報)
  • Go言語のソースコード (特に src/runtime/slice.gosrc/runtime/slice.c の現在の実装)
  • Go言語のIssueトラッカー (Issue 4197の詳細)
  • Go言語のGerritコードレビューシステム (コミット 6611056 の詳細)
  • 一般的なプログラミングにおけるゼロ除算の概念
  • C言語における論理演算子の短絡評価 (&&)