[インデックス 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.cap
とn
が非常に大きな値であった場合、それらの合計が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...)
のように使用し、s
にelems
を追加した新しいスライスを返します。元のスライスの容量が足りない場合、append
は内部的にgrowslice
を呼び出して新しい、より大きな基底配列を確保します。
panic
とrecover
Goにおけるpanic
は、プログラムの通常の実行フローを中断させるランタイムエラーです。通常、回復不可能なエラーやプログラマの論理的誤りを示すために使用されます。recover
はdefer
関数内で使用され、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
:cap
がintgo
型(Goのint
型に対応)にキャストした際に値が変わるかどうかをチェックしています。これは、cap
がintgo
型で表現できる範囲を超えている場合に真となります。cap < old.cap
: 新しい容量cap
が古い容量old.cap
よりも小さいかどうかをチェックしています。これは、容量が減少する異常なケース(通常は増加するはず)を検出します。(t->elem->size > 0 && cap > MaxMem/t->elem->size)
: 要素サイズが0より大きく、かつ新しい容量cap
がMaxMem
(最大メモリ量)を要素サイズで割った値を超えているかどうかをチェックしています。これは、確保しようとするメモリ量がシステムで許容される最大値を超える場合に真となります。
このコミットでは、2番目の条件cap < old.cap
がcap < (int64)old.cap
に変更されました。
if((intgo)cap != cap || cap < (int64)old.cap || (t->elem->size > 0 && cap > MaxMem/t->elem->size))
この変更の理由は、old.cap
がint64
型であるにもかかわらず、以前の比較ではcap
(これもint64
型)と直接比較されていました。しかし、cap = old.cap + n
の計算において、old.cap
とn
の合計がint64
の最大値を超えてオーバーフローした場合、cap
自体が負の値や非常に小さな値になる可能性があります。
例えば、old.cap
がint64
の最大値に近い非常に大きな値で、n
も大きな値だった場合、old.cap + n
がオーバーフローして負の値になったとします。この負の値は、old.cap
よりも小さくなるため、cap < old.cap
という条件は真となり、パニックが検出されるように見えます。
しかし、問題はold.cap
がint64
型であるのに対し、cap
の計算結果がオーバーフローによってintgo
型(Goのint
型)の範囲に収まってしまう可能性があったことです。intgo
は32ビットシステムでは32ビット、64ビットシステムでは64ビットです。もしcap
がintgo
の範囲に収まってしまい、かつold.cap
がintgo
の範囲を超えていた場合、cap < old.cap
の比較が正しく機能しない可能性がありました。
cap < (int64)old.cap
への変更は、old.cap
を明示的にint64
として比較することで、cap
がオーバーフローによって負の値になった場合でも、old.cap
との比較が常にint64
のコンテキストで行われることを保証します。これにより、cap
がold.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
の計算で整数オーバーフローが発生し、cap
がint64
の最大値を超えて負の値になった場合、この比較が意図通りに機能しない可能性がありました。特に、old.cap
が非常に大きく、cap
がオーバーフローによって負の値になった場合、cap
はold.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
)を加算して新しい容量を計算しようとします。length
がint
の最大値であるため、a.cap + b.len
の計算はint64
のオーバーフローを引き起こし、growslice
関数内でパニックが発生することが期待されます。shouldPanic(f)
: このヘルパー関数は、f
関数がパニックを発生させることを検証します。もしパニックが発生しなければ、テストは失敗します。
このテストケースは、まさにgrowslice
関数における容量計算のオーバーフローが原因で発生するパニックを再現し、修正がそのパニックを適切に捕捉して"growslice: cap out of range"
というメッセージでパニックを発生させることを確認します。
関連リンク
- Go Code Review (CL) 83520043: https://golang.org/cl/83520043
- GitHub Commit: https://github.com/golang/go/commit/9121e7e4df8e3867be2929cb2188272fbfe4408e
参考にした情報源リンク
- Go言語公式ドキュメント (Slices): https://go.dev/blog/slices-intro
- Go言語公式ドキュメント (
append
): https://go.dev/doc/effective_go#append - Go言語公式ドキュメント (
panic
andrecover
): https://go.dev/blog/defer-panic-and-recover - 整数オーバーフローに関する一般的な情報 (例: Wikipediaなど)
- Goのランタイムソースコード (
src/runtime/slice.go
またはsrc/pkg/runtime/slice.goc
): Goのソースコードリポジトリ - Goのイシュートラッカー (Issue 7550): 直接的なリンクは見つかりませんでしたが、コミットメッセージに記載されているため、過去のイシューとして存在した可能性が高いです。