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

[インデックス 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フォーマットでは、ファイルのタイムスタンプ(mtimeModTimeに相当)は通常、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.Timetime.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.ModTimeminTimemaxTimeの範囲内にあるかどうかがチェックされます。

	var modTime int64
	if !hdr.ModTime.Before(minTime) && !hdr.ModTime.After(maxTime) {
		modTime = hdr.ModTime.Unix()
	}

このコードスニペットは、hdr.ModTimeminTimeより前ではなく、かつ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

  1. timeパッケージのインポート: import ("time") が追加され、時間関連の操作が可能になりました。

  2. minTimemaxTime変数の定義:

    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フォーマットで表現可能な最大時刻を計算しています。
  3. 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.ModTimeminTimeより前ではなく、かつmaxTimeより後ではない、つまりminTimemaxTimeの範囲内にある場合にtrueとなります。
    • もしhdr.ModTimeがこの有効な範囲内であれば、hdr.ModTime.Unix()(Unixタイムスタンプとしての秒数)がmodTimeに代入されます。
    • もしhdr.ModTimeが範囲外であれば、modTimeint64のゼロ値(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

  1. 新しいテスト関数TestRoundTripの追加: このテストは、archive/tarパッケージの基本的な書き込みと読み込みの機能が正しく動作することを確認するためのものです。
    • bytes.Bufferを使用してメモリ上でtarアーカイブを作成します。
    • NewWritertar.Writerを作成し、Headerを設定してデータを書き込みます。
    • ModTimeのナノ秒精度の調整:
      	// tar only supports second precision.
      	hdr.ModTime = hdr.ModTime.Add(-time.Duration(hdr.ModTime.Nanosecond()) * time.Nanosecond)
      
      Goのtime.Timeはナノ秒精度を持ちますが、tarフォーマットのmtimeは秒精度しかサポートしません。この行は、hdr.ModTimeからナノ秒部分を減算することで、秒精度に丸めています。これにより、書き込んだModTimeと読み戻したModTimeが一致することを保証し、テストの信頼性を高めています。
    • NewReadertar.Readerを作成し、書き込んだアーカイブを読み戻します。
    • reflect.DeepEqualbytes.Equalを使用して、書き込んだHeaderとデータが読み戻したものと一致するかを検証します。

このテストの追加により、ModTimeの範囲チェックの変更が、既存の正常な動作に影響を与えないことが保証されます。

関連リンク

参考にした情報源リンク