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

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

このコミットは、Go言語の標準ライブラリ archive/zip パッケージにおけるZIPファイルのCRC32チェックの挙動を改善するものです。具体的には、データディスクリプタを持たない(非ストリーム形式の)ZIPエントリに対しても、ファイルヘッダや中央ディレクトリにCRC32値が記録されている場合に、その値を用いてデータの整合性チェックを行うように変更されています。これにより、ZIPファイルの読み込み時の堅牢性が向上し、破損したアーカイブからの不正なデータ読み込みを防ぐことができます。

コミット

commit 98cfe6770d8530f6677ecb72a59d939c88504255
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Mar 9 14:45:40 2012 -0800

    archive/zip: verify CRC32s in non-streamed files
    
    We should check the CRC32s of files on EOF, even if there's no
    data descriptor (in streamed files), as long as there's a non-zero
    CRC32 in the file header / TOC.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5794045

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

https://github.com/golang/go/commit/98cfe6770d8530f6677ecb72a59d939c88504255

元コミット内容

archive/zip: verify CRC32s in non-streamed files

「データディスクリプタがない(ストリーム形式ではない)ファイルであっても、ファイルヘッダや中央ディレクトリにゼロ以外のCRC32値が存在する限り、EOF(ファイルの終端)でCRC32を検証すべきである。」

変更の背景

ZIPファイルフォーマットでは、各ファイルエントリのデータ整合性を保証するためにCRC32チェックサムが使用されます。通常、このCRC32値はローカルファイルヘッダと中央ディレクトリファイルヘッダの両方に含まれています。しかし、特定のシナリオ(例えば、圧縮時にファイルのサイズやCRC32値が事前に不明な場合)では、「データディスクリプタ」と呼ばれる追加の構造がファイルのデータセクションの後に配置され、そこに実際のCRC32値やサイズ情報が記録されることがあります。

このコミット以前のarchive/zipパッケージの挙動では、データディスクリプタが存在しないファイル(つまり、ローカルファイルヘッダにCRC32値が直接含まれているファイル)の場合、ファイルの読み込み完了時にそのCRC32値が適切に検証されない可能性がありました。元のコードのコメントにも// TODO(bradfitz): even if there's not a data // descriptor, we could still compare our accumulated // crc32 on EOF with the content-precededing file // header's crc32, if it's non-zero.とあり、この問題が認識されていたことが伺えます。

この変更の背景には、ZIPアーカイブの読み込み処理において、データディスクリプタの有無にかかわらず、利用可能なCRC32情報を用いて常にデータの整合性を検証し、破損したZIPファイルからの不正なデータ読み込みを防ぐという、より堅牢な実装を目指す意図があります。これにより、ユーザーがZIPファイルを解凍する際に、データが破損している場合に早期にエラーを検出できるようになります。

前提知識の解説

このコミットを理解するためには、ZIPファイルフォーマットの基本的な構造と、特に以下の要素に関する知識が必要です。

  1. ZIPファイルフォーマット: ZIPファイルは、複数のファイルやディレクトリを単一のアーカイブにまとめるための一般的なファイルフォーマットです。内部的には、各ファイルエントリは「ローカルファイルヘッダ」「ファイルデータ」「データディスクリプタ(オプション)」で構成され、アーカイブの最後には「中央ディレクトリ」と「中央ディレクトリ終了レコード」が配置されます。

  2. CRC32 (Cyclic Redundancy Check): CRC32は、データの整合性をチェックするためのエラー検出コードです。ZIPファイルでは、圧縮前のオリジナルデータに対してCRC32値が計算され、ファイルエントリのメタデータとして保存されます。ファイルを解凍する際に、再度CRC32を計算し、保存されている値と比較することで、データが転送中や保存中に破損していないかを確認します。値が一致しない場合、データが破損していると判断されます。

  3. ローカルファイルヘッダ (Local File Header): ZIPアーカイブ内の各ファイルエントリの先頭に位置するヘッダです。ファイル名、圧縮方法、圧縮・非圧縮サイズ、そしてCRC32値など、そのファイルに関する基本的なメタデータが含まれています。

  4. 中央ディレクトリ (Central Directory): ZIPアーカイブの末尾に存在する、アーカイブ内の全ファイルエントリのメタデータを集約した構造です。各ファイルエントリに対応する「中央ディレクトリファイルヘッダ (Central Directory File Header, CDFH)」が含まれており、ここにもファイル名、サイズ、CRC32値、そして対応するローカルファイルヘッダへのオフセットなどが記録されています。中央ディレクトリは、アーカイブ全体の内容を素早く把握するために利用されます。

  5. データディスクリプタ (Data Descriptor): これはオプションの構造で、ローカルファイルヘッダの後にファイルデータが続き、その後に配置されます。データディスクリプタは、特にストリーミングでZIPファイルが生成される場合など、ローカルファイルヘッダが書き込まれる時点で圧縮・非圧縮サイズやCRC32値が不明な場合に使用されます。この場合、ローカルファイルヘッダのCRC32フィールドはゼロに設定され、実際のCRC32値はデータディスクリプタに記録されます。データディスクリプタの存在は、ローカルファイルヘッダの「汎用目的ビットフラグ (General Purpose Bit Flag)」の特定のビット(ビット3)によって示されます。

このコミットは、データディスクリプタが存在しない(つまり、ローカルファイルヘッダにCRC32値が直接含まれている)ケースにおいて、そのCRC32値が適切に検証されていなかった問題を修正するものです。

技術的詳細

このコミットの核心は、archive/zipパッケージのchecksumReader構造体のReadメソッドにおけるEOF処理の変更です。

checksumReaderは、ZIPエントリのデータを読み込みながら、同時にそのデータのCRC32値を計算する役割を担っています。ファイルの読み込みがEOFに達した際、このReadメソッドは、計算されたCRC32値と、ZIPファイルに記録されている期待されるCRC32値を比較して整合性を検証します。

変更前は、EOFに達した際に、まずr.desr != nil(データディスクリプタが存在するか)をチェックしていました。

  • r.desr != nil の場合(データディスクリプタが存在する場合):データディスクリプタを読み込み、そこに記録されているCRC32値と、読み込んだデータのCRC32値を比較していました。
  • r.desr == nil の場合(データディスクリプタが存在しない場合):CRC32の比較は行われず、単にEOFエラーを返していました。

この挙動は、データディスクリプタが存在しないファイル(つまり、ローカルファイルヘッダや中央ディレクトリにCRC32値が直接記録されているファイル)の場合に、CRC32検証がスキップされてしまうという問題を引き起こしていました。たとえファイルヘッダにCRC32値が記録されていても、それが検証されないため、データが破損していてもarchive/zipパッケージはエラーを報告せずに読み込みを完了してしまう可能性がありました。

このコミットでは、EOF処理のロジックが以下のように変更されました。

  1. EOFに達した場合、まずr.desr != nilをチェックする点は同じです。
  2. r.desr != nil の場合: 以前と同様にデータディスクリプタを読み込み、そのCRC32値と計算されたCRC32値を比較します。
  3. r.desr == nil の場合: ここが変更点です。データディスクリプタが存在しない場合でも、r.f.CRC32 != 0(ファイルヘッダ/中央ディレクトリに記録されているCRC32値がゼロではない)という条件が追加されました。この条件が真であれば、r.hash.Sum32()(読み込んだデータのCRC32)とr.f.CRC32(ファイルヘッダ/中央ディレクトリのCRC32)を比較し、一致しない場合はErrChecksumエラーを返します。

この修正により、データディスクリプタの有無にかかわらず、ZIPファイルにCRC32値が記録されている限り、その整合性がEOF時に検証されるようになりました。これにより、archive/zipパッケージはより堅牢になり、破損したZIPファイルからの不正なデータ読み込みをより確実に検出できるようになります。

また、この変更を検証するために、reader_test.goに新しいテストケースが追加されています。特に、crc32-not-streamed.zipという新しいテストデータファイルが導入され、データディスクリプタを持たないZIPファイルに対して、正しいCRC32値が検証されるケースと、意図的にCRC32値を破損させた場合にErrChecksumが返されるケースの両方がテストされています。

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

変更は主に以下の2つのファイルで行われています。

  1. src/pkg/archive/zip/reader.go

    • checksumReader構造体のReadメソッド内のEOF処理ロジックが変更されました。
    --- a/src/pkg/archive/zip/reader.go
    +++ b/src/pkg/archive/zip/reader.go
    @@ -159,16 +159,21 @@ func (r *checksumReader) Read(b []byte) (n int, err error) {
     	if err == nil {
     		return
     	}
    -	if err == io.EOF && r.desr != nil {
    -		if err1 := readDataDescriptor(r.desr, r.f); err1 != nil {
    -			err = err1
    -		} else if r.hash.Sum32() != r.f.CRC32 {
    -			err = ErrChecksum
    +	if err == io.EOF {
    +		if r.desr != nil {
    +			if err1 := readDataDescriptor(r.desr, r.f); err1 != nil {
    +				err = err1
    +			} else if r.hash.Sum32() != r.f.CRC32 {
    +				err = ErrChecksum
    +			}
    +		} else {
    +			// If there's not a data descriptor, we still compare
    +			// the CRC32 of what we've read against the file header
    +			// or TOC's CRC32, if it seems like it was set.
    +			if r.f.CRC32 != 0 && r.hash.Sum32() != r.f.CRC32 {
    +				err = ErrChecksum
    +			}
     		}
    -		// TODO(bradfitz): even if there's not a data
    -		// descriptor, we could still compare our accumulated
    -		// crc32 on EOF with the content-precededing file
    -		// header's crc32, if it's non-zero.
     	}
     	r.err = err
     	return
    
  2. src/pkg/archive/zip/reader_test.go

    • 新しいテストケースがtestsスライスに追加されました。これは、データディスクリプタを持たないZIPファイル(crc32-not-streamed.zip)のCRC32検証をテストするものです。
    • readTestFile関数内のエラーチェックロジックが修正され、ft.ContentErrとの比較がより正確に行われるようになりました。
    • messWithというヘルパー関数が追加され、テストデータの破損をより柔軟にシミュレートできるようになりました。
    • returnCorruptNotStreamedZipという新しいテストヘルパー関数が追加され、データディスクリプタを持たないZIPファイルのCRC32を意図的に破損させるシナリオを生成します。
    --- a/src/pkg/archive/zip/reader_test.go
    +++ b/src/pkg/archive/zip/reader_test.go
    @@ -163,6 +163,46 @@ var tests = []ZipTest{
     			},
     		},
     	},
    +	// Tests that we verify (and accept valid) crc32s on files
    +	// with crc32s in their file header (not in data descriptors)
    +	{
    +		Name: "crc32-not-streamed.zip",
    +		File: []ZipTestFile{
    +			{
    +				Name:    "foo.txt",
    +				Content: []byte("foo\n"),
    +				Mtime:   "03-08-12 16:59:10",
    +				Mode:    0644,
    +			},
    +			{
    +				Name:    "bar.txt",
    +				Content: []byte("bar\n"),
    +				Mtime:   "03-08-12 16:59:12",
    +				Mode:    0644,
    +			},
    +		},
    +	},
    +	// Tests that we verify (and reject invalid) crc32s on files
    +	// with crc32s in their file header (not in data descriptors)
    +	{
    +		Name:   "crc32-not-streamed.zip",
    +		Source: returnCorruptNotStreamedZip,
    +		File: []ZipTestFile{
    +			{
    +				Name:       "foo.txt",
    +				Content:    []byte("foo\n"),
    +				Mtime:      "03-08-12 16:59:10",
    +				Mode:       0644,
    +				ContentErr: ErrChecksum,
    +			},
    +			{
    +				Name:    "bar.txt",
    +				Content: []byte("bar\n"),
    +				Mtime:   "03-08-12 16:59:12",
    +				Mode:    0644,
    +			},
    +		},
    +	},
     }\
      \
     var crossPlatform = []ZipTestFile{
    @@ -284,10 +324,10 @@ func readTestFile(t *testing.T, zt ZipTest, ft ZipTestFile, f *File) {
     	}\
      \
     	_, err = io.Copy(&b, r)\
    +	if err != ft.ContentErr {
    +		t.Errorf("%s: copying contents: %v (want %v)", zt.Name, err, ft.ContentErr)
    +	}
     	if err != nil {
    -		if err != ft.ContentErr {
    -			t.Errorf("%s: copying contents: %v", zt.Name, err)
    -		}
     		return
     	}\
      	r.Close()\
    @@ -344,12 +384,34 @@ func TestInvalidFiles(t *testing.T) {
     	}\
      }\
      \
    -func returnCorruptCRC32Zip() (r io.ReaderAt, size int64) {
    -	data, err := ioutil.ReadFile(filepath.Join("testdata", "go-with-datadesc-sig.zip"))
    +func messWith(fileName string, corrupter func(b []byte)) (r io.ReaderAt, size int64) {
    +	data, err := ioutil.ReadFile(filepath.Join("testdata", fileName))
     	if err != nil {
    -		panic(err)
    +		panic("Error reading " + fileName + ": " + err.Error())
     	}\
    -	// Corrupt one of the CRC32s in the data descriptor:
    -	data[0x2d]++
    +	corrupter(data)
     	return bytes.NewReader(data), int64(len(data))\
     }\
    +\
    +func returnCorruptCRC32Zip() (r io.ReaderAt, size int64) {
    +	return messWith("go-with-datadesc-sig.zip", func(b []byte) {
    +		// Corrupt one of the CRC32s in the data descriptor:
    +		b[0x2d]++
    +	})\
    +}\
    +\
    +func returnCorruptNotStreamedZip() (r io.ReaderAt, size int64) {
    +	return messWith("crc32-not-streamed.zip", func(b []byte) {
    +		// Corrupt foo.txt's final crc32 byte, in both
    +		// the file header and TOC. (0x7e -> 0x7f)
    +		b[0x11]++
    +		b[0x9d]++
    +\
    +		// TODO(bradfitz): add a new test that only corrupts
    +		// one of these values, and verify that that's also an
    +		// error. Currently, the reader code doesn't verify the
    +		// fileheader and TOC's crc32 match if they're both
    +		// non-zero and only the second line above, the TOC,
    +		// is what matters.
    +	})\
    +}\
    

コアとなるコードの解説

src/pkg/archive/zip/reader.gochecksumReader.Readメソッドの変更は、ZIPファイルの読み込みが完了した(err == io.EOF)時点でのCRC32検証ロジックを強化しています。

変更前のコードは、if err == io.EOF && r.desr != nilという条件で、データディスクリプタが存在する場合のみCRC32検証を行っていました。これは、データディスクリプタがCRC32値の「最終的な真実」を保持しているという前提に基づいています。

変更後のコードは、if err == io.EOFというより広い条件でEOF処理に入ります。その内部で、まずr.desr != nilをチェックします。

  • r.desr != nil の場合: これはデータディスクリプタが存在するケースです。以前と同様にreadDataDescriptorを呼び出してデータディスクリプタを読み込み、その中のCRC32値(r.f.CRC32に格納される)と、checksumReaderが読み込み中に計算したCRC32値(r.hash.Sum32())を比較します。一致しない場合はErrChecksumエラーを返します。
  • else (つまり r.desr == nil の場合): これはデータディスクリプタが存在しないケースです。この場合、CRC32値はローカルファイルヘッダや中央ディレクトリに直接記録されているはずです。ここで、if r.f.CRC32 != 0 && r.hash.Sum32() != r.f.CRC32という新しい条件が追加されました。
    • r.f.CRC32 != 0: これは、ファイルヘッダ/中央ディレクトリに記録されているCRC32値がゼロではないことを確認します。ゼロである場合は、CRC32値が不明であるか、データディスクリプタに依存していることを示唆するため、検証は行いません。
    • r.hash.Sum32() != r.f.CRC32: 読み込んだデータのCRC32値と、ファイルヘッダ/中央ディレクトリに記録されているCRC32値が一致しない場合に真となります。
    • この両方の条件が真の場合、データが破損していると判断し、err = ErrChecksumを設定します。

このロジックの変更により、データディスクリプタの有無にかかわらず、ZIPファイルにCRC32値が提供されている限り、その整合性が常に検証されるようになりました。これにより、archive/zipパッケージは、より広範なZIPファイルの破損シナリオに対して堅牢になります。

src/pkg/archive/zip/reader_test.goの変更は、この新しい検証ロジックが正しく機能することを保証するためのものです。特に、crc32-not-streamed.zipという新しいテストデータと、それに対応するテストケースは、データディスクリプタを持たないファイルに対するCRC32検証の成功と失敗の両方のシナリオをカバーしています。messWithreturnCorruptNotStreamedZipといったヘルパー関数は、テストの柔軟性と再利用性を高めるために導入されました。

関連リンク

参考にした情報源リンク