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

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

このコミットは、Go言語の標準ライブラリ archive/zip パッケージにおける、ZIP64形式のファイルを読み込む際のバグ修正に関するものです。具体的には、ZIP64の拡張情報フィールド(Extra records)を処理する際に、宣言されたサイズを超えてデータを読み込んでしまう問題を解決しています。

コミット

commit 02a15e716519b71aca6b74a0a388b30e83892e08
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Jan 6 10:43:56 2014 -0800

    archive/zip: fix bug reading zip64 files
    
    ZIP64 Extra records are variably sized, but we weren't capping
    our reading of the extra fields at its previously-declared
    size.
    
    No test because I don't know how to easily create such files
    and don't feel like manually construction one.  But all
    existing tests pass, and this is "obviously correct" (queue
    laughter).
    
    Fixes #7069
    
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/48150043

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

https://github.com/golang/go/commit/02a15e716519b71aca6b74a0a388b30e83892e08

元コミット内容

Go言語の archive/zip パッケージにおいて、ZIP64形式のファイルを読み込む際に発生するバグを修正します。このバグは、ZIP64の拡張情報レコード(Extra records)が可変長であるにもかかわらず、以前に宣言されたサイズで読み取りを制限していなかったために発生していました。

コミットメッセージには、この修正に対するテストが追加されていないことが言及されています。これは、テスト用のZIP64ファイルを簡単に作成する方法が不明であるため、手動での構築も行いたくないという理由からです。しかし、既存のテストはすべてパスしており、この修正は「明らかに正しい」とされています(ただし、この表現には皮肉が込められています)。

このコミットは、Issue #7069 を修正するものです。

変更の背景

ZIPファイルフォーマットは、元々2GBを超えるファイルサイズやエントリ数に対応していませんでした。この制限を克服するために導入されたのがZIP64フォーマットです。ZIP64では、従来のZIPフォーマットでは32ビットで表現されていたファイルサイズやオフセットなどの情報を64ビットで表現することで、より大きなファイルやアーカイブに対応できるようになりました。

ZIP64フォーマットでは、これらの拡張された情報を「Extra records」(拡張情報フィールド)と呼ばれる領域に格納します。このExtra recordsは、特定のIDとそれに続くデータ長、そして実際のデータで構成されており、可変長です。

Go言語の archive/zip パッケージは、ZIPファイルを読み書きするための標準ライブラリですが、ZIP64 Extra recordsを処理する際に、この可変長であるという特性を正しく扱えていませんでした。具体的には、Extra recordsのデータ長が事前に宣言されているにもかかわらず、その長さを無視してバッファ全体を読み込もうとしていました。これにより、Extra recordsのデータが意図せず他のデータと混ざってしまったり、不正なデータを読み込んでしまったりする可能性がありました。これが、このコミットで修正されたバグの根本原因です。

前提知識の解説

ZIPファイルフォーマットとZIP64

ZIPファイルは、複数のファイルを1つにまとめるための一般的なアーカイブフォーマットです。各ファイルは圧縮されて格納され、メタデータ(ファイル名、サイズ、タイムスタンプなど)も含まれます。

従来のZIPフォーマットにはいくつかの制限がありました。

  • ファイルサイズ: 1つのファイルまたはアーカイブ全体のサイズが4GB(正確には2^32-1バイト)を超えることはできませんでした。
  • エントリ数: アーカイブ内のファイルエントリの数が65,535(2^16-1)を超えることはできませんでした。

これらの制限を解消するために、PKWARE社によってZIP64フォーマットが導入されました。ZIP64では、ファイルサイズやオフセット、エントリ数などの情報を64ビットで表現することで、これらの制限を大幅に緩和しました。ZIP64の拡張情報は、既存のZIPヘッダの後に続く「Extra Field」(拡張フィールド)と呼ばれる領域に格納されます。

ZIP64 Extra records (拡張情報フィールド)

ZIP64 Extra recordsは、ZIPファイルフォーマットの「Extra Field」内に存在する特定のデータブロックです。これは、従来のZIPフォーマットの32ビット制限を超える情報を格納するために使用されます。

ZIP64 Extra recordsの一般的な構造は以下のようになります。

フィールド名サイズ (バイト)説明
Header ID2ZIP64 Extra Fieldの識別子 (0x0001)
Data Size2このExtra Fieldのデータ部分のサイズ(Header IDとData Sizeを除く)
Original Size8 (可変)圧縮前のファイルのオリジナルサイズ (64ビット)
Compressed Size8 (可変)圧縮後のファイルのサイズ (64ビット)
Relative Header Offset8 (可変)ローカルファイルヘッダの相対オフセット (64ビット)
Disk Start Number4 (可変)ファイルが開始するディスクの番号 (32ビット)

ここで重要なのは、「Original Size」「Compressed Size」「Relative Header Offset」「Disk Start Number」の各フィールドが可変であるという点です。これらのフィールドは、対応する従来の32ビットフィールドが 0xFFFFFFFF (または 0xFFFF for Disk Start Number) に設定されている場合にのみ、ZIP64 Extra records内に存在します。つまり、常に全てのフィールドが存在するわけではなく、Data Size フィールドが実際に存在するフィールドの合計サイズを示します。

Go言語の archive/zip パッケージ

archive/zip パッケージは、Go言語の標準ライブラリの一部であり、ZIPアーカイブの作成と展開をサポートします。このパッケージは、ZIPファイルフォーマットの仕様に準拠しており、ファイルの圧縮・解凍、アーカイブ内のファイル情報の読み取りなど、ZIP関連の基本的な機能を提供します。

内部的には、ZIPファイルの構造を解析し、各ファイルのヘッダ情報やデータブロックを読み書きするためのロジックが含まれています。このコミットが修正しているのは、特に readDirectoryHeader 関数内でZIP64 Extra recordsを解析する部分です。

技術的詳細

このバグは、src/pkg/archive/zip/reader.go ファイルの readDirectoryHeader 関数内で発生していました。この関数は、ZIPアーカイブの中央ディレクトリ(Central Directory)から各ファイルのヘッダ情報を読み取る役割を担っています。

中央ディレクトリのエントリには、ファイルのメタデータに加えて、Extra Fieldが含まれることがあります。ZIP64 Extra recordsもこのExtra Fieldの一部として格納されます。

問題のコードは、Extra FieldからZIP64 Extra recordsを読み取る際に、readBuf(b) のように記述されていました。ここで b はExtra Field全体のバイトスライスを指していました。しかし、ZIP64 Extra recordsはExtra Field全体を占めるわけではなく、その内部で自身の Data Size を持っています。

元のコード readBuf(b) は、b のスライス全体を readBuf 関数に渡していました。これは、b がExtra Field全体のバイトスライスである場合、ZIP64 Extra recordsの実際のデータサイズを超えて、Extra Field内の他のデータや、場合によってはExtra Fieldの後のデータまで読み込んでしまう可能性がありました。

修正後のコード readBuf(b[:size]) は、b スライスを size で指定された長さにスライスしてから readBuf 関数に渡しています。ここで size は、ZIP64 Extra recordsの Data Size フィールドから取得された、そのレコードの実際のデータ長です。これにより、readBuf 関数はZIP64 Extra recordsのデータ部分のみを正確に読み込むようになり、余分なデータを読み込むことがなくなりました。

この修正は、ZIP64 Extra recordsの構造と、Go言語のスライス操作の正確な理解に基づいています。b[:size] は、Goにおいてスライス b の先頭から size バイト目まで(size は含まない)の新しいスライスを作成する操作です。これにより、データの読み取り範囲が厳密に制御され、バッファオーバーリードのような問題が解消されます。

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

--- a/src/pkg/archive/zip/reader.go
+++ b/src/pkg/archive/zip/reader.go
@@ -253,7 +253,7 @@ func readDirectoryHeader(f *File, r io.Reader) error {
 			}
 			if tag == zip64ExtraId {
 				// update directory values from the zip64 extra block
-				eb := readBuf(b)
+				eb := readBuf(b[:size])
 				if len(eb) >= 8 {
 					f.UncompressedSize64 = eb.uint64()
 				}

コアとなるコードの解説

変更は src/pkg/archive/zip/reader.go ファイルの readDirectoryHeader 関数内、特に tag == zip64ExtraId の条件ブロックにあります。

  • if tag == zip64ExtraId { ... }: このブロックは、現在処理しているExtra FieldのサブフィールドがZIP64 Extra recordsであることを識別子 zip64ExtraId (0x0001) を使って確認しています。

  • eb := readBuf(b) (変更前): b は、現在のExtra Fieldのデータ全体を含むバイトスライスです。readBuf(b) は、この b スライス全体を readBuf 関数に渡していました。readBuf 関数は、おそらくバイトスライスから特定の形式でデータを読み取るユーティリティ関数です。問題は、b がZIP64 Extra recordsのデータ部分だけでなく、その後に続く可能性のある他のExtra Fieldのデータも含む可能性がある点でした。ZIP64 Extra recordsは自身のデータ長 size を持っているため、その size を超えて読み込むべきではありませんでした。

  • eb := readBuf(b[:size]) (変更後): size は、ZIP64 Extra recordsのヘッダ部分で宣言されている、そのレコードの実際のデータ長です。b[:size] は、Go言語のスライス操作で、スライス b の先頭から size バイト目まで(size は含まない)の新しいスライスを作成します。 この変更により、readBuf 関数に渡されるバイトスライス eb は、ZIP64 Extra recordsの実際のデータ部分のみに限定されます。これにより、宣言されたサイズを超えてデータを読み込むバグが修正され、ZIP64 Extra recordsの解析が正確に行われるようになりました。

この修正は、ZIP64 Extra recordsの可変長特性を正しく考慮し、データの読み取り範囲を厳密に制御することで、潜在的なデータ破損や不正なアーカイブ読み込みを防ぐための重要な変更です。

関連リンク

参考にした情報源リンク