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

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

このコミットは、Goランタイムにおけるスライスの容量拡張(growslice関数)時の整数オーバーフローの可能性を修正するものです。具体的には、新しいスライスの容量を計算する際に、その計算結果がint64の最大値を超えたり、符号付き整数として表現できないほど大きな値になったりするのを防ぐためのチェックが追加されました。これにより、不正な容量計算によるパニックや予期せぬ動作を防ぎます。

コミット

このコミットは、Goランタイムのgrowslice関数におけるスライス容量計算の安全性を向上させるものです。既存の容量に新しい要素数を加算する際に発生しうる整数オーバーフローを検出し、適切なパニックを発生させることで、ランタイムの堅牢性を高めています。また、この修正を検証するための新しいテストケースが追加されています。

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

https://github.com/golang/go/commit/9121e7e4df8e3867be2929cb2188272fbfe4408e

元コミット内容

commit 9121e7e4df8e3867be2929cb2188272fbfe4408e
Author: Dave Cheney <dave@cheney.net>
Date:   Thu Apr 3 13:44:44 2014 +1100

    runtime: check that new slice cap doesn't overflow
    
    Fixes #7550.
    
    LGTM=iant
    R=golang-codereviews, iant, josharian
    CC=golang-codereviews
    https://golang.org/cl/83520043

変更の背景

この変更は、Go言語のスライス操作、特にappend関数が内部的に使用するgrowslice関数において、スライスの新しい容量を計算する際に発生しうる整数オーバーフローの脆弱性に対処するために行われました。

元のgrowslice関数では、新しい容量をold.cap + nとして計算していました。ここでold.capは既存のスライスの容量、nは追加される要素の数です。もしold.capnが非常に大きな値であった場合、それらの合計がint64型で表現できる最大値を超えてしまい、結果として負の値になったり、予期せぬ小さな値になったりする「整数オーバーフロー」が発生する可能性がありました。

このようなオーバーフローが発生すると、ランタイムは不正な容量でメモリを確保しようとし、結果としてメモリ破壊、クラッシュ、またはサービス拒否攻撃につながる可能性がありました。Fixes #7550という記述から、この問題がGoの公式イシュートラッカーで報告されたバグ(Issue 7550)を修正するものであることがわかります。このイシューは、append操作によって非常に大きなスライスを作成しようとした際に、ランタイムパニックが発生するというものでした。

この修正の目的は、growslice関数が安全にスライスの容量を計算し、オーバーフローの危険性がある場合には明確にパニックを発生させることで、ランタイムの安定性とセキュリティを向上させることにあります。

前提知識の解説

Goのスライス (Slice)

Goのスライスは、配列をラップした動的なデータ構造です。スライスは「ポインタ」「長さ(length)」「容量(capacity)」の3つの要素で構成されます。

  • ポインタ: スライスが参照する基底配列の先頭要素へのポインタ。
  • 長さ (length): スライスに含まれる要素の数。len()関数で取得できます。
  • 容量 (capacity): スライスが基底配列から拡張できる最大要素数。cap()関数で取得できます。

スライスは、make([]T, length, capacity)で作成できます。append関数を使ってスライスに要素を追加すると、容量が不足した場合にGoランタイムが自動的に新しい、より大きな基底配列を割り当て、既存の要素をコピーし、スライスのポインタを更新します。この容量拡張のロジックがgrowslice関数に実装されています。

growslice関数

growsliceはGoランタイム内部の関数で、スライスに要素を追加する際に現在の容量が不足した場合に呼び出されます。この関数は、新しいスライスの容量を計算し、必要に応じて新しい基底配列を確保し、既存の要素を新しい配列にコピーする役割を担います。

整数オーバーフロー

整数オーバーフローとは、数値計算の結果が、その数値を格納するために使用されるデータ型で表現できる最大値(または最小値)を超えてしまう現象です。例えば、8ビット符号なし整数(0-255)で200 + 100を計算すると、結果は300ですが、255を超えてしまうため、オーバーフローが発生し、結果が44(300 % 256)になることがあります。符号付き整数では、最大値を超えると負の値になることがあります。これはセキュリティ上の脆弱性や予期せぬプログラムの動作を引き起こす可能性があります。

MaxMem

MaxMemはGoランタイム内部で定義されている定数で、Goプログラムが利用できる最大メモリ量を示します。スライスの容量計算において、確保しようとするメモリ量がMaxMemを超える場合、それはシステムが提供できるメモリ量を超えていることを意味し、パニックを発生させるべき状況です。

intgo

intgoはGoランタイム内部で使用される型で、Go言語のint型に対応します。Goのint型は、実行環境のアーキテクチャ(32ビットまたは64ビット)に応じてサイズが異なります。intgoは、このアーキテクチャ依存の整数型を明示的に示すために使用されます。

uint型と^uint(0) >> 1

uintは符号なし整数型です。^uint(0)は、すべてのビットが1であるuint型の値を生成します。これは、uint型の最大値を表します。>> 1は右シフト演算子で、ビットを1つ右にシフトします。これにより、uint型の最大値の半分、つまり符号付き整数型(int)で表現できる最大値が得られます。これは、テストケースで意図的に大きな値を生成し、オーバーフローを誘発するために使用されています。

append関数の動作

append関数は、Goでスライスに要素を追加するための組み込み関数です。append(s, elems...)のように使用し、selemsを追加した新しいスライスを返します。元のスライスの容量が足りない場合、appendは内部的にgrowsliceを呼び出して新しい、より大きな基底配列を確保します。

panicrecover

Goにおけるpanicは、プログラムの通常の実行フローを中断させるランタイムエラーです。通常、回復不可能なエラーやプログラマの論理的誤りを示すために使用されます。recoverdefer関数内で使用され、panicから回復し、パニックの引数を取得するために使用されます。テストケースでは、意図的にパニックを発生させ、それが期待通りに発生したことを確認するためにshouldPanic関数が使用されています。

技術的詳細

このコミットの核心は、src/pkg/runtime/slice.gocファイル内のgrowslice関数における容量計算のチェックロジックの変更です。

変更前のコードは以下のようでした。

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

この行は、新しい容量capが有効であるかをチェックしています。

  • (intgo)cap != cap: capintgo型(Goのint型に対応)にキャストした際に値が変わるかどうかをチェックしています。これは、capintgo型で表現できる範囲を超えている場合に真となります。
  • cap < old.cap: 新しい容量capが古い容量old.capよりも小さいかどうかをチェックしています。これは、容量が減少する異常なケース(通常は増加するはず)を検出します。
  • (t->elem->size > 0 && cap > MaxMem/t->elem->size): 要素サイズが0より大きく、かつ新しい容量capMaxMem(最大メモリ量)を要素サイズで割った値を超えているかどうかをチェックしています。これは、確保しようとするメモリ量がシステムで許容される最大値を超える場合に真となります。

このコミットでは、2番目の条件cap < old.capcap < (int64)old.capに変更されました。

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

この変更の理由は、old.capint64型であるにもかかわらず、以前の比較ではcap(これもint64型)と直接比較されていました。しかし、cap = old.cap + nの計算において、old.capnの合計がint64の最大値を超えてオーバーフローした場合、cap自体が負の値や非常に小さな値になる可能性があります。

例えば、old.capint64の最大値に近い非常に大きな値で、nも大きな値だった場合、old.cap + nがオーバーフローして負の値になったとします。この負の値は、old.capよりも小さくなるため、cap < old.capという条件は真となり、パニックが検出されるように見えます。

しかし、問題はold.capint64型であるのに対し、capの計算結果がオーバーフローによってintgo型(Goのint型)の範囲に収まってしまう可能性があったことです。intgoは32ビットシステムでは32ビット、64ビットシステムでは64ビットです。もしcapintgoの範囲に収まってしまい、かつold.capintgoの範囲を超えていた場合、cap < old.capの比較が正しく機能しない可能性がありました。

cap < (int64)old.capへの変更は、old.capを明示的にint64として比較することで、capがオーバーフローによって負の値になった場合でも、old.capとの比較が常にint64のコンテキストで行われることを保証します。これにより、capold.capよりも小さくなるというオーバーフローの兆候をより確実に捉えることができます。

この修正は、append操作で非常に大きなスライスを作成しようとした際に発生する可能性のある、整数オーバーフローによるランタイムパニックを確実に防ぐためのものです。

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

src/pkg/runtime/slice.goc

--- a/src/pkg/runtime/slice.goc
+++ b/src/pkg/runtime/slice.goc
@@ -65,7 +65,7 @@ func growslice(t *SliceType, old Slice, n int64) (ret Slice) {
 
 	cap = old.cap + n;
 
-	if((intgo)cap != cap || cap < old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))
+	if((intgo)cap != cap || cap < (int64)old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))
 		runtime·panicstring("growslice: cap out of range");
 
 	if(raceenabled) {

test/fixedbugs/issue7550.go (新規追加)

// run

// Copyright 2014 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.

package main

func shouldPanic(f func()) {
        defer func() {
                if recover() == nil {
                        panic("not panicking")
                }
        }()
        f()
}

func f() {
        length := int(^uint(0) >> 1)
        a := make([]struct{}, length)
        b := make([]struct{}, length)
        _ = append(a, b...)
}

func main() {
	shouldPanic(f)
}

コアとなるコードの解説

変更された行はsrc/pkg/runtime/slice.gocの66行目です。

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

この変更は、cap < old.capという条件をcap < (int64)old.capに修正しています。

  • cap: old.cap + nの計算結果として得られた新しいスライスの容量。この値はint64型です。
  • old.cap: 既存のスライスの容量。これもint64型です。

以前のコードでは、cap < old.capと直接比較していました。これは通常問題ありませんが、cap = old.cap + nの計算で整数オーバーフローが発生し、capint64の最大値を超えて負の値になった場合、この比較が意図通りに機能しない可能性がありました。特に、old.capが非常に大きく、capがオーバーフローによって負の値になった場合、capold.capよりも小さくなります。

しかし、Goのint型(ランタイム内部ではintgo)はアーキテクチャ依存であり、32ビットシステムでは32ビット、64ビットシステムでは64ビットです。もしold.capが32ビットintの範囲を超え、かつcapがオーバーフローによって32ビットintの範囲に収まるような値になった場合、比較の挙動が曖6昧になる可能性がありました。

cap < (int64)old.capとすることで、old.capを明示的にint64型として扱うことを保証し、capがオーバーフローによって負の値になった場合でも、int64の範囲内での正確な比較が行われるようになります。これにより、新しい容量が既存の容量よりも小さくなるという、オーバーフローによって引き起こされる異常な状態を確実に検出できるようになります。

追加されたテストケースtest/fixedbugs/issue7550.goは、この修正が正しく機能することを検証します。

  • length := int(^uint(0) >> 1): これは、Goのint型で表現できる最大値をlengthに設定します。
  • a := make([]struct{}, length): このlengthでスライスaを作成します。これにより、aは非常に大きな容量を持つスライスになります。
  • b := make([]struct{}, length): 同様に、スライスbも非常に大きな容量で作成されます。
  • _ = append(a, b...): ここが肝心な部分です。スライスaにスライスbの全要素を追加しようとします。この操作は、aの既存容量にbの要素数(length)を加算して新しい容量を計算しようとします。lengthintの最大値であるため、a.cap + b.lenの計算はint64のオーバーフローを引き起こし、growslice関数内でパニックが発生することが期待されます。
  • shouldPanic(f): このヘルパー関数は、f関数がパニックを発生させることを検証します。もしパニックが発生しなければ、テストは失敗します。

このテストケースは、まさにgrowslice関数における容量計算のオーバーフローが原因で発生するパニックを再現し、修正がそのパニックを適切に捕捉して"growslice: cap out of range"というメッセージでパニックを発生させることを確認します。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント (Slices): https://go.dev/blog/slices-intro
  • Go言語公式ドキュメント (append): https://go.dev/doc/effective_go#append
  • Go言語公式ドキュメント (panic and recover): https://go.dev/blog/defer-panic-and-recover
  • 整数オーバーフローに関する一般的な情報 (例: Wikipediaなど)
  • Goのランタイムソースコード (src/runtime/slice.go または src/pkg/runtime/slice.goc): Goのソースコードリポジトリ
  • Goのイシュートラッカー (Issue 7550): 直接的なリンクは見つかりませんでしたが、コミットメッセージに記載されているため、過去のイシューとして存在した可能性が高いです。