[インデックス 14347] ファイルの概要
このコミットは、Go言語の標準ライブラリである archive/tar
パッケージにおける、tarアーカイブのModTime(最終更新時刻)の書き込みに関する問題を修正するものです。具体的には、ModTime
がtarフォーマットでサポートされる範囲外の値を持つ場合に、正しく処理されるように変更が加えられました。
変更されたファイルは以下の2つです。
src/pkg/archive/tar/tar_test.go
:archive/tar
パッケージのテストファイル。ModTime
の範囲外の値を扱う際の挙動を検証するための新しいテストケースが追加されました。src/pkg/archive/tar/writer.go
:archive/tar
パッケージのライター実装ファイル。ModTime
をtarヘッダーに書き込む際に、その値がサポートされる範囲内にあることを保証するロジックが追加されました。
コミット
commit 0ac317817bfdde4de178893b9489aac007210280
Author: David Symonds <dsymonds@golang.org>
Date: Thu Nov 8 08:22:40 2012 +1100
archive/tar: avoid writing ModTime that is out of range.
Update #4358
Still to do: support binary numeric format in Reader.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6818101
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0ac317817bfdde4de178893b9489aac007210280
元コミット内容
archive/tar: avoid writing ModTime that is out of range.
Update #4358
Still to do: support binary numeric format in Reader.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/6818101
変更の背景
この変更の背景には、archive/tar
パッケージがtarアーカイブにファイルを書き込む際に、ファイルの最終更新時刻(ModTime
)がtarフォーマットの仕様で許容される範囲を超えてしまうという問題がありました。
tarフォーマットでは、ファイルのタイムスタンプ(mtime
、ModTime
に相当)は通常、8進数で表現され、そのフィールドのサイズには限りがあります。具体的には、POSIX tar(ustar)フォーマットでは、mtime
フィールドは12バイト(12桁の8進数)で表現されます。これにより、表現できるタイムスタンプの範囲に上限が設けられます。非常に古い日付や非常に未来の日付、あるいはUnixエポック(1970年1月1日00:00:00 UTC)から非常に長い時間が経過した日付は、この8進数表現の範囲を超えてしまう可能性があります。
範囲外のModTime
をそのまま書き込もうとすると、tarアーカイブが破損したり、他のtarツールで正しく読み取れなくなったりする問題が発生します。このコミットは、このような問題を回避し、archive/tar
パッケージがより堅牢に動作するようにするために行われました。コミットメッセージにあるUpdate #4358
は、この問題に関連するGoのIssueトラッカーの項目を示唆していますが、現在の検索では直接的な情報は見つかりませんでした。しかし、コミットメッセージの内容から、ModTime
の範囲外書き込みが問題であったことは明らかです。
前提知識の解説
tarアーカイブとヘッダー
tar(Tape ARchive)は、複数のファイルを一つのアーカイブファイルにまとめるためのファイルフォーマットです。主にUnix系システムで利用され、ファイルのバックアップや配布によく用いられます。tarアーカイブは、各ファイルの「ヘッダー」と「データ」の連続で構成されます。
- ヘッダー: 各ファイルのメタデータ(ファイル名、サイズ、パーミッション、所有者、グループ、最終更新時刻など)を格納します。
- データ: 実際のファイルの内容を格納します。
tarヘッダーのmtime
フィールド
tarヘッダーには、ファイルの最終更新時刻(modification time)を記録するためのmtime
フィールドが存在します。このフィールドは、通常、Unixエポック(1970年1月1日00:00:00 UTC)からの秒数を8進数で表現して格納されます。
8進数表現の限界
tarヘッダーのmtime
フィールドは、そのサイズが固定されており、通常は12バイト(ustarフォーマットの場合)です。12バイトの8進数で表現できる最大値には限りがあります。
例えば、12桁の8進数で表現できる最大値は 777777777777
(8進数) です。これを10進数に変換すると (8^12 - 1)
となり、これは 68719476735
秒に相当します。Unixエポックからの秒数として考えると、これはおよそ2038年1月19日3時14分7秒(UTC)までしか表現できないという、いわゆる「2038年問題」の一因ともなります。
このコミットでは、ModTime
がこの8進数表現の範囲(特に33ビットの範囲)に収まるように調整することが目的とされています。
Go言語のtime.Time
とtime.Unix
time.Time
: Go言語で日付と時刻を扱うための構造体です。ナノ秒単位の精度を持ちます。time.Unix(sec int64, nsec int64) Time
: Unixエポックからの秒数とナノ秒を指定してtime.Time
オブジェクトを生成する関数です。この関数は、time.Time
オブジェクトをUnixタイムスタンプ(秒数)に変換する際に、その逆の操作として利用されます。
技術的詳細
このコミットの技術的な核心は、archive/tar
パッケージのWriter
がtarヘッダーにModTime
を書き込む際に、その値がtarフォーマットの制約内で表現可能であることを保証する点にあります。
tarフォーマットのmtime
フィールドは、通常、8進数で表現されます。このフィールドのサイズには限りがあり、特にPOSIX tar(ustar)では12バイト(12桁の8進数)が割り当てられています。これにより、表現できるタイムスタンプの範囲に上限が設定されます。
コミットでは、この上限を考慮し、ModTime
が有効な範囲内にあるかどうかをチェックし、もし範囲外であれば、その値を有効な範囲内に「クランプ(clamping)」するロジックが導入されています。
具体的には、以下の定数がwriter.go
に追加されました。
minTime = time.Unix(0, 0)
: Unixエポック(1970年1月1日00:00:00 UTC)を表します。tarフォーマットでは、これより前の時刻は通常表現できません。maxTime = minTime.Add((1<<33 - 1) * time.Second)
: これは、mtime
フィールドが33ビットの8進数で表現できる最大値に対応する時刻を計算しています。1<<33 - 1
は、33ビットで表現できる最大の符号なし整数(2^33 - 1
)であり、これを秒数としてminTime
に加算することで、tarフォーマットで表現可能な最大時刻を定義しています。
WriteHeader
メソッド内で、hdr.ModTime
がminTime
とmaxTime
の範囲内にあるかどうかがチェックされます。
var modTime int64
if !hdr.ModTime.Before(minTime) && !hdr.ModTime.After(maxTime) {
modTime = hdr.ModTime.Unix()
}
このコードスニペットは、hdr.ModTime
がminTime
より前ではなく、かつmaxTime
より後ではない場合にのみ、hdr.ModTime.Unix()
(Unixタイムスタンプとしての秒数)をmodTime
変数に代入しています。もしhdr.ModTime
がこの範囲外であれば、modTime
は初期値の0
(Unixエポック)のままとなり、結果としてtarヘッダーには0
が書き込まれることになります。これにより、範囲外のModTime
が書き込まれることによるアーカイブの破損を防ぎます。
また、tar_test.go
には、TestRoundTrip
という新しいテスト関数が追加されました。このテストは、ファイルをtarアーカイブに書き込み、その後すぐに読み戻すというラウンドトリップテストです。特に、ModTime
がナノ秒精度を持つ可能性があるため、tarが秒精度しかサポートしないことを考慮し、hdr.ModTime
のナノ秒部分をゼロに設定する処理が含まれています。
// tar only supports second precision.
hdr.ModTime = hdr.ModTime.Add(-time.Duration(hdr.ModTime.Nanosecond()) * time.Nanosecond)
このテストは、ModTime
が正しく書き込まれ、読み戻されることを検証し、今回の変更が既存の機能に悪影響を与えないことを保証します。
コアとなるコードの変更箇所
src/pkg/archive/tar/tar_test.go
--- a/src/pkg/archive/tar/tar_test.go
+++ b/src/pkg/archive/tar/tar_test.go
@@ -5,7 +5,10 @@
package tar
import (
+ "bytes"
+ "io/ioutil"
"os"
+ "reflect"
"testing"
"time"
)
@@ -54,3 +57,43 @@ func (symlink) Mode() os.FileMode { return os.ModeSymlink }\n func (symlink) ModTime() time.Time { return time.Time{} }\n func (symlink) IsDir() bool { return false }\n func (symlink) Sys() interface{} { return nil }\n+\n+func TestRoundTrip(t *testing.T) {\n+\tdata := []byte("some file contents")\n+\n+\tvar b bytes.Buffer\n+\ttw := NewWriter(&b)\n+\thdr := &Header{\n+\t\tName: "file.txt",\n+\t\tSize: int64(len(data)),\n+\t\tModTime: time.Now(),\n+\t}\n+\t// tar only supports second precision.\n+\thdr.ModTime = hdr.ModTime.Add(-time.Duration(hdr.ModTime.Nanosecond()) * time.Nanosecond)\n+\tif err := tw.WriteHeader(hdr); err != nil {\n+\t\tt.Fatalf("tw.WriteHeader: %v", err)\n+\t}\n+\tif _, err := tw.Write(data); err != nil {\n+\t\tt.Fatalf("tw.Write: %v", err)\n+\t}\n+\tif err := tw.Close(); err != nil {\n+\t\tt.Fatalf("tw.Close: %v", err)\n+\t}\n+\n+\t// Read it back.\n+\ttr := NewReader(&b)\n+\trHdr, err := tr.Next()\n+\tif err != nil {\n+\t\tt.Fatalf("tr.Next: %v", err)\n+\t}\n+\tif !reflect.DeepEqual(rHdr, hdr) {\n+\t\tt.Errorf("Header mismatch.\\n got %+v\\nwant %+v", rHdr, hdr)\n+\t}\n+\trData, err := ioutil.ReadAll(tr)\n+\tif err != nil {\n+\t\tt.Fatalf("Read: %v", err)\n+\t}\n+\tif !bytes.Equal(rData, data) {\n+\t\tt.Errorf("Data mismatch.\\n got %q\\nwant %q", rData, data)\n+\t}\n+}\n```
### `src/pkg/archive/tar/writer.go`
```diff
--- a/src/pkg/archive/tar/writer.go
+++ b/src/pkg/archive/tar/writer.go
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"strconv"
+ "time"
)
var (
@@ -110,6 +111,12 @@ func (tw *Writer) numeric(b []byte, x int64) {
b[0] |= 0x80 // highest bit indicates binary format
}\n \n+var (\n+\tminTime = time.Unix(0, 0)\n+\t// There is room for 11 octal digits (33 bits) of mtime.\n+\tmaxTime = minTime.Add((1<<33 - 1) * time.Second)\n+)\n+\n // WriteHeader writes hdr and prepares to accept the file's contents.\n // WriteHeader calls Flush if it is not the first header.\n // Calling after a Close will return ErrWriteAfterClose.\n@@ -133,19 +140,25 @@ func (tw *Writer) WriteHeader(hdr *Header) error {\n // TODO(dsymonds): handle names longer than 100 chars\n copy(s.next(100), []byte(hdr.Name))\n \n-\ttw.octal(s.next(8), hdr.Mode) // 100:108\n-\ttw.numeric(s.next(8), int64(hdr.Uid)) // 108:116\n-\ttw.numeric(s.next(8), int64(hdr.Gid)) // 116:124\n-\ttw.numeric(s.next(12), hdr.Size) // 124:136\n-\ttw.numeric(s.next(12), hdr.ModTime.Unix()) // 136:148\n-\ts.next(8) // chksum (148:156)\n-\ts.next(1)[0] = hdr.Typeflag // 156:157\n-\ttw.cString(s.next(100), hdr.Linkname) // linkname (157:257)\n-\tcopy(s.next(8), []byte("ustar\\x0000")) // 257:265\n-\ttw.cString(s.next(32), hdr.Uname) // 265:297\n-\ttw.cString(s.next(32), hdr.Gname) // 297:329\n-\ttw.numeric(s.next(8), hdr.Devmajor) // 329:337\n-\ttw.numeric(s.next(8), hdr.Devminor) // 337:345\n+\t// Handle out of range ModTime carefully.\n+\tvar modTime int64\n+\tif !hdr.ModTime.Before(minTime) && !hdr.ModTime.After(maxTime) {\n+\t\tmodTime = hdr.ModTime.Unix()\n+\t}\n+\n+\ttw.octal(s.next(8), hdr.Mode) // 100:108\n+\ttw.numeric(s.next(8), int64(hdr.Uid)) // 108:116\n+\ttw.numeric(s.next(8), int64(hdr.Gid)) // 116:124\n+\ttw.numeric(s.next(12), hdr.Size) // 124:136\n+\ttw.numeric(s.next(12), modTime) // 136:148\n+\ts.next(8) // chksum (148:156)\n+\ts.next(1)[0] = hdr.Typeflag // 156:157\n+\ttw.cString(s.next(100), hdr.Linkname) // linkname (157:257)\n+\tcopy(s.next(8), []byte("ustar\\x0000")) // 257:265\n+\ttw.cString(s.next(32), hdr.Uname) // 265:297\n+\ttw.cString(s.next(32), hdr.Gname) // 297:329\n+\ttw.numeric(s.next(8), hdr.Devmajor) // 329:337\n+\ttw.numeric(s.next(8), hdr.Devminor) // 337:345\n \n // Use the GNU magic instead of POSIX magic if we used any GNU extensions.\n if tw.usedBinary {
コアとなるコードの解説
src/pkg/archive/tar/writer.go
-
time
パッケージのインポート:import ("time")
が追加され、時間関連の操作が可能になりました。 -
minTime
とmaxTime
変数の定義:var ( minTime = time.Unix(0, 0) // There is room for 11 octal digits (33 bits) of mtime. maxTime = minTime.Add((1<<33 - 1) * time.Second) )
minTime
: Unixエポック(1970年1月1日00:00:00 UTC)を表すtime.Time
オブジェクトです。これより前の時刻はtarフォーマットで表現できません。maxTime
: tarフォーマットのmtime
フィールドが表現できる最大時刻を定義しています。コメントにあるように、11桁の8進数(33ビット)のmtime
に対応します。1<<33 - 1
は、33ビットで表現できる最大の符号なし整数(2^33 - 1
)であり、これを秒数としてminTime
に加算することで、tarフォーマットで表現可能な最大時刻を計算しています。
-
WriteHeader
メソッド内のModTime
処理の変更:WriteHeader
メソッド内で、hdr.ModTime.Unix()
を直接tw.numeric
に渡す代わりに、modTime
という新しい変数が導入されました。// Handle out of range ModTime carefully. var modTime int64 if !hdr.ModTime.Before(minTime) && !hdr.ModTime.After(maxTime) { modTime = hdr.ModTime.Unix() }
!hdr.ModTime.Before(minTime) && !hdr.ModTime.After(maxTime)
: この条件式は、hdr.ModTime
がminTime
より前ではなく、かつmaxTime
より後ではない、つまりminTime
とmaxTime
の範囲内にある場合にtrue
となります。- もし
hdr.ModTime
がこの有効な範囲内であれば、hdr.ModTime.Unix()
(Unixタイムスタンプとしての秒数)がmodTime
に代入されます。 - もし
hdr.ModTime
が範囲外であれば、modTime
はint64
のゼロ値(0
)のままとなり、結果としてtarヘッダーには0
が書き込まれることになります。これにより、範囲外のModTime
が書き込まれることによるアーカイブの破損を防ぎます。
そして、この
modTime
変数がtw.numeric
関数に渡されるようになりました。- tw.numeric(s.next(12), hdr.ModTime.Unix()) // 136:148 + tw.numeric(s.next(12), modTime) // 136:148
src/pkg/archive/tar/tar_test.go
- 新しいテスト関数
TestRoundTrip
の追加: このテストは、archive/tar
パッケージの基本的な書き込みと読み込みの機能が正しく動作することを確認するためのものです。bytes.Buffer
を使用してメモリ上でtarアーカイブを作成します。NewWriter
でtar.Writer
を作成し、Header
を設定してデータを書き込みます。ModTime
のナノ秒精度の調整:
Goの// tar only supports second precision. hdr.ModTime = hdr.ModTime.Add(-time.Duration(hdr.ModTime.Nanosecond()) * time.Nanosecond)
time.Time
はナノ秒精度を持ちますが、tarフォーマットのmtime
は秒精度しかサポートしません。この行は、hdr.ModTime
からナノ秒部分を減算することで、秒精度に丸めています。これにより、書き込んだModTime
と読み戻したModTime
が一致することを保証し、テストの信頼性を高めています。NewReader
でtar.Reader
を作成し、書き込んだアーカイブを読み戻します。reflect.DeepEqual
とbytes.Equal
を使用して、書き込んだHeader
とデータが読み戻したものと一致するかを検証します。
このテストの追加により、ModTime
の範囲チェックの変更が、既存の正常な動作に影響を与えないことが保証されます。
関連リンク
- Go CL 6818101: https://golang.org/cl/6818101
参考にした情報源リンク
- Go Documentation:
archive/tar
package: https://pkg.go.dev/archive/tar - Go Documentation:
time
package: https://pkg.go.dev/time - POSIX.1-1988 (ustar) format: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html#tag_20_92_13_06 (特に
mtime
フィールドの記述) - Tar (computing) - Wikipedia: https://en.wikipedia.org/wiki/Tar_(computing)
- 2038年問題 - Wikipedia: https://ja.wikipedia.org/wiki/2038%E5%B9%B4%E5%95%8F%E9%A1%8C